Phase B/2. The enabled-plugins LIST round-trips (#434) and the
marketplace installer fetches binaries for registry plugins (#439,
Phase B/1) — but a BRAT / sideloaded plugin isn't on the marketplace,
so its code never reached another machine.
ShadowVaultBootstrap.pull/pushPluginBinaries round-trip the plugin
*code* (manifest.json / main.js / styles.css; never data.json) through
the remote vault's canonical `.obsidian/plugins/<id>/`. Wired into
runAutoConnect AFTER the marketplace installer so the plugins it
fetched live aren't re-pulled (pull only stages what's still missing =
the non-marketplace ones, preserving the installer's live load and
acting as a fallback when a marketplace fetch fails). The push makes
the remote `.obsidian/plugins/` a complete vault. A pulled binary loads
on the next vault open (Obsidian scans the plugins dir at startup).
- pull never overwrites a local file (local install authoritative)
- push is diff-gated (no churn) and skips remote-ssh (self-managed)
- per-file SSH errors are swallowed, never abort the batch
Tests: 7 unit (in-memory fakes) + 3 integration (real docker sshd:
cross-session round-trip, push→fresh-pull, local-authoritative).
Refs #429#342
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase B, step 1. The marketplace installer ran in prepareForAutoConnect
(before connect) and not at all on a reconnect, so a plugin the remote
community-plugins pull adds to the list had no binary staged and didn't
load until the next Obsidian restart — kazink's "plugins not kept
between sessions".
runAutoConnect now re-runs installMissingShadowPlugins right after
pull/pushCommunityPlugins, when the list is current. enablePluginAndSave
loads a marketplace plugin live (no restart); the call is idempotent so
already-installed ids are skipped, and it now also covers the reconnect
path (which never hit prepareForAutoConnect). installMissingShadowPlugins
is made public for this second call site.
Remaining Phase B (separate): binary round-trip for BRAT / non-
marketplace plugins (their binary isn't on the remote), and a pre-spawn
pull so app.json is canonical at boot without a reload.
Refs #429#342
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Independent integration spec (vitest + docker sshd, no Obsidian UI) that
exercises the real bootstrap + pull/push round-trip against a live SSH
server — the empirical guard the adversarial review found missing:
- community plugins enabled on the remote round-trip into a fresh shadow
(keeping remote-ssh)
- a plugin enabled in session 1 persists into a fresh session 2 (via the
remote as the canonical store)
- push unions with the remote and never drops a plugin enabled elsewhere
(the #437 clobber-guard, verified over real SSH, not just fakes)
- a settings change stays consistent across write → push → fresh-pull
The remote vault's `.obsidian/` is the single source of truth; a fresh
shadow vault stands in for a new session / machine.
Refs #429#342
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adversarial review of #434 found a data-loss regression: pull swallows a
remote read-failure internally (returns pulled:false, no throw), so the
subsequent push ran with a stale/minimal local list and could overwrite
the remote community-plugins.json — dropping plugins enabled on another
machine on a transient SSH hiccup.
pushCommunityPlugins is now self-protecting: it re-reads the remote and
unions (remote ∪ local ∪ remote-ssh) before writing, and ABORTS without
writing if the remote file exists but can't be read or parsed. An absent
remote is still seeded from local. Signature takes a reader+writer (the
patched adapter already satisfies both); the connect-flow caller is
unchanged.
Tests: seed-when-absent, union-never-drops, no-clobber-on-read-fail,
no-clobber-on-corrupt-json.
Refs #429#342
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Take the golang.org/x/image 0.42.0→0.43.0 server dep bump (#426) into
the beta. (Manual bump: the branch was update-branched under my actor,
which lifts the dependabot version-check exemption.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Tighten the just-added #429 notes (glossary entry, compat basePath
bullet + Direct-disk footgun + Templater/Kanban/Importer notes + survey
correction, and the SftpDataAdapter comments) — same facts, roughly half
the words. Optimised for catch-up: details live behind the issue refs.
Refs #429
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The shadowBasePath constructor-param doc and the `basePath` getter doc
in SftpDataAdapter carried the same misconception the compat guide did:
that Node `fs` reads/writes under the shadow basePath are "mirrored
locally by the file watcher" and "propagate to the remote." No such
vault-tree mirror or watcher exists — the vault tree is served
virtually from the remote, so those `fs` ops only touch the local
shadow copy. Comment-only; behaviour unchanged.
Refs #429
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Plugins set up on the remote vault didn't load in the shadow vault, and
reopening "lost" installed plugins: the enabled-plugins list
(community-plugins.json) was only ever seeded locally as ["remote-ssh"]
and never synced, unlike app/appearance/core-plugins/hotkeys.
Add `ShadowVaultBootstrap.pullCommunityPlugins` / `pushCommunityPlugins`
that round-trip community-plugins.json with a forced `remote-ssh` union
(a verbatim copy of a remote list omitting remote-ssh would disable this
very plugin, which is why it stays out of SHARED_OBSIDIAN_CONFIG_FILES).
Pull merges remote→local (the marketplace installer then fetches any
missing binaries on the next startup); push sends the merged list back
so a plugin enabled on one machine reaches another. The connect flow
pulls then pushes — pushing after the merge can never drop a plugin the
remote already had.
Refs #429#342
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
app/appearance/core-plugins/hotkeys already round-trip via
pull/pushSharedObsidianConfig, but the enabled community-plugins list
does not — it is only seeded locally as ["remote-ssh"]. So a plugin
enabled on one machine never reaches another machine's shadow vault and
reopening "loses" installed plugins (#429 kazink follow-up, #342
residual). RED cases for a dedicated pull/push pair that must keep
remote-ssh enabled through the round-trip:
- pull merges the remote list, keeping remote-ssh
- pull keeps remote-ssh even when the remote list omits it
- pull is a benign no-op when the remote has none yet
- pull does not clobber a healthy local list on corrupt remote JSON
- push writes the local list to the remote, preserving remote-ssh
Refs #429#342
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Connect to hosts reachable only through a subprocess transport — e.g. a
Cloudflare tunnel:
Host homelab
HostName lab.example.com
ProxyCommand cloudflared access ssh --hostname %h
IdentityAgent ~/.1password/agent.sock
- SshConfigReader parses `ProxyCommand` (raw, %-tokens preserved) and
`IdentityAgent` (tilde-expanded), incl. Host * defaults + host
override.
- New ProxyCommandTunnel: `expandProxyCommandTokens` (%h/%p/%r/%%) +
`createProxyCommandTunnel`, which spawns the command through the shell
and bridges its stdio as a Duplex, with backpressure and child
teardown on close. ssh2 takes it as `sock` (same seam as the jump
tunnel).
- SshProfile gains `proxyCommand`; SftpClient.connect opens the tunnel
when set (jumpHost takes precedence — the two are mutually exclusive
in OpenSSH).
- ProfileForm: "Import from SSH config" maps ProxyCommand → proxyCommand
and IdentityAgent → agentSocket(+agent auth); plus a manual
"Proxy command" text field.
Desktop only (child_process). The SshConfigReader IdentityAgent test
compares via path.join so it's OS-agnostic.
Refs #430
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A user behind a Cloudflare tunnel connects via
ProxyCommand cloudflared access ssh --hostname %h
and authenticates through 1Password's agent socket
IdentityAgent ~/.1password/agent.sock
Neither is supported today. RED cases:
- SshConfigReader: parse ProxyCommand (raw, %-tokens preserved),
host-specific override of Host * default, and IdentityAgent
(tilde-expanded) — 5 cases.
- ProxyCommandTunnel (new module, import currently fails): token
expansion (%h/%p/%r/%%) and a subprocess-as-Duplex transport that
bridges child stdio and tears the child down on close — 10 cases.
Refs #430
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A note dragged into a freshly-created folder could vanish from File
Explorer until the vault was reopened. The writer-reflect rename bailed
("writer vault model is stale") whenever the destination folder was not
yet in the vault model: `renameOne` resolved the new parent strictly and
returned false, so the entry was detached from the old path but never
attached to the new one.
reflectRename now mirrors reflectWrite: it asks renameOne to synthesise
the missing parent chain (`ensureParents`), so the move lands even when
the destination subtree is stale. It also returns early — without a
misleading warning — when Obsidian's own rename already relocated the
entry. The low-level renameOne stays strict by default, so the
bulk-build and remote-echo (FsChangeListener) paths keep their
missing-parent sanity check.
Refs #423
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
reflectRename bails ("writer vault model is stale") when the
destination folder is not yet modelled — the exact field repro from
issue #423, where dragging notes into a freshly-created folder makes
them vanish from File Explorer until a full vault reopen. Four cases,
all RED today:
- move into a not-yet-modelled folder must synthesise the parent
- the same for several notes into one fresh folder (field-log shape)
- a deeply nested fresh subtree
- a benign echo (Obsidian moved the file first) must not warn "stale"
Refs #423
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Second beta of the 1.1.5 cycle — the release-flow / version-check
reconciliation (main ← next only). Beta channel only.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
First beta of the 1.1.5 cycle — the README refresh. Beta channel only; stable
manifest.json + versions.json stay pinned to 1.1.4.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Stable release notes came out empty: the changelog step used `git-cliff
--unreleased` (commits since the previous TAG), but for a stable promotion
the previous tag is the last BETA of the cycle, so the range held only the
promote-plumbing commits — all filtered by cliff.toml → empty body.
release.yml now branches the changelog range by channel:
- beta -> `--unreleased` (incremental, unchanged).
- stable -> `<last-stable-tag>..HEAD --ignore-tags beta`, so the notes carry
everything accumulated across the cycle's betas, consolidated under one
version heading (intermediate beta tags would otherwise split the output
into repeated per-tag sections).
cliff.toml: also skip `chore(release):` version-bump commits so the notes
show real changes, not bump plumbing.
Verified locally with git-cliff over 1.1.2..1.1.3: one consolidated set of
grouped sections (Features / Bug Fixes / Tests / …), bump commits dropped.
On main's topology (only stable promote-merges sit between stable tags) the
range is clean; the real release runs there.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
First beta of the 1.1.4 cycle, opened after the 1.1.3 stable release.
Carries the new-profile first-connect restart guidance. Beta channel only;
stable manifest.json + versions.json stay pinned to 1.1.3.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A freshly-registered shadow vault is added to obsidian.json, but a running
Obsidian only reads that file at startup — so the obsidian://open fired on
the first connect to a NEW profile is a no-op and no window appears (the SSH
connection itself succeeds). The plugin previously showed a misleading
"opened in new window" notice for a window that never opened.
openShadowVaultFor now branches on result.registryCreated: a newly-registered
vault gets a PERSISTENT notice (duration 0) telling the user to fully quit and
reopen Obsidian, then reconnect; the reused path keeps the normal "opened in
new window" notice. One-time per profile.
Docs: first-connect gains an [!important] restart callout; troubleshooting
gains a "Clicked Connect but no window opened (new profile)" entry. Also fixed
a stale shadow-vault path in first-connect
(<plugin-dir>/shadow-vaults/ -> ~/.obsidian-remote/vaults/).
Tests: openShadowVaultFor — newly-registered -> restart notice (not "opened");
reused -> "opened in new window" (no restart notice).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
From the multi-agent review of #407:
- connectProfile loud-fail test now asserts a Notice surfaced AND that the
silent SFTP-downgrade path was NOT taken (the "loud" part was previously
only inferred from ERROR state, which a return-early bug could fake).
- both connectProfile tests assert conn.connectSsh was called, so a refactor
that short-circuits the connect cannot leave them green.
- reconnectAttempt test: corrected a stale comment ("re-prompt consent" no
longer applies post-#406, which persists the decline) to the real harm
(burns retries + a misleading "reconnect failed").
- makePlugin: one intermediate cast instead of four; replayOfflineQueue mock
resolves a promise to match the fire-and-forget call shape.
Tests-only. Affected suites green; lint + tsc clean.
Refs #406, #407
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Third beta of the 1.1.3 cycle: test coverage for the daemon RPC→SFTP
downgrade paths + executable specs for the deferred #406-review hardening.
Beta channel only; stable manifest.json + versions.json stay pinned to 1.1.2.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Turns the #406-review follow-ups into test code instead of tracking issues:
- connectProfile (new connectProfile.daemon-downgrade.test.ts): a
DaemonUnavailableError downgrades to SFTP and still reaches CONNECTED;
a DaemonVerificationError fails loud (ERROR + disconnect), not a silent
downgrade — pins the I-3 boundary.
- ConnectionManager: reconnectAttempt catches DaemonUnavailableError and
continues on SFTP instead of throwing into ReconnectManager's retry loop
(pins I-1).
- DaemonDownloader: three it.todo specs as runnable trackers for the
deferred hardening — cosign .bundle verification, a shared cross-vault
binary cache, and cache-hit re-verification.
Tests-only; no production change. Full suite green (1120 passed, 3 todo);
lint + tsc clean.
Refs #397, #406
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Findings from the multi-agent review of #406:
- I-1 reconnect: reconnectAttempt now catches DaemonUnavailableError and
continues on SFTP instead of letting ReconnectManager retry a permanent
daemon-unavailable condition (and re-prompt consent) maxRetries times.
- I-2 consent: persist the consent DECISION (accept OR decline) via the new
pure resolveDaemonConsent() gate, so a decline no longer re-prompts on
every connect/restart; check `=== true`.
- I-3 tamper: a sha256/DaemonVerificationError is rethrown, not swallowed
into a quiet SFTP downgrade — integrity failures surface loudly.
- I-5 atomic cache: writeExecutable writes a temp sibling then renames, so a
crash mid-download can't leave a torn binary that a later cacheHit returns
unverified.
- uname probe: a non-zero exit / exec error is logged distinctly, kept
separate from "unsupported arch".
- harden: validate the sha-manifest shape (isShaManifest) instead of an `as`
cast; lowercase BOTH sides of the sha compare.
- tests: resolveDaemonConsent (accept/decline/already-consented), uppercase
manifest sha, and ConnectionManager startRpcSession fallback (download →
deploy, both-null → DaemonUnavailableError, staged → skip download).
- cleanups: pluginDir() helper de-dupes the three plugin-dir path builds;
DAEMON_RELEASE_REPO moved out from between imports; consent-modal text
("faster directory listing", not "fast sync"); accurate JSDoc on
DaemonUnavailableError / the version dep / locateDaemonBinary.
Deferred (flagged in review, intentionally not here): cosign .bundle
verification (heavy; HTTPS+sha256 with fail-safe SFTP is the current bar),
a shared cross-vault daemon cache, and extracting confirmDaemonDownload
into a Modal subclass.
Refs #397, #406
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Second beta of the 1.1.3 cycle, adding daemon auto-download (#397) on
top of the #399 shadow-vault secrets fix. Beta channel only: stable
manifest.json + versions.json stay pinned to 1.1.2.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Community-store installs ship only main.js/manifest.json/styles.css, so the
daemon binary (server-bin/) never reaches users -> default RPC failed with a
dev-facing "run npm run build:server" error (#397).
Now, when no binary is staged locally, the plugin:
- probes the remote os/arch via `uname`,
- downloads the matching obsidian-remote-server-<os>-<arch> from this
plugin's GitHub release,
- verifies it against the release's daemon-manifest.json (sha256),
- caches it under server-bin/ for subsequent connects.
A one-time consent dialog gates the download (Obsidian discourages silent
external fetches; remembered in settings). Unsupported remote arches, a
declined download, or any failure downgrade the session to SFTP via the new
DaemonUnavailableError instead of hard-failing.
New DaemonDownloader.ts (pure, dependency-injected) + 17 tests. ConnectionDeps
gains ensureDaemonBinary; startRpcSession uses
locateDaemonBinary() ?? ensureDaemonBinary(). main.ts wires requestUrl + fs +
the consent Modal; connectProfile catches DaemonUnavailableError and continues
on SFTP.
Refs #397.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
First beta of the 1.1.3 cycle, carrying the #399 shadow-vault secrets
fix. Beta channel only: plugin/manifest.json + manifest-beta.json
advance; the stable manifest.json + versions.json stay pinned to 1.1.2.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A password entered in the source (local) vault never reached the shadow
vault that actually runs the SSH connect, so the shadow auto-connect
failed with "No password stored for profile" and the vault opened empty.
Two gaps combined:
1. ConnectModal.persistSecret only writes the password to the in-memory
SecretStore; openShadowVaultFor spawned the shadow before any save, so
the bootstrap read a source data.json whose `secrets` were still empty.
2. ShadowVaultBootstrap.readBaseDataJson prefers the existing shadow
data.json on re-bootstrap, so even a persisted source secret never
propagated after the first spawn.
Fix:
- openShadowVaultFor flushes settings (saveSettings) before the spawn so
the freshly-typed password + passwordRef land in the source data.json.
A save failure is logged and swallowed rather than aborting the spawn.
- bootstrapSync unions the source vault's `secrets` into the shadow
data.json on every bootstrap (source wins on a conflicting ref; a
secret typed directly in the shadow window survives).
Tests (red before the fix):
- ShadowVaultBootstrap: re-bootstrap propagates a new source secret;
union precedence (shadow-only survives, source wins on conflict).
- openShadowVaultFor: settings are flushed before the shadow spawn.
Refs #399
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
With the root devDeps now resolving types, the validator surfaced a few real
`any` leaks plus a CSS portability note. All fixed:
- ResourceBridge: name the range fetcher's return type (BinaryRange) and
annotate `let result: BinaryRange`. `let result;` had decayed to `any`
because evolving-any doesn't survive the try/catch reassignment, so
result.mtime/.totalSize/.bytes were all unsafe.
- QueueReplayer: same pattern — extract `ReplayOutcome`, annotate
`let outcome: ReplayOutcome`.
- logger: cast the bound console methods to `ConsoleFn` (the snapshot's
declared type) rather than `typeof console.warn`.
- AdapterPatcher: replace `fn.bind(x)` (whose typing varied by env) with an
equivalent arrow wrapper.
- styles.css: fold xterm's text-decoration-line + -style sub-properties into
the text-decoration shorthand (only the shorthand is fully supported on
older Obsidian; the sub-properties were flagged partial).
Verified: validator-sim eslint (root deps, cwd=root) -> 0; plugin tsc + lint -> 0.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* build(workspaces): root npm-workspaces so the validator resolves plugin types
ObsidianReviewBot scans from the repo ROOT (getManifest reads cwd/manifest.json),
installs at the root, and type-checks plugin/src via ./tsconfig.json with
@typescript-eslint/no-unsafe-member-access. The plugin lives in plugin/ with deps in
plugin/node_modules, so a root install resolved nothing -> @types/node + obsidian
unresolved -> false "unsafe member access" warnings. #392 (typeRoots) couldn't help
because plugin/node_modules doesn't exist in the validator's root-only install.
Declare the root as an npm workspace ({ workspaces: ["plugin"] }). A root install now
hoists the plugin's deps into root/node_modules; the root tsconfig's typeRoots picks
them up. Verified by simulating the validator (rm plugin/node_modules; install at root
only): @types/node + obsidian hoisted, eslint from cwd=root -> 0 unsafe-member-access.
CI unaffected: cd plugin; npm ci still works (plugin/package-lock.json retained, 439
packages, 0 vuln) -- no workflow changes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* build(deps): pin obsidian to ~1.12.3 so the validator type-checks deprecation-free
With the workspace root now resolving types, a root install pulled obsidian "latest"
= 1.13.0, which deprecates SettingTab.display() and ButtonComponent.setWarning(). The
validator's @typescript-eslint/no-deprecated then flagged 9 errors in SettingsTab.ts
that the plugin's own CI never saw (plugin lock pinned 1.12.3).
"obsidian": "latest" is a non-reproducible anti-pattern regardless. Pin to ~1.12.3
(the last release before those APIs were deprecated). Verified by simulating the
validator (root install -> obsidian 1.12.3; eslint from cwd=root): 0 no-deprecated,
0 unsafe-member-access, EXIT 0.
The declarative getSettingDefinitions()/setDestructive() migration is deferred until
we intentionally raise the obsidian floor.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* build: plain root devDeps instead of workspaces (keeps CI green)
The workspaces approach broke CI: npm hoists the plugin's deps into
root/node_modules, but the plugin's CI invokes binaries by direct path
(`node node_modules/typescript/bin/tsc`, `node node_modules/vitest/...`),
which are then absent from plugin/node_modules. Hoisting is intrinsic to
workspaces, so any workspaces setup would require workflow changes.
Instead, make the root package.json a plain (non-workspace) manifest that
declares only the deps the validator needs to type-check plugin/src from
the repo root. A root `npm install` populates root/node_modules and the
root tsconfig resolves types there. The plugin's own `cd plugin; npm ci`
is untouched (no hoisting) -> CI stays green with zero workflow changes.
Verified: move plugin/node_modules aside, eslint from cwd=root with root
deps only -> 0 unsafe-member-access, 0 no-deprecated, 0 cannot-be-resolved.
Trade-off: type-dep versions are duplicated between root and plugin and
must be kept in sync (noted in the root package.json description).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The validator's static type-checker reads the root tsconfig but couldn't find
@types/node (no root node_modules — it lives in plugin/node_modules), so
Buffer / ArrayBufferView / net.Server / EventEmitter resolved to `any`,
producing the false "unsafe member access" warnings. Point typeRoots at
plugin/node_modules/@types and pin types:["node"]. Verified: tsc -p
tsconfig.json now resolves Node types (no Buffer/net errors).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: strip Quartz off main/next so the validator scans only the plugin
The Obsidian plugin validator's static preview was scanning the Quartz docs
site that lived at the repo root, surfacing false-positive warnings (globby/
js-yaml "should be replaced", docs-site unsafe-member-access from unresolved
types). Quartz now lives entirely on the docs/quartz-framework branch (#390
staged the config/deps/types there).
- docs.yml: checkout the full Quartz set from the docs branch BEFORE
setup-node (so its package-lock cache key resolves), then build docs/ and
deploy gh-pages stable=main / dev=next (deploy logic unchanged).
- Remove from root: package.json (@jackyzha0/quartz), package-lock.json,
quartz.config.ts, quartz.layout.ts, index.d.ts, globals.d.ts,
tsconfig.quartz.json, scripts/check-docs-links.mjs
main/next now carry only the plugin + docs/ content (.md, not scanned).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(release): bump beta for the Quartz strip
Version bump so version-check (PR version > base) passes; the Quartz removal
itself carried no version change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
window.localStorage does not trigger no-restricted-globals (bare
localStorage does); the disable comment was both unnecessary and
itself flagged by the bot as disallowing a rule that cannot be suppressed.
moduleResolution:bundler fails to resolve obsidian package because its
package.json has main:"" (empty string) with no exports field. paths
mapping also does not override this behaviour in bundler mode.
Switch the root tsconfig (used only by ESLint/ObsidianReviewBot, not by
the actual plugin build) to module:commonjs + moduleResolution:node.
Classic Node resolution walks up from each source file in plugin/src/
to find plugin/node_modules/obsidian, @xterm/*, and @types/ssh2 reliably.