@usegraft/cli 1.0.0-beta.2 → 1.0.0-beta.3

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.
@@ -6,6 +6,7 @@ import {
6
6
  // src/commands/asset.ts
7
7
  import { readFileSync, statSync } from "fs";
8
8
  import {
9
+ AssetKeyTakenError,
9
10
  contentTypeFor,
10
11
  createStorage,
11
12
  defaultKeyFor,
@@ -26,9 +27,28 @@ async function assetPutCommand(options) {
26
27
  details: { file: options.file }
27
28
  });
28
29
  }
29
- let storage;
30
+ const storage = options.storage ?? storageFromEnv();
31
+ const key = options.key ?? defaultKeyFor(options.file);
32
+ const keyTaken = () => new GraftError({
33
+ code: "ASSET_EXISTS",
34
+ message: `Asset key "${key}" already holds a binary.`,
35
+ fix: "Pick a distinct key (the store keeps no version history), or pass --overwrite if replacing the existing binary is the actual intent.",
36
+ details: { key }
37
+ });
38
+ const overwrite = options.overwrite === true;
39
+ if (!overwrite && await storage.exists(key)) throw keyTaken();
40
+ const contentType = contentTypeFor(options.file);
41
+ try {
42
+ await storage.put(key, body, contentType, { ifAbsent: !overwrite });
43
+ } catch (error) {
44
+ if (error instanceof AssetKeyTakenError) throw keyTaken();
45
+ throw error;
46
+ }
47
+ return { key, contentType, bytes: body.byteLength };
48
+ }
49
+ function storageFromEnv() {
30
50
  try {
31
- storage = createStorage(storageConfigFromEnv());
51
+ return createStorage(storageConfigFromEnv());
32
52
  } catch (error) {
33
53
  throw new GraftError({
34
54
  code: "ENV_VAR_MISSING",
@@ -37,10 +57,6 @@ async function assetPutCommand(options) {
37
57
  details: { variables: ["S3_ENDPOINT", "S3_ACCESS_KEY", "S3_SECRET_KEY", "S3_BUCKET"] }
38
58
  });
39
59
  }
40
- const key = options.key ?? defaultKeyFor(options.file);
41
- const contentType = contentTypeFor(options.file);
42
- await storage.put(key, body, contentType);
43
- return { key, contentType, bytes: body.byteLength };
44
60
  }
45
61
  export {
46
62
  assetPutCommand,
@@ -120,8 +120,34 @@ function localGitSha(cwd) {
120
120
  return void 0;
121
121
  }
122
122
  }
123
- function trustedIssuersFromEnv() {
124
- return (process.env.GRAFT_TRUSTED_ISSUERS ?? "").split(/[,\s]+/).map((issuer) => issuer.trim()).filter(Boolean).map((issuer) => ({ issuer }));
123
+ function listFromEnv(raw) {
124
+ return (raw ?? "").split(/[,\s]+/).map((item) => item.trim()).filter(Boolean);
125
+ }
126
+ function trustedIssuersFromEnv(env = process.env) {
127
+ const issuers = listFromEnv(env.GRAFT_TRUSTED_ISSUERS);
128
+ const audience = listFromEnv(env.GRAFT_TRUSTED_AUDIENCE);
129
+ if (env.GRAFT_TRUSTED_AUDIENCE?.trim() && audience.length === 0) {
130
+ throw new GraftError({
131
+ code: "INPUT_VALIDATION_FAILED",
132
+ message: `GRAFT_TRUSTED_AUDIENCE is "${env.GRAFT_TRUSTED_AUDIENCE}", which names no audience.`,
133
+ fix: "Set GRAFT_TRUSTED_AUDIENCE to the audience your provider puts in tokens for this API (comma-separated for several), or unset it.",
134
+ details: { variable: "GRAFT_TRUSTED_AUDIENCE", value: env.GRAFT_TRUSTED_AUDIENCE }
135
+ });
136
+ }
137
+ const kind = env.GRAFT_TRUSTED_ACTOR_KIND?.trim() || "agent";
138
+ if (kind !== "agent" && kind !== "human") {
139
+ throw new GraftError({
140
+ code: "INPUT_VALIDATION_FAILED",
141
+ message: `GRAFT_TRUSTED_ACTOR_KIND is "${kind}"; it must be "agent" or "human".`,
142
+ fix: "Set GRAFT_TRUSTED_ACTOR_KIND=human to record token holders as people, or unset it to record them as agents.",
143
+ details: { variable: "GRAFT_TRUSTED_ACTOR_KIND", value: kind }
144
+ });
145
+ }
146
+ return issuers.map((issuer) => {
147
+ const trusted = { issuer, actorKind: kind };
148
+ if (audience.length > 0) trusted.audience = audience;
149
+ return trusted;
150
+ });
125
151
  }
126
152
  async function startServe(options) {
127
153
  loadProjectEnv(options.cwd);
@@ -141,13 +167,13 @@ async function startServe(options) {
141
167
  import("@usegraft/db"),
142
168
  enableStudio ? import("@usegraft/studio") : Promise.resolve(null)
143
169
  ]);
170
+ const issuers = trustedIssuersFromEnv();
144
171
  const control = createDb(url);
145
172
  const branchName = options.branchId ?? "main";
146
173
  const branch = await resolveBranchHandle(control.db, branchName, { databaseUrl: url });
147
174
  const writeBranch = scopeWriteBranch(branch.scope);
148
175
  const devToken = process.env.GRAFT_DEV_TOKEN;
149
- const scopes = (process.env.GRAFT_DEV_SCOPES ?? "").split(/[,\s]+/).map((s) => s.trim()).filter(Boolean);
150
- const issuers = trustedIssuersFromEnv();
176
+ const scopes = listFromEnv(process.env.GRAFT_DEV_SCOPES);
151
177
  const resolveActor = createActorResolver({
152
178
  issuers,
153
179
  devTokens: devToken ? { [devToken]: { kind: "agent", id: "graft-serve", scopes } } : void 0
@@ -244,6 +270,11 @@ async function startServe(options) {
244
270
  "[graft serve] WARNING: binding beyond loopback with no identity configured \u2014 MCP will refuse every caller, because there is nothing to authenticate them against. Set GRAFT_DEV_TOKEN or GRAFT_TRUSTED_ISSUERS."
245
271
  );
246
272
  }
273
+ if (issuers.length > 0 && issuers.every((issuer) => issuer.audience === void 0)) {
274
+ console.warn(
275
+ "[graft serve] WARNING: GRAFT_TRUSTED_ISSUERS is set without GRAFT_TRUSTED_AUDIENCE \u2014 any token those issuers sign is accepted, including one minted for another of your apps. Set GRAFT_TRUSTED_AUDIENCE to the audience your provider puts in tokens for this API."
276
+ );
277
+ }
247
278
  if (approvalPolicy === "unattended") {
248
279
  console.warn(
249
280
  '[graft serve] WARNING: approvalPolicy = "unattended" in graft.config.ts \u2014 destructive functions run without human approval. Deletes are not recoverable from git: deleteRecord removes rows outright and the asset store keeps no history. Audit rows are still written. This applies to POST /api/fn only; run_function over /api/mcp stays gated, because the policy is not part of the MCP surface.'
@@ -330,6 +361,7 @@ export {
330
361
  createServeRouter,
331
362
  allowedHostsFor,
332
363
  createNodeListener,
364
+ trustedIssuersFromEnv,
333
365
  startServe,
334
366
  serveCommand
335
367
  };
@@ -15,8 +15,18 @@ function formatCompileResult(result) {
15
15
  for (const key of removed) lines.push(` removed ${key}`);
16
16
  return lines.join("\n");
17
17
  }
18
+ function formatCompileJson(branch, result) {
19
+ const { added, changed, removed, unchanged } = result.changes;
20
+ const output = {
21
+ branch,
22
+ gitSha: result.gitSha,
23
+ changes: { added, changed, removed, unchanged }
24
+ };
25
+ return JSON.stringify(output);
26
+ }
18
27
 
19
28
  export {
20
29
  printGraftError,
21
- formatCompileResult
30
+ formatCompileResult,
31
+ formatCompileJson
22
32
  };
@@ -66,13 +66,14 @@ Author one as migrations/0001-<name>.ts default-exporting defineContentMigration
66
66
  listAppliedMigrations,
67
67
  recordAppliedMigration,
68
68
  resolveBranchHandle,
69
- scopeWriteBranch
69
+ scopeWriteBranch,
70
+ withMigrationLock
70
71
  }
71
72
  ] = await Promise.all([import("@usegraft/compiler"), import("@usegraft/db")]);
72
73
  const control = createDb(url);
73
74
  const branch = await resolveBranchHandle(control.db, branchId, { databaseUrl: url });
74
75
  const writeBranch = scopeWriteBranch(branch.scope);
75
- try {
76
+ const run = async () => {
76
77
  const appliedRows = await listAppliedMigrations(branch.db, writeBranch);
77
78
  const appliedIds = new Set(appliedRows.map((row) => row.migrationId));
78
79
  const applied = migrations.filter((m) => appliedIds.has(m.id)).map((m) => m.id);
@@ -161,6 +162,15 @@ ${outcomes.length} migration(s) applied. Commit the changes \u2014 git is the ve
161
162
  );
162
163
  }
163
164
  return { applied, pending: outcomes, didApply: options.apply === true };
165
+ };
166
+ try {
167
+ if (!options.apply) return await run();
168
+ return await withMigrationLock(branch.db, writeBranch, run, {
169
+ onWait: () => (
170
+ // A merge into this branch holds the same lock, so name both.
171
+ console.log(`A migration or merge is running on "${branchId}"; waiting for it to finish\u2026`)
172
+ )
173
+ });
164
174
  } finally {
165
175
  await branch.close();
166
176
  await control.close();
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ formatCompileJson,
3
4
  formatCompileResult
4
- } from "./chunk-GPA66AP5.js";
5
+ } from "./chunk-NGTFEGKI.js";
5
6
  import {
6
7
  findConfig,
7
8
  loadConfig,
@@ -21,6 +22,22 @@ function assertNoStaticBranch(branchId) {
21
22
  });
22
23
  }
