Commit graph

178 commits

Author SHA1 Message Date
Souta
99d4ad09c0 chore(release): promote 1.1.7-beta.1 -> 1.1.7 (stable)
Strip the -beta.N suffix so release.yml accepts the version landing on
main. Syncs plugin + root manifest.json / manifest-beta.json /
versions.json (minAppVersion 1.5.0). Version-only -- no code change.

Ships the #342 / #429 fix: third-party plugin settings now survive a
restart (config write-through to the local shadow disk) and are
per-device on the remote, so two machines can no longer collide on one
settings file.
2026-07-14 14:44:48 +09:00
Souta
e022e28a5c chore(release): promote 1.1.6-beta.18 -> 1.1.6 (stable)
Strip the -beta.N suffix so release.yml accepts the version landing on
main. Syncs plugin + root manifest.json / manifest-beta.json /
versions.json (minAppVersion 1.5.0). Version-only -- no code change.
2026-07-03 14:56:25 +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
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
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
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
8dbefaaef6
release: prepare 1.1.1 promotion (drop -beta suffix) (#374)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 17:34:40 +09:00
Souta
088b1c5aa3 chore(release): promote 1.1.0-beta.8 → 1.1.0 (stable) 2026-05-18 11:09:31 +09:00
Souta
5cb54b214a chore(release): promote 1.0.49-beta.2 → 1.0.49 (stable)
Closes the 1.0.49 beta cycle. next → main promotion: every manifest
+ versions.json advanced to the plain 1.0.49 stable version (STABLE
channel rule — all manifests must agree). Drops the -beta suffix so
the merge to main publishes the canonical "Latest" release.

Contents promoted from next (all CI green on next HEAD):
- #341 writer self-reflect (SFTP) + #342 shared-config round-trip,
  via the 3-layer sync-test framework (#343)
- RPC writer-reflect + LocalOpRegistry echo de-dup (#341 fully
  transport-independent) + property-harness hermetic fix (#347)
- docs: Quartz 404Page TDZ repair + docs build now runs on PRs (#348)

The stray docs-site/quartz/components/Head.tsx formatting change is
left uncommitted on purpose — it is not part of next's verified
content and must not ride into a stable release unreviewed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 16:02:25 +09:00
Souta
6a9790de03 chore(release): 1.0.48 (promote 1.0.48-beta.2 to stable)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 22:57:10 +09:00
Souta
72ccd9649d release: prepare 1.0.47 promotion (drop -beta suffix) 2026-05-11 00:41:12 +09:00
Souta
66ae8aa908 release: prepare 1.0.46 promotion (1.0.45 → 1.0.46) 2026-05-10 23:16:17 +09:00
Souta
4d029a9599 release: prepare 1.0.45 promotion (drop -beta suffix) 2026-05-10 20:45:24 +09:00
Souta
ec5ff40c49 release: prepare 1.0.44 promotion (drop -beta suffix) 2026-05-10 18:03:35 +09:00
Souta
6f921792db chore: bump plugin to 1.0.43 2026-05-10 11:32:55 +09:00
sotashimozono
17c064b23a
fix(docs): invoke Quartz via node directly — unblock stable deploy (1.0.42)
The promotion to main triggered docs.yml's stable build, which failed
with `sh: 1: quartz: Permission denied` (twice — including a re-run).

Same commit content built fine on next as the dev preview, so it isn't
a code issue. Most likely npx fell back to a bin shim that lost its
executable bit on cache restore.

Fix: invoke Quartz directly via `node ./quartz/bootstrap-cli.mjs build`
instead of `npx quartz build`. Quartz's package.json declares
`bin: { quartz: './quartz/bootstrap-cli.mjs' }`, so this is the same
script — just one indirection removed.

Applied to both the stable and dev jobs so they stay symmetric.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 10:32:39 +09:00
sotashimozono
c47ed62abc
fix(docs-site): npm audit fix — resolve 6 HIGH-severity transitives (1.0.41)
Quartz pulled in vulnerable transitive deps that the SARIF-based "Trivy"
Code Scanning check rejected on the next→main promotion PR (#285):

  - minimatch (3.x → 10.x)        ReDoS via repeated wildcards
  - picomatch (2.x → 4.x)         Method injection in POSIX char classes
  - immutable (5.1.4 → 5.1.5)     Prototype pollution
  - @xmldom/xmldom (0.8.10 → ≥0.8.13)  XML injection via CDATA
  - @isaacs/brace-expansion       Uncontrolled resource consumption
  - serve-handler                 Inherited fix via minimatch bump

`npm audit fix` resolved all six in-place; `npx quartz build` still passes.
No quartz.config.ts change needed — these are deep transitives.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 10:10:02 +09:00
sotashimozono
6d147e810a
docs(readme): add Docs site badges + retarget CI badges to next (1.0.40)
- New row of two Docs badges:
  * Docs (stable)      → http://codes.sota-shimozono.com/obsidian-remote-ssh/
  * Docs (dev preview) → http://codes.sota-shimozono.com/obsidian-remote-ssh/dev/
- New Docs workflow status badge (next branch)
- Retarget CI / Integration / Security / codecov badges from main → next.
  Day-to-day work merges to next; main only sees promotion PRs, so the
  badges were always showing the last release's status, not current health.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 09:43:25 +09:00
sotashimozono
eaf248faf5
docs(site): Quartz v4 Phase 0 — two-channel docs deploy (1.0.39)
* docs(site): Quartz v4 Phase 0 scaffold — two-channel docs deploy (1.0.39)

Establishes the Quartz v4 static site under docs-site/ with a
two-channel GitHub Pages deployment:

  - push to main  → build with QUARTZ_BASE_URL=.../obsidian-remote-ssh
                    → deploy to root of gh-pages branch (stable)
  - push to next  → build with QUARTZ_BASE_URL=.../obsidian-remote-ssh/dev
                    → deploy to /dev/ subtree of gh-pages branch

Content source is docs/ (also an Obsidian vault via docs/.obsidian/).
Initial content: docs/index.md (language router) + docs/en/index.md
(English landing page).

Quartz customisations over upstream defaults:
  - baseUrl driven by QUARTZ_BASE_URL env var (same source, two URLs)
  - analytics: null (no telemetry)
  - typography: Inter + JetBrains Mono
  - colours: purple accent (#7b5ea7) matching the plugin icon palette
  - footer links updated to this repo's GitHub + Issues

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs(site): use codes.sota-shimozono.com custom domain for Pages URLs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 09:33:17 +09:00
sotashimozono
3059f0bf20
docs: add documentation strategy & requirements spec (1.0.38)
* docs: add documentation strategy & requirements spec

Captures decisions for the docs site project before scaffolding work
begins. One artefact, no code or workflow changes -- review surface
small, easy to revise.

Confirmed:
  - Quartz v4 (chosen for native Obsidian Publish look & feel)
  - docs/ is a real Obsidian vault; .obsidian/ committed
  - English-only at launch; en/ + ja/ folders separated from day one
    so JA can mirror without URL churn
  - GitHub Pages, default URL inheriting the user-pages CNAME
  - GitHub Actions for build + Pages deploy

Covers folder structure (with i18n routing), tech stack, hosting,
CI/quality gates, authoring conventions (frontmatter, wikilinks,
callouts, code, images, voice), auto-generation strategy for
protocol/methods.md and reference/settings.md from src/proto/types.ts
and src/types.ts, phasing roadmap, and open questions deferred until
they bite (versioning, comments, custom domain, mobile graph view).

Lives at docs/en/contributing/documentation.md so it gets published
through the same pipeline it specifies -- eat-own-dogfood.

* docs: add documentation strategy & requirements spec (1.0.38)

Version bump to satisfy version-check gate.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: include package.json in 1.0.38 bump

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 09:18:48 +09:00
sotashimozono
9428fcfe97
chore(ci,docs): introduce next + main two-channel branching model (1.0.37)
* chore(ci,docs): introduce next + main two-channel branching model

Establishes the integration/stable split that the bump-version comment
has been pointing at since pre-1.0:

  - `next`: integration branch where day-to-day PRs land. BRAT users
    follow it via `manifest-beta.json` from the repo's default branch,
    so every merge here is automatically in front of beta testers.
  - `main`: stable channel; release.yml fires only on push to main.
    Releases are cut by opening a fast-forward promotion PR
    `next` -> `main`.

Changes in this PR:

  - .github/workflows/version-check.yml: add `next` as a permitted base
    branch. Strict-version-bump assertion still applies on both bases
    so the release pipeline always has a unique tag to publish.
  - README.md: replace the "pre-1.0 every merge to main = beta"
    callout with the two-channel description.
  - CONTRIBUTING.md: add a "Branching model" section with a
    "where do I open my PR" decision table; tweak the version-bump and
    stack-PR notes that previously assumed a single `main` base.

Deferred to follow-up PRs (kept out of scope here so the model can land
without bundled bump-script churn):

  - bump-version.mjs branch-aware behaviour (advancing manifest-beta.json
    independently of manifest.json on `next`-only experimental work).
  - Branch protection rules on `main` and `next`.
  - GitHub default-branch change (high-severity infra; awaiting explicit
    user confirmation).

* docs(readme): point downloaders at Releases / main from the top of README

* chore(ci,docs): introduce next + main two-channel branching model (1.0.37)

Version bump only — no code change. Required by version-check gate.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: include package.json in 1.0.37 bump

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: align package.json to 1.0.37 (match manifest.json)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ci: run CI on next branch PRs and pushes

CI was only wired to main; now that next is the integration branch
all day-to-day PRs target next so CI must fire there too.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 09:09:18 +09:00
sotashimozono
34ea26255b
fix(tests): repair 6 CI failures + bump 1.0.36 (#278)
* fix(tests): repair 6 CI failures introduced by recent PR merges

ObsidianRegistry.test.ts + ShadowVaultBootstrap.test.ts:
  Wrap `import * as fs from "fs"` in `vi.mock("fs", () => ({ ...actual }))`
  so vitest creates a configurable namespace. The raw ESM `fs` namespace
  is non-configurable in Node + Vitest 4, which made the existing
  `vi.spyOn(fs, "writeFileSync"|"renameSync"|"symlinkSync")` calls throw
  `Cannot redefine property` at test setup time. The mock re-exports
  `actual` so all non-spied fs calls keep their real behaviour.

ShadowVaultBootstrap.test.ts (collectPendingPluginSuggestions):
  Two tests asserted `data.pendingPluginSuggestions` equal `[]` when
  source `community-plugins.json` is invalid/non-array. The implementation
  (ShadowVaultBootstrap.ts:121-126) deliberately omits the key entirely
  when `pending.length === 0` -- the absent key is the "no suggestions"
  signal that ShadowStartupCoordinator relies on for its no-op fast path.
  Aligned the test with the implementation contract.

ConflictResolver.test.ts:58:
  `swapClient` test asserted `writeBinary` was called with a third
  `undefined` arg. Other tests in the file (e.g. line 91) and the
  source (ConflictResolver.ts:88,92,129) confirm the call passes only
  two args. Removed the spurious trailing `undefined`.

* chore: bump version to 1.0.36

* ci: bump Node heap to 6 GiB to stop intermittent test-job OOMs

Vitest 4's default `forks` pool plus jsdom on every test file pushed
peak RSS just past Node's default ~4 GiB old-space ceiling once the
suite grew to ~70 files / ~1000 tests. The CI logs showed `FATAL ERROR:
Reached heap limit Allocation failed - JavaScript heap out of memory`
during ubuntu-latest, ubuntu-node22, macos-latest, and windows-latest
test jobs on the last several main-branch runs (it predates this PR;
the earlier 6 unit-test failures were just the first symptom).

Apply the same env to release.yml's test gate so a release tag push
does not flake on the same wall.

GitHub-hosted runners advertise 7+ GiB, so 6 GiB is safe headroom
without crowding the kernel/page-cache budget.

* fix(tests): SftpClient unit tests caused worker OOM via infinite BFS

Root cause of the CI test-job crashes:

  tests/SftpClient.unit.test.ts > rmdir() > recursive: removes all
  files and subdirs before root

mocked sftp.readdir with a path-agnostic `mockImplementation` that
returned `[file.md, sub (dir)]` for every call. SftpClient.rmdir's
recursive branch calls listRecursive, whose BFS then re-discovered
`sub` inside `/vault/sub`, then `/vault/sub/sub`, then
`/vault/sub/sub/sub`, ... -- growing the visited Set, the queue, and
the result array without bound until the worker hit
`FATAL ERROR: Reached heap limit`. Replaced with a path-aware mock so
only `/vault` has children; the BFS terminates after one level.

Also fixed `listRecursive() returns files from nested subdirectories
via BFS`: the assertion expected `['deep.md', ...]` but
`SftpClient.listRecursive` deliberately returns the full path under
root as relativePath (`sub/deep.md`), not the basename -- every
downstream consumer needs the unambiguous form.

Plus pass `--max-old-space-size=6144` to vitest workers via
`test.execArgv` so future legitimately-large suites have headroom
without needing per-job NODE_OPTIONS overrides. (In Vitest 4 the
top-level `poolOptions` was removed; both `pool` and `execArgv` are
now top-level keys inside `test:`.)
2026-05-09 23:26:01 +09:00
sotashimozono
036db15d0e
chore: bump version to 1.0.35 (#277) 2026-05-09 22:34:13 +09:00
sotashimozono
6afcb0ac2d
chore: bump version to 1.0.34 (#276)
* chore: bump version to 1.0.31

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: bump version to 1.0.34

Batch: PR #265 (ConflictResolver full coverage), PR #261 (AuthResolver + ReconnectManager).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 22:22:42 +09:00
sotashimozono
60a865402a
chore: bump version to 1.0.33 (#275)
* chore: bump version to 1.0.31

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: bump version to 1.0.33

Batch: PRs #260 (Logger/RpcClient/RpcConnection/WindowSpawner), #268 (RpcConnection auth branches), #270 (ShadowVaultBootstrap coverage).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 22:17:42 +09:00
sotashimozono
972c38eb70
chore: bump version to 1.0.32 (#274)
* chore: bump version to 1.0.31

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: bump version to 1.0.32

Batch: merged PR #271 (SftpDataAdapter), #262 (JumpHostTunnel), #263 (deployer/registry), #264 (RpcError/errorMessage), #266 (SftpClient unit tests).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 22:08:33 +09:00
sotashimozono
7adadf0539
chore: bump version to 1.0.31 (#273)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 22:06:06 +09:00
sotashimozono
9631b4b54c
ci: add weekly deep cron back to e2e.yml as a drift sentinel (#256)
right primary gate. But pure per-PR loses one signal the old
nightly cron had: environment drift that doesn't go through a PR.
Examples we already lived through — the Obsidian AppImage URL
breaking (#246) and the AppImage cache going poisoned (#245) —
would have surfaced on a regular cron run, but per-PR alone only
catches them when someone happens to push a PR after the drift.

Add `schedule: '0 3 * * 0'` (Sundays 03:00 UTC / 12:00 JST) on top
of the existing pull_request + workflow_dispatch triggers.
Per-PR stays the primary gate; the weekly run is a low-cost drift
sentinel that flags environment rot within ~7 days of it landing.

Bump to 1.0.30.
2026-05-05 15:04:16 +09:00
sotashimozono
1d26328a24
refactor(e2e): extract driveConnectFlow + findShadowVaultPath, demo uses them (#255)
The connect flow's UI driving (palette → type → Enter → click
Connect on the passphrase modal) and the obsidian.json polling that
finds the shadow vault path were inlined twice — once in
connectAndOpenShadow, once in demo.spec.ts. Demo had its own copy
because its screencast lifecycle has to wrap the UI part: stop
screencast AFTER the Connect click, then poll for the shadow vault.

Extract two new exports from helpers/obsidian.ts:

- driveConnectFlow(page) — palette → type → Enter → Connect button.
  Doesn't wait for the shadow vault; the caller decides when.
- findShadowVaultPath(scaffoldPath, timeoutMs) — polls obsidian.json
  with the same exists-on-disk + max-ts filter that the inline
  version had. Throws on timeout (canonical "connect didn't fire").

connectAndOpenShadow now composes both, so its public behaviour is
identical. demo.spec.ts replaces ~30 lines of inline duplication
with two helper calls.

Bump to 1.0.29.
2026-05-05 14:48:50 +09:00
sotashimozono
d48a24bdd8
test(e2e): smoke test 2 actually asserts the plugin tab is visible (#254)
Previously this test only `test.info().annotations.push(...)` when
the Remote SSH tab was visible — it never failed. A missing tab
slipped through with the rest of the suite green. Replace the
annotation with `expect(pluginTab).toBeVisible()` and assert the
modal closes after Esc, so the test now genuinely fails when the
plugin's settings tab is unreachable.

Bump to 1.0.28.
2026-05-05 14:09:44 +09:00
sotashimozono
615fb5bc55
ci: gate E2E on PR (paths-filter) instead of nightly cron (#252)
* ci: gate E2E on PR (paths-filter) instead of nightly cron

Three days running, the nightly e2e workflow reported success on
the dashboard while every smoke test silently skipped — Obsidian's
window failed to boot, and `continue-on-error: true` swallowed the
red. The dashboard said green; reality was zero coverage.

Fix the structure:

- Replace `on.schedule` (nightly 03:00 UTC) with `on.pull_request`
  scoped via paths-filter to plugin/src, plugin/e2e, server,
  docker/test-{sshd,vault}, and the workflow itself. Docs/license/
  README PRs now skip the heavy job; PRs that change the SUT pay
  the ~3-5 min for an actual signal.

- Drop `continue-on-error: true` from the Run E2E step. Real
  failures need to be visible status checks, not artifacts no
  one watches.

`workflow_dispatch` stays for manual re-runs. After a few PR
cycles establish the new gate works, smoke.spec.ts test 5 +
reflect.spec.ts's connected-guard heuristic still need follow-up
fixes — those land in subsequent PRs.

Bump to 1.0.24.

* fix(e2e): focus editor before typing + pick freshest shadow vault entry

The rebased per-PR e2e on #252 surfaced two more bugs that the
nightly's continue-on-error had been hiding.

1. demo.spec.ts: Ctrl+N created `Untitled.md` on the remote vault
   but Obsidian didn't auto-open it in an editor pane (the
   workspace stayed on the empty-state "No file is open" panel).
   Subsequent `keyboard.type` calls went nowhere, autosave
   committed an empty file, and the SFTP read assertion saw `""`.
   Fix: after Ctrl+N, wait for `.cm-editor .cm-content` and click
   into it; if it never appears, click the new file in the file
   explorer first to force-open. Also bump the autosave wait from
   2.5 s to 4 s for CI's slower debounce.

2. helpers/obsidian.ts `connectAndOpenShadow`: obsidian.json is
   the user-global config and accumulates entries across spec
   files. After demo.spec.ts ran, the vaults map held BOTH the
   shadow vault AND a stale entry for the deleted demo scaffold.
   Picking "first non-scaffold entry" returned the stale path
   roughly half the time, so reflect.spec.ts and sync.spec.ts'
   beforeAll either crashed in launchObsidian or skipped en bloc.
   Fix: filter to entries whose path actually exists on disk and
   pick the highest-`ts` (most recently registered).

Bump to 1.0.27.
2026-05-05 13:47:19 +09:00
sotashimozono
fc73ffbef4
fix(e2e): shared connectAndOpenShadow helper, drop false-positive guards (#253)
The per-PR e2e gate from #252 surfaced what the nightly was hiding:
the inline connect helpers in reflect.spec.ts, sync.spec.ts, and
smoke.spec.ts test 4 were never adapted to the per-profile
passphrase modal Obsidian now opens. They clicked the suggestion
in the palette, then waited 10 s for "something to happen" — but
the modal stayed up uncommitted, the connect never fired, and
later assertions failed with `Target page has been closed`.

On top of that, every spec used the same broken `connected =
items > 0` heuristic to decide whether to skip. The scaffold
seeds 5 `local_demo*.md` files into the local vault, so that
counter was always > 0, so the guard was a false positive.

Replace both:

- helpers/obsidian.ts: new exported `connectAndOpenShadow(handle,
  scaffoldVaultPath)`. Drives palette → Enter → passphrase-modal
  Connect button → polls `~/.config/obsidian/obsidian.json` until
  the new vault entry appears (15 s budget) → cleans up the
  original handle → returns a fresh handle attached to the shadow
  vault. Throws on no-shadow-vault — the canonical "connect did
  not actually run" signal.

- reflect.spec.ts / sync.spec.ts: replace inline connect + the
  `items > 0` guard with `connectAndOpenShadow`. `obsidian` is
  now reassigned to the SHADOW vault handle, where remote files
  are actually visible. The local `connectToRemote` helper in
  reflect.spec.ts is removed (was the broken inline copy).

- smoke.spec.ts test 4: same swap, plus tighten the assertion
  from "any .notice is visible" (which fired on connection
  FAILURE notices too) to `statusBar.toContainText('Remote SSH:
  Connected')` — now actually verifies the live link.

- smoke.spec.ts test 5: anchor on the seeded `remote_demo*.md`
  filenames rather than `items > 0`. The previous count-only
  assertion would have passed on a fully-disconnected scaffold.

Bump to 1.0.25.
2026-05-04 21:55:08 +09:00
sotashimozono
efd1925de8
feat(e2e): demo creates a new note + SFTP-verifies it landed on remote (#251)
Extends the demo walkthrough so the GIF doesn't just show "you can
see remote files" but also "you can WRITE to them".

After Phase 2 captures the shadow vault state with remote_demo*.md,
the spec now:

  1. Ctrl+N — opens a new editor pane in the (now-remote) vault
  2. Types `# Demonstration\n\n` + a body sentence at human-ish
     keystroke speed so the screencast records a natural-looking
     typing flow
  3. Idles 2.5 s for Obsidian's autosave + the daemon's SFTP
     write round-trip
  4. (NON-VISUAL) connects via RemoteVerifier directly to the
     docker test sshd, lists /home/tester/vault, finds the new
     note (the one .md that isn't pre-seeded `remote_demo*.md`),
     reads it, and asserts the typed content actually landed on
     the SSH host

Visual coverage stops at the typing — the GIF is for telling the
story. The CLI/SFTP read is the real assertion: if the daemon
dropped the write, the GIF would still look right but this step
fails loud.

Bump to 1.0.24.
2026-05-04 21:33:08 +09:00
sotashimozono
eff54d6bfb
fix(e2e): pre-config trusted vault + 6-frame demo with local→remote swap (#250)
Builds on the earlier scaffold/trust-dialog/PNG-slideshow work in
this branch. Adds the explicit local_demo* / remote_demo* contrast
the demo needed:

- docker/test-vault: replaces ad-hoc fixtures (welcome_remote,
  notes_remote, README) with remote_demo1.md ... remote_demo5.md.
  Same number of files as the local seed so the GIF reads as a
  clean filename swap.

- plugin/e2e/helpers/vault-scaffold.ts: seedDemoNotes now writes
  local_demo1.md ... local_demo5.md (replacing welcome_local /
  setup_local). Five files, uniform shape.

- plugin/e2e/demo.spec.ts: extends the walkthrough to 6 frames:
  vault loaded → settings → palette → profile picker → connected
  (original window) → shadow vault file explorer (the new
  Electron BrowserWindow Obsidian opens for the connected
  profile, captured by enumerating browser.contexts() before
  vs after the connect press). The shadow_vault frame is the
  payoff: file explorer flips from local_demo*.md to
  remote_demo*.md, proving the live local→remote swap.

- .gitignore: replaces the *_remote.md / README.md allowlist
  with the new remote_demo*.md pattern.

Trust-dialog dismissal, restricted-mode pre-config, x11grab→
PNG-slideshow GIF pipeline, and SIGINT recorder shutdown
are all from earlier commits in this branch (now squashed
into this single commit).

Bump to 1.0.17.
2026-05-04 21:20:15 +09:00
sotashimozono
a3fbde061a
fix(e2e): load plugin via app.plugins API + verify on instance/commands (#249)
#248 was the right intent but two bugs in the implementation:

1. UI-driving the "Turn on community plugins" button missed the
   newer Obsidian DOM (it's a checkbox toggle now, not a button).
   The helper silently no-op'd and left the vault in restricted
   mode.

2. `enabledPlugins.has(id)` returned true the moment the id was in
   community-plugins.json — even when the plugin's instance never
   loaded — so waitForPluginEnabled passed in 9s on a vault where
   the plugin was actually still inert. The latest GIF on the media
   branch shows it: command palette filtered to "Remote SSH"
   returns "no matches".

Both replaced:

- ensureCommunityPluginsEnabled -> ensurePluginLoaded — calls
  `app.plugins.setEnable(true)` then `enablePluginAndSave(id)`
  via page.evaluate. Idempotent across reruns; doesn't depend on
  any DOM that drifts between Obsidian versions.

- waitForPluginEnabled -> waitForPluginLoaded — checks
  `app.plugins.plugins[id]` (the actual instance) AND that at
  least one `${id}:*` entry exists in `app.commands.commands`. A
  command-prefix match doesn't pin to a specific id so a future
  rename won't silently skip the guard.

Bump to 1.0.8.
2026-05-04 18:04:01 +09:00
sotashimozono
c550027c52
fix(e2e): actually load remote-ssh plugin in test vault (#248)
The scaffold writes `community-plugins.json` with the plugin id, but
that ALONE doesn't enable community plugins in Obsidian — every new
vault opens in Restricted Mode where the listed plugins are inert
until the user clicks "Turn on community plugins" plus the trust
follow-up. The result, until now: every spec downstream of
`launchObsidian` was running against an empty Obsidian — no commands
registered, palette empty, the `Remote SSH not found` you can see
in the latest demo GIF.

Two new helpers in plugin/e2e/helpers/obsidian.ts, both called from
launchObsidian after the workspace becomes visible:

- ensureCommunityPluginsEnabled() — Settings -> Community plugins
  -> click "Turn on community plugins" -> accept trust prompt.
  Safe to call on already-trusted vaults (no-op when the button is
  absent).

- waitForPluginEnabled(page, 'remote-ssh', 30s) — page.waitForFunction
  on `app.plugins.enabledPlugins.has(id)`. This is the canonical
  "did the plugin actually start" check; throws with a clear message
  if the plugin never loaded.

Bump to 1.0.7. Tighter spec assertions and removal of e2e.yml's
continue-on-error follow in subsequent commits — easier to bisect
the foundational change first.
2026-05-04 17:05:48 +09:00
sotashimozono
beb202bc54
fix(demo): drop crashing connect step + harden post-test pipeline (#247)
The demo walkthrough's "Trigger connect" step (Cmd+P → "Remote SSH:
Connect" → click) crashed the page mid-recording on CI because the
scaffold vault has no configured profile, and the click took down
the screen recorder with it. The result was a green AppImage
boot but a missing GIF on every dispatch.

Two changes, both narrow:

1. demo.spec.ts: drop steps 4-5 (connect attempt + post-connect
   screenshot). The walkthrough now stops at the command palette
   filtered to "Remote SSH" — perfectly fine for a marketing GIF,
   and the pages survives. Live connect/edit flow remains covered
   by sync.spec.ts and reflect.spec.ts.

2. demo-capture.yml: `continue-on-error: true` on the walkthrough
   step plus `if: always()` on stop-recording / GIF-conversion /
   media-push steps so a partial walkthrough still produces a
   usable GIF instead of nothing at all.

Bump to 1.0.6.
2026-05-04 16:49:32 +09:00
sotashimozono
a88234f5da
ci: switch AppImage download to obsidianmd/obsidian-releases (#246)
The nicehash/Obsidian mirror is dead (both v1.8.9 and v1.7.7 URLs
return 404). With `-f` now in place from #245 the workflow fails
loudly instead of cache-poisoning, but the download itself still
fails on every run.

Switch both demo-capture.yml and e2e.yml to the official
obsidianmd/obsidian-releases mirror, which carries the same
AppImage filenames (Obsidian-1.8.9.AppImage, Obsidian-1.7.7.AppImage)
and is the canonical distribution channel for the desktop app.

Verified via `gh release view v1.8.9 --repo obsidianmd/obsidian-releases`
that both target assets exist there before swapping the URLs.

Bump to 1.0.5.
2026-05-04 16:36:53 +09:00
sotashimozono
7d9c74c13c
ci: bust poisoned AppImage cache + harden curl/extract steps (#245)
The cache entry under `obsidian-appimage-1.8.9` was poisoned: an
earlier run hit a 404 from the nicehash mirror, and the curl call
without `-f` saved the "Not Found" HTML body as Obsidian.AppImage.
That bogus file then survived in the actions/cache layer and broke
every subsequent run that hit the cache (`./Obsidian.AppImage:
line 1: Not: command not found` -> ENOENT on the binary).

Three changes, applied to both demo-capture.yml and e2e.yml since
they share the cache key:

1. Bump cache key to `obsidian-appimage-1.8.9-v2` so the bad entry
   is abandoned and a fresh download happens on next run.
2. Add `-f` to both curl invocations so future 4xx/5xx fail loud
   instead of poisoning the cache again.
3. New verification step that asserts `file Obsidian.AppImage`
   reports an ELF binary, and remove `|| true` from the extract
   step. If the AppImage is wrong, the workflow now fails on the
   verify step with a clear error instead of in `--appimage-extract`.

Bump to 1.0.4.
2026-05-04 16:32:23 +09:00
sotashimozono
8ec67a8555
fix(e2e): expand leading ~ in OBSIDIAN_PATH before spawn (#244)
`child_process.spawn` does not run a shell, so a literal `~/foo` from
the OBSIDIAN_PATH env var (the form the CI workflows use, including
e2e.yml and demo-capture.yml) ENOENTs at exec time. Replace the
leading `~/` with `os.homedir()` in `resolveObsidianPath` so both CI
and any user who sets OBSIDIAN_PATH with `~` get a working path.

Same masking pattern as #243: e2e.yml's `continue-on-error: true`
hid this; demo-capture.yml surfaced it cleanly.

Bump to 1.0.3.
2026-05-04 16:19:54 +09:00
sotashimozono
55ae4d44be
fix(e2e): mkdir Obsidian config parent before writing obsidian.json (#243)
`registerVault` writes `~/.config/obsidian/obsidian.json` (or %APPDATA%
on Windows) so Obsidian opens the scaffold vault on launch. On fresh
CI runners — where Obsidian has never started — that directory does
not exist yet and `fs.writeFileSync` fails with ENOENT.

This was masked in `e2e.yml` by `continue-on-error: true` on the smoke
step but surfaced cleanly in `demo-capture.yml`, which has no such
escape hatch. Add `fs.mkdirSync(parent, { recursive: true })` on the
no-existing-config branch.

Bump to 1.0.2 to satisfy the version-check gate.
2026-05-04 16:12:22 +09:00
sotashimozono
87c4ec5968
ci: add demo-capture workflow + embed GIF in README (#242)
Records the existing demo.spec.ts walkthrough on a Xvfb display via
ffmpeg x11grab, converts to a palette-optimised GIF, and force-pushes
to an orphan `media` branch. The README links to the raw URL so the
demo stays current without binary churn on main. Triggered manually
via workflow_dispatch only — too expensive to run on every push.

Bump to 1.0.1 to satisfy the version-check gate.
2026-05-04 15:45:19 +09:00
sotashimozono
8c4b893b82
chore!: cut v1.0.0 — leave the 0.x preview, set minAppVersion 1.5.0 (#237)
First stable release. Drops the 0.x preview status. Coordinated with
the Phase H pre-store-submission polish (#126).

## What this means for users

- **BRAT users** (both stable and --beta channels) receive 1.0.0 on
  the next poll — `manifest.json` and `manifest-beta.json` are still
  byte-identical pre-`next` branch, per the parity invariant set up
  in #231.
- **Manual installs** from this release land 1.0.0 / minAppVersion 1.5.0.
- **Obsidian < 1.5.0** users will see this release as not-installable.
  The previous 0.4.131 stays available on GitHub Releases for any
  hold-out on the 1.4.x line.
- **Compatibility**: the per-release `versions.json` entry maps
  `1.0.0 → 1.5.0`, so Obsidian's plugin loader correctly hides this
  release from app-version < 1.5.

## Why minAppVersion 1.5.0 (not 1.6+ or holding at 1.4.0)

- `1.4.0` was the floor through Phase A-G development. It's >18 months
  old at the 1.0 cut and forces compat-tail maintenance for APIs the
  plugin doesn't actually use.
- `1.5.0` is conservative: it bumps the floor by one minor and gates
  out the very oldest installations without cutting off the bulk of
  active users (most users update within a few minor releases).
- `1.6+` was considered but rejected for v1.0; `revealLeaf` (the
  feature gating most aggressive bumps) is intentionally NOT used
  per the #234 review (we use `setActiveLeaf` instead).
- The floor can be bumped further in a 1.x point release if a future
  feature genuinely requires a newer Obsidian API.

## What's deliberately not in this PR

- **F30 community-store registry submission** — separate workflow PR.
  Cutting 1.0.0 here makes the manifest ready for submission; the
  actual submission to `obsidianmd/obsidian-releases` is the next
  Phase H step.
- **Screenshots / GIF in README** — needs human capture against a
  real dev vault. Tracked in #126's Phase H "README polish" item.
- **CI gate switches** (`PERF_GATE`, coverage tightening, security
  workflow flip) — separate sequencing in #126.

## Files

- `plugin/manifest.json` + root `manifest.json` + `manifest-beta.json`:
  version 0.4.131 → 1.0.0, minAppVersion 1.4.0 → 1.5.0
- `plugin/package.json` + `plugin/package-lock.json`: version cut
- `plugin/versions.json` + root `versions.json`: append `"1.0.0": "1.5.0"`
- `README.md`: Obsidian shield badge `1.4+` → `1.5+`

## Verified
- `tsc --noEmit` clean
- `npm run lint` clean
- `vitest run` 780 / 1 Windows-skipped
- All four manifest sources + both versions.json mirrors agree on
  1.0.0 / minAppVersion 1.5.0

## Risk acknowledgement

Phase G's BRAT 2-week bug-bash (#126 G) hasn't formally completed —
this 1.0.0 cut is "B-now" per the explicit decision after the choice
between (B-now / B-defer / B-prerelease). If a P0 surfaces during
external-tester use of 1.0.0, a 1.0.1 quick-fix release follows the
same release.yml pipeline.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 15:04:43 +09:00
sotashimozono
d795872f76
docs: add 'Why this plugin (vs the alternatives)' section (Phase H) (#236)
First Phase H deliverable: README polish for community-store-quality
positioning. Adds a differentiator section between 'What you can do'
and 'Install' that names the alternatives by name and explains the
niche obsidian-remote-ssh fills.

Honest framing: each alternative is described in one paragraph
("use it when..."), no badmouthing. The plugin's niche is described
in four bullets — large vaults, source-of-truth-on-remote, no
third-party storage, remote-side tooling.

No images yet. The screenshot/GIF half of the Phase H README work
needs human capture against a real dev vault and will land in a
follow-up PR. This change is content-only and shipped-quality on
its own.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 08:53:26 +09:00
sotashimozono
91da0f1e12
test: complete Phase C E2E case matrix (#109) (#235)
Closes #109. Brings the Phase C E2E suite from 8/9 cases to 9/9 by
adding the disconnect/queue/reconnect case and deepening two
existing cases per the issue spec.

## What changed in plugin/tests/integration/sync.e2e.test.ts

- "large file (1 MB)" → "large file (10 MB)". The issue specs 10 MB
  to push the wire-transfer envelope and verify the daemon doesn't
  time out on long writes. Budget multiplier bumped 5x → 20x for
  proportional headroom on slow CI links (~50 MB/s effective sftp
  throughput → ~200 ms transfer + reader's fs.changed → stat round-trip).

- 3-way conflict deepened. The pre-existing test only asserted the
  daemon rejects with PreconditionFailed (-32020). The new variant
  wires a real ConflictResolver + AncestorTracker on a third "Bob"
  adapter and verifies the full pipeline:
    Alice creates "version-1"  (becomes Bob's ancestor)
    Bob reads                  → ancestor remembered
    Alice overwrites with "alice-edit"  (becomes "theirs" on Bob)
    Bob writes "bob-edit"      → PreconditionFailed
      → ConflictResolver.resolve → onTextConflict({ancestor,mine,theirs})
      → stub returns keep-mine
    Remote ends up holding "bob-edit"
  This is the production wiring used by ThreeWayMergeModal — the old
  test only proved the daemon rejects, not that the resolver chain
  runs end-to-end.

- New: "disconnect → queue → reconnect" case. Pre-populates an
  OfflineQueue with mkdir + 3 writes and runs QueueReplayer against
  the connected writerAdapter. Asserts:
    - report.errors === []
    - report.drained === ops.length
    - queue is empty after drain
    - all 4 paths land on the reader's FakeFileExplorer
  Validates F3 (offline queue) + F6 (replay) + F19 (in-order
  delivery) from the plan's functional matrix. The "forcibly close
  the SSH socket" half of the spec is AdapterManager orchestration
  already covered by unit tests; this integration assertion is the
  one the unit suite can't make (queued ops surfacing as fs.changed
  events on the reader's vault).

## Imports added
- node:os + node:path (for the queue's tmp directory)
- ConflictResolver, ThreeWayPanes, AncestorTracker
- OfflineQueue, QueuedOp, QueueReplayer

## Verified locally
- tsc --noEmit clean
- npm run lint clean
- vitest run (unit suite) 780 pass / 1 Windows-skipped (integration
  tests don't run under `vitest run`; they're routed through
  vitest.integration.config.ts and require Docker test sshd)

## CI
The new integration tests will run under the `SSH integration` job
in .github/workflows/integration.yml, which spins up the Docker
test sshd. Local Windows can't exercise them; CI is the verification
path.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 08:45:21 +09:00
sotashimozono
e44c476c68
feat: integrated remote terminal pane (xterm.js + ssh2 shell channel) (#149) (#234)
Closes #149. Adds a single-pane remote terminal in the right sidebar,
backed by xterm.js and an ssh2 PTY channel multiplexed onto the
already-authenticated SSH connection that SftpClient owns.

## Files

- `src/ssh/SftpClient.ts` — `openShell({rows, cols, term?, cmd?})`
  method (mirrors existing `openUnixStream` / `exec` shape). When `cmd`
  is supplied, runs it under the PTY (e.g. `/usr/bin/zsh -l`); otherwise
  the remote launches the user's default login shell.
- `src/ssh/RemoteShell.ts` (NEW) — thin lifecycle wrapper around the
  ssh2 ClientChannel. `open() / write() / resize() / close() / isOpen()`.
  Stdout + stderr both surface as a single `onData(chunk)`. Two
  synchronous-flag guards keep `open()` race-free:
  `opening` blocks concurrent open() calls before they reach the
  await; the post-await `if (this.closed) ch.end()` check prevents a
  channel leak when close() runs while open() is pending.
- `src/ui/RemoteTerminalView.ts` (NEW) — `extends ItemView`, renders
  xterm.js + FitAddon, debounced 100 ms ResizeObserver → setWindow,
  graceful "Not connected" / "Failed to open shell" disconnected
  states. Reads font/scrollback/shell from plugin settings each `onOpen`.
  The `onClose` callback logs `reason`/`cause` unconditionally so the
  channel-close audit trail isn't lost when the View has torn down.
  The `shell.open()` catch block disposes + nulls shell/term/fit so
  late channel events can't reach a half-constructed view.
- `src/main.ts` — `registerView(VIEW_TYPE_REMOTE_TERMINAL, ...)` plus
  `addCommand("Open remote terminal")` (checkCallback gates on
  client.isAlive). `disconnect()` calls
  `app.workspace.detachLeavesOfType(VIEW_TYPE_REMOTE_TERMINAL)` so the
  shell channel close fires while ssh2.Client is still around.
  `openRemoteTerminal()` uses `setActiveLeaf` rather than `revealLeaf`
  to keep the minAppVersion 1.4.0 contract (revealLeaf needs 1.7.2).
  `openingTerminal` re-entrant flag prevents two leaves being created
  by rapid command-palette activations.
- `src/types.ts` + `src/constants.ts` — three new optional settings:
  `terminalShell?: string`, `terminalFontSize?: number` (default 12),
  `terminalScrollback?: number` (default 1000).
- `src/settings/SettingsTab.ts` — Terminal section with the three
  inputs (font validated 6-32 px, scrollback 100-100_000 lines).
- `styles.css` — terminal pane host/disconnected styling + inlined
  xterm.js v5 default stylesheet (~3 KB, MIT license preserved) so
  users don't need a separate CSS asset.
- `package.json` — `@xterm/xterm@^5.5` + `@xterm/addon-fit@^0.10`
  added as production dependencies.
- `.github/workflows/release.yml` + `.github/workflows/ci.yml` —
  bundle cap raised 600 -> 800 KB to absorb the bundled xterm
  (770 KB actual). esbuild splitting needs ESM which Obsidian doesn't
  support, so single-bundle is the only option. Comment in workflows
  explains the rationale.
- `tests/RemoteShell.test.ts` (NEW) — 20 unit tests against a fake
  ssh2 ClientChannel: open/double-open guards, concurrent-open guard,
  close-while-pending end()s the channel + bypasses listener-attach,
  stdout+stderr routing, write/resize/close, lifecycle invariants
  (no double-onClose, suppressed-after-close events, etc.).
- `vitest.config.ts` — `RemoteTerminalView.ts` added to coverage
  exclude (xterm.js + ResizeObserver under jsdom is impractical to
  unit-test; covered by manual smoke against a real Obsidian window,
  consistent with the other src/ui/*Modal/Bar exclusions).

## Verified

- `tsc --noEmit` clean
- `npm run lint` clean (sentence-case + active-window-timers
  + no-floating-promises + minAppVersion gates all satisfied)
- `vitest run` 780 pass / 1 Windows-skipped (was 760)
- `node esbuild.config.mjs production` -> 770 KB main.js (under 800 KB cap)

## Out of scope (per #149 v1)

- Multiple terminal tabs
- Persisted scrollback across re-opens
- Split panes
- Search-in-scrollback
- back-pressure on huge pastes (ssh2 handles it; we trust write() return)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 08:17:11 +09:00
sotashimozono
319422e0ed
docs: correct obsidian-git compat status (verified by source clone) (#233)
* docs: correct obsidian-git compat status (verified by source clone)

Cloned Vinzent03/obsidian-git@v2.38.2 to verify the previous
"un-fixable at this layer" claim. The picture is more nuanced:

- `SimpleGit` (used on desktop, hardcoded by `Platform.isDesktopApp`)
  shells out to local `git` against `getBasePath()` — confirmed broken,
  silently mis-routes commits to the shadow vault.
- `IsomorphicGit` (used on mobile) wraps `vault.adapter` via
  `gitManager/myAdapter.ts`, with no shell-out. In our shadow vault
  `vault.adapter` is the remote-routing adapter, so this path would
  work transparently against the remote. push/pull go through
  Obsidian's `requestUrl` from the local process — same as
  obsidian-git on mobile already does.
- The selector is hardcoded with no settings toggle:
  ```
  // main.ts:539
  get useSimpleGit(): boolean {
      return Platform.isDesktopApp;
  }
  ```

Updates the four mentions in this doc to:
1. Replace "un-fixable at this layer" with "fixable upstream / we
   won't ship a workaround"
2. Drop the now-stale `tracked in #150` cross-ref (#150 is closed
   as won't-do)
3. Point users wanting git on a remote vault to the integrated
   terminal pane (#149) and to filing an upstream feature request
   for a `forceIsomorphicGit` toggle

No code change; documentation accuracy only.

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

* chore: bump version to 0.4.128

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 07:15:51 +09:00
sotashimozono
00f334942c
test: M11 Playwright + Electron remote-→-Obsidian reflect scenarios (#125) (#232)
* test: M11 Playwright + Electron remote-→-Obsidian reflect scenarios (#125)

Closes the #125 gap between mock-based unit/integration tests and a
real Obsidian Electron window. Adds 5 reflection scenarios that drive
the daemon's `fs.watch` → RPC push → ChangeListener → Vault adapter
→ File Explorer chain — the path no existing spec exercises.

What was already there:
- `plugin/e2e/{playwright.config.ts, smoke.spec.ts, sync.spec.ts,
  demo.spec.ts, helpers/}` — full E2E scaffolding (CDP launcher,
  vault scaffolder, RemoteVerifier ground-truth check)
- `.github/workflows/e2e.yml` — nightly cron + workflow_dispatch
- All of it broken because `@playwright/test` wasn't a devDep and
  `npm run test:e2e` wasn't a script

This PR:
- Add `@playwright/test@^1.59` devDep
- Add `test:e2e` + `test:e2e:reflect` npm scripts
- Extend RemoteVerifier with `stat()` (mtime+size for the modify
  scenario), `rename()`, `writeBinaryFile()` (1 MB PNG fixture)
- New `e2e/reflect.spec.ts` (5 scenarios per #125):
    1. create  — remote write → File Explorer shows the file
    2. modify  — remote overwrite → daemon-side mtime advances
    3. delete  — remote unlink → File Explorer drops the path
    4. rename  — remote rename → old path gone, new path present
    5. large-file image render — 1 MB PNG via markdown embed →
       <img>.naturalWidth > 0 (catches ResourceBridge regressions)
- CONTRIBUTING.md — Dev setup section gains "E2E against a real
  Obsidian window" snippet + nightly trigger note

Local validation: tsc clean, lint clean, vitest 763 pass / 1 skipped,
`npx playwright test --list` discovers 14 tests across 4 spec files.
Local run requires Linux/macOS + Obsidian + Docker test sshd; nightly
e2e.yml workflow handles the actual CI run.

Out of scope for this PR (file separately if needed):
- Workflow Obsidian AppImage URL audit (currently points at a
  third-party mirror — `nicehash/Obsidian` — worth verifying)
- Upstream license clearance for using Obsidian binary in CI

Closes #125 (M11).

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

* test: address PR #232 self-review (M1+M2+L1)

- M1: afterAll cleanup now also removes ${STAMP}-rename-src.md (and
  ${STAMP}-delete.md, also missed). Both lingered when their test
  crashed mid-flight.
- M2: scenario 2 (modify) opens the note in the editor before the
  remote overwrite and asserts the new content reaches .cm-content
  with a unique sentinel string. Closes the gap where the test only
  proved the SFTP write landed, not that fs.watch → RPC push →
  Vault adapter → editor refresh delivered the change.
- L1: rename the shadowed `r` resolver inside waitForMtimeChange's
  setTimeout lambda to `resolve` so the outer RemoteVerifier `r`
  parameter isn't shadowed (no-shadow lint clean).

L2/L3/L4 from the self-review intentionally skipped — pre-existing
patterns the PR description already calls out as out-of-scope.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 06:45:41 +09:00
sotashimozono
9d231235c0
feat: BRAT manifest-beta channel + release asset (Phase G) (#231)
Issue #126 Phase G — enable BRAT --beta installs from this repo so
external testers can subscribe to the auto-update channel before the
1.0 community-store submission.

Changes:
- manifest-beta.json (NEW, root) — what BRAT --beta polls. Pre-1.0
  it tracks manifest.json byte-for-byte (every merge IS a beta);
  diverges from stable when a `next` branch is cut for post-1.0 work.
- plugin/scripts/bump-version.mjs — npm version flow now writes
  manifest-beta.json alongside the existing four files. No new public
  API; doc-comment updated to spell out the BRAT contract.
- plugin/package.json — `version` script `git add`s the new file.
- .github/workflows/version-check.yml — new `manifest-beta == manifest`
  assertion guards against a hand-edit drifting the two channels apart.
  Drop this assert when `next` branch is introduced.
- .github/workflows/release.yml — manifest-beta.json added to the
  GitHub Release `files:` block so BRAT can fetch it as a release asset.
- README.md — Install section split into Option A (BRAT, recommended)
  and Option B (manual). The previous one-line BRAT mention was easy
  to miss; promoting it as the primary path matches the bug-bash flow
  for the rest of Phase G.
- Version bump 0.4.125 → 0.4.126 (touches manifest.json,
  manifest-beta.json, plugin/manifest.json, plugin/versions.json,
  versions.json, plugin/package.json, plugin/package-lock.json).

Verified locally: bump-version.mjs syncs all four manifests + the new
beta to the same version; `diff manifest.json manifest-beta.json`
returns clean (CI assert will pass).

Closes the `manifest-beta.json` checkbox in Phase G of #126.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 06:19:41 +09:00
Souta
6c5d9b9e56 chore: bump version to 0.4.125
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 19:08:29 +09:00