Commit graph

14 commits

Author SHA1 Message Date
Souta
79aace49c3 fix(daemon): sha-verify on the fast path + keep the dev-daemon marker in E2E
Addresses /review-pr findings on #447 and the E2E smoke failure they caused.

- F1 (code-reviewer + silent-failure): the connect fast-path trusted
  existsSync alone, re-opening the "deploy an unverified binary" gap one layer
  above DaemonDownloader's fixed sha-check. Now records `daemonBinarySha` and
  re-hashes the cached file on the fast path; a post-write-corrupted binary
  fails the check and is re-fetched (network-free). Version + sha must both
  match to reuse.
- F2 (silent-failure): a saveSettings() failure after a successful download
  was caught by the generic "download failed -> SFTP" handler and misreported.
  Marker persistence is now its own non-fatal try — the verified binary is
  still returned; a persist failure just re-verifies next connect.
- F3 (comment-analyzer): corrected the dev-install marker comment (a stale
  binary is refreshed only when its sha differs, not "always on a bump").
- F4/F5 (pr-test): new ensureDaemonBinary.test.ts covers the locateDaemonBinary
  .dev-daemon gate and the fast-path reuse/skip (sha match, sha mismatch,
  version mismatch).

E2E fix: the new locateDaemonBinary `.dev-daemon` gate broke the Obsidian smoke
run — E2E stages the daemon via `build:server` (no dev-install), so the staged
binary was unmarked -> gated out -> no RPC -> the live-reflect specs failed.
build-server.mjs now writes the `.dev-daemon` marker; the E2E vault-scaffold
copies server-bin/ verbatim so it rides along. (Release daemons are built via
the Makefile, never marked — correct: users' downloads stay subject to the sha
refresh.)

