sotashimozono_obsidian-remo.../plugin/scripts/dev-install.mjs
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

76 lines
2.8 KiB
JavaScript

#!/usr/bin/env node
/*
* dev-install — copy build artifacts (main.js, manifest.json, styles.css)
* into the dev vault's plugins folder.
*
* Usage:
* node scripts/dev-install.mjs
* node scripts/dev-install.mjs <vault-root>
*
* The default vault root is read from the `REMOTE_SSH_DEV_VAULT` env var
* or falls back to `../../SelfArchive-dev` relative to this script
* (i.e. a sibling of the repo root, since the plugin lives under
* `<repo>/plugin/`).
*/
import * as fs from 'node:fs';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const pluginRoot = path.resolve(__dirname, '..'); // <repo>/plugin
const repoRoot = path.resolve(pluginRoot, '..'); // <repo>
const vaultRoot = process.argv[2]
?? process.env.REMOTE_SSH_DEV_VAULT
?? path.resolve(repoRoot, '..', 'SelfArchive-dev');
const manifestPath = path.join(pluginRoot, 'manifest.json');
if (!fs.existsSync(manifestPath)) {
console.error(`manifest.json not found at ${manifestPath}`);
process.exit(1);
}
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
const targetDir = path.join(vaultRoot, '.obsidian', 'plugins', manifest.id);
if (!fs.existsSync(path.join(vaultRoot, '.obsidian'))) {
console.error(`Vault root does not look like an Obsidian vault: ${vaultRoot}`);
console.error(`(no .obsidian directory found)`);
process.exit(1);
}
fs.mkdirSync(targetDir, { recursive: true });
const files = ['main.js', 'manifest.json', 'styles.css'];
for (const f of files) {
const src = path.join(pluginRoot, f);
if (!fs.existsSync(src)) {
console.error(`missing build artifact: ${src} (run npm run build first)`);
process.exit(1);
}
const dst = path.join(targetDir, f);
fs.copyFileSync(src, dst);
console.log(`copied ${f} -> ${dst}`);
}
// Stage the obsidian-remote-server binaries (if any) so the plugin
// can ship one to the remote at connect time. Built by
// `npm run build:server`; missing means "no auto-deploy this run",
// not an error.
const serverBinDir = path.join(pluginRoot, 'server-bin');
if (fs.existsSync(serverBinDir)) {
const binTarget = path.join(targetDir, 'server-bin');
fs.mkdirSync(binTarget, { recursive: true });
for (const f of fs.readdirSync(serverBinDir)) {
const src = path.join(serverBinDir, f);
if (!fs.statSync(src).isFile()) continue;
const dst = path.join(binTarget, f);
fs.copyFileSync(src, dst);
console.log(`copied server-bin/${f} -> ${dst}`);
}
} else {
console.log('server-bin/ not present — skipping daemon staging (run `npm run build:server` to build it)');
}
console.log(`\nplugin '${manifest.id}' v${manifest.version} installed to ${targetDir}`);
console.log('Reload the plugin in Obsidian (Settings → Community plugins → toggle off/on).');