23
24
  async function compileCommand(options) {
25
+ if (!options.json) return (await runCompile(options)).result;
26
+ const { log, info, debug } = console;
27
+ const write = process.stdout.write;
28
+ console.log = console.info = console.debug = console.error;
29
+ process.stdout.write = process.stderr.write.bind(process.stderr);
30
+ let compiled;
31
+ try {
32
+ compiled = await runCompile(options);
33
+ } finally {
34
+ process.stdout.write = write;
35
+ Object.assign(console, { log, info, debug });
36
+ }
37
+ console.log(formatCompileJson(compiled.branch, compiled.result));
38
+ return compiled.result;
39
+ }
40
+ async function runCompile(options) {
24
41
  loadProjectEnv(options.cwd);
25
42
  const config = await loadConfig(findConfig(options.cwd));
26
43
  if (config.index.driver === "static") {
@@ -33,7 +50,7 @@ async function compileCommand(options) {
33
50
  indexPath: config.index.path
34
51
  });
35
52
  console.log(formatCompileResult(result));
36
- return result;
53
+ return { branch: "main", result };
37
54
  }
38
55
  const url = requireDatabaseUrl();
39
56
  const [{ compile }, { createDb, resolveBranchHandle, scopeWriteBranch }] = await Promise.all([
@@ -58,7 +75,7 @@ async function compileCommand(options) {
58
75
  pruneUnknown: options.pruneUnknown
59
76
  });
60
77
  console.log(formatCompileResult(result));
61
- return result;
78
+ return { branch: branch.name, result };
62
79
  } finally {
63
80
  await branch.close();
64
81
  }
