@usegraft/cli 1.0.0-beta.1 → 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.
- package/dist/{asset-JWER6AOG.js → asset-CW4YLLHI.js} +22 -6
- package/dist/{chunk-CONTLKSZ.js → chunk-3MR73KWS.js} +42 -13
- package/dist/{chunk-GPA66AP5.js → chunk-NGTFEGKI.js} +11 -1
- package/dist/{chunk-DW6WTDRX.js → chunk-OUS3SWFF.js} +12 -2
- package/dist/{chunk-2N3HXYNN.js → chunk-QKZ25AA5.js} +20 -3
- package/dist/{compile-AWPSEA7U.js → compile-TOEGDGNU.js} +2 -2
- package/dist/{dev-WHTIRWMR.js → dev-LOELDVLT.js} +2 -2
- package/dist/index.js +229 -16
- package/dist/{mcp-77QFFJXK.js → mcp-7RP4G34K.js} +2 -2
- package/dist/{merge-ZWYZJVFE.js → merge-JMJYARB4.js} +18 -3
- package/dist/{migrate-6ZYG2H5C.js → migrate-JK5RLOOI.js} +1 -1
- package/dist/{serve-5ADUQG66.js → serve-2FRRKKGY.js} +5 -3
- package/dist/studio-GJB5NFPB.js +166 -0
- package/package.json +14 -14
- package/dist/studio-7VAYFEWS.js +0 -109
|
@@ -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
|
-
|
|
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
|
-
|
|
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,
|
|
@@ -102,7 +102,7 @@ function createNodeListener(handler, options = {}) {
|
|
|
102
102
|
res.end(
|
|
103
103
|
JSON.stringify({
|
|
104
104
|
error: "FUNCTION_EXECUTION_FAILED",
|
|
105
|
-
message:
|
|
105
|
+
message: "graft serve failed to relay the request.",
|
|
106
106
|
fix: "Check the server logs; if this reproduces, file it \u2014 the adapter should never throw."
|
|
107
107
|
})
|
|
108
108
|
);
|
|
@@ -120,8 +120,34 @@ function localGitSha(cwd) {
|
|
|
120
120
|
return void 0;
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
|
-
function
|
|
124
|
-
return (
|
|
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
|
|
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
|
|
@@ -208,14 +234,11 @@ async function startServe(options) {
|
|
|
208
234
|
const health = async () => {
|
|
209
235
|
try {
|
|
210
236
|
await branch.db.execute(sql`select 1`);
|
|
211
|
-
} catch
|
|
212
|
-
return new Response(
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}),
|
|
217
|
-
{ status: 503, headers: { "content-type": "application/json" } }
|
|
218
|
-
);
|
|
237
|
+
} catch {
|
|
238
|
+
return new Response(JSON.stringify({ ok: false, error: "database_unavailable" }), {
|
|
239
|
+
status: 503,
|
|
240
|
+
headers: { "content-type": "application/json" }
|
|
241
|
+
});
|
|
219
242
|
}
|
|
220
243
|
return new Response(
|
|
221
244
|
JSON.stringify({
|
|
@@ -247,6 +270,11 @@ async function startServe(options) {
|
|
|
247
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."
|
|
248
271
|
);
|
|
249
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
|
+
}
|
|
250
278
|
if (approvalPolicy === "unattended") {
|
|
251
279
|
console.warn(
|
|
252
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.'
|
|
@@ -333,6 +361,7 @@ export {
|
|
|
333
361
|
createServeRouter,
|
|
334
362
|
allowedHostsFor,
|
|
335
363
|
createNodeListener,
|
|
364
|
+
trustedIssuersFromEnv,
|
|
336
365
|
startServe,
|
|
337
366
|
serveCommand
|
|
338
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
|
-
|
|
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-
|
|
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
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
assertNoStaticBranch
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-QKZ25AA5.js";
|
|
5
5
|
import {
|
|
6
6
|
formatCompileResult,
|
|
7
7
|
printGraftError
|
|
8
|
-
} from "./chunk-
|
|
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-
|
|
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).
|
|
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
|
-
"
|
|
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
|
|
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-
|
|
200
|
-
await compileCommand({
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
572
|
+
const { studioCommand } = await import("./studio-GJB5NFPB.js");
|
|
360
573
|
await studioCommand({
|
|
361
574
|
cwd,
|
|
362
575
|
branchId: args.branchId,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
discoverMigrations
|
|
4
|
-
} from "./chunk-
|
|
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
|
-
|
|
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();
|
|
@@ -4,13 +4,15 @@ import {
|
|
|
4
4
|
createNodeListener,
|
|
5
5
|
createServeRouter,
|
|
6
6
|
serveCommand,
|
|
7
|
-
startServe
|
|
8
|
-
|
|
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
|
};
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
allowedHostsFor,
|
|
4
|
+
createNodeListener
|
|
5
|
+
} from "./chunk-3MR73KWS.js";
|
|
6
|
+
import {
|
|
7
|
+
findConfig,
|
|
8
|
+
loadConfig,
|
|
9
|
+
loadProjectEnv,
|
|
10
|
+
requireDatabaseUrl
|
|
11
|
+
} from "./chunk-2LG6TWYY.js";
|
|
12
|
+
|
|
13
|
+
// src/commands/studio.ts
|
|
14
|
+
import { execFile } from "child_process";
|
|
15
|
+
import { createServer } from "http";
|
|
16
|
+
import { userInfo } from "os";
|
|
17
|
+
import { GraftError } from "@usegraft/contracts";
|
|
18
|
+
function isLoopback(host) {
|
|
19
|
+
return host === "127.0.0.1" || host === "localhost" || host === "::1";
|
|
20
|
+
}
|
|
21
|
+
function operatorIdentity() {
|
|
22
|
+
try {
|
|
23
|
+
return { kind: "human", id: userInfo().username };
|
|
24
|
+
} catch {
|
|
25
|
+
return { kind: "human", id: process.env.USERNAME ?? process.env.USER ?? "operator" };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function isGitPreviewBranch(branch) {
|
|
29
|
+
if (branch === "" || branch === "@") return false;
|
|
30
|
+
if (branch.includes("..") || branch.includes("@{") || branch.includes("//") || branch.includes("\\")) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
if (branch.startsWith("/") || branch.endsWith("/") || branch.endsWith(".")) return false;
|
|
34
|
+
if (!/^[A-Za-z0-9._/@+-]+$/.test(branch)) return false;
|
|
35
|
+
return branch.split("/").every((part) => part.length > 0 && !part.startsWith(".") && !part.endsWith(".lock"));
|
|
36
|
+
}
|
|
37
|
+
function assertGitPreviewBranch(branch) {
|
|
38
|
+
if (isGitPreviewBranch(branch)) return;
|
|
39
|
+
throw new GraftError({
|
|
40
|
+
code: "INPUT_VALIDATION_FAILED",
|
|
41
|
+
message: `Branch "${branch}" is not a safe preview query value.`,
|
|
42
|
+
fix: "Use a git-ref-shaped branch name (letters, digits, . _ / @ + -)."
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
function studioPreviewUrl(port, branch) {
|
|
46
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
47
|
+
throw new GraftError({
|
|
48
|
+
code: "INPUT_VALIDATION_FAILED",
|
|
49
|
+
message: `"${port}" is not a valid bound port.`,
|
|
50
|
+
fix: "Pass --port <1-65535> (0 picks a free port, then we read the one that bound)."
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
assertGitPreviewBranch(branch);
|
|
54
|
+
const preview = new URL(`http://127.0.0.1:${port}/`);
|
|
55
|
+
preview.searchParams.set("branch", branch);
|
|
56
|
+
if (preview.protocol !== "http:" || preview.hostname !== "127.0.0.1") {
|
|
57
|
+
throw new GraftError({
|
|
58
|
+
code: "INPUT_VALIDATION_FAILED",
|
|
59
|
+
message: `Preview URL is not loopback http.`,
|
|
60
|
+
fix: "graft studio only opens http://127.0.0.1."
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return preview.href;
|
|
64
|
+
}
|
|
65
|
+
function openBrowser(url) {
|
|
66
|
+
let parsed;
|
|
67
|
+
try {
|
|
68
|
+
parsed = new URL(url);
|
|
69
|
+
} catch {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (parsed.protocol !== "http:" || parsed.hostname !== "127.0.0.1") return;
|
|
73
|
+
const platform = process.platform;
|
|
74
|
+
const cmd = platform === "win32" ? "cmd" : platform === "darwin" ? "open" : "xdg-open";
|
|
75
|
+
const args = platform === "win32" ? ["/c", "start", "", parsed.href] : [parsed.href];
|
|
76
|
+
execFile(cmd, args, () => {
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
async function studioCommand(options) {
|
|
80
|
+
loadProjectEnv(options.cwd);
|
|
81
|
+
const config = await loadConfig(findConfig(options.cwd));
|
|
82
|
+
const url = requireDatabaseUrl();
|
|
83
|
+
const [{ createStudioHandler }, { createDb, resolveBranchHandle, scopeWriteBranch }] = await Promise.all([import("@usegraft/studio"), import("@usegraft/db")]);
|
|
84
|
+
const branchName = options.branchId ?? "main";
|
|
85
|
+
assertGitPreviewBranch(branchName);
|
|
86
|
+
const control = createDb(url);
|
|
87
|
+
try {
|
|
88
|
+
const branch = await resolveBranchHandle(control.db, branchName, { databaseUrl: url });
|
|
89
|
+
const writeBranch = scopeWriteBranch(branch.scope);
|
|
90
|
+
let server;
|
|
91
|
+
try {
|
|
92
|
+
const host = options.host ?? process.env.HOST ?? "127.0.0.1";
|
|
93
|
+
const requestedPort = options.port ?? Number(process.env.GRAFT_STUDIO_PORT ?? 4983);
|
|
94
|
+
if (!Number.isInteger(requestedPort) || requestedPort < 0 || requestedPort > 65535) {
|
|
95
|
+
throw new GraftError({
|
|
96
|
+
code: "INPUT_VALIDATION_FAILED",
|
|
97
|
+
message: `"${options.port ?? process.env.GRAFT_STUDIO_PORT}" is not a valid port.`,
|
|
98
|
+
fix: "Pass --port <0-65535> (0 picks a free port)."
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
const loopback = isLoopback(host);
|
|
102
|
+
const devToken = process.env.GRAFT_DEV_TOKEN;
|
|
103
|
+
if (!loopback && !devToken) {
|
|
104
|
+
console.warn(
|
|
105
|
+
"[graft studio] WARNING: binding beyond loopback with no GRAFT_DEV_TOKEN \u2014 set a bearer token before exposing Studio to a network."
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
const authenticate = !loopback && devToken ? (request) => {
|
|
109
|
+
const header = request.headers.get("authorization") ?? "";
|
|
110
|
+
if (header !== `Bearer ${devToken}`) return null;
|
|
111
|
+
return {
|
|
112
|
+
...operatorIdentity(),
|
|
113
|
+
scopes: ["studio:read", "studio:write", "approvals:decide"]
|
|
114
|
+
};
|
|
115
|
+
} : !loopback ? () => null : void 0;
|
|
116
|
+
const handler = createStudioHandler({
|
|
117
|
+
db: branch.db,
|
|
118
|
+
collections: config.collections,
|
|
119
|
+
contentDir: config.contentDir,
|
|
120
|
+
mdxTrust: config.mdxTrust,
|
|
121
|
+
defaultBranch: writeBranch,
|
|
122
|
+
decider: operatorIdentity,
|
|
123
|
+
authenticate
|
|
124
|
+
});
|
|
125
|
+
const listening = createServer(
|
|
126
|
+
createNodeListener(handler, { allowedHosts: allowedHostsFor(host) })
|
|
127
|
+
);
|
|
128
|
+
server = listening;
|
|
129
|
+
await new Promise((resolve, reject) => {
|
|
130
|
+
listening.once("error", reject);
|
|
131
|
+
listening.listen(requestedPort, host, () => resolve());
|
|
132
|
+
});
|
|
133
|
+
const address = listening.address();
|
|
134
|
+
const port = typeof address === "object" && address ? address.port : requestedPort;
|
|
135
|
+
const origin = loopback ? `http://127.0.0.1:${port}` : `http://${host}:${port}`;
|
|
136
|
+
console.log(
|
|
137
|
+
[
|
|
138
|
+
`graft studio \u2014 branch "${branchName}" (opt-in)`,
|
|
139
|
+
` ui GET ${origin}/`,
|
|
140
|
+
` openapi GET ${origin}/api/studio/v1/openapi.json`,
|
|
141
|
+
` Edit content, approve/deny \u2014 same ops as MCP/CLI.`
|
|
142
|
+
].join("\n")
|
|
143
|
+
);
|
|
144
|
+
if (loopback) openBrowser(studioPreviewUrl(port, branchName));
|
|
145
|
+
await new Promise((resolve) => {
|
|
146
|
+
const stop = () => resolve();
|
|
147
|
+
process.once("SIGINT", stop);
|
|
148
|
+
process.once("SIGTERM", stop);
|
|
149
|
+
});
|
|
150
|
+
} finally {
|
|
151
|
+
const bound = server;
|
|
152
|
+
if (bound?.listening) {
|
|
153
|
+
await new Promise((resolve, reject) => {
|
|
154
|
+
bound.close((err) => err ? reject(err) : resolve());
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
await branch.close();
|
|
158
|
+
}
|
|
159
|
+
} finally {
|
|
160
|
+
await control.close();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
export {
|
|
164
|
+
studioCommand,
|
|
165
|
+
studioPreviewUrl
|
|
166
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usegraft/cli",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
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.
|
|
38
|
-
"@usegraft/auth": "1.0.0-beta.
|
|
39
|
-
"@usegraft/compiler": "1.0.0-beta.
|
|
40
|
-
"@usegraft/content-api": "1.0.0-beta.
|
|
41
|
-
"@usegraft/content-migrations": "1.0.0-beta.
|
|
42
|
-
"@usegraft/contracts": "1.0.0-beta.
|
|
43
|
-
"@usegraft/core": "1.0.0-beta.
|
|
44
|
-
"@usegraft/db": "1.0.0-beta.
|
|
45
|
-
"@usegraft/mcp": "1.0.0-beta.
|
|
46
|
-
"@usegraft/mdx-safety": "1.0.0-beta.
|
|
47
|
-
"@usegraft/registry": "1.0.0-beta.
|
|
48
|
-
"@usegraft/studio": "1.0.0-beta.
|
|
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.
|
|
52
|
+
"@usegraft/sdk-core": "1.0.0-beta.3"
|
|
53
53
|
},
|
|
54
54
|
"engines": {
|
|
55
55
|
"node": ">=22.16"
|
package/dist/studio-7VAYFEWS.js
DELETED
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
allowedHostsFor,
|
|
4
|
-
createNodeListener
|
|
5
|
-
} from "./chunk-CONTLKSZ.js";
|
|
6
|
-
import {
|
|
7
|
-
findConfig,
|
|
8
|
-
loadConfig,
|
|
9
|
-
loadProjectEnv,
|
|
10
|
-
requireDatabaseUrl
|
|
11
|
-
} from "./chunk-2LG6TWYY.js";
|
|
12
|
-
|
|
13
|
-
// src/commands/studio.ts
|
|
14
|
-
import { execFile } from "child_process";
|
|
15
|
-
import { createServer } from "http";
|
|
16
|
-
import { userInfo } from "os";
|
|
17
|
-
import { GraftError } from "@usegraft/contracts";
|
|
18
|
-
function isLoopback(host) {
|
|
19
|
-
return host === "127.0.0.1" || host === "localhost" || host === "::1";
|
|
20
|
-
}
|
|
21
|
-
function operatorIdentity() {
|
|
22
|
-
try {
|
|
23
|
-
return { kind: "human", id: userInfo().username };
|
|
24
|
-
} catch {
|
|
25
|
-
return { kind: "human", id: process.env.USERNAME ?? process.env.USER ?? "operator" };
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
function openBrowser(url) {
|
|
29
|
-
const platform = process.platform;
|
|
30
|
-
const cmd = platform === "win32" ? "cmd" : platform === "darwin" ? "open" : "xdg-open";
|
|
31
|
-
const args = platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
32
|
-
execFile(cmd, args, () => {
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
async function studioCommand(options) {
|
|
36
|
-
loadProjectEnv(options.cwd);
|
|
37
|
-
const config = await loadConfig(findConfig(options.cwd));
|
|
38
|
-
const url = requireDatabaseUrl();
|
|
39
|
-
const [{ createStudioHandler }, { createDb, resolveBranchHandle, scopeWriteBranch }] = await Promise.all([import("@usegraft/studio"), import("@usegraft/db")]);
|
|
40
|
-
const control = createDb(url);
|
|
41
|
-
const branchName = options.branchId ?? "main";
|
|
42
|
-
const branch = await resolveBranchHandle(control.db, branchName, { databaseUrl: url });
|
|
43
|
-
const writeBranch = scopeWriteBranch(branch.scope);
|
|
44
|
-
const host = options.host ?? process.env.HOST ?? "127.0.0.1";
|
|
45
|
-
const requestedPort = options.port ?? Number(process.env.GRAFT_STUDIO_PORT ?? 4983);
|
|
46
|
-
if (!Number.isInteger(requestedPort) || requestedPort < 0 || requestedPort > 65535) {
|
|
47
|
-
await branch.close();
|
|
48
|
-
await control.close();
|
|
49
|
-
throw new GraftError({
|
|
50
|
-
code: "INPUT_VALIDATION_FAILED",
|
|
51
|
-
message: `"${options.port ?? process.env.GRAFT_STUDIO_PORT}" is not a valid port.`,
|
|
52
|
-
fix: "Pass --port <0-65535> (0 picks a free port)."
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
const loopback = isLoopback(host);
|
|
56
|
-
const devToken = process.env.GRAFT_DEV_TOKEN;
|
|
57
|
-
if (!loopback && !devToken) {
|
|
58
|
-
console.warn(
|
|
59
|
-
"[graft studio] WARNING: binding beyond loopback with no GRAFT_DEV_TOKEN \u2014 set a bearer token before exposing Studio to a network."
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
const authenticate = !loopback && devToken ? (request) => {
|
|
63
|
-
const header = request.headers.get("authorization") ?? "";
|
|
64
|
-
if (header !== `Bearer ${devToken}`) return null;
|
|
65
|
-
return {
|
|
66
|
-
...operatorIdentity(),
|
|
67
|
-
scopes: ["studio:read", "studio:write", "approvals:decide"]
|
|
68
|
-
};
|
|
69
|
-
} : !loopback ? () => null : void 0;
|
|
70
|
-
const handler = createStudioHandler({
|
|
71
|
-
db: branch.db,
|
|
72
|
-
collections: config.collections,
|
|
73
|
-
contentDir: config.contentDir,
|
|
74
|
-
mdxTrust: config.mdxTrust,
|
|
75
|
-
defaultBranch: writeBranch,
|
|
76
|
-
decider: operatorIdentity,
|
|
77
|
-
authenticate
|
|
78
|
-
});
|
|
79
|
-
const server = createServer(createNodeListener(handler, { allowedHosts: allowedHostsFor(host) }));
|
|
80
|
-
await new Promise((resolve, reject) => {
|
|
81
|
-
server.once("error", reject);
|
|
82
|
-
server.listen(requestedPort, host, () => resolve());
|
|
83
|
-
});
|
|
84
|
-
const address = server.address();
|
|
85
|
-
const port = typeof address === "object" && address ? address.port : requestedPort;
|
|
86
|
-
const base = `http://${host}:${port}`;
|
|
87
|
-
console.log(
|
|
88
|
-
[
|
|
89
|
-
`graft studio \u2014 branch "${branchName}" (opt-in)`,
|
|
90
|
-
` ui GET ${base}/`,
|
|
91
|
-
` openapi GET ${base}/api/studio/v1/openapi.json`,
|
|
92
|
-
` Edit content, approve/deny \u2014 same ops as MCP/CLI.`
|
|
93
|
-
].join("\n")
|
|
94
|
-
);
|
|
95
|
-
if (loopback) openBrowser(`${base}/?branch=${encodeURIComponent(branchName)}`);
|
|
96
|
-
await new Promise((resolve) => {
|
|
97
|
-
const stop = () => resolve();
|
|
98
|
-
process.once("SIGINT", stop);
|
|
99
|
-
process.once("SIGTERM", stop);
|
|
100
|
-
});
|
|
101
|
-
await new Promise((resolve, reject) => {
|
|
102
|
-
server.close((err) => err ? reject(err) : resolve());
|
|
103
|
-
});
|
|
104
|
-
await branch.close();
|
|
105
|
-
await control.close();
|
|
106
|
-
}
|
|
107
|
-
export {
|
|
108
|
-
studioCommand
|
|
109
|
-
};
|