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.
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.
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>
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>
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>
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>
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>
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>
- 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>
* 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>
* 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>
* 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>
* 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:`.)
* 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>
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.
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.
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.
* 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.
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.
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.
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.
#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.
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.
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.
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.
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.
`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.
`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.
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.
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>
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>
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>
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>
* 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>
* 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>
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>