@@ -2,8 +2,8 @@
2
2
  import {
3
3
  assertNoStaticBranch,
4
4
  compileCommand
5
- } from "./chunk-2N3HXYNN.js";
6
- import "./chunk-GPA66AP5.js";
5
+ } from "./chunk-QKZ25AA5.js";
6
+ import "./chunk-NGTFEGKI.js";
7
7
  import "./chunk-2LG6TWYY.js";
8
8
  export {
9
9
  assertNoStaticBranch,
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  assertNoStaticBranch
4
- } from "./chunk-2N3HXYNN.js";
4
+ } from "./chunk-QKZ25AA5.js";
5
5
  import {
6
6
  formatCompileResult,
7
7
  printGraftError
8
- } from "./chunk-GPA66AP5.js";
8
+ } from "./chunk-NGTFEGKI.js";
9
9
  import {
10
10
  CONFIG_FILENAMES,
11
11
  findConfig,
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  printGraftError
4
- } from "./chunk-GPA66AP5.js";
4
+ } from "./chunk-NGTFEGKI.js";
5
5
 
6
6
  // src/cli.ts
7
7
  import { createRequire } from "module";
@@ -35,7 +35,7 @@ function printHelp() {
35
35
  " add <item> Copy an owned primitive from the registry into graft/",
36
36
  " (+ its deps; regenerates the graft/ barrel \u2014 no config edit)",
37
37
  " mcp Serve the project MCP over stdio (content + function tools;",
38
- " for .mcp.json / local agents). Requires DATABASE_URL.",
38
+ " for .mcp.json / local agents). Postgres needs DATABASE_URL.",
39
39
  " serve Run the headless Graft runtime over HTTP: POST /api/fn/<name>,",
40
40
  " POST /api/mcp, GET /healthz (what a self-host container runs)",
41
41
  " studio Opt-in Studio UI (edit content, approve/deny, OpenAPI)",
@@ -62,14 +62,212 @@ function printHelp() {
62
62
  " --postgres Scaffold `graft init` for the Postgres index (default: static)",
63
63
  " --static Scaffold `graft init` for the static index (the default)",
64
64
  " --dry-run Preview `graft add` without writing; list pending `graft db migrate`",
65
- " --overwrite Let `graft add` replace files that differ",
65
+ " --overwrite Let `graft add` replace files that differ, and `graft asset put`",
66
+ " replace a binary already stored under the key",
66
67
  " --prune-unknown Let `graft compile` remove index rows in collections this schema",
67
68
  " doesn't know (default: refuse \u2014 the shared-DATABASE_URL guard)",
68
- " -h, --help Show this help",
69
+ " --json `graft compile` prints { branch, gitSha, changes } as JSON on stdout",
70
+ " (logs go to stderr), ready to POST to a revalidate route",
71
+ " -h, --help Show this help, or one command's help after the command name",
69
72
  " -v, --version Show version"
70
73
  ];
71
74
  console.log(lines.join("\n"));
72
75
  }
