@usegraft/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/add-UBAZMSP7.js +61 -0
- package/dist/approvals-Z3EOK5AM.js +68 -0
- package/dist/asset-FPEL463J.js +49 -0
- package/dist/branch-HRG46YIL.js +75 -0
- package/dist/chunk-BUPFXTUG.js +182 -0
- package/dist/chunk-GPA66AP5.js +22 -0
- package/dist/chunk-KZ3FDWUK.js +71 -0
- package/dist/chunk-LTFTDG6Z.js +172 -0
- package/dist/chunk-TUWL7WFD.js +256 -0
- package/dist/compilations-6P45MPAE.js +29 -0
- package/dist/compile-AAUC4G7H.js +11 -0
- package/dist/content-DY2YI3LT.js +44 -0
- package/dist/db-VGDWG4QA.js +48 -0
- package/dist/dev-MX7TQOJ2.js +113 -0
- package/dist/harden-HQHONZPH.js +43 -0
- package/dist/index.js +427 -0
- package/dist/init-3H2IP5US.js +117 -0
- package/dist/mcp-US3QFLOR.js +61 -0
- package/dist/merge-CZUNMMB5.js +175 -0
- package/dist/migrate-RRW4Z6RB.js +10 -0
- package/dist/serve-Q63UTTER.js +14 -0
- package/dist/studio-TNPOB665.js +103 -0
- package/package.json +46 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
findConfig,
|
|
4
|
+
loadConfig,
|
|
5
|
+
loadProjectEnv,
|
|
6
|
+
requireDatabaseUrl
|
|
7
|
+
} from "./chunk-BUPFXTUG.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
|
+
branchId: writeBranch,
|
|
103
|
+
gitSha
|
|
104
|
+
});
|
|
105
|
+
await recordAppliedMigration(branch.db, {
|
|
106
|
+
branchId: writeBranch,
|
|
107
|
+
migrationId: id,
|
|
108
|
+
kind: "content",
|
|
109
|
+
collection: migration.collection.name,
|
|
110
|
+
docCount: report.changed,
|
|
111
|
+
gitSha
|
|
112
|
+
});
|
|
113
|
+
console.log(
|
|
114
|
+
`applied ${label}
|
|
115
|
+
${report.changed} file(s) rewritten, ${report.unchanged} unchanged; compiled: +${compiled.changes.added.length} ~${compiled.changes.changed.length} -${compiled.changes.removed.length}`
|
|
116
|
+
);
|
|
117
|
+
} else {
|
|
118
|
+
console.log(
|
|
119
|
+
`pending ${label}
|
|
120
|
+
would rewrite ${report.changed} file(s) (${report.unchanged} unchanged)`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
outcomes.push({
|
|
124
|
+
id,
|
|
125
|
+
kind: "content",
|
|
126
|
+
collection: migration.collection.name,
|
|
127
|
+
description: migration.description,
|
|
128
|
+
report
|
|
129
|
+
});
|
|
130
|
+
} else {
|
|
131
|
+
const report = await runDataMigration({
|
|
132
|
+
db: branch.db,
|
|
133
|
+
migration,
|
|
134
|
+
migrationId: id,
|
|
135
|
+
branchId: writeBranch,
|
|
136
|
+
gitSha,
|
|
137
|
+
apply: options.apply
|
|
138
|
+
});
|
|
139
|
+
console.log(
|
|
140
|
+
options.apply ? `applied ${label}
|
|
141
|
+
${report.changed} row(s) updated, ${report.unchanged} unchanged` : `pending ${label}
|
|
142
|
+
would update ${report.changed} of ${report.rows} row(s)`
|
|
143
|
+
);
|
|
144
|
+
outcomes.push({
|
|
145
|
+
id,
|
|
146
|
+
kind: "data",
|
|
147
|
+
collection: migration.collection.name,
|
|
148
|
+
description: migration.description,
|
|
149
|
+
report
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (!options.apply) {
|
|
154
|
+
console.log(`
|
|
155
|
+
Dry run \u2014 nothing was written. Run \`graft migrate --apply\` to execute.`);
|
|
156
|
+
} else {
|
|
157
|
+
console.log(
|
|
158
|
+
`
|
|
159
|
+
${outcomes.length} migration(s) applied. Commit the changes \u2014 git is the version history.`
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
return { applied, pending: outcomes, didApply: options.apply === true };
|
|
163
|
+
} finally {
|
|
164
|
+
await branch.close();
|
|
165
|
+
await control.close();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export {
|
|
170
|
+
discoverMigrations,
|
|
171
|
+
migrateCommand
|
|
172
|
+
};
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
findConfig,
|
|
4
|
+
loadConfig,
|
|
5
|
+
loadProjectEnv,
|
|
6
|
+
requireDatabaseUrl
|
|
7
|
+
} from "./chunk-BUPFXTUG.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
|
+
function createServeRouter(routes) {
|
|
15
|
+
return async (request) => {
|
|
16
|
+
const { pathname } = new URL(request.url);
|
|
17
|
+
if (pathname === "/healthz") return routes.health(request);
|
|
18
|
+
if (pathname === "/api/mcp") return routes.mcp(request);
|
|
19
|
+
if (pathname === "/api/fn" || pathname.startsWith("/api/fn/")) return routes.fn(request);
|
|
20
|
+
if (routes.studio && (pathname === "/studio" || pathname.startsWith("/studio/") || pathname.startsWith("/api/studio/"))) {
|
|
21
|
+
return routes.studio(request);
|
|
22
|
+
}
|
|
23
|
+
const studioHint = routes.studio ? ", GET /studio (opt-in Studio)" : " \u2014 or pass --studio / GRAFT_STUDIO=1 for the opt-in Studio UI";
|
|
24
|
+
const error = new GraftError({
|
|
25
|
+
code: "ROUTE_NOT_FOUND",
|
|
26
|
+
message: `Nothing is mounted at ${pathname}.`,
|
|
27
|
+
fix: `Use POST /api/fn/<name> (typed functions), POST /api/mcp (MCP Streamable HTTP), or GET /healthz${studioHint}.`,
|
|
28
|
+
details: { pathname }
|
|
29
|
+
});
|
|
30
|
+
return new Response(JSON.stringify(error.toJSON()), {
|
|
31
|
+
status: 404,
|
|
32
|
+
headers: { "content-type": "application/json" }
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
var HOP_BY_HOP = /* @__PURE__ */ new Set([
|
|
37
|
+
"connection",
|
|
38
|
+
"keep-alive",
|
|
39
|
+
"transfer-encoding",
|
|
40
|
+
"content-length",
|
|
41
|
+
"upgrade",
|
|
42
|
+
"proxy-connection"
|
|
43
|
+
]);
|
|
44
|
+
function createNodeListener(handler) {
|
|
45
|
+
return async (req, res) => {
|
|
46
|
+
try {
|
|
47
|
+
const chunks = [];
|
|
48
|
+
for await (const chunk of req) chunks.push(chunk);
|
|
49
|
+
const body = Buffer.concat(chunks);
|
|
50
|
+
const headers = new Headers();
|
|
51
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
52
|
+
if (value === void 0 || HOP_BY_HOP.has(key)) continue;
|
|
53
|
+
if (Array.isArray(value)) for (const v of value) headers.append(key, v);
|
|
54
|
+
else headers.set(key, value);
|
|
55
|
+
}
|
|
56
|
+
const method = req.method ?? "GET";
|
|
57
|
+
const request = new Request(`http://${req.headers.host ?? "localhost"}${req.url ?? "/"}`, {
|
|
58
|
+
method,
|
|
59
|
+
headers,
|
|
60
|
+
body: method === "GET" || method === "HEAD" || body.length === 0 ? void 0 : body
|
|
61
|
+
});
|
|
62
|
+
const response = await handler(request);
|
|
63
|
+
const outHeaders = {};
|
|
64
|
+
response.headers.forEach((value, key) => {
|
|
65
|
+
if (!HOP_BY_HOP.has(key)) outHeaders[key] = value;
|
|
66
|
+
});
|
|
67
|
+
res.writeHead(response.status, outHeaders);
|
|
68
|
+
res.end(Buffer.from(await response.arrayBuffer()));
|
|
69
|
+
} catch (error) {
|
|
70
|
+
res.writeHead(500, { "content-type": "application/json" });
|
|
71
|
+
res.end(
|
|
72
|
+
JSON.stringify({
|
|
73
|
+
error: "FUNCTION_EXECUTION_FAILED",
|
|
74
|
+
message: `graft serve failed to relay the request: ${error instanceof Error ? error.message : String(error)}`,
|
|
75
|
+
fix: "Check the server logs; if this reproduces, file it \u2014 the adapter should never throw."
|
|
76
|
+
})
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function localGitSha(cwd) {
|
|
82
|
+
try {
|
|
83
|
+
return execFileSync("git", ["rev-parse", "HEAD"], {
|
|
84
|
+
cwd,
|
|
85
|
+
encoding: "utf8",
|
|
86
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
87
|
+
}).trim();
|
|
88
|
+
} catch {
|
|
89
|
+
return void 0;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function trustedIssuersFromEnv() {
|
|
93
|
+
return (process.env.GRAFT_TRUSTED_ISSUERS ?? "").split(/[,\s]+/).map((issuer) => issuer.trim()).filter(Boolean).map((issuer) => ({ issuer }));
|
|
94
|
+
}
|
|
95
|
+
async function startServe(options) {
|
|
96
|
+
loadProjectEnv(options.cwd);
|
|
97
|
+
const config = await loadConfig(findConfig(options.cwd));
|
|
98
|
+
const url = requireDatabaseUrl();
|
|
99
|
+
const enableStudio = options.studio === true || process.env.GRAFT_STUDIO === "1";
|
|
100
|
+
const [
|
|
101
|
+
{ createFunctionsHandler },
|
|
102
|
+
{ createGraftMcpHandler },
|
|
103
|
+
{ createDb, resolveBranchHandle, scopeWriteBranch, sql },
|
|
104
|
+
studioMod
|
|
105
|
+
] = await Promise.all([
|
|
106
|
+
import("@usegraft/core"),
|
|
107
|
+
import("@usegraft/mcp"),
|
|
108
|
+
import("@usegraft/db"),
|
|
109
|
+
enableStudio ? import("@usegraft/studio") : Promise.resolve(null)
|
|
110
|
+
]);
|
|
111
|
+
const control = createDb(url);
|
|
112
|
+
const branchName = options.branchId ?? "main";
|
|
113
|
+
const branch = await resolveBranchHandle(control.db, branchName, { databaseUrl: url });
|
|
114
|
+
const writeBranch = scopeWriteBranch(branch.scope);
|
|
115
|
+
const devToken = process.env.GRAFT_DEV_TOKEN;
|
|
116
|
+
const scopes = (process.env.GRAFT_DEV_SCOPES ?? "").split(/[,\s]+/).map((s) => s.trim()).filter(Boolean);
|
|
117
|
+
const issuers = trustedIssuersFromEnv();
|
|
118
|
+
const resolveActor = createActorResolver({
|
|
119
|
+
issuers,
|
|
120
|
+
devTokens: devToken ? { [devToken]: { kind: "agent", id: "graft-serve", scopes } } : void 0
|
|
121
|
+
});
|
|
122
|
+
const approvalPolicy = process.env.GRAFT_APPROVAL_POLICY === "human" ? "human" : "none";
|
|
123
|
+
const requireMcpActor = process.env.GRAFT_MCP_REQUIRE_AUTH === "1";
|
|
124
|
+
const functions = config.functions ?? {};
|
|
125
|
+
const gitSha = localGitSha(options.cwd);
|
|
126
|
+
const fnHandler = createFunctionsHandler({
|
|
127
|
+
functions,
|
|
128
|
+
db: branch.db,
|
|
129
|
+
branch: writeBranch,
|
|
130
|
+
actor: resolveActor,
|
|
131
|
+
approvalPolicy,
|
|
132
|
+
// Backstop; per-function rateLimit overrides it.
|
|
133
|
+
rateLimit: { limit: 60, windowSeconds: 60 },
|
|
134
|
+
gitSha
|
|
135
|
+
});
|
|
136
|
+
const mcpHandler = createGraftMcpHandler({
|
|
137
|
+
name: "graft-serve",
|
|
138
|
+
contentDir: config.contentDir,
|
|
139
|
+
collections: config.collections,
|
|
140
|
+
functions,
|
|
141
|
+
db: branch.db,
|
|
142
|
+
branchId: writeBranch,
|
|
143
|
+
scope: branch.scope,
|
|
144
|
+
actor: resolveActor,
|
|
145
|
+
requireActor: requireMcpActor
|
|
146
|
+
});
|
|
147
|
+
const health = async () => {
|
|
148
|
+
try {
|
|
149
|
+
await branch.db.execute(sql`select 1`);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
return new Response(
|
|
152
|
+
JSON.stringify({
|
|
153
|
+
ok: false,
|
|
154
|
+
error: error instanceof Error ? error.message : String(error)
|
|
155
|
+
}),
|
|
156
|
+
{ status: 503, headers: { "content-type": "application/json" } }
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
return new Response(
|
|
160
|
+
JSON.stringify({
|
|
161
|
+
ok: true,
|
|
162
|
+
branch: branchName,
|
|
163
|
+
collections: Object.keys(config.collections).length,
|
|
164
|
+
functions: Object.keys(functions).length,
|
|
165
|
+
gitSha: gitSha ?? null
|
|
166
|
+
}),
|
|
167
|
+
{ status: 200, headers: { "content-type": "application/json" } }
|
|
168
|
+
);
|
|
169
|
+
};
|
|
170
|
+
const host = options.host ?? process.env.HOST ?? "127.0.0.1";
|
|
171
|
+
const requestedPort = options.port ?? Number(process.env.PORT ?? 3903);
|
|
172
|
+
if (!Number.isInteger(requestedPort) || requestedPort < 0 || requestedPort > 65535) {
|
|
173
|
+
await branch.close();
|
|
174
|
+
await control.close();
|
|
175
|
+
throw new GraftError({
|
|
176
|
+
code: "INPUT_VALIDATION_FAILED",
|
|
177
|
+
message: `"${options.port ?? process.env.PORT}" is not a valid port.`,
|
|
178
|
+
fix: "Pass --port <0-65535> (0 picks a free port) or set PORT."
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
const loopback = host === "127.0.0.1" || host === "localhost" || host === "::1";
|
|
182
|
+
if (!loopback && !requireMcpActor && issuers.length === 0 && !devToken) {
|
|
183
|
+
console.warn(
|
|
184
|
+
"[graft serve] WARNING: binding beyond loopback with no identity configured \u2014 set GRAFT_MCP_REQUIRE_AUTH=1 and provide GRAFT_DEV_TOKEN or GRAFT_TRUSTED_ISSUERS before exposing this to a network."
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
let studioHandler;
|
|
188
|
+
if (studioMod) {
|
|
189
|
+
const authorize = !loopback && (devToken || issuers.length > 0) ? async (request) => {
|
|
190
|
+
const actor = await resolveActor(request);
|
|
191
|
+
return actor.kind !== "anonymous";
|
|
192
|
+
} : !loopback ? () => false : void 0;
|
|
193
|
+
studioHandler = studioMod.createStudioHandler({
|
|
194
|
+
db: branch.db,
|
|
195
|
+
collections: config.collections,
|
|
196
|
+
contentDir: config.contentDir,
|
|
197
|
+
defaultBranch: writeBranch,
|
|
198
|
+
decidedBy: "studio-serve",
|
|
199
|
+
uiBasePath: "/studio",
|
|
200
|
+
authorize
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
const server = createServer(
|
|
204
|
+
createNodeListener(
|
|
205
|
+
createServeRouter({ fn: fnHandler, mcp: mcpHandler, health, studio: studioHandler })
|
|
206
|
+
)
|
|
207
|
+
);
|
|
208
|
+
await new Promise((resolve, reject) => {
|
|
209
|
+
server.once("error", reject);
|
|
210
|
+
server.listen(requestedPort, host, () => resolve());
|
|
211
|
+
});
|
|
212
|
+
const address = server.address();
|
|
213
|
+
const port = typeof address === "object" && address ? address.port : requestedPort;
|
|
214
|
+
return {
|
|
215
|
+
port,
|
|
216
|
+
host,
|
|
217
|
+
branch: branchName,
|
|
218
|
+
close: async () => {
|
|
219
|
+
await new Promise((resolve, reject) => {
|
|
220
|
+
server.close((err) => err ? reject(err) : resolve());
|
|
221
|
+
});
|
|
222
|
+
await branch.close();
|
|
223
|
+
await control.close();
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
async function serveCommand(options) {
|
|
228
|
+
const running = await startServe(options);
|
|
229
|
+
const base = `http://${running.host}:${running.port}`;
|
|
230
|
+
const studioOn = options.studio === true || process.env.GRAFT_STUDIO === "1";
|
|
231
|
+
console.log(
|
|
232
|
+
[
|
|
233
|
+
`graft serve \u2014 branch "${running.branch}"`,
|
|
234
|
+
` functions POST ${base}/api/fn/<name>`,
|
|
235
|
+
` mcp POST ${base}/api/mcp`,
|
|
236
|
+
` health GET ${base}/healthz`,
|
|
237
|
+
...studioOn ? [
|
|
238
|
+
` studio GET ${base}/studio`,
|
|
239
|
+
` openapi GET ${base}/api/studio/v1/openapi.json`
|
|
240
|
+
] : []
|
|
241
|
+
].join("\n")
|
|
242
|
+
);
|
|
243
|
+
await new Promise((resolve) => {
|
|
244
|
+
const stop = () => resolve();
|
|
245
|
+
process.once("SIGINT", stop);
|
|
246
|
+
process.once("SIGTERM", stop);
|
|
247
|
+
});
|
|
248
|
+
await running.close();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export {
|
|
252
|
+
createServeRouter,
|
|
253
|
+
createNodeListener,
|
|
254
|
+
startServe,
|
|
255
|
+
serveCommand
|
|
256
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
loadProjectEnv,
|
|
4
|
+
requireDatabaseUrl
|
|
5
|
+
} from "./chunk-BUPFXTUG.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,44 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
findConfig,
|
|
4
|
+
loadConfig,
|
|
5
|
+
loadProjectEnv,
|
|
6
|
+
requireDatabaseUrl
|
|
7
|
+
} from "./chunk-BUPFXTUG.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
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
findConfig,
|
|
4
|
+
loadConfig,
|
|
5
|
+
loadProjectEnv,
|
|
6
|
+
requireDatabaseUrl
|
|
7
|
+
} from "./chunk-BUPFXTUG.js";
|
|
8
|
+
|
|
9
|
+
// src/commands/db.ts
|
|
10
|
+
import { GraftError } from "@usegraft/contracts";
|
|
11
|
+
async function dbMigrateCommand(options) {
|
|
12
|
+
loadProjectEnv(options.cwd);
|
|
13
|
+
const config = await loadConfig(findConfig(options.cwd));
|
|
14
|
+
if (config.index.driver === "static") {
|
|
15
|
+
throw new GraftError({
|
|
16
|
+
code: "NEEDS_DATABASE",
|
|
17
|
+
message: "This project's content index is static (a SQLite artifact), so there is no database schema to migrate.",
|
|
18
|
+
fix: 'Static projects need no migrations \u2014 just `graft compile`. To move to the Postgres tier: set `export const index = "postgres"` in graft.config.ts and DATABASE_URL in .env, then re-run `graft db migrate` followed by `graft compile`.',
|
|
19
|
+
details: { index: config.index.driver }
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
const url = requireDatabaseUrl();
|
|
23
|
+
const { runMigrations } = await import("@usegraft/db");
|
|
24
|
+
return runMigrations(url, { dryRun: options.dryRun });
|
|
25
|
+
}
|
|
26
|
+
function formatMigrateResult(result) {
|
|
27
|
+
if (result.dryRun) {
|
|
28
|
+
return result.pending.length === 0 ? "Schema is up to date \u2014 no migrations pending." : [
|
|
29
|
+
`${result.pending.length} migration(s) pending:`,
|
|
30
|
+
...result.pending.map((tag) => ` ${tag}`),
|
|
31
|
+
"",
|
|
32
|
+
"Run `graft db migrate` (without --dry-run) to apply them."
|
|
33
|
+
].join("\n");
|
|
34
|
+
}
|
|
35
|
+
if (result.applied.length === 0) {
|
|
36
|
+
return "Schema is up to date \u2014 nothing to apply.";
|
|
37
|
+
}
|
|
38
|
+
return [
|
|
39
|
+
`Applied ${result.applied.length} migration(s):`,
|
|
40
|
+
...result.applied.map((tag) => ` ${tag}`),
|
|
41
|
+
"",
|
|
42
|
+
"Next: `graft compile` to project your content into the index."
|
|
43
|
+
].join("\n");
|
|
44
|
+
}
|
|
45
|
+
export {
|
|
46
|
+
dbMigrateCommand,
|
|
47
|
+
formatMigrateResult
|
|
48
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
assertNoStaticBranch
|
|
4
|
+
} from "./chunk-KZ3FDWUK.js";
|
|
5
|
+
import {
|
|
6
|
+
formatCompileResult,
|
|
7
|
+
printGraftError
|
|
8
|
+
} from "./chunk-GPA66AP5.js";
|
|
9
|
+
import {
|
|
10
|
+
CONFIG_FILENAMES,
|
|
11
|
+
findConfig,
|
|
12
|
+
loadConfig,
|
|
13
|
+
loadProjectEnv,
|
|
14
|
+
requireDatabaseUrl
|
|
15
|
+
} from "./chunk-BUPFXTUG.js";
|
|
16
|
+
|
|
17
|
+
// src/commands/dev.ts
|
|
18
|
+
import { existsSync, watch } from "fs";
|
|
19
|
+
import { basename, relative } from "path";
|
|
20
|
+
import { GraftError } from "@usegraft/contracts";
|
|
21
|
+
var DEBOUNCE_MS = 150;
|
|
22
|
+
async function devCommand(options) {
|
|
23
|
+
loadProjectEnv(options.cwd);
|
|
24
|
+
const configPath = findConfig(options.cwd);
|
|
25
|
+
let config = await loadConfig(configPath);
|
|
26
|
+
if (!existsSync(config.contentDir)) {
|
|
27
|
+
throw new GraftError({
|
|
28
|
+
code: "CONTENT_DIR_NOT_FOUND",
|
|
29
|
+
message: `Content directory ${config.contentDir} does not exist.`,
|
|
30
|
+
fix: "Create it (documents live at <contentDir>/<collection>/<slug>.mdx) or fix the `contentDir` export in graft.config.ts, then restart `graft dev`."
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
const isStatic = config.index.driver === "static";
|
|
34
|
+
if (isStatic) assertNoStaticBranch(options.branchId);
|
|
35
|
+
const url = isStatic ? void 0 : requireDatabaseUrl();
|
|
36
|
+
const [compiler, { createDb, resolveBranchHandle, scopeWriteBranch }] = await Promise.all([
|
|
37
|
+
import("@usegraft/compiler"),
|
|
38
|
+
import("@usegraft/db")
|
|
39
|
+
]);
|
|
40
|
+
const { compile, compileStatic } = compiler;
|
|
41
|
+
const control = url !== void 0 ? createDb(url) : void 0;
|
|
42
|
+
const branch = control !== void 0 && url !== void 0 ? await resolveBranchHandle(control.db, options.branchId ?? "main", { databaseUrl: url }) : void 0;
|
|
43
|
+
const writeBranch = branch !== void 0 ? scopeWriteBranch(branch.scope) : "main";
|
|
44
|
+
let timer;
|
|
45
|
+
let compiling = false;
|
|
46
|
+
let dirty = false;
|
|
47
|
+
let configDirty = false;
|
|
48
|
+
const runCompile = async () => {
|
|
49
|
+
if (compiling) {
|
|
50
|
+
dirty = true;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
compiling = true;
|
|
54
|
+
try {
|
|
55
|
+
if (configDirty) {
|
|
56
|
+
configDirty = false;
|
|
57
|
+
config = await loadConfig(configPath);
|
|
58
|
+
console.log(`reloaded ${basename(configPath)}`);
|
|
59
|
+
}
|
|
60
|
+
const result = branch !== void 0 ? await compile({
|
|
61
|
+
db: branch.db,
|
|
62
|
+
contentDir: config.contentDir,
|
|
63
|
+
collections: config.collections,
|
|
64
|
+
branchId: writeBranch
|
|
65
|
+
}) : await compileStatic({
|
|
66
|
+
contentDir: config.contentDir,
|
|
67
|
+
collections: config.collections,
|
|
68
|
+
indexPath: config.index.path
|
|
69
|
+
});
|
|
70
|
+
console.log(formatCompileResult(result));
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (error instanceof GraftError) printGraftError(error);
|
|
73
|
+
else console.error(error);
|
|
74
|
+
} finally {
|
|
75
|
+
compiling = false;
|
|
76
|
+
if (dirty) {
|
|
77
|
+
dirty = false;
|
|
78
|
+
schedule();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
const schedule = (configChanged = false) => {
|
|
83
|
+
if (configChanged) configDirty = true;
|
|
84
|
+
clearTimeout(timer);
|
|
85
|
+
timer = setTimeout(() => void runCompile(), DEBOUNCE_MS);
|
|
86
|
+
};
|
|
87
|
+
await runCompile();
|
|
88
|
+
const watchers = [
|
|
89
|
+
watch(config.contentDir, { recursive: true }, () => schedule()),
|
|
90
|
+
watch(config.projectDir, (_event, filename) => {
|
|
91
|
+
if (filename && CONFIG_FILENAMES.includes(filename)) schedule(true);
|
|
92
|
+
})
|
|
93
|
+
];
|
|
94
|
+
const watchedContent = relative(options.cwd, config.contentDir) || ".";
|
|
95
|
+
const branchLabel = branch === void 0 ? "static index" : branch.scope.kind === "physical" ? `${branch.name} (neon)` : options.branchId ?? "main";
|
|
96
|
+
console.log(
|
|
97
|
+
`Watching ${watchedContent} + ${basename(configPath)} (branch: ${branchLabel}). Ctrl+C to stop.`
|
|
98
|
+
);
|
|
99
|
+
await new Promise((resolveStopped) => {
|
|
100
|
+
const stop = () => {
|
|
101
|
+
clearTimeout(timer);
|
|
102
|
+
for (const watcher of watchers) watcher.close();
|
|
103
|
+
resolveStopped();
|
|
104
|
+
};
|
|
105
|
+
process.once("SIGINT", stop);
|
|
106
|
+
process.once("SIGTERM", stop);
|
|
107
|
+
});
|
|
108
|
+
await branch?.close();
|
|
109
|
+
await control?.close();
|
|
110
|
+
}
|
|
111
|
+
export {
|
|
112
|
+
devCommand
|
|
113
|
+
};
|