Commit graph

407 commits

Author SHA1 Message Date
Souta
57def8564e feat(shadow): round-trip plugin binaries for BRAT / non-marketplace (#429b)
Phase B/2. The enabled-plugins LIST round-trips (#434) and the
marketplace installer fetches binaries for registry plugins (#439,
Phase B/1) — but a BRAT / sideloaded plugin isn't on the marketplace,
so its code never reached another machine.

ShadowVaultBootstrap.pull/pushPluginBinaries round-trip the plugin
*code* (manifest.json / main.js / styles.css; never data.json) through
the remote vault's canonical `.obsidian/plugins/<id>/`. Wired into
runAutoConnect AFTER the marketplace installer so the plugins it
fetched live aren't re-pulled (pull only stages what's still missing =
the non-marketplace ones, preserving the installer's live load and
acting as a fallback when a marketplace fetch fails). The push makes
the remote `.obsidian/plugins/` a complete vault. A pulled binary loads
on the next vault open (Obsidian scans the plugins dir at startup).

- pull never overwrites a local file (local install authoritative)
- push is diff-gated (no churn) and skips remote-ssh (self-managed)
- per-file SSH errors are swallowed, never abort the batch

Tests: 7 unit (in-memory fakes) + 3 integration (real docker sshd:
cross-session round-trip, push→fresh-pull, local-authoritative).

Refs #429 #342

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 20:24:51 +09:00
Souta
2b7fcd1c87 chore(release): 1.1.6-beta.6
Install pulled plugins after the connect-time pull into the beta.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 20:07:29 +09:00
Souta
72d75f2cb6 fix(shadow): install pulled plugins after the connect-time pull (#429b)
Phase B, step 1. The marketplace installer ran in prepareForAutoConnect
(before connect) and not at all on a reconnect, so a plugin the remote
community-plugins pull adds to the list had no binary staged and didn't
load until the next Obsidian restart — kazink's "plugins not kept
between sessions".

runAutoConnect now re-runs installMissingShadowPlugins right after
pull/pushCommunityPlugins, when the list is current. enablePluginAndSave
loads a marketplace plugin live (no restart); the call is idempotent so
already-installed ids are skipped, and it now also covers the reconnect
path (which never hit prepareForAutoConnect). installMissingShadowPlugins
is made public for this second call site.

Remaining Phase B (separate): binary round-trip for BRAT / non-
marketplace plugins (their binary isn't on the remote), and a pre-spawn
pull so app.json is canonical at boot without a reload.

Refs #429 #342

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 20:07:11 +09:00
Souta
68c5998033 test(integration): config consistency across connect cycles (#429, #342)
Independent integration spec (vitest + docker sshd, no Obsidian UI) that
exercises the real bootstrap + pull/push round-trip against a live SSH
server — the empirical guard the adversarial review found missing:

- community plugins enabled on the remote round-trip into a fresh shadow
  (keeping remote-ssh)
- a plugin enabled in session 1 persists into a fresh session 2 (via the
  remote as the canonical store)
- push unions with the remote and never drops a plugin enabled elsewhere
  (the #437 clobber-guard, verified over real SSH, not just fakes)
- a settings change stays consistent across write → push → fresh-pull

The remote vault's `.obsidian/` is the single source of truth; a fresh
shadow vault stands in for a new session / machine.

Refs #429 #342

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 18:42:45 +09:00
Souta
7699657f67 chore(release): 1.1.6-beta.5
Take the community-plugins push clobber-guard into the beta.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 18:25:23 +09:00
Souta
756eee4400 fix(shadow): pushCommunityPlugins re-reads + unions remote (no clobber)
Adversarial review of #434 found a data-loss regression: pull swallows a
remote read-failure internally (returns pulled:false, no throw), so the
subsequent push ran with a stale/minimal local list and could overwrite
the remote community-plugins.json — dropping plugins enabled on another
machine on a transient SSH hiccup.

pushCommunityPlugins is now self-protecting: it re-reads the remote and
unions (remote ∪ local ∪ remote-ssh) before writing, and ABORTS without
writing if the remote file exists but can't be read or parsed. An absent
remote is still seeded from local. Signature takes a reader+writer (the
patched adapter already satisfies both); the connect-flow caller is
unchanged.

Tests: seed-when-absent, union-never-drops, no-clobber-on-read-fail,
no-clobber-on-corrupt-json.

Refs #429 #342

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 18:25:05 +09:00
dependabot[bot]
61c8185884
chore(deps-dev): bump the dev-deps group across 1 directory with 6 updates
Bumps the dev-deps group with 4 updates in the /plugin directory: [@playwright/test](https://github.com/microsoft/playwright), [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin), [@vitest/coverage-v8](https://github.com/vitest-dev/vitest/tree/HEAD/packages/coverage-v8) and [eslint](https://github.com/eslint/eslint).


Updates `@playwright/test` from 1.60.0 to 1.61.1
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.60.0...v1.61.1)

Updates `@typescript-eslint/eslint-plugin` from 8.61.0 to 8.62.1
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.62.1/packages/eslint-plugin)

Updates `@typescript-eslint/parser` from 8.61.0 to 8.62.1
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.62.1/packages/parser)

Updates `@vitest/coverage-v8` from 4.1.8 to 4.1.9
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.9/packages/coverage-v8)

Updates `eslint` from 10.5.0 to 10.6.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.5.0...v10.6.0)

Updates `vitest` from 4.1.8 to 4.1.9
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.9/packages/vitest)