76
+ var BRANCH_OPTION = " --branch <id> Content branch (default: main)";
77
+ var COMMAND_HELP = /* @__PURE__ */ new Map([
78
+ [
79
+ "init",
80
+ [
81
+ "Usage: graft init [dir] [--static | --postgres]",
82
+ "",
83
+ "Scaffold a Graft project (graft.config.ts, content/, llms.txt) in dir (default: here).",
84
+ "",
85
+ " --static Static index: no database, no env (the default)",
86
+ " --postgres Postgres index: operational data, functions, DB branching"
87
+ ]
88
+ ],
89
+ [
90
+ "compile",
91
+ [
92
+ "Usage: graft compile [--branch <id>] [--prune-unknown] [--json]",
93
+ "",
94
+ "Project the content tree into the content index once.",
95
+ "",
96
+ BRANCH_OPTION,
97
+ " --prune-unknown Remove index rows in collections this schema doesn't know",
98
+ " (default: refuse, the shared-DATABASE_URL guard)",
99
+ " --json Print { branch, gitSha, changes } as JSON on stdout (logs go to stderr)"
100
+ ]
101
+ ],
102
+ [
103
+ "dev",
104
+ [
105
+ "Usage: graft dev [--branch <id>]",
106
+ "",
107
+ "Watch content/ and graft.config.ts and recompile on change.",
108
+ "",
109
+ BRANCH_OPTION
110
+ ]
111
+ ],
112
+ [
113
+ "db",
114
+ [
115
+ "Usage: graft db migrate [--dry-run]",
116
+ "",
117
+ "Apply the Postgres schema migrations that ship with @usegraft/db.",
118
+ "",
119
+ " --dry-run List pending schema migrations without applying them"
120
+ ]
121
+ ],
122
+ [
123
+ "asset",
124
+ [
125
+ "Usage: graft asset put <file> [key] [--overwrite]",
126
+ "",
127
+ "Upload a binary to the asset store (S3_* env) and print the frontmatter reference.",
128
+ "The key defaults to assets/<sanitized file name>. A key that already holds a",
129
+ "binary is refused with ASSET_EXISTS, because the store keeps no version history.",
130
+ "",
131
+ " --overwrite Replace the binary already stored under the key"
132
+ ]
133
+ ],
134
+ [
135
+ "approvals",
136
+ [
137
+ "Usage: graft approvals",
138
+ "",
139
+ "List pending approvals for human-gated (destructive) function calls."
140
+ ]
141
+ ],
142
+ [
143
+ "approve",
144
+ [
145
+ "Usage: graft approve <approval-id>",
146
+ "",
147
+ "Approve a pending approval. The caller retries with x-graft-approval: <id>."
148
+ ]
149
+ ],
150
+ ["deny", ["Usage: graft deny <approval-id>", "", "Deny a pending approval."]],
151
+ [
152
+ "migrate",
153
+ [
154
+ "Usage: graft migrate [--apply] [--branch <id>]",
155
+ "",
156
+ "Show pending content and data migrations (dry-run). --apply runs them and records",
157
+ "each in the ledger. A concurrent --apply on the same branch waits for this one",
158
+ "and then skips what it applied.",
159
+ "",
160
+ BRANCH_OPTION,
161
+ " --apply Run the pending migrations (default is a dry-run report)"
162
+ ]
163
+ ],
164
+ [
165
+ "branch",
166
+ [
167
+ "Usage: graft branch",
168
+ " graft branch create <name> [--from <parent>] [--backend overlay|neon]",
169
+ " graft branch drop <name>",
170
+ "",
171
+ "List, register, or drop preview branches.",
172
+ "",
173
+ " --from <name> Parent to fork from (default: main)",
174
+ " --backend <kind> overlay (default) or neon (forks a physical Neon branch)"
175
+ ]
176
+ ],
177
+ [
178
+ "merge",
179
+ [
180
+ "Usage: graft merge <branch> [--into <target>] [--apply]",
181
+ "",
182
+ "Merge a branch into its target: replay the ledger, move data rows, recompile.",
183
+ "",
184
+ " --into <name> Merge target (default: main)",
185
+ " --apply Execute the merge (default is a dry-run report)"
186
+ ]
187
+ ],
188
+ [
189
+ "add",
190
+ [
191
+ "Usage: graft add <item...> [--dry-run] [--overwrite]",
192
+ "",
193
+ "Copy an owned primitive (and its deps) from the registry into graft/.",
194
+ "",
195
+ " --dry-run Preview without writing",
196
+ " --overwrite Replace files that differ"
197
+ ]
198
+ ],
199
+ [
200
+ "mcp",
201
+ [
202
+ "Usage: graft mcp [--branch <id>] [--elicit-approvals]",
203
+ "",
204
+ "Serve the project MCP over stdio (content and function tools). On the Postgres engine it needs DATABASE_URL.",
205
+ "",
206
+ BRANCH_OPTION,
207
+ " --elicit-approvals",
208
+ " Ask this terminal's operator to decide a destructive call instead",
209
+ " of failing with an id for `graft approve`"
210
+ ]
211
+ ],
212
+ [
213
+ "serve",
214
+ [
215
+ "Usage: graft serve [--port <n>] [--host <h>] [--branch <id>] [--studio]",
216
+ "",
217
+ "Run the headless Graft runtime over HTTP: POST /api/fn/<name>, POST /api/mcp,",
218
+ "GET /api/content/v1/*, GET /healthz.",
219
+ "",
220
+ BRANCH_OPTION,
221
+ " --port <n> Port (default: 3903, or PORT)",
222
+ " --host <h> Host (default: 127.0.0.1, or HOST)",
223
+ " --studio Mount Studio at /studio (or GRAFT_STUDIO=1)"
224
+ ]
225
+ ],
226
+ [
227
+ "studio",
228
+ [
229
+ "Usage: graft studio [--port <n>] [--host <h>] [--branch <id>]",
230
+ "",
231
+ "Run the opt-in Studio UI (edit content, approve/deny, OpenAPI).",
232
+ "",
233
+ BRANCH_OPTION,
234
+ " --port <n> Port (default: 4983, or GRAFT_STUDIO_PORT)",
235
+ " --host <h> Host (default: 127.0.0.1, or HOST)"
236
+ ]
237
+ ],
238
+ [
239
+ "content",
240
+ [
241
+ "Usage: graft content [--branch <id>]",
242
+ "",
243
+ "List the content tree from the compiled index.",
244
+ "",
245
+ BRANCH_OPTION
246
+ ]
247
+ ],
248
+ [
249
+ "compilations",
250
+ [
251
+ "Usage: graft compilations [--branch <id>]",
252
+ "",
253
+ "List recent content projection trail rows.",
254
+ "",
255
+ BRANCH_OPTION
256
+ ]
257
+ ],
258
+ [
259
+ "harden",
260
+ [
261
+ "Usage: graft harden <role>",
262
+ "",
263
+ "Grant an existing Postgres role the runtime privilege set: it can request and",
264
+ "consume approvals but never decide them."
265
+ ]
266
+ ]
267
+ ]);
268
+ function isHelpFlag(arg) {
269
+ return arg === "--help" || arg === "-h";
270
+ }
73
271
  var UsageError = class extends Error {
74
272
  };
