@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.
- package/LICENSE +21 -0
- package/README.md +64 -0
- package/dist/add-ASXTMYCX.js +61 -0
- package/dist/approvals-OEEQQN75.js +14 -0
- package/dist/asset-DMNX5JBZ.js +49 -0
- package/dist/branch-IDY42TAV.js +75 -0
- package/dist/chunk-2NH6KYTL.js +193 -0
- package/dist/chunk-3CP4IY73.js +68 -0
- package/dist/chunk-6Z27SGVO.js +173 -0
- package/dist/chunk-GDTOV3DU.js +73 -0
- package/dist/chunk-GPA66AP5.js +22 -0
- package/dist/chunk-WKRIEXBQ.js +328 -0
- package/dist/compilations-72DJ2VEP.js +29 -0
- package/dist/compile-TCBGZZAU.js +11 -0
- package/dist/content-VLKNYIHL.js +44 -0
- package/dist/db-WACSH3HN.js +48 -0
- package/dist/dev-4YSMWIAS.js +124 -0
- package/dist/harden-QASY2KMI.js +43 -0
- package/dist/index.js +437 -0
- package/dist/init-TRPSGUYW.js +117 -0
- package/dist/mcp-DKFT2M4Q.js +74 -0
- package/dist/merge-XMTFPLHO.js +176 -0
- package/dist/migrate-QB44VL6R.js +10 -0
- package/dist/serve-5ABBWZU2.js +16 -0
- package/dist/studio-6ZO3FMDR.js +109 -0
- package/package.json +63 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
CONFIG_FILENAMES
|
|
4
|
+
} from "./chunk-2NH6KYTL.js";
|
|
5
|
+
|
|
6
|
+
// src/commands/init.ts
|
|
7
|
+
import { existsSync, mkdirSync, writeFileSync } from "fs";
|
|
8
|
+
import { dirname, join, resolve } from "path";
|
|
9
|
+
import { GraftError } from "@usegraft/contracts";
|
|
10
|
+
import { barrelSource } from "@usegraft/registry";
|
|
11
|
+
var configTemplate = (driver) => `/**
|
|
12
|
+
* The schema for this project \u2014 collections defined as owned code.
|
|
13
|
+
* Agents: this is the single source of truth for what content exists.
|
|
14
|
+
* Add fields here, then author documents in content/<collection>/<slug>.mdx.
|
|
15
|
+
*
|
|
16
|
+
* Primitives you add with \`graft add\` live under graft/ and are merged in via
|
|
17
|
+
* the generated graft/index.ts barrel \u2014 you never edit the import below.
|
|
18
|
+
*/
|
|
19
|
+
import { defineCollection, field, mergePrimitives } from "@usegraft/core";
|
|
20
|
+
import * as primitives from "./graft";
|
|
21
|
+
|
|
22
|
+
${driver === "static" ? `// Where the content index lives. "static" compiles to .graft/index.db \u2014
|
|
23
|
+
// no database, no env vars. Switch to "postgres" (with DATABASE_URL) when you
|
|
24
|
+
// need operational data, typed functions, or database branching.
|
|
25
|
+
export const index = "static";` : `// The content index lives in Postgres (DATABASE_URL). Content-only projects
|
|
26
|
+
// can use \`export const index = "static"\` instead and drop the database.
|
|
27
|
+
export const index = "postgres";`}
|
|
28
|
+
|
|
29
|
+
export const pages = defineCollection({
|
|
30
|
+
name: "pages",
|
|
31
|
+
description: "Pages rendered by your site.",
|
|
32
|
+
fields: {
|
|
33
|
+
title: field.string({ description: "Page headline and <title>." }),
|
|
34
|
+
tagline: field.string({ optional: true, description: "Short line under the headline." }),
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Your own collections/functions + everything under graft/ (added via \`graft add\`).
|
|
39
|
+
// mergePrimitives throws CONFIG_INVALID on a duplicate key \u2014 never a silent override.
|
|
40
|
+
export const { collections, functions } = mergePrimitives([{ collections: { pages } }, primitives]);
|
|
41
|
+
`;
|
|
42
|
+
var homeTemplate = (driver) => `---
|
|
43
|
+
title: Hello, Graft
|
|
44
|
+
tagline: ${driver === "static" ? "Content lives in git; the index is a file." : "Content lives in git; Postgres is the index."}
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
Edit this file, then run \`graft compile\` (or keep \`graft dev\` running) to
|
|
48
|
+
project it into the content index.
|
|
49
|
+
`;
|
|
50
|
+
var GITIGNORE_TEMPLATE = `# The compiled content index \u2014 derived from the files in git.
|
|
51
|
+
# Rebuild it any time with \`graft compile\`; build it in CI before your app builds.
|
|
52
|
+
.graft/
|
|
53
|
+
`;
|
|
54
|
+
var llmsTemplate = (driver) => `# Graft project \u2014 agent guide
|
|
55
|
+
|
|
56
|
+
Everything is code. Content lives in MDX files; git is authoritative; the
|
|
57
|
+
content index is derived. If they disagree, git wins \u2014 recompile.
|
|
58
|
+
|
|
59
|
+
- Schema: graft.config.ts (defineCollection/field over Zod). The single source of truth.
|
|
60
|
+
- Documents: content/<collection>/<slug>.mdx \u2014 frontmatter must satisfy the collection schema.
|
|
61
|
+
- Slugs are kebab-case; a frontmatter \`slug:\` overrides the filename.
|
|
62
|
+
- Project into the index: \`graft compile\` (one-shot) or \`graft dev\` (watch mode).
|
|
63
|
+
${driver === "static" ? `- This project uses the **static index** (\`index = "static"\` in graft.config):
|
|
64
|
+
compile writes .graft/index.db \u2014 a SQLite artifact. No database, no env vars.
|
|
65
|
+
- The artifact is derived, so it is git-ignored. Run \`graft compile\` before your
|
|
66
|
+
app builds (e.g. build command: \`graft compile && next build\`) so it ships with
|
|
67
|
+
the deploy.
|
|
68
|
+
- Read it from your app with \`openStaticIndex(".graft/index.db")\` from
|
|
69
|
+
"@usegraft/db", passed to \`createClient({ index, collections })\`.
|
|
70
|
+
- Branches here are just **git branches**: each checkout compiles its own artifact.
|
|
71
|
+
- Operational data (form submissions, orders, comments), typed functions, and
|
|
72
|
+
database branching need the Postgres tier. Adding one fails with NEEDS_DATABASE,
|
|
73
|
+
whose \`fix\` is the upgrade: set DATABASE_URL and \`export const index = "postgres"\`.` : `- Both need DATABASE_URL in .env (any parent directory works).
|
|
74
|
+
- One project = one database. Never point DATABASE_URL at another project's
|
|
75
|
+
index \u2014 compile refuses with INDEX_OWNERSHIP rather than purge its documents.`}
|
|
76
|
+
- Primitives: add owned building blocks with \`graft add <item>\` \u2014 they land under
|
|
77
|
+
graft/ and wire in automatically (the generated graft/index.ts barrel; no config
|
|
78
|
+
edit). Run \`graft add\` with no name to list what's available.
|
|
79
|
+
- Every Graft error carries a \`fix\` \u2014 do what it says, then retry.
|
|
80
|
+
- Ship changes as git commits; every projection records the git SHA it compiled from.
|
|
81
|
+
`;
|
|
82
|
+
function scaffoldFor(driver) {
|
|
83
|
+
const files = {
|
|
84
|
+
"graft.config.ts": configTemplate(driver),
|
|
85
|
+
// The generated barrel graft add regenerates; empty until the first `graft add`.
|
|
86
|
+
[join("graft", "index.ts")]: barrelSource([]),
|
|
87
|
+
[join("content", "pages", "home.mdx")]: homeTemplate(driver),
|
|
88
|
+
"llms.txt": llmsTemplate(driver)
|
|
89
|
+
};
|
|
90
|
+
if (driver === "static") files[".gitignore"] = GITIGNORE_TEMPLATE;
|
|
91
|
+
return files;
|
|
92
|
+
}
|
|
93
|
+
function initCommand(options) {
|
|
94
|
+
const projectDir = resolve(options.targetDir);
|
|
95
|
+
const driver = options.driver ?? "static";
|
|
96
|
+
for (const name of CONFIG_FILENAMES) {
|
|
97
|
+
if (existsSync(join(projectDir, name))) {
|
|
98
|
+
throw new GraftError({
|
|
99
|
+
code: "ALREADY_INITIALIZED",
|
|
100
|
+
message: `${name} already exists in ${projectDir}.`,
|
|
101
|
+
fix: "This is already a Graft project \u2014 evolve it by editing the existing graft.config.ts, or run `graft init <dir>` against an empty directory."
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const created = [];
|
|
106
|
+
for (const [relPath, body] of Object.entries(scaffoldFor(driver))) {
|
|
107
|
+
const absPath = join(projectDir, relPath);
|
|
108
|
+
if (existsSync(absPath)) continue;
|
|
109
|
+
mkdirSync(dirname(absPath), { recursive: true });
|
|
110
|
+
writeFileSync(absPath, body, "utf8");
|
|
111
|
+
created.push(relPath);
|
|
112
|
+
}
|
|
113
|
+
return { projectDir, driver, created };
|
|
114
|
+
}
|
|
115
|
+
export {
|
|
116
|
+
initCommand
|
|
117
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
assertNoStaticBranch
|
|
4
|
+
} from "./chunk-GDTOV3DU.js";
|
|
5
|
+
import "./chunk-GPA66AP5.js";
|
|
6
|
+
import {
|
|
7
|
+
operatorName
|
|
8
|
+
} from "./chunk-3CP4IY73.js";
|
|
9
|
+
import {
|
|
10
|
+
findConfig,
|
|
11
|
+
loadConfig,
|
|
12
|
+
loadProjectEnv,
|
|
13
|
+
requireDatabaseUrl
|
|
14
|
+
} from "./chunk-2NH6KYTL.js";
|
|
15
|
+
|
|
16
|
+
// src/commands/mcp.ts
|
|
17
|
+
import { createActorResolver } from "@usegraft/auth";
|
|
18
|
+
async function mcpCommand(options) {
|
|
19
|
+
loadProjectEnv(options.cwd);
|
|
20
|
+
const config = await loadConfig(findConfig(options.cwd));
|
|
21
|
+
const isStatic = config.index.driver === "static";
|
|
22
|
+
if (isStatic) assertNoStaticBranch(options.branchId);
|
|
23
|
+
const url = isStatic ? void 0 : requireDatabaseUrl();
|
|
24
|
+
const [{ createGraftMcp, serveStdio }, { createDb, resolveBranchHandle, scopeWriteBranch }] = await Promise.all([import("@usegraft/mcp"), import("@usegraft/db")]);
|
|
25
|
+
const control = url !== void 0 ? createDb(url) : void 0;
|
|
26
|
+
const branchName = options.branchId ?? "main";
|
|
27
|
+
const branch = control !== void 0 && url !== void 0 ? await resolveBranchHandle(control.db, branchName, { databaseUrl: url }) : void 0;
|
|
28
|
+
try {
|
|
29
|
+
const devToken = process.env.GRAFT_DEV_TOKEN;
|
|
30
|
+
const scopes = (process.env.GRAFT_DEV_SCOPES ?? "").split(/[,\s]+/).map((s) => s.trim()).filter(Boolean);
|
|
31
|
+
const cliActor = {
|
|
32
|
+
kind: "agent",
|
|
33
|
+
id: "graft-cli",
|
|
34
|
+
scopes: [.../* @__PURE__ */ new Set(["content:write", ...scopes])]
|
|
35
|
+
};
|
|
36
|
+
const resolveActor = createActorResolver({
|
|
37
|
+
issuers: [],
|
|
38
|
+
devTokens: devToken ? { [devToken]: cliActor } : void 0
|
|
39
|
+
});
|
|
40
|
+
const resolveStdioActor = async (request) => {
|
|
41
|
+
const resolved = await resolveActor(request);
|
|
42
|
+
return resolved.kind === "anonymous" ? cliActor : resolved;
|
|
43
|
+
};
|
|
44
|
+
const approvalElicitation = options.elicitApprovals ? { decider: { kind: "human", id: operatorName() } } : void 0;
|
|
45
|
+
const server = createGraftMcp({
|
|
46
|
+
name: "graft",
|
|
47
|
+
contentDir: config.contentDir,
|
|
48
|
+
collections: config.collections,
|
|
49
|
+
functions: config.functions,
|
|
50
|
+
mdxTrust: config.mdxTrust,
|
|
51
|
+
...branch !== void 0 ? {
|
|
52
|
+
db: branch.db,
|
|
53
|
+
branchId: scopeWriteBranch(branch.scope),
|
|
54
|
+
// The handle's resolved scope makes search_content overlay-aware: an
|
|
55
|
+
// overlay branch searches its full parent chain, not just its own rows.
|
|
56
|
+
scope: branch.scope
|
|
57
|
+
} : { staticIndexPath: config.index.path },
|
|
58
|
+
actor: resolveStdioActor,
|
|
59
|
+
connectionActor: cliActor,
|
|
60
|
+
// Local agents may upload a file from the project; remote mounts get no
|
|
61
|
+
// `path` argument at all.
|
|
62
|
+
localUploadRoot: config.projectDir,
|
|
63
|
+
defaultAuthorization: devToken,
|
|
64
|
+
approvalElicitation
|
|
65
|
+
});
|
|
66
|
+
await serveStdio(server);
|
|
67
|
+
} finally {
|
|
68
|
+
await branch?.close();
|
|
69
|
+
await control?.close();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export {
|
|
73
|
+
mcpCommand
|
|
74
|
+
};
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
discoverMigrations
|
|
4
|
+
} from "./chunk-6Z27SGVO.js";
|
|
5
|
+
import {
|
|
6
|
+
findConfig,
|
|
7
|
+
loadConfig,
|
|
8
|
+
loadProjectEnv,
|
|
9
|
+
requireDatabaseUrl
|
|
10
|
+
} from "./chunk-2NH6KYTL.js";
|
|
11
|
+
|
|
12
|
+
// src/commands/merge.ts
|
|
13
|
+
import { basename } from "path";
|
|
14
|
+
import { GraftError } from "@usegraft/contracts";
|
|
15
|
+
function pendingLedgerRows(branchRows, targetRows) {
|
|
16
|
+
const applied = new Set(targetRows.map((row) => row.migrationId));
|
|
17
|
+
return branchRows.filter((row) => !applied.has(row.migrationId)).sort((a, b) => a.migrationId.localeCompare(b.migrationId));
|
|
18
|
+
}
|
|
19
|
+
async function mergeCommand(options) {
|
|
20
|
+
const into = options.into ?? "main";
|
|
21
|
+
if (options.branch === into) {
|
|
22
|
+
throw new GraftError({
|
|
23
|
+
code: "BRANCH_INVALID",
|
|
24
|
+
message: `Cannot merge "${options.branch}" into itself.`,
|
|
25
|
+
fix: "Pass the preview branch as the argument and the target via --into (default: main).",
|
|
26
|
+
details: { branch: options.branch, into }
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
if (options.branch === "main") {
|
|
30
|
+
throw new GraftError({
|
|
31
|
+
code: "BRANCH_INVALID",
|
|
32
|
+
message: "main is the root of the topology and cannot be merged into another branch.",
|
|
33
|
+
fix: "Merge preview branches into main (graft merge <branch>), not the other way around.",
|
|
34
|
+
details: { branch: options.branch, into }
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
loadProjectEnv(options.cwd);
|
|
38
|
+
const config = await loadConfig(findConfig(options.cwd));
|
|
39
|
+
const url = requireDatabaseUrl();
|
|
40
|
+
const [
|
|
41
|
+
{ compile, resolveGitSha },
|
|
42
|
+
{
|
|
43
|
+
createDb,
|
|
44
|
+
getBranch,
|
|
45
|
+
listAppliedMigrations,
|
|
46
|
+
recordAppliedMigration,
|
|
47
|
+
countBranchRows,
|
|
48
|
+
moveDataRecords,
|
|
49
|
+
copyDataRecords,
|
|
50
|
+
resolveBranchHandle,
|
|
51
|
+
scopeWriteBranch
|
|
52
|
+
},
|
|
53
|
+
{ runDataMigration }
|
|
54
|
+
] = await Promise.all([
|
|
55
|
+
import("@usegraft/compiler"),
|
|
56
|
+
import("@usegraft/db"),
|
|
57
|
+
import("@usegraft/core")
|
|
58
|
+
]);
|
|
59
|
+
const control = createDb(url);
|
|
60
|
+
await getBranch(control.db, options.branch).catch(async (e) => {
|
|
61
|
+
await control.close();
|
|
62
|
+
throw e;
|
|
63
|
+
});
|
|
64
|
+
await getBranch(control.db, into).catch(async (e) => {
|
|
65
|
+
await control.close();
|
|
66
|
+
throw e;
|
|
67
|
+
});
|
|
68
|
+
const src = await resolveBranchHandle(control.db, options.branch, { databaseUrl: url });
|
|
69
|
+
const target = await resolveBranchHandle(control.db, into, { databaseUrl: url });
|
|
70
|
+
const srcWrite = scopeWriteBranch(src.scope);
|
|
71
|
+
const targetWrite = scopeWriteBranch(target.scope);
|
|
72
|
+
const sameDb = src.scope.kind === "overlay" && target.scope.kind === "overlay";
|
|
73
|
+
try {
|
|
74
|
+
const [branchLedger, targetLedger] = await Promise.all([
|
|
75
|
+
listAppliedMigrations(src.db, srcWrite),
|
|
76
|
+
listAppliedMigrations(target.db, targetWrite)
|
|
77
|
+
]);
|
|
78
|
+
const pending = pendingLedgerRows(branchLedger, targetLedger);
|
|
79
|
+
const discovered = await discoverMigrations(config.migrationsDir);
|
|
80
|
+
const byId = new Map(discovered.map((m) => [m.id, m]));
|
|
81
|
+
const missing = pending.filter((row) => !byId.has(row.migrationId));
|
|
82
|
+
if (missing.length > 0) {
|
|
83
|
+
throw new GraftError({
|
|
84
|
+
code: "MIGRATION_FAILED",
|
|
85
|
+
message: `The "${options.branch}" ledger references ${missing.length} migration(s) with no file in ${basename(config.migrationsDir)}/: ${missing.map((row) => row.migrationId).join(", ")}.`,
|
|
86
|
+
fix: "git-merge the branch first \u2014 the merged tree must contain the branch's migrations/ files (and its rewritten content) before `graft merge` replays the ledger.",
|
|
87
|
+
details: { missing: missing.map((row) => row.migrationId) }
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
const gitSha = resolveGitSha(config.projectDir);
|
|
91
|
+
const replayed = [];
|
|
92
|
+
for (const row of pending) {
|
|
93
|
+
const entry = byId.get(row.migrationId);
|
|
94
|
+
if (!entry) continue;
|
|
95
|
+
const { migration } = entry;
|
|
96
|
+
const label = `${row.migrationId} [${row.kind}] ${row.collection}`;
|
|
97
|
+
if (migration.kind === "data") {
|
|
98
|
+
const report = await runDataMigration({
|
|
99
|
+
db: target.db,
|
|
100
|
+
migration,
|
|
101
|
+
migrationId: row.migrationId,
|
|
102
|
+
branchId: targetWrite,
|
|
103
|
+
gitSha,
|
|
104
|
+
apply: options.apply
|
|
105
|
+
});
|
|
106
|
+
console.log(
|
|
107
|
+
options.apply ? `replayed ${label} \u2014 ${report.changed} row(s) updated on "${into}", ${report.unchanged} unchanged` : `would replay ${label} \u2014 ${report.changed} of ${report.rows} row(s) on "${into}"`
|
|
108
|
+
);
|
|
109
|
+
} else if (options.apply) {
|
|
110
|
+
await recordAppliedMigration(target.db, {
|
|
111
|
+
branchId: targetWrite,
|
|
112
|
+
migrationId: row.migrationId,
|
|
113
|
+
kind: "content",
|
|
114
|
+
collection: row.collection,
|
|
115
|
+
docCount: row.docCount,
|
|
116
|
+
gitSha
|
|
117
|
+
});
|
|
118
|
+
console.log(`recorded ${label} \u2014 content arrived via the git merge`);
|
|
119
|
+
} else {
|
|
120
|
+
console.log(`would record ${label} \u2014 content arrives via the git merge`);
|
|
121
|
+
}
|
|
122
|
+
replayed.push(row.migrationId);
|
|
123
|
+
}
|
|
124
|
+
if (pending.length === 0) console.log(`Ledger: nothing to replay on "${into}".`);
|
|
125
|
+
const counts = await countBranchRows(src.db, srcWrite);
|
|
126
|
+
const verb = sameDb ? "move" : "copy";
|
|
127
|
+
let dataMoved = counts.data;
|
|
128
|
+
if (options.apply && counts.data > 0) {
|
|
129
|
+
dataMoved = sameDb ? await moveDataRecords(control.db, { from: options.branch, into }) : await copyDataRecords(src.db, target.db, {
|
|
130
|
+
fromBranch: srcWrite,
|
|
131
|
+
intoBranch: targetWrite
|
|
132
|
+
});
|
|
133
|
+
console.log(
|
|
134
|
+
`${verb === "move" ? "moved" : "copied"} ${dataMoved} data_records row(s) from "${options.branch}" onto "${into}"`
|
|
135
|
+
);
|
|
136
|
+
} else {
|
|
137
|
+
console.log(
|
|
138
|
+
counts.data > 0 ? `would ${verb} ${counts.data} data_records row(s) from "${options.branch}" onto "${into}"` : `Data: "${options.branch}" owns no data_records rows.`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
let compiled;
|
|
142
|
+
if (options.apply) {
|
|
143
|
+
const result = await compile({
|
|
144
|
+
db: target.db,
|
|
145
|
+
contentDir: config.contentDir,
|
|
146
|
+
collections: config.collections,
|
|
147
|
+
mdxTrust: config.mdxTrust,
|
|
148
|
+
branchId: targetWrite,
|
|
149
|
+
gitSha
|
|
150
|
+
});
|
|
151
|
+
compiled = result.changes;
|
|
152
|
+
console.log(
|
|
153
|
+
`recompiled "${into}": +${compiled.added.length} ~${compiled.changed.length} -${compiled.removed.length} (${compiled.unchanged} unchanged)`
|
|
154
|
+
);
|
|
155
|
+
console.log(
|
|
156
|
+
`
|
|
157
|
+
Merge complete. Drop the branch when you're done with it: graft branch drop ${options.branch}`
|
|
158
|
+
);
|
|
159
|
+
} else {
|
|
160
|
+
console.log(`would recompile the working tree into "${into}"`);
|
|
161
|
+
console.log(
|
|
162
|
+
`
|
|
163
|
+
Dry run \u2014 nothing was written. git-merge the branch's commits first if you haven't, then run \`graft merge ${options.branch} --apply\`.`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
return { replayed, dataMoved, compiled, didApply: options.apply === true };
|
|
167
|
+
} finally {
|
|
168
|
+
await src.close();
|
|
169
|
+
await target.close();
|
|
170
|
+
await control.close();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
export {
|
|
174
|
+
mergeCommand,
|
|
175
|
+
pendingLedgerRows
|
|
176
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
allowedHostsFor,
|
|
4
|
+
createNodeListener,
|
|
5
|
+
createServeRouter,
|
|
6
|
+
serveCommand,
|
|
7
|
+
startServe
|
|
8
|
+
} from "./chunk-WKRIEXBQ.js";
|
|
9
|
+
import "./chunk-2NH6KYTL.js";
|
|
10
|
+
export {
|
|
11
|
+
allowedHostsFor,
|
|
12
|
+
createNodeListener,
|
|
13
|
+
createServeRouter,
|
|
14
|
+
serveCommand,
|
|
15
|
+
startServe
|
|
16
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
allowedHostsFor,
|
|
4
|
+
createNodeListener
|
|
5
|
+
} from "./chunk-WKRIEXBQ.js";
|
|
6
|
+
import {
|
|
7
|
+
findConfig,
|
|
8
|
+
loadConfig,
|
|
9
|
+
loadProjectEnv,
|
|
10
|
+
requireDatabaseUrl
|
|
11
|
+
} from "./chunk-2NH6KYTL.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
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@usegraft/cli",
|
|
3
|
+
"version": "0.0.0-canary-20260831153011",
|
|
4
|
+
"description": "The graft CLI: scaffold, compile authored content, serve the runtime, and split your database credentials.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"agent",
|
|
7
|
+
"ai",
|
|
8
|
+
"cli",
|
|
9
|
+
"cms",
|
|
10
|
+
"content",
|
|
11
|
+
"graft",
|
|
12
|
+
"headless-cms",
|
|
13
|
+
"mcp",
|
|
14
|
+
"postgres",
|
|
15
|
+
"scaffolding",
|
|
16
|
+
"static-site",
|
|
17
|
+
"typescript"
|
|
18
|
+
],
|
|
19
|
+
"homepage": "https://github.com/AndersonDesign1/graft#readme",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/AndersonDesign1/graft.git",
|
|
24
|
+
"directory": "packages/cli"
|
|
25
|
+
},
|
|
26
|
+
"bin": {
|
|
27
|
+
"graft": "./dist/index.js"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist"
|
|
31
|
+
],
|
|
32
|
+
"type": "module",
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@usegraft/assets": "0.0.0-canary-20260831153011",
|
|
38
|
+
"@usegraft/auth": "0.0.0-canary-20260831153011",
|
|
39
|
+
"@usegraft/compiler": "0.0.0-canary-20260831153011",
|
|
40
|
+
"@usegraft/content-api": "0.0.0-canary-20260831153011",
|
|
41
|
+
"@usegraft/content-migrations": "0.0.0-canary-20260831153011",
|
|
42
|
+
"@usegraft/contracts": "0.0.0-canary-20260831153011",
|
|
43
|
+
"@usegraft/core": "0.0.0-canary-20260831153011",
|
|
44
|
+
"@usegraft/db": "0.0.0-canary-20260831153011",
|
|
45
|
+
"@usegraft/mcp": "0.0.0-canary-20260831153011",
|
|
46
|
+
"@usegraft/mdx-safety": "0.0.0-canary-20260831153011",
|
|
47
|
+
"@usegraft/registry": "0.0.0-canary-20260831153011",
|
|
48
|
+
"@usegraft/studio": "0.0.0-canary-20260831153011",
|
|
49
|
+
"jiti": "^2.4.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@usegraft/sdk-core": "0.0.0-canary-20260831153011"
|
|
53
|
+
},
|
|
54
|
+
"engines": {
|
|
55
|
+
"node": ">=22.16"
|
|
56
|
+
},
|
|
57
|
+
"scripts": {
|
|
58
|
+
"build": "tsup",
|
|
59
|
+
"dev": "tsup --watch",
|
|
60
|
+
"typecheck": "tsc --noEmit",
|
|
61
|
+
"test": "vitest run --passWithNoTests"
|
|
62
|
+
}
|
|
63
|
+
}
|