@usegraft/cli 0.0.0-canary-20260831153011

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.
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ findConfig,
4
+ loadConfig,
5
+ loadProjectEnv,
6
+ requireDatabaseUrl
7
+ } from "./chunk-2NH6KYTL.js";
8
+
9
+ // src/commands/migrate.ts
10
+ import { existsSync, readdirSync, statSync } from "fs";
11
+ import { basename, join } from "path";
12
+ import { GraftError } from "@usegraft/contracts";
13
+ import {
14
+ runContentMigration
15
+ } from "@usegraft/content-migrations";
16
+ import { runDataMigration } from "@usegraft/core";
17
+ import { createJiti } from "jiti";
18
+ async function discoverMigrations(migrationsDir) {
19
+ if (!existsSync(migrationsDir) || !statSync(migrationsDir).isDirectory()) return [];
20
+ const files = readdirSync(migrationsDir).filter((name) => /\.(ts|mts|js|mjs)$/.test(name)).sort().map((name) => join(migrationsDir, name));
21
+ const jiti = createJiti(migrationsDir, { moduleCache: false });
22
+ const discovered = [];
23
+ for (const file of files) {
24
+ const id = basename(file).replace(/\.(ts|mts|js|mjs)$/, "");
25
+ let mod;
26
+ try {
27
+ mod = await jiti.import(file);
28
+ } catch (error) {
29
+ throw new GraftError({
30
+ code: "MIGRATION_FAILED",
31
+ message: `Migration file ${basename(file)} failed to load: ${error instanceof Error ? error.message : String(error)}`,
32
+ fix: "Fix the error in the file \u2014 it must be importable and default-export defineContentMigration(\u2026) or defineDataMigration(\u2026).",
33
+ details: { file }
34
+ });
35
+ }
36
+ const migration = mod.default;
37
+ if (!migration || migration.kind !== "content" && migration.kind !== "data" || typeof migration.transform !== "function") {
38
+ throw new GraftError({
39
+ code: "MIGRATION_FAILED",
40
+ message: `${basename(file)} does not default-export a migration.`,
41
+ fix: `Add \`export default defineContentMigration({ \u2026 })\` (from "@usegraft/content-migrations") or \`export default defineDataMigration({ \u2026 })\` (from "@usegraft/core").`,
42
+ details: { file, exports: Object.keys(mod) }
43
+ });
44
+ }
45
+ discovered.push({ id, file, migration });
46
+ }
47
+ return discovered;
48
+ }
49
+ async function migrateCommand(options) {
50
+ loadProjectEnv(options.cwd);
51
+ const config = await loadConfig(findConfig(options.cwd));
52
+ const branchId = options.branchId ?? "main";
53
+ const migrations = await discoverMigrations(config.migrationsDir);
54
+ if (migrations.length === 0) {
55
+ console.log(
56
+ `No migrations found in ${config.migrationsDir}.
57
+ Author one as migrations/0001-<name>.ts default-exporting defineContentMigration or defineDataMigration.`
58
+ );
59
+ return { applied: [], pending: [], didApply: false };
60
+ }
61
+ const url = requireDatabaseUrl();
62
+ const [
63
+ { compile, resolveGitSha },
64
+ {
65
+ createDb,
66
+ listAppliedMigrations,
67
+ recordAppliedMigration,
68
+ resolveBranchHandle,
69
+ scopeWriteBranch
70
+ }
71
+ ] = await Promise.all([import("@usegraft/compiler"), import("@usegraft/db")]);
72
+ const control = createDb(url);
73
+ const branch = await resolveBranchHandle(control.db, branchId, { databaseUrl: url });
74
+ const writeBranch = scopeWriteBranch(branch.scope);
75
+ try {
76
+ const appliedRows = await listAppliedMigrations(branch.db, writeBranch);
77
+ const appliedIds = new Set(appliedRows.map((row) => row.migrationId));
78
+ const applied = migrations.filter((m) => appliedIds.has(m.id)).map((m) => m.id);
79
+ const pending = migrations.filter((m) => !appliedIds.has(m.id));
80
+ if (applied.length > 0) {
81
+ console.log(`Already applied on "${branchId}": ${applied.join(", ")}`);
82
+ }
83
+ if (pending.length === 0) {
84
+ console.log("Nothing pending \u2014 the branch is up to date.");
85
+ return { applied, pending: [], didApply: false };
86
+ }
87
+ const gitSha = resolveGitSha(config.projectDir);
88
+ const outcomes = [];
89
+ for (const { id, migration } of pending) {
90
+ const label = `${id} [${migration.kind}] ${migration.collection.name} \u2014 ${migration.description}`;
91
+ if (migration.kind === "content") {
92
+ const report = await runContentMigration({
93
+ contentDir: config.contentDir,
94
+ migration,
95
+ apply: options.apply
96
+ });
97
+ if (options.apply) {
98
+ const compiled = await compile({
99
+ db: branch.db,
100
+ contentDir: config.contentDir,
101
+ collections: config.collections,
102
+ mdxTrust: config.mdxTrust,
103
+ branchId: writeBranch,
104
+ gitSha
105
+ });
106
+ await recordAppliedMigration(branch.db, {
107
+ branchId: writeBranch,
108
+ migrationId: id,
109
+ kind: "content",
110
+ collection: migration.collection.name,
111
+ docCount: report.changed,
112
+ gitSha
113
+ });
114
+ console.log(
115
+ `applied ${label}
116
+ ${report.changed} file(s) rewritten, ${report.unchanged} unchanged; compiled: +${compiled.changes.added.length} ~${compiled.changes.changed.length} -${compiled.changes.removed.length}`
117
+ );
118
+ } else {
119
+ console.log(
120
+ `pending ${label}
121
+ would rewrite ${report.changed} file(s) (${report.unchanged} unchanged)`
122
+ );
123
+ }
124
+ outcomes.push({
125
+ id,
126
+ kind: "content",
127
+ collection: migration.collection.name,
128
+ description: migration.description,
129
+ report
130
+ });
131
+ } else {
132
+ const report = await runDataMigration({
133
+ db: branch.db,
134
+ migration,
135
+ migrationId: id,
136
+ branchId: writeBranch,
137
+ gitSha,
138
+ apply: options.apply
139
+ });
140
+ console.log(
141
+ options.apply ? `applied ${label}
142
+ ${report.changed} row(s) updated, ${report.unchanged} unchanged` : `pending ${label}
143
+ would update ${report.changed} of ${report.rows} row(s)`
144
+ );
145
+ outcomes.push({
146
+ id,
147
+ kind: "data",
148
+ collection: migration.collection.name,
149
+ description: migration.description,
150
+ report
151
+ });
152
+ }
153
+ }
154
+ if (!options.apply) {
155
+ console.log(`
156
+ Dry run \u2014 nothing was written. Run \`graft migrate --apply\` to execute.`);
157
+ } else {
158
+ console.log(
159
+ `
160
+ ${outcomes.length} migration(s) applied. Commit the changes \u2014 git is the version history.`
161
+ );
162
+ }
163
+ return { applied, pending: outcomes, didApply: options.apply === true };
164
+ } finally {
165
+ await branch.close();
166
+ await control.close();
167
+ }
168
+ }
169
+
170
+ export {
171
+ discoverMigrations,
172
+ migrateCommand
173
+ };
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ formatCompileResult
4
+ } from "./chunk-GPA66AP5.js";
5
+ import {
6
+ findConfig,
7
+ loadConfig,
8
+ loadProjectEnv,
9
+ requireDatabaseUrl
10
+ } from "./chunk-2NH6KYTL.js";
11
+
12
+ // src/commands/compile.ts
13
+ import { GraftError } from "@usegraft/contracts";
14
+ function assertNoStaticBranch(branchId) {
15
+ if (branchId === void 0 || branchId === "main") return;
16
+ throw new GraftError({
17
+ code: "NEEDS_DATABASE",
18
+ message: `--branch ${branchId} needs the Postgres index: this project runs in static mode, where a "branch" is just your git branch.`,
19
+ fix: `Check out the git branch and compile (each checkout builds its own artifact), or upgrade to Postgres for DB branching: set DATABASE_URL and \`export const index = "postgres"\` in graft.config.`,
20
+ details: { branchId }
21
+ });
22
+ }
23
+ async function compileCommand(options) {
24
+ loadProjectEnv(options.cwd);
25
+ const config = await loadConfig(findConfig(options.cwd));
26
+ if (config.index.driver === "static") {
27
+ assertNoStaticBranch(options.branchId);
28
+ const { compileStatic } = await import("@usegraft/compiler");
29
+ const result = await compileStatic({
30
+ contentDir: config.contentDir,
31
+ collections: config.collections,
32
+ mdxTrust: config.mdxTrust,
33
+ indexPath: config.index.path
34
+ });
35
+ console.log(formatCompileResult(result));
36
+ return result;
37
+ }
38
+ const url = requireDatabaseUrl();
39
+ const [{ compile }, { createDb, resolveBranchHandle, scopeWriteBranch }] = await Promise.all([
40
+ import("@usegraft/compiler"),
41
+ import("@usegraft/db")
42
+ ]);
43
+ const control = createDb(url);
44
+ try {
45
+ const branch = await resolveBranchHandle(control.db, options.branchId ?? "main", {
46
+ databaseUrl: url
47
+ });
48
+ try {
49
+ if (branch.scope.kind === "physical") {
50
+ console.log(`neon branch "${branch.name}" \u2014 compiling into its own database`);
51
+ }
52
+ const result = await compile({
53
+ db: branch.db,
54
+ contentDir: config.contentDir,
55
+ collections: config.collections,
56
+ mdxTrust: config.mdxTrust,
57
+ branchId: scopeWriteBranch(branch.scope),
58
+ pruneUnknown: options.pruneUnknown
59
+ });
60
+ console.log(formatCompileResult(result));
61
+ return result;
62
+ } finally {
63
+ await branch.close();
64
+ }
65
+ } finally {
66
+ await control.close();
67
+ }
68
+ }
69
+
70
+ export {
71
+ assertNoStaticBranch,
72
+ compileCommand
73
+ };
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/report.ts
4
+ function printGraftError(error) {
5
+ console.error(`error ${error.code}: ${error.message}`);
6
+ if (error.fix) console.error(` fix: ${error.fix}`);
7
+ if (error.details) console.error(` details: ${JSON.stringify(error.details)}`);
8
+ }
9
+ function formatCompileResult(result) {
10
+ const { added, changed, removed, unchanged } = result.changes;
11
+ const lines = [
12
+ `Compiled ${result.count} doc(s) @ ${result.gitSha?.slice(0, 7) ?? "no-git"}: +${added.length} added, ~${changed.length} changed, -${removed.length} removed, ${unchanged} unchanged`
13
+ ];
14
+ for (const key of [...added, ...changed]) lines.push(` upserted ${key}`);
15
+ for (const key of removed) lines.push(` removed ${key}`);
16
+ return lines.join("\n");
17
+ }
18
+
19
+ export {
20
+ printGraftError,
21
+ formatCompileResult
22
+ };
@@ -0,0 +1,328 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ findConfig,
4
+ loadConfig,
5
+ loadProjectEnv,
6
+ requireDatabaseUrl
7
+ } from "./chunk-2NH6KYTL.js";
8
+
9
+ // src/commands/serve.ts
10
+ import { execFileSync } from "child_process";
11
+ import { createServer } from "http";
12
+ import { createActorResolver } from "@usegraft/auth";
13
+ import { GraftError } from "@usegraft/contracts";
14
+ import { setRequestPeer } from "@usegraft/core";
15
+ function createServeRouter(routes) {
16
+ return async (request) => {
17
+ const { pathname } = new URL(request.url);
18
+ if (pathname === "/healthz") return routes.health(request);
19
+ if (pathname === "/api/mcp") return routes.mcp(request);
20
+ if (pathname === "/api/fn" || pathname.startsWith("/api/fn/")) return routes.fn(request);
21
+ if (pathname === "/api/content/v1" || pathname.startsWith("/api/content/v1/")) {
22
+ return routes.content(request);
23
+ }
24
+ if (routes.studio && (pathname === "/studio" || pathname.startsWith("/studio/") || pathname.startsWith("/api/studio/"))) {
25
+ return routes.studio(request);
26
+ }
27
+ const studioHint = routes.studio ? ", GET /studio (opt-in Studio)" : " \u2014 or pass --studio / GRAFT_STUDIO=1 for the opt-in Studio UI";
28
+ const error = new GraftError({
29
+ code: "ROUTE_NOT_FOUND",
30
+ message: `Nothing is mounted at ${pathname}.`,
31
+ fix: `Use POST /api/fn/<name> (typed functions), POST /api/mcp (MCP Streamable HTTP), GET /api/content/v1/documents (authored content), GET /api/content/v1/search, or GET /healthz${studioHint}.`,
32
+ details: { pathname }
33
+ });
34
+ return new Response(JSON.stringify(error.toJSON()), {
35
+ status: 404,
36
+ headers: { "content-type": "application/json" }
37
+ });
38
+ };
39
+ }
40
+ var HOP_BY_HOP = /* @__PURE__ */ new Set([
41
+ "connection",
42
+ "keep-alive",
43
+ "transfer-encoding",
44
+ "content-length",
45
+ "upgrade",
46
+ "proxy-connection"
47
+ ]);
48
+ function allowedHostsFor(host) {
49
+ if (host === "0.0.0.0" || host === "::" || host === "") return void 0;
50
+ if (host === "127.0.0.1" || host === "localhost" || host === "::1") {
51
+ return ["127.0.0.1", "localhost", "[::1]"];
52
+ }
53
+ return [host];
54
+ }
55
+ function hostname(host) {
56
+ const trimmed = host.trim().toLowerCase();
57
+ if (trimmed.startsWith("[")) return trimmed.slice(0, trimmed.indexOf("]") + 1);
58
+ const colon = trimmed.lastIndexOf(":");
59
+ return colon === -1 ? trimmed : trimmed.slice(0, colon);
60
+ }
61
+ function createNodeListener(handler, options = {}) {
62
+ const allowed = options.allowedHosts?.map(hostname);
63
+ return async (req, res) => {
64
+ try {
65
+ if (allowed && !allowed.includes(hostname(req.headers.host ?? ""))) {
66
+ res.writeHead(400, { "content-type": "application/json" });
67
+ res.end(
68
+ JSON.stringify({
69
+ error: "INPUT_VALIDATION_FAILED",
70
+ message: `This server does not answer to Host "${req.headers.host ?? ""}".`,
71
+ fix: `Reach it at one of: ${allowed.join(", ")}. A mismatched Host is either a misconfigured proxy or a DNS-rebinding attempt.`
72
+ })
73
+ );
74
+ return;
75
+ }
76
+ const chunks = [];
77
+ for await (const chunk of req) chunks.push(chunk);
78
+ const body = Buffer.concat(chunks);
79
+ const headers = new Headers();
80
+ for (const [key, value] of Object.entries(req.headers)) {
81
+ if (value === void 0 || HOP_BY_HOP.has(key)) continue;
82
+ if (Array.isArray(value)) for (const v of value) headers.append(key, v);
83
+ else headers.set(key, value);
84
+ }
85
+ const method = req.method ?? "GET";
86
+ const request = new Request(`http://${req.headers.host ?? "localhost"}${req.url ?? "/"}`, {
87
+ method,
88
+ headers,
89
+ body: method === "GET" || method === "HEAD" || body.length === 0 ? void 0 : body
90
+ });
91
+ const peer = req.socket.remoteAddress;
92
+ if (peer) setRequestPeer(request, peer);
93
+ const response = await handler(request);
94
+ const outHeaders = {};
95
+ response.headers.forEach((value, key) => {
96
+ if (!HOP_BY_HOP.has(key)) outHeaders[key] = value;
97
+ });
98
+ res.writeHead(response.status, outHeaders);
99
+ res.end(Buffer.from(await response.arrayBuffer()));
100
+ } catch (error) {
101
+ res.writeHead(500, { "content-type": "application/json" });
102
+ res.end(
103
+ JSON.stringify({
104
+ error: "FUNCTION_EXECUTION_FAILED",
105
+ message: `graft serve failed to relay the request: ${error instanceof Error ? error.message : String(error)}`,
106
+ fix: "Check the server logs; if this reproduces, file it \u2014 the adapter should never throw."
107
+ })
108
+ );
109
+ }
110
+ };
111
+ }
112
+ function localGitSha(cwd) {
113
+ try {
114
+ return execFileSync("git", ["rev-parse", "HEAD"], {
115
+ cwd,
116
+ encoding: "utf8",
117
+ stdio: ["ignore", "pipe", "ignore"]
118
+ }).trim();
119
+ } catch {
120
+ return void 0;
121
+ }
122
+ }
123
+ function trustedIssuersFromEnv() {
124
+ return (process.env.GRAFT_TRUSTED_ISSUERS ?? "").split(/[,\s]+/).map((issuer) => issuer.trim()).filter(Boolean).map((issuer) => ({ issuer }));
125
+ }
126
+ async function startServe(options) {
127
+ loadProjectEnv(options.cwd);
128
+ const config = await loadConfig(findConfig(options.cwd));
129
+ const url = requireDatabaseUrl();
130
+ const enableStudio = options.studio === true || process.env.GRAFT_STUDIO === "1";
131
+ const [
132
+ { createFunctionsHandler },
133
+ { createGraftMcpHandler },
134
+ { createContentApiHandler },
135
+ { createDb, createDbIndexReader, resolveBranchHandle, scopeWriteBranch, sql },
136
+ studioMod
137
+ ] = await Promise.all([
138
+ import("@usegraft/core"),
139
+ import("@usegraft/mcp"),
140
+ import("@usegraft/content-api"),
141
+ import("@usegraft/db"),
142
+ enableStudio ? import("@usegraft/studio") : Promise.resolve(null)
143
+ ]);
144
+ const control = createDb(url);
145
+ const branchName = options.branchId ?? "main";
146
+ const branch = await resolveBranchHandle(control.db, branchName, { databaseUrl: url });
147
+ const writeBranch = scopeWriteBranch(branch.scope);
148
+ 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();
151
+ const resolveActor = createActorResolver({
152
+ issuers,
153
+ devTokens: devToken ? { [devToken]: { kind: "agent", id: "graft-serve", scopes } } : void 0
154
+ });
155
+ const approvalPolicy = process.env.GRAFT_APPROVAL_POLICY === "human" ? "human" : process.env.GRAFT_APPROVAL_POLICY === "unattended" ? "unattended" : "none";
156
+ const host = options.host ?? process.env.HOST ?? "127.0.0.1";
157
+ const loopback = host === "127.0.0.1" || host === "localhost" || host === "::1";
158
+ const anonymousOptIn = process.env.GRAFT_MCP_ALLOW_ANONYMOUS === "1";
159
+ const allowAnonymousMcp = loopback || anonymousOptIn;
160
+ const functions = config.functions ?? {};
161
+ const gitSha = localGitSha(options.cwd);
162
+ const fnHandler = createFunctionsHandler({
163
+ functions,
164
+ db: branch.db,
165
+ branch: writeBranch,
166
+ actor: resolveActor,
167
+ approvalPolicy,
168
+ // Backstop; per-function rateLimit overrides it.
169
+ rateLimit: { limit: 60, windowSeconds: 60 },
170
+ gitSha
171
+ });
172
+ const mcpHandler = createGraftMcpHandler({
173
+ name: "graft-serve",
174
+ contentDir: config.contentDir,
175
+ collections: config.collections,
176
+ functions,
177
+ mdxTrust: config.mdxTrust,
178
+ db: branch.db,
179
+ branchId: writeBranch,
180
+ scope: branch.scope,
181
+ actor: resolveActor,
182
+ allowAnonymous: allowAnonymousMcp
183
+ });
184
+ const allowedOriginsRaw = process.env.GRAFT_CONTENT_ALLOWED_ORIGINS?.trim();
185
+ const allowedOrigins = allowedOriginsRaw === void 0 || allowedOriginsRaw === "" ? void 0 : allowedOriginsRaw === "*" ? "*" : allowedOriginsRaw.split(",").map((entry) => entry.trim()).filter(Boolean);
186
+ const contentHandler = createContentApiHandler({
187
+ collections: Object.keys(config.collections),
188
+ branch: writeBranch,
189
+ index: createDbIndexReader(branch.db),
190
+ ...allowedOrigins === void 0 ? {} : { allowedOrigins },
191
+ // The same backstop the functions handler gets, for the same reason. These
192
+ // routes authenticate nobody and run database listings and full-text
193
+ // searches, so without a limit here one anonymous caller can keep the index
194
+ // busy indefinitely — and `graft serve` is the mount most likely to face
195
+ // the open internet.
196
+ rateLimit: { limit: 60, windowSeconds: 60 }
197
+ });
198
+ const health = async () => {
199
+ try {
200
+ await branch.db.execute(sql`select 1`);
201
+ } catch (error) {
202
+ return new Response(
203
+ JSON.stringify({
204
+ ok: false,
205
+ error: error instanceof Error ? error.message : String(error)
206
+ }),
207
+ { status: 503, headers: { "content-type": "application/json" } }
208
+ );
209
+ }
210
+ return new Response(
211
+ JSON.stringify({
212
+ ok: true,
213
+ branch: branchName,
214
+ collections: Object.keys(config.collections).length,
215
+ functions: Object.keys(functions).length,
216
+ gitSha: gitSha ?? null
217
+ }),
218
+ { status: 200, headers: { "content-type": "application/json" } }
219
+ );
220
+ };
221
+ const requestedPort = options.port ?? Number(process.env.PORT ?? 3903);
222
+ if (!Number.isInteger(requestedPort) || requestedPort < 0 || requestedPort > 65535) {
223
+ await branch.close();
224
+ await control.close();
225
+ throw new GraftError({
226
+ code: "INPUT_VALIDATION_FAILED",
227
+ message: `"${options.port ?? process.env.PORT}" is not a valid port.`,
228
+ fix: "Pass --port <0-65535> (0 picks a free port) or set PORT."
229
+ });
230
+ }
231
+ if (!loopback && anonymousOptIn) {
232
+ console.warn(
233
+ "[graft serve] WARNING: GRAFT_MCP_ALLOW_ANONYMOUS=1 while bound beyond loopback \u2014 unauthenticated callers can write content, upload assets and decide approvals. Only do this behind an auth proxy you control."
234
+ );
235
+ } else if (!loopback && issuers.length === 0 && !devToken) {
236
+ console.warn(
237
+ "[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."
238
+ );
239
+ }
240
+ if (approvalPolicy === "unattended") {
241
+ console.warn(
242
+ "[graft serve] WARNING: GRAFT_APPROVAL_POLICY=unattended \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."
243
+ );
244
+ }
245
+ let studioHandler;
246
+ if (studioMod) {
247
+ const authenticate = !loopback && (devToken || issuers.length > 0) ? async (request) => {
248
+ const actor = await resolveActor(request);
249
+ if (actor.kind === "anonymous" || !actor.id) return null;
250
+ return { kind: actor.kind, id: actor.id, scopes: actor.scopes ?? [] };
251
+ } : !loopback ? () => null : void 0;
252
+ studioHandler = studioMod.createStudioHandler({
253
+ db: branch.db,
254
+ collections: config.collections,
255
+ contentDir: config.contentDir,
256
+ mdxTrust: config.mdxTrust,
257
+ defaultBranch: writeBranch,
258
+ // Only reached on a loopback mount, where there is no caller identity to
259
+ // attribute to. Off loopback the authenticated principal is used instead.
260
+ decider: { kind: "agent", id: "studio-serve" },
261
+ uiBasePath: "/studio",
262
+ authenticate
263
+ });
264
+ }
265
+ const server = createServer(
266
+ createNodeListener(
267
+ createServeRouter({
268
+ fn: fnHandler,
269
+ mcp: mcpHandler,
270
+ content: contentHandler,
271
+ health,
272
+ studio: studioHandler
273
+ }),
274
+ { allowedHosts: allowedHostsFor(host) }
275
+ )
276
+ );
277
+ await new Promise((resolve, reject) => {
278
+ server.once("error", reject);
279
+ server.listen(requestedPort, host, () => resolve());
280
+ });
281
+ const address = server.address();
282
+ const port = typeof address === "object" && address ? address.port : requestedPort;
283
+ return {
284
+ port,
285
+ host,
286
+ branch: branchName,
287
+ close: async () => {
288
+ await new Promise((resolve, reject) => {
289
+ server.close((err) => err ? reject(err) : resolve());
290
+ });
291
+ await branch.close();
292
+ await control.close();
293
+ }
294
+ };
295
+ }
296
+ async function serveCommand(options) {
297
+ const running = await startServe(options);
298
+ const base = `http://${running.host}:${running.port}`;
299
+ const studioOn = options.studio === true || process.env.GRAFT_STUDIO === "1";
300
+ console.log(
301
+ [
302
+ `graft serve \u2014 branch "${running.branch}"`,
303
+ ` functions POST ${base}/api/fn/<name>`,
304
+ ` mcp POST ${base}/api/mcp`,
305
+ ` documents GET ${base}/api/content/v1/documents`,
306
+ ` search GET ${base}/api/content/v1/search`,
307
+ ` health GET ${base}/healthz`,
308
+ ...studioOn ? [
309
+ ` studio GET ${base}/studio`,
310
+ ` openapi GET ${base}/api/studio/v1/openapi.json`
311
+ ] : []
312
+ ].join("\n")
313
+ );
314
+ await new Promise((resolve) => {
315
+ const stop = () => resolve();
316
+ process.once("SIGINT", stop);
317
+ process.once("SIGTERM", stop);
318
+ });
319
+ await running.close();
320
+ }
321
+
322
+ export {
323
+ createServeRouter,
324
+ allowedHostsFor,
325
+ createNodeListener,
326
+ startServe,
327
+ serveCommand
328
+ };
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ loadProjectEnv,
4
+ requireDatabaseUrl
5
+ } from "./chunk-2NH6KYTL.js";
6
+
7
+ // src/commands/compilations.ts
8
+ async function compilationsListCommand(options) {
9
+ loadProjectEnv(options.cwd);
10
+ const url = requireDatabaseUrl();
11
+ const { createDb, listCompilations } = await import("@usegraft/db");
12
+ const handle = createDb(url);
13
+ try {
14
+ return await listCompilations(handle.db, {
15
+ branchId: options.branchId,
16
+ limit: options.limit
17
+ });
18
+ } finally {
19
+ await handle.close();
20
+ }
21
+ }
22
+ function formatCompilation(row) {
23
+ const sha = row.gitSha ? row.gitSha.slice(0, 7) : "no-sha";
24
+ return `${row.createdAt.toISOString()} ${row.branchId} ${sha} docs=${row.docCount} +${row.added} ~${row.changed} -${row.removed} ${row.id}`;
25
+ }
26
+ export {
27
+ compilationsListCommand,
28
+ formatCompilation
29
+ };
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ assertNoStaticBranch,
4
+ compileCommand
5
+ } from "./chunk-GDTOV3DU.js";
6
+ import "./chunk-GPA66AP5.js";
7
+ import "./chunk-2NH6KYTL.js";
8
+ export {
9
+ assertNoStaticBranch,
10
+ compileCommand
11
+ };
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ findConfig,
4
+ loadConfig,
5
+ loadProjectEnv,
6
+ requireDatabaseUrl
7
+ } from "./chunk-2NH6KYTL.js";
8
+
9
+ // src/commands/content.ts
10
+ async function contentListCommand(options) {
11
+ loadProjectEnv(options.cwd);
12
+ const config = await loadConfig(findConfig(options.cwd));
13
+ const url = requireDatabaseUrl();
14
+ const { createDb, readContent, resolveBranchScope } = await import("@usegraft/db");
15
+ const handle = createDb(url);
16
+ const branch = options.branchId ?? "main";
17
+ try {
18
+ const scope = await resolveBranchScope(handle.db, branch);
19
+ const lines = [];
20
+ for (const name of Object.keys(config.collections).sort()) {
21
+ const rows = await readContent(handle.db, scope, { collection: name });
22
+ for (const row of rows) {
23
+ const title = typeof row.data.title === "string" ? row.data.title : typeof row.data.name === "string" ? row.data.name : void 0;
24
+ lines.push({
25
+ collection: name,
26
+ slug: row.slug,
27
+ sourcePath: row.sourcePath,
28
+ ...title ? { title } : {}
29
+ });
30
+ }
31
+ }
32
+ return { branch, lines };
33
+ } finally {
34
+ await handle.close();
35
+ }
36
+ }
37
+ function formatContentLine(line) {
38
+ const label = line.title ? `${line.slug} (${line.title})` : line.slug;
39
+ return `${line.collection}/${label} ${line.sourcePath}`;
40
+ }
41
+ export {
42
+ contentListCommand,
43
+ formatContentLine
44
+ };