---
updated-dependencies:
- dependency-name: "@playwright/test"
  dependency-version: 1.61.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-deps
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-version: 8.62.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-deps
- dependency-name: "@typescript-eslint/parser"
  dependency-version: 8.62.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-deps
- dependency-name: "@vitest/coverage-v8"
  dependency-version: 4.1.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-deps
- dependency-name: eslint
  dependency-version: 10.6.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-deps
- dependency-name: vitest
  dependency-version: 4.1.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-deps
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-30 07:19:00 +00:00
Souta
d696b0da8b chore(release): 1.1.6-beta.4
Take the golang.org/x/image 0.42.0→0.43.0 server dep bump (#426) into
the beta. (Manual bump: the branch was update-branched under my actor,
which lifts the dependabot version-check exemption.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 16:15:27 +09:00
dependabot[bot]
13970e647e
chore(deps-dev): bump @types/node from 25.9.3 to 26.0.1 in /plugin
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 25.9.3 to 26.0.1.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 26.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-30 07:12:55 +00:00
dependabot[bot]
c6ff2f9bbb
chore(deps-dev): bump undici from 7.25.0 to 7.28.0 in /plugin
Bumps [undici](https://github.com/nodejs/undici) from 7.25.0 to 7.28.0.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v7.25.0...v7.28.0)

---
updated-dependencies:
- dependency-name: undici
  dependency-version: 7.28.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-30 07:09:38 +00:00
Souta
52c1c5d5ba docs: compress the #429 corrections to minimal wording
Tighten the just-added #429 notes (glossary entry, compat basePath
bullet + Direct-disk footgun + Templater/Kanban/Importer notes + survey
correction, and the SftpDataAdapter comments) — same facts, roughly half
the words. Optimised for catch-up: details live behind the issue refs.

Refs #429

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 15:49:49 +09:00
Souta
f46bd5f367 docs(adapter): correct stale "fs writes propagate to remote" comments (#429)
The shadowBasePath constructor-param doc and the `basePath` getter doc
in SftpDataAdapter carried the same misconception the compat guide did:
that Node `fs` reads/writes under the shadow basePath are "mirrored
locally by the file watcher" and "propagate to the remote." No such
vault-tree mirror or watcher exists — the vault tree is served
virtually from the remote, so those `fs` ops only touch the local
shadow copy. Comment-only; behaviour unchanged.

Refs #429

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 15:16:34 +09:00
Souta
4f22eaffa5 chore(release): 1.1.6-beta.3
Take the #429 docs correction into the beta.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 15:07:15 +09:00
Souta
a53c95e226 chore(release): 1.1.6-beta.2
Take the #429/#342 community-plugins round-trip into the beta.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 14:49:03 +09:00
Souta
cdad499421 Merge remote-tracking branch 'origin/next' into fix/community-plugins-roundtrip 2026-06-30 14:48:43 +09:00
Souta
c1c47a37ac chore(release): 1.1.6-beta.1
Take the #430 SSH ProxyCommand / IdentityAgent support into the beta.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 14:46:37 +09:00
Souta
59bd5f1c41 Merge remote-tracking branch 'origin/next' into feat/ssh-proxycommand-430 2026-06-30 14:46:17 +09:00
Souta
41a3d8a247 chore(release): 1.1.6-beta.0
Start the 1.1.6 beta cycle with the #423 drag-into-fresh-folder fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 14:43:10 +09:00
Souta
1136d07dde fix(shadow): round-trip the community-plugins list (#429, #342)
Plugins set up on the remote vault didn't load in the shadow vault, and
reopening "lost" installed plugins: the enabled-plugins list
(community-plugins.json) was only ever seeded locally as ["remote-ssh"]
and never synced, unlike app/appearance/core-plugins/hotkeys.

Add `ShadowVaultBootstrap.pullCommunityPlugins` / `pushCommunityPlugins`
that round-trip community-plugins.json with a forced `remote-ssh` union
(a verbatim copy of a remote list omitting remote-ssh would disable this
very plugin, which is why it stays out of SHARED_OBSIDIAN_CONFIG_FILES).
Pull merges remote→local (the marketplace installer then fetches any
missing binaries on the next startup); push sends the merged list back
so a plugin enabled on one machine reaches another. The connect flow
pulls then pushes — pushing after the merge can never drop a plugin the
remote already had.

Refs #429 #342

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 14:20:38 +09:00
Souta
54c18cabbf test(shadow): capture community-plugins list not round-tripping
app/appearance/core-plugins/hotkeys already round-trip via
pull/pushSharedObsidianConfig, but the enabled community-plugins list
does not — it is only seeded locally as ["remote-ssh"]. So a plugin
enabled on one machine never reaches another machine's shadow vault and
reopening "loses" installed plugins (#429 kazink follow-up, #342
residual). RED cases for a dedicated pull/push pair that must keep
remote-ssh enabled through the round-trip:

- pull merges the remote list, keeping remote-ssh
- pull keeps remote-ssh even when the remote list omits it
- pull is a benign no-op when the remote has none yet
- pull does not clobber a healthy local list on corrupt remote JSON
- push writes the local list to the remote, preserving remote-ssh

Refs #429 #342

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 14:14:39 +09:00
Souta
8ff3867310 feat(ssh): support ProxyCommand and IdentityAgent (#430)
Connect to hosts reachable only through a subprocess transport — e.g. a
Cloudflare tunnel:

  Host homelab
    HostName lab.example.com
    ProxyCommand cloudflared access ssh --hostname %h
    IdentityAgent ~/.1password/agent.sock

- SshConfigReader parses `ProxyCommand` (raw, %-tokens preserved) and
  `IdentityAgent` (tilde-expanded), incl. Host * defaults + host
  override.
- New ProxyCommandTunnel: `expandProxyCommandTokens` (%h/%p/%r/%%) +
  `createProxyCommandTunnel`, which spawns the command through the shell
  and bridges its stdio as a Duplex, with backpressure and child
  teardown on close. ssh2 takes it as `sock` (same seam as the jump
  tunnel).
- SshProfile gains `proxyCommand`; SftpClient.connect opens the tunnel
  when set (jumpHost takes precedence — the two are mutually exclusive
  in OpenSSH).
- ProfileForm: "Import from SSH config" maps ProxyCommand → proxyCommand
  and IdentityAgent → agentSocket(+agent auth); plus a manual
  "Proxy command" text field.

Desktop only (child_process). The SshConfigReader IdentityAgent test
compares via path.join so it's OS-agnostic.

Refs #430

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 14:13:37 +09:00
Souta
f41fcbfa0e test(ssh): capture missing ProxyCommand / IdentityAgent support
A user behind a Cloudflare tunnel connects via
  ProxyCommand cloudflared access ssh --hostname %h
and authenticates through 1Password's agent socket
  IdentityAgent ~/.1password/agent.sock
Neither is supported today. RED cases:

- SshConfigReader: parse ProxyCommand (raw, %-tokens preserved),
  host-specific override of Host * default, and IdentityAgent
  (tilde-expanded) — 5 cases.
- ProxyCommandTunnel (new module, import currently fails): token
  expansion (%h/%p/%r/%%) and a subprocess-as-Duplex transport that
  bridges child stdio and tears the child down on close — 10 cases.

Refs #430

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 14:03:26 +09:00
Souta
f50787f704 fix(vault): reflectRename synthesises a stale destination subtree (#423)
A note dragged into a freshly-created folder could vanish from File
Explorer until the vault was reopened. The writer-reflect rename bailed
("writer vault model is stale") whenever the destination folder was not
yet in the vault model: `renameOne` resolved the new parent strictly and
returned false, so the entry was detached from the old path but never
attached to the new one.

reflectRename now mirrors reflectWrite: it asks renameOne to synthesise
the missing parent chain (`ensureParents`), so the move lands even when
the destination subtree is stale. It also returns early — without a
misleading warning — when Obsidian's own rename already relocated the
entry. The low-level renameOne stays strict by default, so the
bulk-build and remote-echo (FsChangeListener) paths keep their
missing-parent sanity check.

Refs #423

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 14:01:56 +09:00
Souta
69db7c3a07 test(vault): capture #423 move-into-fresh-folder data loss
reflectRename bails ("writer vault model is stale") when the
destination folder is not yet modelled — the exact field repro from
issue #423, where dragging notes into a freshly-created folder makes
them vanish from File Explorer until a full vault reopen. Four cases,
all RED today:

- move into a not-yet-modelled folder must synthesise the parent
- the same for several notes into one fresh folder (field-log shape)
- a deeply nested fresh subtree
- a benign echo (Obsidian moved the file first) must not warn "stale"

Refs #423

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 13:59:33 +09:00
Souta
3b2d122393 release: 1.1.5 (stable)
Promotion-staging bump on next (new flow: main only ever merges from next, no
release/ branch). Strips the -beta suffix and advances the stable manifest.json
+ versions.json. The next → main promotion that follows is what publishes.

First exercise of the version-check change from 1.1.5-beta.1 (#416): a plain
X.Y.Z on a next PR is accepted as a promotion-staging shape.

Cycle content is docs + CI only (README refresh #415, release-flow/version-check
#416) — no plugin behavior change vs 1.1.4.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 10:16:04 +09:00
dependabot[bot]
8eb427b4e6
chore(deps-dev): bump js-yaml from 4.1.1 to 4.2.0 in /plugin
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.1 to 4.2.0.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/commits)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.2.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-16 00:47:41 +00:00
Souta
4c18dacf8c chore(release): 1.1.5-beta.1
Second beta of the 1.1.5 cycle — the release-flow / version-check
reconciliation (main ← next only). Beta channel only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 21:06:15 +09:00
Souta
64ff9bd80c chore(release): 1.1.5-beta.0
First beta of the 1.1.5 cycle — the README refresh. Beta channel only; stable
manifest.json + versions.json stay pinned to 1.1.4.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 20:57:13 +09:00
Souta
b617316c9e release: 1.1.4 (stable)
Promote next→main: ships the new-profile first-connect restart guidance
(#410) and the stable-release-notes automation (#412). Strips the -beta
suffix and advances the stable manifest.json + versions.json (minAppVersion
unchanged) for the community store.

This is the first stable release cut with the #412 changelog automation, so
release.yml auto-generates the release body from the 1.1.3..1.1.4 range.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 19:51:52 +09:00
Souta
6f13959a8b ci(release): aggregate the whole beta cycle into stable release notes
Stable release notes came out empty: the changelog step used `git-cliff
--unreleased` (commits since the previous TAG), but for a stable promotion
the previous tag is the last BETA of the cycle, so the range held only the
promote-plumbing commits — all filtered by cliff.toml → empty body.

release.yml now branches the changelog range by channel:
- beta   -> `--unreleased` (incremental, unchanged).
- stable -> `<last-stable-tag>..HEAD --ignore-tags beta`, so the notes carry
  everything accumulated across the cycle's betas, consolidated under one
  version heading (intermediate beta tags would otherwise split the output
  into repeated per-tag sections).

cliff.toml: also skip `chore(release):` version-bump commits so the notes
show real changes, not bump plumbing.

Verified locally with git-cliff over 1.1.2..1.1.3: one consolidated set of
grouped sections (Features / Bug Fixes / Tests / …), bump commits dropped.
On main's topology (only stable promote-merges sit between stable tags) the
range is clean; the real release runs there.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 19:44:10 +09:00
Souta
84f338fa9a chore(release): 1.1.4-beta.0
First beta of the 1.1.4 cycle, opened after the 1.1.3 stable release.
Carries the new-profile first-connect restart guidance. Beta channel only;
stable manifest.json + versions.json stay pinned to 1.1.3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 19:19:33 +09:00
Souta
cd00285ac9 fix(shadow): tell the user to restart on first connect to a new profile
A freshly-registered shadow vault is added to obsidian.json, but a running
Obsidian only reads that file at startup — so the obsidian://open fired on
the first connect to a NEW profile is a no-op and no window appears (the SSH
connection itself succeeds). The plugin previously showed a misleading
"opened in new window" notice for a window that never opened.

openShadowVaultFor now branches on result.registryCreated: a newly-registered
vault gets a PERSISTENT notice (duration 0) telling the user to fully quit and
reopen Obsidian, then reconnect; the reused path keeps the normal "opened in
new window" notice. One-time per profile.

Docs: first-connect gains an [!important] restart callout; troubleshooting
gains a "Clicked Connect but no window opened (new profile)" entry. Also fixed
a stale shadow-vault path in first-connect
(<plugin-dir>/shadow-vaults/ -> ~/.obsidian-remote/vaults/).

Tests: openShadowVaultFor — newly-registered -> restart notice (not "opened");
reused -> "opened in new window" (no restart notice).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 19:19:16 +09:00
Souta
ddd24b41ee release: 1.1.3 (stable)
Promote next→main: ships the #399 shadow-vault secrets fix and the #397
daemon auto-download (soaked through 1.1.3-beta.0..beta.2). Strips the
-beta suffix and advances the stable manifest.json + versions.json
(minAppVersion 1.5.0) that the community store reads.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 18:17:00 +09:00
Souta
0c4065b507 test(daemon): harden #407 review findings — loud-fail assertions, connectSsh pin, comment fix
From the multi-agent review of #407:
- connectProfile loud-fail test now asserts a Notice surfaced AND that the
  silent SFTP-downgrade path was NOT taken (the "loud" part was previously
  only inferred from ERROR state, which a return-early bug could fake).
- both connectProfile tests assert conn.connectSsh was called, so a refactor
  that short-circuits the connect cannot leave them green.
- reconnectAttempt test: corrected a stale comment ("re-prompt consent" no
  longer applies post-#406, which persists the decline) to the real harm
  (burns retries + a misleading "reconnect failed").
- makePlugin: one intermediate cast instead of four; replayOfflineQueue mock
  resolves a promise to match the fire-and-forget call shape.

Tests-only. Affected suites green; lint + tsc clean.

Refs #406, #407

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 18:02:40 +09:00
Souta
3e139fab61 chore(release): 1.1.3-beta.2
Third beta of the 1.1.3 cycle: test coverage for the daemon RPC→SFTP
downgrade paths + executable specs for the deferred #406-review hardening.
Beta channel only; stable manifest.json + versions.json stay pinned to 1.1.2.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 17:51:09 +09:00
Souta
0616e38df0 test(daemon): cover RPC→SFTP downgrade paths; track deferred hardening as specs
Turns the #406-review follow-ups into test code instead of tracking issues:

- connectProfile (new connectProfile.daemon-downgrade.test.ts): a
  DaemonUnavailableError downgrades to SFTP and still reaches CONNECTED;
  a DaemonVerificationError fails loud (ERROR + disconnect), not a silent
  downgrade — pins the I-3 boundary.
- ConnectionManager: reconnectAttempt catches DaemonUnavailableError and
  continues on SFTP instead of throwing into ReconnectManager's retry loop
  (pins I-1).
- DaemonDownloader: three it.todo specs as runnable trackers for the
  deferred hardening — cosign .bundle verification, a shared cross-vault
  binary cache, and cache-hit re-verification.

Tests-only; no production change. Full suite green (1120 passed, 3 todo);
lint + tsc clean.

Refs #397, #406

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 17:50:59 +09:00
Souta
e077bd095a fix(daemon): address #406 review — reconnect downgrade, consent durability, loud tamper, atomic cache
Findings from the multi-agent review of #406:

- I-1 reconnect: reconnectAttempt now catches DaemonUnavailableError and
  continues on SFTP instead of letting ReconnectManager retry a permanent
  daemon-unavailable condition (and re-prompt consent) maxRetries times.
- I-2 consent: persist the consent DECISION (accept OR decline) via the new
  pure resolveDaemonConsent() gate, so a decline no longer re-prompts on
  every connect/restart; check `=== true`.
- I-3 tamper: a sha256/DaemonVerificationError is rethrown, not swallowed
  into a quiet SFTP downgrade — integrity failures surface loudly.
- I-5 atomic cache: writeExecutable writes a temp sibling then renames, so a
  crash mid-download can't leave a torn binary that a later cacheHit returns
  unverified.
- uname probe: a non-zero exit / exec error is logged distinctly, kept
  separate from "unsupported arch".
- harden: validate the sha-manifest shape (isShaManifest) instead of an `as`
  cast; lowercase BOTH sides of the sha compare.
- tests: resolveDaemonConsent (accept/decline/already-consented), uppercase
  manifest sha, and ConnectionManager startRpcSession fallback (download →
  deploy, both-null → DaemonUnavailableError, staged → skip download).
- cleanups: pluginDir() helper de-dupes the three plugin-dir path builds;
  DAEMON_RELEASE_REPO moved out from between imports; consent-modal text
  ("faster directory listing", not "fast sync"); accurate JSDoc on
  DaemonUnavailableError / the version dep / locateDaemonBinary.

Deferred (flagged in review, intentionally not here): cosign .bundle
verification (heavy; HTTPS+sha256 with fail-safe SFTP is the current bar),
a shared cross-vault daemon cache, and extracting confirmDaemonDownload
into a Modal subclass.

Refs #397, #406

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 15:53:32 +09:00
Souta
7fa0f27227 chore(release): 1.1.3-beta.1
Second beta of the 1.1.3 cycle, adding daemon auto-download (#397) on
top of the #399 shadow-vault secrets fix. Beta channel only: stable
manifest.json + versions.json stay pinned to 1.1.2.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 15:26:52 +09:00
Souta
7bae432e42 feat(daemon): auto-download the daemon binary from the GitHub release
Community-store installs ship only main.js/manifest.json/styles.css, so the
daemon binary (server-bin/) never reaches users -> default RPC failed with a
dev-facing "run npm run build:server" error (#397).

Now, when no binary is staged locally, the plugin:
  - probes the remote os/arch via `uname`,
  - downloads the matching obsidian-remote-server-<os>-<arch> from this
    plugin's GitHub release,
  - verifies it against the release's daemon-manifest.json (sha256),
  - caches it under server-bin/ for subsequent connects.

A one-time consent dialog gates the download (Obsidian discourages silent
external fetches; remembered in settings). Unsupported remote arches, a
declined download, or any failure downgrade the session to SFTP via the new
DaemonUnavailableError instead of hard-failing.

New DaemonDownloader.ts (pure, dependency-injected) + 17 tests. ConnectionDeps
gains ensureDaemonBinary; startRpcSession uses
locateDaemonBinary() ?? ensureDaemonBinary(). main.ts wires requestUrl + fs +
the consent Modal; connectProfile catches DaemonUnavailableError and continues
on SFTP.

Refs #397.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 15:26:40 +09:00
Souta
c837bf31f0 chore(release): 1.1.3-beta.0
First beta of the 1.1.3 cycle, carrying the #399 shadow-vault secrets
fix. Beta channel only: plugin/manifest.json + manifest-beta.json
advance; the stable manifest.json + versions.json stay pinned to 1.1.2.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 15:15:10 +09:00
Souta
55f76397b9 fix(shadow): propagate source-vault secrets into the shadow vault (#399)
A password entered in the source (local) vault never reached the shadow
vault that actually runs the SSH connect, so the shadow auto-connect
failed with "No password stored for profile" and the vault opened empty.

Two gaps combined:

1. ConnectModal.persistSecret only writes the password to the in-memory
   SecretStore; openShadowVaultFor spawned the shadow before any save, so
   the bootstrap read a source data.json whose `secrets` were still empty.
2. ShadowVaultBootstrap.readBaseDataJson prefers the existing shadow
   data.json on re-bootstrap, so even a persisted source secret never
   propagated after the first spawn.

Fix:
- openShadowVaultFor flushes settings (saveSettings) before the spawn so
  the freshly-typed password + passwordRef land in the source data.json.
  A save failure is logged and swallowed rather than aborting the spawn.
- bootstrapSync unions the source vault's `secrets` into the shadow
  data.json on every bootstrap (source wins on a conflicting ref; a
  secret typed directly in the shadow window survives).

Tests (red before the fix):
- ShadowVaultBootstrap: re-bootstrap propagates a new source secret;
  union precedence (shadow-only survives, source wins on conflict).
- openShadowVaultFor: settings are flushed before the shadow spawn.

Refs #399

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 15:14:42 +09:00
dependabot[bot]
9a28553428
chore(deps-dev): bump the dev-deps group in /plugin with 4 updates (#403)
Bumps the dev-deps group in /plugin with 4 updates: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node), [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin), [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) and [eslint](https://github.com/eslint/eslint).


Updates `@types/node` from 25.9.2 to 25.9.3
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `@typescript-eslint/eslint-plugin` from 8.60.1 to 8.61.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.61.0/packages/eslint-plugin)

Updates `@typescript-eslint/parser` from 8.60.1 to 8.61.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.61.0/packages/parser)

Updates `eslint` from 10.4.1 to 10.5.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.4.1...v10.5.0)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.9.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-deps
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-version: 8.61.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-deps
- dependency-name: "@typescript-eslint/parser"
  dependency-version: 8.61.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-deps
- dependency-name: eslint
  dependency-version: 10.5.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-deps
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: sotashimozono <shimozono-sota631@g.ecc.u-tokyo.ac.jp>
2026-06-15 14:42:55 +09:00
dependabot[bot]
0e0c033b2a
chore(deps-dev): bump esbuild from 0.28.0 to 0.28.1 in /plugin (#400)
Bumps [esbuild](https://github.com/evanw/esbuild) from 0.28.0 to 0.28.1.
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.28.0...v0.28.1)

---
updated-dependencies:
- dependency-name: esbuild
  dependency-version: 0.28.1
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-13 20:44:01 +09:00
sotashimozono
085edf436a
release: 1.1.2 (stable) — validator-clean (#395)
Promotes the validator-cleanup work to a stable release.

- CSS: collapse xterm's text-decoration style (double/wavy/dotted/dashed)
  and simultaneous overline+underline to a plain underline. Obsidian's
  bundled Chromium only partially supports the style and multiple-line
  forms, which the community-plugin validator flagged. Single overline /
  underline / line-through are kept (fully supported).
- Version: 1.1.2-beta.14 -> 1.1.2 (drops -beta; syncs all manifests +
  versions.json, minAppVersion 1.5.0).

Cumulative since 1.1.1: root npm devDeps so the validator resolves plugin
types (#393), obsidian pinned ~1.12.3, and the unsafe-* / CSS warnings
cleared (#394 + this).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 15:28:18 +09:00
sotashimozono
195a3041b4
fix(types): eliminate validator unsafe-* / CSS-portability warnings (#394)
With the root devDeps now resolving types, the validator surfaced a few real
`any` leaks plus a CSS portability note. All fixed:

- ResourceBridge: name the range fetcher's return type (BinaryRange) and
  annotate `let result: BinaryRange`. `let result;` had decayed to `any`
  because evolving-any doesn't survive the try/catch reassignment, so
  result.mtime/.totalSize/.bytes were all unsafe.
- QueueReplayer: same pattern — extract `ReplayOutcome`, annotate
  `let outcome: ReplayOutcome`.
- logger: cast the bound console methods to `ConsoleFn` (the snapshot's
  declared type) rather than `typeof console.warn`.
- AdapterPatcher: replace `fn.bind(x)` (whose typing varied by env) with an
  equivalent arrow wrapper.
- styles.css: fold xterm's text-decoration-line + -style sub-properties into
  the text-decoration shorthand (only the shorthand is fully supported on
  older Obsidian; the sub-properties were flagged partial).

Verified: validator-sim eslint (root deps, cwd=root) -> 0; plugin tsc + lint -> 0.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 15:04:58 +09:00
sotashimozono
7d1830d549
build: root devDeps + obsidian pin so the validator passes (no workflow changes) (#393)
* build(workspaces): root npm-workspaces so the validator resolves plugin types

ObsidianReviewBot scans from the repo ROOT (getManifest reads cwd/manifest.json),
installs at the root, and type-checks plugin/src via ./tsconfig.json with
@typescript-eslint/no-unsafe-member-access. The plugin lives in plugin/ with deps in
plugin/node_modules, so a root install resolved nothing -> @types/node + obsidian
unresolved -> false "unsafe member access" warnings. #392 (typeRoots) couldn't help
because plugin/node_modules doesn't exist in the validator's root-only install.

Declare the root as an npm workspace ({ workspaces: ["plugin"] }). A root install now
hoists the plugin's deps into root/node_modules; the root tsconfig's typeRoots picks
them up. Verified by simulating the validator (rm plugin/node_modules; install at root
only): @types/node + obsidian hoisted, eslint from cwd=root -> 0 unsafe-member-access.

CI unaffected: cd plugin; npm ci still works (plugin/package-lock.json retained, 439
packages, 0 vuln) -- no workflow changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* build(deps): pin obsidian to ~1.12.3 so the validator type-checks deprecation-free

With the workspace root now resolving types, a root install pulled obsidian "latest"
= 1.13.0, which deprecates SettingTab.display() and ButtonComponent.setWarning(). The
validator's @typescript-eslint/no-deprecated then flagged 9 errors in SettingsTab.ts
that the plugin's own CI never saw (plugin lock pinned 1.12.3).

"obsidian": "latest" is a non-reproducible anti-pattern regardless. Pin to ~1.12.3
(the last release before those APIs were deprecated). Verified by simulating the
validator (root install -> obsidian 1.12.3; eslint from cwd=root): 0 no-deprecated,
0 unsafe-member-access, EXIT 0.

The declarative getSettingDefinitions()/setDestructive() migration is deferred until
we intentionally raise the obsidian floor.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* build: plain root devDeps instead of workspaces (keeps CI green)

The workspaces approach broke CI: npm hoists the plugin's deps into
root/node_modules, but the plugin's CI invokes binaries by direct path
(`node node_modules/typescript/bin/tsc`, `node node_modules/vitest/...`),
which are then absent from plugin/node_modules. Hoisting is intrinsic to
workspaces, so any workspaces setup would require workflow changes.

Instead, make the root package.json a plain (non-workspace) manifest that
declares only the deps the validator needs to type-check plugin/src from
the repo root. A root `npm install` populates root/node_modules and the
root tsconfig resolves types there. The plugin's own `cd plugin; npm ci`
is untouched (no hoisting) -> CI stays green with zero workflow changes.

Verified: move plugin/node_modules aside, eslint from cwd=root with root
deps only -> 0 unsafe-member-access, 0 no-deprecated, 0 cannot-be-resolved.

Trade-off: type-dep versions are duplicated between root and plugin and
must be kept in sync (noted in the root package.json description).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 14:38:19 +09:00
sotashimozono
6dcfd97694
fix(tsconfig): resolve Node types for ObsidianReviewBot via typeRoots (#392)
The validator's static type-checker reads the root tsconfig but couldn't find
@types/node (no root node_modules — it lives in plugin/node_modules), so
Buffer / ArrayBufferView / net.Server / EventEmitter resolved to `any`,
producing the false "unsafe member access" warnings. Point typeRoots at
plugin/node_modules/@types and pin types:["node"]. Verified: tsc -p
tsconfig.json now resolves Node types (no Buffer/net errors).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 12:55:19 +09:00
sotashimozono
8bca9eed60
chore: strip Quartz off main/next (validator scans plugin only) (#391)
* chore: strip Quartz off main/next so the validator scans only the plugin

The Obsidian plugin validator's static preview was scanning the Quartz docs
site that lived at the repo root, surfacing false-positive warnings (globby/
js-yaml "should be replaced", docs-site unsafe-member-access from unresolved
types). Quartz now lives entirely on the docs/quartz-framework branch (#390
staged the config/deps/types there).

- docs.yml: checkout the full Quartz set from the docs branch BEFORE
  setup-node (so its package-lock cache key resolves), then build docs/ and
  deploy gh-pages stable=main / dev=next (deploy logic unchanged).
- Remove from root: package.json (@jackyzha0/quartz), package-lock.json,
  quartz.config.ts, quartz.layout.ts, index.d.ts, globals.d.ts,
  tsconfig.quartz.json, scripts/check-docs-links.mjs

main/next now carry only the plugin + docs/ content (.md, not scanned).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(release): bump beta for the Quartz strip

Version bump so version-check (PR version > base) passes; the Quartz removal
itself carried no version change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 12:38:06 +09:00
sotashimozono
f1dc48d9ad
fix(mobile): remove disallowed eslint-disable on no-restricted-globals (#389)
window.localStorage does not trigger no-restricted-globals (bare
localStorage does); the disable comment was both unnecessary and
itself flagged by the bot as disallowing a rule that cannot be suppressed.
2026-06-09 10:01:38 +09:00
sotashimozono
4be16e9226
fix(plugin): switch root tsconfig to commonjs/node resolution for bot (#388)
moduleResolution:bundler fails to resolve obsidian package because its
package.json has main:"" (empty string) with no exports field. paths
mapping also does not override this behaviour in bundler mode.

Switch the root tsconfig (used only by ESLint/ObsidianReviewBot, not by
the actual plugin build) to module:commonjs + moduleResolution:node.
Classic Node resolution walks up from each source file in plugin/src/
to find plugin/node_modules/obsidian, @xterm/*, and @types/ssh2 reliably.
2026-06-08 15:17:35 +00:00