omgbase 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,9 +2,9 @@ import { type SyncSource, type Store, type RepoRow } from "@omgbase/core";
2
2
  /** The built-in filesystem adapter's registry name. */
3
3
  export declare const FS_ADAPTER = "fs";
4
4
  /** Seed the built-in `fs` adapter row (idempotent). Required before creating an
5
- * fs source (the `sources.adapter` FK is enforced). The stored command is the
6
- * globally-installed bin name; the actual spawn uses the bundled binary via
7
- * execPath (see spawnSource) so a source is never pinned to a stale path. */
5
+ * fs source (the `sources.adapter` FK is enforced). The stored command is
6
+ * registry data only (spec/sync §2, §5): the host never runs it for `fs` — see
7
+ * spawnSource — so a source is never pinned to a stale path. */
8
8
  export declare function ensureFsAdapter(store: Store): void;
9
9
  /**
10
10
  * Build the live filesystem SyncSource for a repo from its attached `fs` source
@@ -1,6 +1,5 @@
1
- import { execPath } from "node:process";
2
- import { createExternalSource, ensureAdapter, sourcesForRepo, renderConfigFlags, } from "@omgbase/core";
3
- import { fsAdapterBinPath } from "@omgbase/fs-adapter";
1
+ import { createExternalSource, ensureAdapter, listAdapters, sourcesForRepo, renderConfigFlags, } from "@omgbase/core";
2
+ import { fsAdapterLaunch } from "@omgbase/fs-adapter";
4
3
  // Resolve a repo's live sync source for `omg watch`/`omg mcp`/`omg sync`
5
4
  // (sync-plugins, ADR-014 Stage 3). Resolution now prefers the source REGISTRY
6
5
  // (adapters/sources/attachments tables): a repo's attached filesystem source is
@@ -11,19 +10,23 @@ import { fsAdapterBinPath } from "@omgbase/fs-adapter";
11
10
  /** The built-in filesystem adapter's registry name. */
12
11
  export const FS_ADAPTER = "fs";
13
12
  /** Seed the built-in `fs` adapter row (idempotent). Required before creating an
14
- * fs source (the `sources.adapter` FK is enforced). The stored command is the
15
- * globally-installed bin name; the actual spawn uses the bundled binary via
16
- * execPath (see spawnSource) so a source is never pinned to a stale path. */
13
+ * fs source (the `sources.adapter` FK is enforced). The stored command is
14
+ * registry data only (spec/sync §2, §5): the host never runs it for `fs` — see
15
+ * spawnSource — so a source is never pinned to a stale path. */
17
16
  export function ensureFsAdapter(store) {
18
17
  ensureAdapter(store, FS_ADAPTER, "omgbase-fs-adapter", []);
19
18
  }