75
273
  function parseArgs(rest) {
@@ -86,6 +284,7 @@ function parseArgs(rest) {
86
284
  let pruneUnknown = false;
87
285
  let studio = false;
88
286
  let elicitApprovals = false;
287
+ let json = false;
89
288
  let initDriver;
90
289
  const value = (flag, raw) => {
91
290
  if (!raw || raw.startsWith("-")) {
@@ -123,10 +322,13 @@ function parseArgs(rest) {
123
322
  studio = true;
124
323
  } else if (arg === "--elicit-approvals") {
125
324
  elicitApprovals = true;
325
+ } else if (arg === "--json") {
326
+ json = true;
126
327
  } else if (arg === "--postgres") {
127
328
  initDriver = "postgres";
128
329
  } else if (arg === "--static") {
129
330
  initDriver = "static";
331
+ } else if (isHelpFlag(arg)) {
130
332
  } else if (arg.startsWith("-")) {
131
333
  throw new UsageError(`unknown option "${arg}"`);
132
334
  } else {
@@ -147,6 +349,7 @@ function parseArgs(rest) {
147
349
  pruneUnknown,
148
350
  studio,
149
351
  elicitApprovals,
352
+ json,
150
353
  initDriver
151
354
  };
152
355
  }
@@ -157,10 +360,15 @@ async function run(argv, options = {}) {
157
360
  console.log(VERSION);
158
361
  return 0;
159
362
  }
160
- if (!command || command === "-h" || command === "--help") {
363
+ if (!command || isHelpFlag(command)) {
161
364
  printHelp();
162
365
  return 0;
163
366
  }
367
+ const usage = COMMAND_HELP.get(command);
368
+ if (usage && rest.some(isHelpFlag)) {
369
+ console.log(usage.join("\n"));
370
+ return 0;
371
+ }
164
372
  try {
165
373
  const args = parseArgs(rest);
166
374
  switch (command) {
@@ -196,12 +404,17 @@ async function run(argv, options = {}) {
196
404
  return 0;
197
405
  }
198
406
  case "compile": {
199
- const { compileCommand } = await import("./compile-AWPSEA7U.js");
200
- await compileCommand({ cwd, branchId: args.branchId, pruneUnknown: args.pruneUnknown });
407
+ const { compileCommand } = await import("./compile-TOEGDGNU.js");
408
+ await compileCommand({
409
+ cwd,
410
+ branchId: args.branchId,
411
+ pruneUnknown: args.pruneUnknown,
412
+ json: args.json
413
+ });
201
414
  return 0;
202
415
  }
203
416
  case "dev": {
204
- const { devCommand } = await import("./dev-WHTIRWMR.js");
417
+ const { devCommand } = await import("./dev-LOELDVLT.js");
205
418
  await devCommand({ cwd, branchId: args.branchId });
206
419
  return 0;
207
420
  }
@@ -220,10 +433,10 @@ async function run(argv, options = {}) {
220
433
  case "asset": {
221
434
  const [subcommand, file, key] = args.positionals;
222
435
  if (subcommand !== "put" || !file) {
223
- throw new UsageError("usage: graft asset put <file> [key]");
436
+ throw new UsageError("usage: graft asset put <file> [key] [--overwrite]");
224
437
  }
225
- const { assetPutCommand } = await import("./asset-JWER6AOG.js");
226
- const result = await assetPutCommand({ cwd, file, key });
438
+ const { assetPutCommand } = await import("./asset-CW4YLLHI.js");
439
+ const result = await assetPutCommand({ cwd, file, key, overwrite: args.overwrite });
227
440
  console.log(`Uploaded ${result.key} (${result.contentType}, ${result.bytes} bytes)`);
228
441
  console.log(
229
442
  [
@@ -268,7 +481,7 @@ async function run(argv, options = {}) {
268
481
  return 0;
269
482
  }
270
483
  case "migrate": {
271
- const { migrateCommand } = await import("./migrate-6ZYG2H5C.js");
484
+ const { migrateCommand } = await import("./migrate-JK5RLOOI.js");
272
485
  await migrateCommand({ cwd, branchId: args.branchId, apply: args.apply });
273
486
  return 0;
274
487
  }
@@ -325,7 +538,7 @@ async function run(argv, options = {}) {
325
538
  const branch = args.positionals[0];
326
539
  if (!branch)
327
540
  throw new UsageError("usage: graft merge <branch> [--into <target>] [--apply]");
328
- const { mergeCommand } = await import("./merge-ZWYZJVFE.js");
541
+ const { mergeCommand } = await import("./merge-JMJYARB4.js");
329
542
  await mergeCommand({ cwd, branch, into: args.into, apply: args.apply });
330
543
  return 0;
331
544
  }
@@ -340,12 +553,12 @@ async function run(argv, options = {}) {
340
553
  return 0;
341
554
  }
342
555
  case "mcp": {
343
- const { mcpCommand } = await import("./mcp-77QFFJXK.js");
556
+ const { mcpCommand } = await import("./mcp-7RP4G34K.js");
344
557
  await mcpCommand({ cwd, branchId: args.branchId, elicitApprovals: args.elicitApprovals });
345
558
  return 0;
346
559
  }
347
560
  case "serve": {
348
- const { serveCommand } = await import("./serve-OP4OPK37.js");
561
+ const { serveCommand } = await import("./serve-2FRRKKGY.js");
349
562
  await serveCommand({
350
563
  cwd,
351
564
  branchId: args.branchId,
@@ -356,7 +569,7 @@ async function run(argv, options = {}) {
356
569
  return 0;
357
570
  }
358
571
  case "studio": {
359
- const { studioCommand } = await import("./studio-HUGOTZOY.js");
572
+ const { studioCommand } = await import("./studio-GJB5NFPB.js");
360
573
  await studioCommand({
361
574
  cwd,
362
575
  branchId: args.branchId,
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  assertNoStaticBranch
4
- } from "./chunk-2N3HXYNN.js";
5
- import "./chunk-GPA66AP5.js";
4
+ } from "./chunk-QKZ25AA5.js";
5
+ import "./chunk-NGTFEGKI.js";
6
6
  import {
7
7
  operatorName
8
8
  } from "./chunk-NBJ4N4O6.js";
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  discoverMigrations
4
- } from "./chunk-DW6WTDRX.js";
4
+ } from "./chunk-OUS3SWFF.js";
5
5
  import {
6
6
  findConfig,
7
7
  loadConfig,
@@ -48,7 +48,8 @@ async function mergeCommand(options) {
48
48
  moveDataRecords,
49
49
  copyDataRecords,
50
50
  resolveBranchHandle,
51
- scopeWriteBranch
51
+ scopeWriteBranch,
52
+ withMigrationLock
52
53
  },
53
54
  { runDataMigration }
54
55
  ] = await Promise.all([
@@ -70,7 +71,7 @@ async function mergeCommand(options) {
70
71
  const srcWrite = scopeWriteBranch(src.scope);
71
72
  const targetWrite = scopeWriteBranch(target.scope);
72
73
  const sameDb = src.scope.kind === "overlay" && target.scope.kind === "overlay";
73
- try {
74
+ const run = async () => {
74
75
  const [branchLedger, targetLedger] = await Promise.all([
75
76
  listAppliedMigrations(src.db, srcWrite),
76
77
  listAppliedMigrations(target.db, targetWrite)
@@ -164,6 +165,20 @@ Dry run \u2014 nothing was written. git-merge the branch's commits first if you
164
165
  );
165
166
  }
166
167
  return { replayed, dataMoved, compiled, didApply: options.apply === true };
168
+ };
169
+ try {
170
+ if (!options.apply) return await run();
171
+ const [first, second] = [
172
+ { db: src.db, branch: srcWrite, name: options.branch },
173
+ { db: target.db, branch: targetWrite, name: into }
174
+ ].sort((a, b) => a.name.localeCompare(b.name));
175
+ const waitFor = (name) => () => console.log(`A migration or merge is running on "${name}"; waiting for it to finish\u2026`);
176
+ return await withMigrationLock(
177
+ first.db,
178
+ first.branch,
179
+ () => withMigrationLock(second.db, second.branch, run, { onWait: waitFor(second.name) }),
180
+ { onWait: waitFor(first.name) }
181
+ );
167
182
  } finally {
168
183
  await src.close();
169
184
  await target.close();
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  discoverMigrations,
4
4
  migrateCommand
5
- } from "./chunk-DW6WTDRX.js";
5
+ } from "./chunk-OUS3SWFF.js";
6
6
  import "./chunk-2LG6TWYY.js";
7
7
  export {
8
8
  discoverMigrations,
@@ -4,13 +4,15 @@ import {
4
4
  createNodeListener,
5
5
  createServeRouter,
6
6
  serveCommand,
7
- startServe
8
- } from "./chunk-PSJ2OBWG.js";
7
+ startServe,
8
+ trustedIssuersFromEnv
9
+ } from "./chunk-3MR73KWS.js";
9
10
  import "./chunk-2LG6TWYY.js";
10
11
  export {
11
12
  allowedHostsFor,
12
13
  createNodeListener,
13
14
  createServeRouter,
14
15
  serveCommand,
15
- startServe
16
+ startServe,
17
+ trustedIssuersFromEnv
16
18
  };
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  allowedHostsFor,
4
4
  createNodeListener
5
- } from "./chunk-PSJ2OBWG.js";
5
+ } from "./chunk-3MR73KWS.js";
6
6
  import {
7
7
  findConfig,
8
8
  loadConfig,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usegraft/cli",
3
- "version": "1.0.0-beta.2",
3
+ "version": "1.0.0-beta.3",
4
4
  "description": "The graft CLI: scaffold, compile authored content, serve the runtime, and split your database credentials.",
5
5
  "keywords": [
6
6
  "agent",
@@ -34,22 +34,22 @@
34
34
  "access": "public"
35
35
  },
36
36
  "dependencies": {
37
- "@usegraft/assets": "1.0.0-beta.2",
38
- "@usegraft/auth": "1.0.0-beta.2",
39
- "@usegraft/compiler": "1.0.0-beta.2",
40
- "@usegraft/content-api": "1.0.0-beta.2",
41
- "@usegraft/content-migrations": "1.0.0-beta.2",
42
- "@usegraft/contracts": "1.0.0-beta.2",
43
- "@usegraft/core": "1.0.0-beta.2",
44
- "@usegraft/db": "1.0.0-beta.2",
45
- "@usegraft/mcp": "1.0.0-beta.2",
46
- "@usegraft/mdx-safety": "1.0.0-beta.2",
47
- "@usegraft/registry": "1.0.0-beta.2",
48
- "@usegraft/studio": "1.0.0-beta.2",
37
+ "@usegraft/assets": "1.0.0-beta.3",
38
+ "@usegraft/auth": "1.0.0-beta.3",
39
+ "@usegraft/compiler": "1.0.0-beta.3",
40
+ "@usegraft/content-api": "1.0.0-beta.3",
41
+ "@usegraft/content-migrations": "1.0.0-beta.3",
42
+ "@usegraft/contracts": "1.0.0-beta.3",
43
+ "@usegraft/core": "1.0.0-beta.3",
44
+ "@usegraft/db": "1.0.0-beta.3",
45
+ "@usegraft/mcp": "1.0.0-beta.3",
46
+ "@usegraft/mdx-safety": "1.0.0-beta.3",
47
+ "@usegraft/registry": "1.0.0-beta.3",
48
+ "@usegraft/studio": "1.0.0-beta.3",
49
49
  "jiti": "^2.7.0"
50
50
  },
51
51
  "devDependencies": {
52
- "@usegraft/sdk-core": "1.0.0-beta.2"
52
+ "@usegraft/sdk-core": "1.0.0-beta.3"
53
53
  },
54
54
  "engines": {
55
55
  "node": ">=22.16"