@usegraft/cli 1.0.0-beta.0 → 1.0.0-beta.2
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/README.md
CHANGED
|
@@ -9,7 +9,7 @@ Part of [Graft](https://github.com/AndersonDesign1/graft), a CMS built so an AI
|
|
|
9
9
|
```bash
|
|
10
10
|
npm i -D @usegraft/cli
|
|
11
11
|
# or run it without installing
|
|
12
|
-
npx @usegraft/cli init
|
|
12
|
+
npx @usegraft/cli@latest init
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Once installed, the command is `graft`. Run it without installing under the
|
|
@@ -21,8 +21,8 @@ package, so `npx graft` fetches the wrong thing.
|
|
|
21
21
|
A content project needs no services at all. `graft compile` writes the index to a SQLite artifact your app reads embedded.
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
|
-
npx @usegraft/cli init # scaffolds graft.config.ts, content/, llms.txt
|
|
25
|
-
npx @usegraft/cli compile # → .graft/index.db
|
|
24
|
+
npx @usegraft/cli@latest init # scaffolds graft.config.ts, content/, llms.txt
|
|
25
|
+
npx @usegraft/cli@latest compile # → .graft/index.db
|
|
26
26
|
```
|
|
27
27
|
|
|
28
28
|
Move to Postgres by setting `DATABASE_URL` and changing one line in `graft.config.ts`:
|
|
@@ -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
|
);
|
|
@@ -208,14 +208,11 @@ async function startServe(options) {
|
|
|
208
208
|
const health = async () => {
|
|
209
209
|
try {
|
|
210
210
|
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
|
-
);
|
|
211
|
+
} catch {
|
|
212
|
+
return new Response(JSON.stringify({ ok: false, error: "database_unavailable" }), {
|
|
213
|
+
status: 503,
|
|
214
|
+
headers: { "content-type": "application/json" }
|
|
215
|
+
});
|
|
219
216
|
}
|
|
220
217
|
return new Response(
|
|
221
218
|
JSON.stringify({
|
package/dist/index.js
CHANGED
|
@@ -345,7 +345,7 @@ async function run(argv, options = {}) {
|
|
|
345
345
|
return 0;
|
|
346
346
|
}
|
|
347
347
|
case "serve": {
|
|
348
|
-
const { serveCommand } = await import("./serve-
|
|
348
|
+
const { serveCommand } = await import("./serve-OP4OPK37.js");
|
|
349
349
|
await serveCommand({
|
|
350
350
|
cwd,
|
|
351
351
|
branchId: args.branchId,
|
|
@@ -356,7 +356,7 @@ async function run(argv, options = {}) {
|
|
|
356
356
|
return 0;
|
|
357
357
|
}
|
|
358
358
|
case "studio": {
|
|
359
|
-
const { studioCommand } = await import("./studio-
|
|
359
|
+
const { studioCommand } = await import("./studio-HUGOTZOY.js");
|
|
360
360
|
await studioCommand({
|
|
361
361
|
cwd,
|
|
362
362
|
branchId: args.branchId,
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
allowedHostsFor,
|
|
4
|
+
createNodeListener
|
|
5
|
+
} from "./chunk-PSJ2OBWG.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.2",
|
|
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.
|
|
49
|
-
"jiti": "^2.
|
|
37
|
+
"@usegraft/assets": "1.0.0-beta.2",
|
|
38
|
+
"@usegraft/auth": "1.0.0-beta.2",
|
|
39
|
+
"@usegraft/compiler": "1.0.0-beta.2",
|
|
40
|
+
"@usegraft/content-api": "1.0.0-beta.2",
|
|
41
|
+
"@usegraft/content-migrations": "1.0.0-beta.2",
|
|
42
|
+
"@usegraft/contracts": "1.0.0-beta.2",
|
|
43
|
+
"@usegraft/core": "1.0.0-beta.2",
|
|
44
|
+
"@usegraft/db": "1.0.0-beta.2",
|
|
45
|
+
"@usegraft/mcp": "1.0.0-beta.2",
|
|
46
|
+
"@usegraft/mdx-safety": "1.0.0-beta.2",
|
|
47
|
+
"@usegraft/registry": "1.0.0-beta.2",
|
|
48
|
+
"@usegraft/studio": "1.0.0-beta.2",
|
|
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.2"
|
|
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
|
-
};
|