20
19
  /** Spawn one registered source as a live SyncSource. The built-in `fs` adapter
21
- * is served by the bundled `@omgbase/fs-adapter` (execPath + bin path); other
22
- * adapters spawn their stored command. Source config renders to argv flags. */
23
- function spawnSource(source) {
20
+ * is launched per spec/sync §5 (1.2): `$OMGBASE_FS_ADAPTER` whitespace-split
21
+ * when set and non-empty, else the bundled `@omgbase/fs-adapter` bin under the
22
+ * running `node`; then the row's fixed `args`, then the rendered config flags
23
+ * (`fsAdapterLaunch`). Other adapters would spawn their stored command. */
24
+ function spawnSource(store, source) {
24
25
  const flags = renderConfigFlags(source.config);
25
26
  if (source.adapter === FS_ADAPTER) {
26
- return createExternalSource({ command: execPath, args: [fsAdapterBinPath(), ...flags], ...(hasEnv(source) ? { env: source.env } : {}) });
27
+ const row = listAdapters(store).find((a) => a.name === FS_ADAPTER);
28
+ const launch = fsAdapterLaunch(row?.args ?? [], flags);
29
+ return createExternalSource({ ...launch, ...(hasEnv(source) ? { env: source.env } : {}) });
27
30
  }
28
31
  // Custom adapter: spawn its stored command (looked up by the caller).
29
32
  throw new Error(`no launcher for adapter '${source.adapter}' (only the built-in '${FS_ADAPTER}' is wired)`);
@@ -42,6 +45,6 @@ export async function openRepoSource(store, repo) {
42
45
  const root = fsSource.config.root;
43
46
  if (typeof root !== "string" || root === "")
44
47
  return null;
45
- return spawnSource(fsSource);
48
+ return spawnSource(store, fsSource);
46
49
  }
47
50
  //# sourceMappingURL=_source.js.map
@@ -1,5 +1,5 @@
1
1
  import { parseArgs } from "node:util";
2
- import { serveStdio, Watcher, WatchLease, watchLeaseLive, freshnessSweep, EmbedDrainer, EngineError, pinClock, sequentialMinter, setIdMinter } from "@omgbase/core";
2
+ import { serveStdio, Watcher, WatchLease, watchLeaseLive, freshnessSweep, awaitReady, WATCH_READY_PATIENCE_MS, EmbedDrainer, EngineError, pinClock, sequentialMinter, setIdMinter } from "@omgbase/core";
3
3
  import { CliUsageError, EngineErrorLike, EXIT_OK, renderHelp } from "../output.js";
4
4
  import { loadEmbedding } from "./_embed.js";
5
5
  import { openRepoSource } from "./_source.js";
@@ -127,10 +127,10 @@ async function runMcp(cli, args) {
127
127
  if (wantWatch) {
128
128
  lease = WatchLease.tryAcquire(ws.omgbaseDir);
129
129
  if (lease) {
130
- // Prime with a one-shot sweep so the session starts fresh, then watch via
131
- // the external fs-adapter process (chokidar lives there, not in-engine).
132
- if (repo.rootPath)
133
- freshnessSweep(ws.store, repo.repoId, repo.rootPath);
130
+ // spec/sync §5 order: open the source (the external fs-adapter process —
131
+ // chokidar lives there, not in-engine), `watch`, wait for `ready` (bounded),
132
+ // THEN the priming sweep, then report live. An edit landing before the feed
133
+ // is primed is caught by the sweep, one after it by the feed.
134
134
  source = await openRepoSource(ws.store, repo);
135
135
  if (source) {
136
136
  watcher = new Watcher(ws.store, repo.repoId, source, {
@@ -144,6 +144,11 @@ async function runMcp(cli, args) {
144
144
  onError: (err) => cli.io.err(cli.style.dim(`[watch] error: ${String(err)}`)),
145
145
  });
146
146
  await watcher.start();
147
+ if (!(await awaitReady(watcher.ready(), WATCH_READY_PATIENCE_MS))) {
148
+ cli.io.err(cli.style.dim(`[watch] the fs adapter did not report ready within ${WATCH_READY_PATIENCE_MS / 1000}s — proceeding as if ready`));
149
+ }
150
+ if (repo.rootPath)
151
+ freshnessSweep(ws.store, repo.repoId, repo.rootPath); // prime: anything that landed before the feed was ready
147
152
  cli.io.err(cli.style.dim(`[mcp] serving ${repo.slug} on stdio · watcher live`));
148
153
  }
149
154
  else {
@@ -67,19 +67,20 @@ async function runInsert(cli, args) {
67
67
  const { values, positionals } = parseArgs({
68
68
  args: content.rest,
69
69
  allowPositionals: true,
70
- options: { at: { type: "string" }, actor: { type: "string" }, help: { type: "boolean" } },
70
+ options: { at: { type: "string" }, expect: { type: "string" }, actor: { type: "string" }, help: { type: "boolean" } },
71
71
  });
72
72
  if (values.help) {
73
73
  return renderHelp(cli, {
74
74
  name: "insert",
75
75
  summary: "Insert markdown as new block(s) under a parent block or heading",
76
- usage: "insert <to> (-m <markdown> | -f <file> | -) [--at end|start|before <id>|after <id>] [--actor <s>] [--dry-run]",
76
+ usage: "insert <to> (-m <markdown> | -f <file> | -) [--at end|start|before <id>|after <id>] [--expect <parent_children_hash>] [--actor <s>] [--dry-run]",
77
77
  options: [
78
78
  ["<to>", "parent block id, or a heading block id to append into its section"],
79
79
  ["-m <markdown>", "content inline"],
80
80
  ["-f <file>", "content from a file"],
81
81
  ["-", "content from stdin"],
82
82
  ["--at <pos>", "end (default) | start | before <id> | after <id>"],
83
+ ["--expect <hash>", "destination CAS: fail with stale_expectation unless the parent's direct child ids (joined by \",\", sha256 hex) still hash to this"],
83
84
  ["--actor <s>", "commit actor (default human:$USER)"],
84
85
  ],
85
86
  });
@@ -88,12 +89,14 @@ async function runInsert(cli, args) {
88
89
  if (!to)
89
90
  throw new CliUsageError("insert requires a <to> parent (block id, or a heading id for section append)");
90
91
  const markdown = readContent(content);
92
+ // The destination-parent CAS (spec/mutate §1.2) rides as the op-level expect.
93
+ const expect = values.expect ? { expect: { parent_children_hash: values.expect } } : {};
91
94
  if (cli.flags.server)
92
- return runOpsRemote(cli, "blocks_insert", { to, markdown, at: parseAt(values) });
95
+ return runOpsRemote(cli, "blocks_insert", { to, markdown, at: parseAt(values), ...expect });
93
96
  const ws = cli.workspace();
94
97
  const repo = cli.repo(ws);
95
98
  const parent = block(cli, ws, repo.repoId, to);
96
- const op = { op: "insert", to: { parent, at: parseAt(values) }, markdown };
99
+ const op = { op: "insert", to: { parent, at: parseAt(values) }, markdown, ...expect };
97
100
  return runOps(cli, ws, repo, [op], actorOf(values));
98
101
  }
99
102
  // ---- update -----------------------------------------------------------------
@@ -143,17 +146,18 @@ async function runMove(cli, args) {
143
146
  const { values, positionals } = parseArgs({
144
147
  args,
145
148
  allowPositionals: true,
146
- options: { to: { type: "string" }, at: { type: "string" }, actor: { type: "string" }, help: { type: "boolean" } },
149
+ options: { to: { type: "string" }, at: { type: "string" }, expect: { type: "string" }, actor: { type: "string" }, help: { type: "boolean" } },
147
150
  });
148
151
  if (values.help) {
149
152
  return renderHelp(cli, {
150
153
  name: "move",
151
154
  summary: "Move block(s) under a new parent, identity preserved",
152
- usage: "move <blocks…|-> --to <parent> [--at end|start|before <id>|after <id>] [--actor <s>] [--dry-run]",
155
+ usage: "move <blocks…|-> --to <parent> [--at end|start|before <id>|after <id>] [--expect <parent_children_hash>] [--actor <s>] [--dry-run]",
153
156
  options: [
154
157
  ["<blocks…>", "block ids; `-` reads them from stdin, one per line"],
155
158
  ["--to <parent>", "destination parent block"],
156
159
  ["--at <pos>", "end (default) | start | before <id> | after <id>"],
160
+ ["--expect <hash>", "destination CAS: fail with stale_expectation unless the destination's direct child ids (joined by \",\", sha256 hex) still hash to this"],
157
161
  ["--actor <s>", "commit actor (default human:$USER)"],
158
162
  ],
159
163
  });
@@ -163,13 +167,15 @@ async function runMove(cli, args) {
163
167
  const refs = expandBlockArgs(positionals);
164
168
  if (refs.length === 0)
165
169
  throw new CliUsageError("move requires one or more blocks (or - for stdin)");
170
+ // The destination-parent CAS (spec/mutate §1.2), checked once for the run.
171
+ const expect = values.expect ? { expect: { parent_children_hash: values.expect } } : {};
166
172
  if (cli.flags.server)
167
- return runOpsRemote(cli, "blocks_move", { blocks: refs, to: values.to, at: parseAt(values) });
173
+ return runOpsRemote(cli, "blocks_move", { blocks: refs, to: values.to, at: parseAt(values), ...expect });
168
174
  const ws = cli.workspace();
169
175
  const repo = cli.repo(ws);
170
176
  const blocks = refs.map((r) => block(cli, ws, repo.repoId, r));
171
177
  const parent = block(cli, ws, repo.repoId, values.to);
172
- const op = { op: "move", blocks, to: { parent, at: parseAt(values) } };
178
+ const op = { op: "move", blocks, to: { parent, at: parseAt(values) }, ...expect };
173
179
  return runOps(cli, ws, repo, [op], actorOf(values));
174
180
  }
175
181
  // ---- rm (blocks; --doc handled by the doc-level command) --------------------
@@ -1,6 +1,6 @@
1
1
  import { resolve } from "node:path";
2
2
  import { parseArgs } from "node:util";
3
- import { Watcher, WatchLease, EmbedDrainer, freshnessSweep, EngineError } from "@omgbase/core";
3
+ import { Watcher, WatchLease, EmbedDrainer, freshnessSweep, awaitReady, WATCH_READY_PATIENCE_MS, EngineError } from "@omgbase/core";
4
4
  import { runFsMirror } from "@omgbase/sync";
5
5
  import { EngineErrorLike, EXIT_OK, renderHelp } from "../output.js";
6
6
  import { loadEmbedding } from "./_embed.js";
@@ -97,8 +97,11 @@ function runOneShot(cli, ws, repo) {
97
97
  io.out(` ${style.dim("already up to date")}`);
98
98
  return EXIT_OK;
99
99
  }
100
- // Local live watcher (the former `omg watch`): prime with a sweep, then run the
101
- // external fs-adapter watcher until a signal, keeping embeddings warm.
100
+ // Local live watcher (the former `omg watch`), in the order of spec/sync §5:
101
+ // take the lease, open the source, `watch`, wait for `ready` (bounded), THEN the
102
+ // priming sweep, then report live — so an edit landing before the feed is primed
103
+ // is caught by the sweep and one landing after it by the feed. Runs until a
104
+ // signal, keeping embeddings warm.
102
105
  async function runLocalWatch(cli, ws, repo) {
103
106
  const lease = WatchLease.tryAcquire(ws.omgbaseDir);
104
107
  if (!lease)
@@ -124,8 +127,6 @@ async function runLocalWatch(cli, ws, repo) {
124
127
  onError: (err) => cli.io.err(cli.style.dim(` embed drain failed: ${String(err)}`)),
125
128
  })
126
129
  : null;
127
- if (repo.rootPath)
128
- freshnessSweep(ws.store, repo.repoId, repo.rootPath); // start fresh
129
130
  const source = await openRepoSource(ws.store, repo);
130
131
  if (!source) {
131
132
  cli.io.err(cli.style.dim(` ${repo.slug} has no filesystem source — nothing to watch`));
@@ -146,6 +147,11 @@ async function runLocalWatch(cli, ws, repo) {
146
147
  onError: (err) => cli.io.err(cli.style.err(` watch error: ${String(err)}`)),
147
148
  });
148
149
  await watcher.start();
150
+ if (!(await awaitReady(watcher.ready(), WATCH_READY_PATIENCE_MS))) {
151
+ cli.io.err(cli.style.dim(` the fs adapter did not report ready within ${WATCH_READY_PATIENCE_MS / 1000}s — proceeding as if ready (an edit made before now may be missed until the next sweep)`));
152
+ }
153
+ if (repo.rootPath)
154
+ freshnessSweep(ws.store, repo.repoId, repo.rootPath); // prime: anything that landed before the feed was ready
149
155
  drainer?.schedule(); // embed anything already stale at startup, in the background
150
156
  cli.io.err(cli.style.dim(` watching ${repo.slug} — Ctrl-C to stop${drainer ? " · auto-embed on" : ""}`));
151
157
  await new Promise((resolveWait) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omgbase",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Open Markdown Graph Base — CLI",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -45,9 +45,9 @@
45
45
  "omg": "./dist/src/main.js"
46
46
  },
47
47
  "dependencies": {
48
- "@omgbase/core": "^0.3.0",
49
- "@omgbase/fs-adapter": "^0.1.0",
50
- "@omgbase/sync": "^0.3.0",
48
+ "@omgbase/core": "^0.4.0",
49
+ "@omgbase/fs-adapter": "^0.2.0",
50
+ "@omgbase/sync": "^0.4.0",
51
51
  "yaml": "^2.9.0"
52
52
  },
53
53
  "devDependencies": {