tsc/lint/vitest green (1193). Still beta.14 (same PR #447).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 19:07:38 +09:00
Souta
8e258b4ce6 feat(daemon): version-aware provisioning — refresh the daemon on plugin upgrade
Dogfooding beta.13: the static-daemon fix shipped, but users still ran the
old dynamically-linked binary — the cache was keyed by ARCH only
(cacheHit = existsSync), so a plugin upgrade never re-downloaded the daemon.
And locateDaemonBinary returned any staged binary at the dev filename, so a
prior *download* masqueraded as a dev build and skipped the refresh entirely.

Now (single cached file, no per-version pileup):
- DaemonDownloader validates the cache by SHA against the release manifest,
  not mere existence: reuse iff the cached bytes still match the expected sha,
  else re-download (atomic overwrite = old dropped). A daemon that didn't
  change across a bump is NOT re-downloaded; a changed/stale one is.
- ensureDaemonBinary fast-paths on `settings.daemonBinaryVersion === version`
  (cached file present) -> no GitHub round-trip, works offline; on mismatch it
  runs the sha re-check and records the new version after provisioning.
- locateDaemonBinary now requires a `.dev-daemon` marker (written by
  dev-install) to treat a staged binary as a genuine dev build; an unmarked
  download is re-validated instead of trusted. This is what makes an existing
  store/BRAT user's stale binary auto-replace seamlessly (no manual delete).

Implements the deferred #406 "re-verify cached binary sha before reuse".
tsc/lint/vitest green (1188). Ships as beta.14.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 18:48:49 +09:00
Souta
4757bb18e4 feat(release): formalize next=beta / main=stable two-channel model
Version shape is the single source of truth for the channel:
- X.Y.Z-beta.N → next (BRAT --beta consumers)
- X.Y.Z       → main (Obsidian Community Plugins store)

bump-version.mjs is now branch-aware: a beta bump only updates
plugin/manifest.json + manifest-beta.json (root manifest.json +
versions.json stay pinned to the last stable). A stable bump
advances every file together.

version-check.yml enforces the branch ↔ channel invariant:
- PR into next: head must be > base (semver-aware), root manifest.json
  + versions.json must NOT have moved
- PR into main: head must be plain X.Y.Z (no beta suffix), all
  manifests must agree
- Sync PRs (head=main → base=next) skip the workflow entirely

release.yml triggers on push to main AND next, picks prerelease vs
stable from the version shape, and gates: stable-on-next pushes
(promotion-staging commits) are skipped so only the eventual main
push publishes the stable release.

sync-main-to-next.yml automates the post-promotion sync: after a
push to main, it opens a PR main → next and enables auto-merge so
the histories rejoin without manual `git merge main` on next.
commitlint also skips on sync PRs (the autogenerated merge titles
don't follow Conventional Commits).

New npm scripts (run from plugin/):
- bump:beta:start  →  X.Y.Z → X.Y.(Z+1)-beta.0  (start of cycle)
- bump:beta        →  -beta.N → -beta.N+1       (within cycle)
- bump:stable      →  -beta.N suffix dropped    (promotion)

CONTRIBUTING.md documents the lifecycle + promotion flow.

Self-test: this PR bumps next from 1.0.43 to 1.0.44-beta.0 using
the new bump:beta:start script — only the beta-channel files moved.
2026-05-10 12:11:35 +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
ff93c72e67 chore: mirror versions.json to repo root and add .nvmrc
Obsidian's community plugin registry validator may look for
versions.json at the repo root alongside manifest.json. The
bump-version script and npm version lifecycle hook now sync
both root mirrors (manifest.json + versions.json) automatically,
and version-check CI asserts they stay in sync.

Also adds .nvmrc (Node 20) to pin the development runtime,
matching the engines field in plugin/package.json.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-30 20:53:15 +09:00
Souta
1df4a05368 ci(replay): replay previous release tag's tests against current source
Catches PRs where source and tests are modified in lockstep so a
previously-asserted invariant ships green even though it's broken.
The replay script copies the most recent published tag's
`plugin/tests/` over the PR's `plugin/src/`, runs vitest, and
fails the (advisory) check on regression.

How it works:
- `git describe --tags --abbrev=0 origin/main` resolves the
  baseline tag (e.g. `0.4.88`).
- `git worktree add` checks the tag out into a temp dir.
- The full `plugin/tests/` tree is copied into the gitignored
  scratch dir `plugin/tests-replay/` (whole tree, including
  `integration/helpers/`, because some unit tests import shared
  helpers from there).
- A dedicated `vitest.replay.config.ts` narrows discovery to
  `tests-replay/**/*.test.ts` and excludes `tests-replay/integration/**`
  so integration tests don't run.
- Failures point at `.github/replay-skip.txt`, where a deliberately-
  retired test can be added with a "# reason / PR ref" comment.

Files:
- `plugin/scripts/replay-prev-tests.mjs` — orchestrator, runnable
  locally via `npm run test:replay` (also wired into `package.json`).
- `plugin/vitest.replay.config.ts` — replay-only test config.
- `.github/replay-skip.txt` — append-only retirement list, empty.
- `.github/workflows/replay.yml` — PR-only trigger, advisory
  (`continue-on-error: true`); flip to required-check once the
  signal-to-noise has been validated over real PRs.
- `.gitignore` — exclude `plugin/tests-replay/` scratch dir.

Verified locally: replay against tag `0.4.88` runs vitest with
626 tests, all pass against current source. (One test-collection
detail surfaced during dev: `AssertSyncReflect.test.ts` and
`PerfAggregator.test.ts` import helpers from `integration/helpers/`,
which is why the copy step now mirrors the entire tests tree
rather than skipping `integration/` at the directory level — the
exclude is enforced by the vitest config's `exclude` glob.)

No plugin-version bump: this is CI-infrastructure only and ships
nothing to end users.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 17:54:22 +09:00
Souta
1c49941ae4 chore(release): mirror manifest.json at repo root for community store
Obsidian's community plugin browser fetches manifest.json from the
repo root (raw.githubusercontent.com/<repo>/HEAD/manifest.json — the
same convention our own PluginMarketplaceInstaller uses). Plugin
source lives under plugin/ for repo-layout reasons (server/, proto/,
deploy/ siblings), so we mirror to root rather than restructure.

- bump-version.mjs writes both plugin/manifest.json and ./manifest.json
- version-check.yml asserts they stay byte-identical (catches drift
  before submission to obsidianmd/obsidian-releases)

Required to submit to the community plugin store.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 12:11:03 +09:00
Souta
6476372550 ci(plugin): Phase C.M10 — perf-bench off the PR critical path; nightly + workflow_dispatch (0.4.48)
Closes the second half of the Phase C MVP, but with a structurally
different cadence than the original M10 design (PR #106 v1).

The bench (`plugin/tests/integration/perf.sync.bench.test.ts`)
takes ~6 minutes per run — 96 % of the integration job's wall
time, all of it in the SSH-tunneled RPC RTT (~41 ms × ~3000 calls).
Running it on every PR was wrong on two counts:

  1. **Feedback latency**: every PR adds 6 minutes to CI for a
     comment-only gate. Souta correctly called out that the bench
     is overkill for the per-PR feedback loop.
  2. **Bench purpose**: the bench is a *trend tracker* and an
     *on-demand perf microscope*. Trends want a stable cadence
     (nightly), not noise from every PR; a microscope wants to be
     pulled out when there's a hypothesis to test, not run on
     every keystroke.

So this PR splits the workflow:

  - **integration.yml** (PR + push to main): runs everything
    EXCEPT the bench. Drops back to ~30 s wall time.
  - **bench.yml** (new, nightly cron + workflow_dispatch +
    push-to-main): runs ONLY the bench. Updates `perf-baseline`
    branch on nightly + main-push. workflow_dispatch with an
    optional `pr_number` input posts the diff comment to the
    named PR — preserves the on-demand "hey what does this
    branch look like perf-wise" capability without paying the
    6-minute tax on every push.

What ships:

- `plugin/scripts/perf/compare.mjs` (new, pure)

  Comparison core. Reads two NDJSON datasets, joins by
  `(span, transport, sizeBytes)`, computes p95 deltas, classifies
  each bucket as regressed / improved / unchanged / new / removed
  against per-span tolerance (default 25 %; S.fs widened to 40 %
  because shared-CI disk variance is high; S.note to 50 % for
  fsnotify debounce noise). Renders a Markdown table for the PR
  comment with arrows (🔴↑/🟢↓/−/🆕/), bolds regressed rows,
  appends a pass/fail summary + a footer with the baseline / head
  SHA pair + run URL. Status-ordered output (regressions first)
  so reviewers see the bad news without scrolling.

- `plugin/scripts/perf/run-compare.mjs` (new, CLI)

  Picks the latest NDJSON in `plugin/perf-results/`, fetches the
  baseline at `<workspace>/perf-baseline/baseline.ndjson`, calls
  `compare()`, posts via `gh pr comment`. **Upserts** the comment
  by sentinel header so a long-running PR with multiple
  workflow_dispatch invocations gets one rolling comment instead
  of a per-trigger spam stack. Exits 0 unless `PERF_GATE=1` AND
  there's a regression past tolerance.

- `plugin/scripts/perf/run-update-baseline.mjs` (new, CLI)

  Bootstraps the orphan `perf-baseline` branch on first run; on
  subsequent triggers, copies the latest NDJSON onto it as
  `baseline.ndjson` + writes a sidecar `baseline.sha`. Skips the
  commit when the baseline bytes haven't changed (no-op pushes
  on idle main). Uses `git worktree` to isolate the baseline
  checkout from the main checkout.

- `.github/workflows/bench.yml` (new)

  Triggers: nightly cron (UTC 19:00 = JST 04:00) + manual
  `workflow_dispatch` + push to main. Manual triggers can pass an
  optional `pr_number` input to comment the diff to a PR (the
  on-demand spot-check path). Permissions:
  `contents: write` (baseline branch push) +
  `pull-requests: write` (PR comment).

- `plugin/package.json`

  - `test:integration` — now excludes the bench file
    (`--exclude **/perf.sync.bench.test.ts`) so PR runs of the
    integration job stay fast.
  - `test:integration:bench` (new) — runs ONLY the bench file
    (used by bench.yml).

- `plugin/tests/PerfCompare.test.ts` (new, 19 tests)

  Pure-module unit coverage for the comparison logic — runs in
  the fast unit suite via vitest's ESM loader (no .mjs build step
  needed; ~280 ms).

  Covered:
    parseNDJSON: empty, well-formed, malformed (line number in
      error), missing required fields.
    compare status semantics: new / removed / unchanged (within
      ±5 % band) / regressed (past tolerance) / improved (clear
      drop). Per-span tolerance override (S.fs 40 %). Drops to
      'new' when baseline p95 is 0 (can't compute %).
    ordering: regressions surface first (status-grouped), then
      improvements, unchanged, new, removed, each lex-sorted.
    isolation: buckets across (span, transport, sizeBytes) tuples
      stay separate.
    formatMarkdown: header / table / pass-summary on no-
      regressions,  + bolded rows on regressions, "no buckets"
      message on empty input, gate-enabled hint surface.
    round-trip: NDJSON in → table out, end-to-end smoke.

- `.github/workflows/integration.yml`

  Unchanged from main — this PR deliberately does NOT add perf-
  related steps to the integration workflow. integration.yml
  stays the home of functional integration tests; bench.yml is
  the home of perf bench + baseline. The split keeps each
  workflow's purpose discoverable from its name.

Architecture summary:

  PR push          → integration.yml (no bench, ~30 s)
  push to main     → integration.yml (no bench)
                   → bench.yml (writes perf-baseline)
  nightly cron     → bench.yml (refreshes perf-baseline)
  workflow_dispatch→ bench.yml (no baseline write; optional PR comment)

Verification:

- `npx vitest run` — 37 files / 501 tests green (was 482, +19 new)
- `npx tsc --noEmit -p tsconfig.json` — clean
- `npm run test:integration` (locally if Docker present) — bench
  excluded via `--exclude` flag; only the functional integration
  tests run.
- workflow YAML: validated against the existing repo's structural
  patterns.

manifest/package/versions bumped 0.4.46 → 0.4.48 (one ahead of
the merged PR #105 / M9 at 0.4.47).

Roadmap context:
- M0 → M9 (Phase C MVP) ✓
- **M10 (this)** wires the gate but off the PR critical path.
- Issue #110 (PERF_GATE flip) needs an update to track "flip on
  schedule + workflow_dispatch in bench.yml" rather than the old
  "flip in integration.yml" wording.
- Issue #109 (E2E expansion) and #108 (modify case revival) are
  unaffected; they unlock more bench cells, not bench cadence.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 08:39:01 +09:00
Souta
c9d465d1d0 fix(test): integration sshd healthcheck (install iproute2 + dump logs on fail)
CI on PR #56 first run came back "sshd entered unhealthy state:
unhealthy". Two issues compounded:

1. The compose healthcheck shells `ss -lnt | grep ':22 '`, but
   `ss` isn't shipped in the minimal `ubuntu:22.04` base — the
   command exited non-zero every interval, so health stayed
   unhealthy past the retry budget. Adding `iproute2` to the
   apt-get install line gives us `ss` (~600 KB add).

2. When the start script gave up, it printed the timeout message
   and exited. We had no visibility into *why* sshd was unhealthy
   because docker logs weren't captured. Added `dumpContainerLogs`
   which calls `docker logs --tail 200` and `docker inspect
   --format '{{json .State.Health}}'` on every failure path so
   the next CI run (or local run) shows the actual sshd output
   and the healthcheck output history without manual digging.
2026-04-27 14:23:59 +09:00
Souta
da3a2e9ffd feat(test): docker sshd integration test environment (Tier B-1+B-3, 0.4.0)
All existing tests mock ssh2, so the actual auth + SFTP channel
code has been entirely untested up to now. Three real-world bugs
in PRs #49 (ssh_config) and #50 (jump host) — neither caught by
mocks — confirmed it was time to wire up real-network tests.

This PR adds a reproducible docker sshd container, a vitest
integration suite that talks to it via the actual SftpClient, and
a CI job that brings the container up, runs the suite, and tears
it down.

## Components

- `docker/test-sshd/Dockerfile` — Ubuntu 22.04 + openssh-server.
  Pre-creates a `tester` user (UID 1000) with `vault/` in their
  home, pubkey-only auth, `StrictModes no` so a bind-mounted
  authorized_keys works regardless of host UID.
- `docker-compose.yml` (repo root) — single `sshd` service on
  127.0.0.1:2222, bind-mounts the public key + a host-side
  `docker/test-vault/` directory, healthcheck on port 22.
- `docker/test-vault/` — bind-mounted vault root the test writes
  into. Contents are gitignored aside from a README.
- `docker/keys/` — gitignored; populated on first run.
- `scripts/start-test-sshd.mjs` — generates an ed25519 keypair on
  first run (otherwise reuses), `docker compose up -d --build`,
  polls `Health.Status` until healthy. Idempotent.
- `scripts/stop-test-sshd.mjs` — `docker compose down -v`.
- `vitest.integration.config.ts` — separate config that includes
  `tests/integration/**` and serialises files (no fileParallelism)
  so they don't fight over the single sshd. Default
  `vitest.config.ts` excludes the integration dir so `npm test`
  stays fast.
- `tests/integration/ssh.integration.test.ts` — opens a real SSH
  session with the generated keypair and exercises list / read /
  write (small text + 64KB binary) / stat / exists / remove
  through the actual SftpClient. Each run uses a unique subdir
  under `/home/tester/vault` so leftovers don't collide.
- `package.json` scripts: `sshd:start`, `sshd:stop`,
  `test:integration`. `npm test` is unchanged.
- `.github/workflows/integration.yml` — separate from `ci.yml` so
  the integration run doesn't gate other PR signals. Brings the
  container up before the test step, tears it down in a
  finally-style step, uploads `docker logs` as an artefact on
  failure.

## Local usage

```
cd plugin
npm run sshd:start
npm run test:integration
npm run sshd:stop
```

Requires Docker (Desktop or daemon) on PATH. Unit tests
(`npm test`) work without Docker.

## Out of scope (B-2 / B-4)

- ProxyJump / two-container bastion → target topology
- Daemon (obsidian-remote-server) auto-deploy + RPC handshake
  end-to-end against the container
Both queued for a follow-up PR once this base lands.

## Version

0.3.2 → 0.4.0 (new feature surface, minor bump).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 14:05:55 +09:00
Souta
de3d0d6c32 chore(security): replace personal info with generic placeholders (0.3.2)
Security check turned up three places where the maintainer's
personal info had ended up in tracked source:

- plugin/tests/AdapterPatcher.test.ts:15
  `target.greet('souta')` → `target.greet('alice')`
- plugin/tests/pathUtils.test.ts:15
  `/home/souta/work/VaultDev/` → `/home/alice/vault/`
- plugin/scripts/dev-install.mjs
  Default fallback for the dev vault path was hard-coded to
  `../../SelfArchive-dev` (the maintainer's own dev vault folder
  name). Renamed to the generic `../../dev-vault`. The
  `REMOTE_SSH_DEV_VAULT` env var still wins, and the script accepts
  a positional argv override too — the maintainer's actual
  `SelfArchive-dev` keeps working with
  `REMOTE_SSH_DEV_VAULT=/path/to/SelfArchive-dev npm run build:full`.

What stays:
- `github.com/sotashimozono/...` in every Go file's import path —
  Go module canonical URL; this matches the repo URL on GitHub
  and isn't replaceable.
- `work/VaultDev` mentions in README / docs / a couple of test
  fixtures — generic enough not to identify the maintainer
  (anyone could name a folder that), kept as-is.

No private keys / tokens / IPs / emails / hostnames were in the
tree to begin with — those live only in the user's local
`data.json` and ssh agent, not in the repo.

Bumps to 0.3.2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 11:30:25 +09:00
Souta
e15a90dc30 chore(ci): version-check workflow + auto-release-on-merge + 0.3.0 bump
Three pieces of release infrastructure that travel together:

1. `.github/workflows/version-check.yml`
   On every PR, asserts that
   - plugin/manifest.json and plugin/package.json agree on version
   - the PR's manifest.json version is strictly greater than
     base's (rejects same-or-lower)
   Pure-shell semver compare; no external action.

2. `.github/workflows/release-on-merge.yml`
   On every push to main, reads plugin/manifest.json, pushes the
   matching X.Y.Z tag if missing, then waits up to 30 s for the
   existing release.yml (which triggers on the tag) to publish a
   release. Falls back to a `gh release create --generate-notes`
   if release.yml is unavailable. Marked as pre-release while
   pre-1.0 (toggle the IS_PRERELEASE env to flip).

3. `plugin/scripts/bump-version.mjs`
   The npm `version` lifecycle script the package.json already
   referenced but didn't actually have. Reads the version npm just
   wrote into package.json and mirrors it into manifest.json +
   versions.json (with manifest.minAppVersion as the value). Now
   `cd plugin && npm version patch` updates all three files at
   once and the version-check passes.

Bumps to 0.3.0 in this PR (this is itself a non-trivial PR adding
release infra, so a minor bump fits).

Note on the original question: `Plugin remote-ssh v0.X.Y loaded`
in the runtime log uses `this.manifest.version` — i.e. it reads
from manifest.json, the Obsidian convention for plugin runtime
metadata. The version-check workflow asserts manifest.json and
package.json agree, so the runtime log effectively shows the same
number npm sees.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 11:00:04 +09:00
Souta
874e4338ae feat(plugin): auto-deploy obsidian-remote-server over SSH
The "Debug: test RPC tunnel" command now ships, restarts, and verifies
the daemon end-to-end without any manual scp + nohup steps. Build
pipeline grew an `npm run build:server` that cross-compiles the Go
daemon for Linux/amd64 and stages it next to main.js so the plugin
can find it at runtime.

ServerDeployer (plugin/src/transport/ServerDeployer.ts)
- mkdir -p ~/.obsidian-remote && chmod 700 it
- pkill any prior daemon and remove stale socket / token
- SFTP-upload the local binary, chmod +x
- nohup the daemon with --vault-root / --socket / --token-file
  / --verbose, redirect stdout+stderr to ~/.obsidian-remote/server.log,
  detach
- poll the token file for up to 5s; return the token + paths
- stop() helper for the inverse: pkill + cleanup

Built on a tiny `DeployerSshClient` interface so unit tests don't
have to bring up a real SSH session. shellQuote single-quotes every
argument so paths with spaces survive the shell.

SftpClient
- uploadFile(localPath, remotePath) — sftp.fastPut wrapper.
- exec(cmd) — runs a one-shot command, collects stdout/stderr/exit.
  Used for mkdir / pkill / chmod / nohup. Long-running streams must
  not go through here (they'd never close).

Build pipeline
- plugin/scripts/build-server.mjs cross-compiles
  ./cmd/obsidian-remote-server with GOOS=linux GOARCH=amd64
  CGO_ENABLED=0 and writes the result to plugin/server-bin/
  obsidian-remote-server-linux-amd64. Falls back to
  ~/tools/go-portable/go on the dev box when go isn't on PATH.
  REMOTE_SSH_SKIP_SERVER_BUILD=1 short-circuits for hosts without a
  Go toolchain.
- dev-install.mjs additionally copies plugin/server-bin/* into
  <vault>/.obsidian/plugins/remote-ssh/server-bin/ so the plugin
  finds the binary at runtime.
- New scripts: `npm run build:server`, `npm run build:full`
  (server + plugin + install in one shot).
- Root .gitignore now excludes plugin/server-bin/.

main.ts
- locateDaemonBinary() resolves
  <vault>/.obsidian/plugins/<id>/server-bin/obsidian-remote-server-
  linux-amd64; returns null when missing.
- debugTestRpcTunnel():
    1. locateDaemonBinary, error early when missing
    2. ServerDeployer.deploy(...) ships and starts the daemon
    3. openUnixStream + establishRpcConnection
    4. RpcRemoteFsClient.list(activeRemoteBasePath) for the smoke
       check, log each step, surface a Notice with daemon version.

Tests (vitest)
- ServerDeployer.test.ts (8 cases): order of mkdir → kill → upload
  → chmod → start → wait-for-token, killExisting=false skips the
  kill, custom remote paths flow into the start command, paths
  with spaces are single-quoted, non-zero exit on a non-tolerant
  command surfaces, token-read retries until present, deadline is
  enforced, stop() runs pkill + cleanup.

Verification
- cd plugin && npx tsc --noEmit             clean
- cd plugin && npm test                     123 / 123 pass (8 new)
- cd plugin && npm run build:full           Go cross-compile (3.75
                                            MiB) + plugin build +
                                            dev vault install all
                                            green; bundle 382 KB
                                            (+3 KB over 5-D.3).

Dogfood (after merge)
1. cd plugin && npm run build:full
2. Reload the plugin in dev vault.
3. Run "Connect to remote vault" against the Panza profile.
4. Run "Debug: test RPC tunnel against obsidian-remote-server".
   Expect: a Notice with daemon version + entry count, and a
   per-step trace in console.log. No manual scp / start steps.

Follow-ups
- Phase 5-D.4: switch SftpDataAdapter's client over to
  RpcRemoteFsClient automatically when a profile carries an
  rpcSocketPath, with a fallback to the SFTP path.
- Cross-arch staging (linux/arm64, darwin/*) for non-amd64 hosts.
- ServerDeployer.stop wired into Plugin.disconnect so daemons don't
  outlive the Obsidian session.
2026-04-25 17:40:45 +09:00
Souta
21bb70a5d0 chore: monorepo restructure (plugin/ + server/ + proto/)
Move the entire Obsidian plugin tree under plugin/ and add an empty
server/ tree (Go) and proto/ directory so the upcoming
obsidian-remote-server daemon has a home in the same repo. The plugin
remains the only thing that builds today; the server is a hello-world
skeleton that prints "not implemented yet" and exits non-zero, with a
Makefile that knows how to cross-compile for linux/darwin × amd64/arm64
once real code lands.

Background:
  Approach A (vault-adapter monkey-patch) hit structural ceilings —
  vault index frozen at startup, third-party plugins bypass DataAdapter,
  empty-remote .obsidian read failures cascade through Templater/Kanban
  etc. We are pivoting to the VS Code Remote-SSH model: a small Go
  daemon on the remote host, JSON-RPC over a SSH-tunnelled WebSocket,
  fsnotify-driven push events, attachment serving over HTTP on the same
  socket. This restructure is the first mechanical step before the
  protocol and transport land.

Mechanical changes:
- git mv all plugin sources, manifest, configs, scripts, and tests
  into plugin/.
- plugin/scripts/dev-install.mjs walks one extra parent (`pluginRoot`
  → `repoRoot` → `..` for the dev vault) so REMOTE_SSH_DEV_VAULT
  defaulting still resolves to ../SelfArchive-dev relative to the
  repo, not to plugin/.
- CI: ci.yml runs lint/test/build with `working-directory: plugin`
  and a server job that builds + tests the Go module. release.yml
  builds out of plugin/ and uploads plugin/{main.js,manifest.json,
  styles.css}.
- Dependabot: npm directory becomes /plugin; new gomod directory /server.
- Labeler: paths re-anchored to plugin/, with new plugin/server/proto
  area labels.

Verification:
  cd plugin && npm install && npx tsc --noEmit && npm test (76/76)
  cd plugin && npm run build:install (367 KB; dev vault refreshed).
  Go side waits on CI (no Go toolchain on the dev host).
2026-04-25 10:52:52 +09:00