@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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Anderson Joseph
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# @usegraft/cli
|
|
2
|
+
|
|
3
|
+
> The `graft` command. Scaffold a project, compile authored content, serve the runtime, split your database credentials.
|
|
4
|
+
|
|
5
|
+
Part of [Graft](https://github.com/AndersonDesign1/graft), a CMS built so an AI agent is the primary operator.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm i -D @usegraft/cli
|
|
11
|
+
# or run it without installing
|
|
12
|
+
npx @usegraft/cli init
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Once installed, the command is `graft`. Run it without installing under the
|
|
16
|
+
full package name: the bare name `graft` on npm belongs to an unrelated
|
|
17
|
+
package, so `npx graft` fetches the wrong thing.
|
|
18
|
+
|
|
19
|
+
## Quickstart, with no database
|
|
20
|
+
|
|
21
|
+
A content project needs no services at all. `graft compile` writes the index to a SQLite artifact your app reads embedded.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npx @usegraft/cli init # scaffolds graft.config.ts, content/, llms.txt
|
|
25
|
+
npx @usegraft/cli compile # → .graft/index.db
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Move to Postgres by setting `DATABASE_URL` and changing one line in `graft.config.ts`:
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
export const index = "postgres";
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Commands
|
|
35
|
+
|
|
36
|
+
| Command | Does |
|
|
37
|
+
| ------------------------------ | ---------------------------------------------------------------------------------------------- |
|
|
38
|
+
| `graft init` | Scaffold a project |
|
|
39
|
+
| `graft compile` | Validate authored MDX and project it into the index |
|
|
40
|
+
| `graft dev` | Recompile on save |
|
|
41
|
+
| `graft serve` | Run the headless runtime: typed functions, MCP, authored-content reads, `/healthz` |
|
|
42
|
+
| `graft studio` | Open the optional editing UI |
|
|
43
|
+
| `graft mcp` | Serve MCP on stdio, for `.mcp.json` |
|
|
44
|
+
| `graft add <item>` | Copy a registry primitive into your repo, shadcn-style |
|
|
45
|
+
| `graft approve <id>` | Decide a pending approval. Operator credential only |
|
|
46
|
+
| `graft harden <role>` | Grant a Postgres role exactly what a deployment needs, and nothing that can decide an approval |
|
|
47
|
+
| `graft branch` / `graft merge` | Copy-on-write content branches |
|
|
48
|
+
| `graft migrate` | Run content codemods when the schema changes |
|
|
49
|
+
|
|
50
|
+
Run `graft <command> --help` for flags.
|
|
51
|
+
|
|
52
|
+
## Two credentials
|
|
53
|
+
|
|
54
|
+
`graft harden` splits the operator credential from the runtime one. The runtime can serve, project content, and request approvals. It can never decide one, and Postgres enforces that rather than the application.
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
graft harden graft_runtime
|
|
58
|
+
# deploy with the runtime role's URL as DATABASE_URL
|
|
59
|
+
# keep the operator URL for compile, migrate, merge and approve
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
MIT. [Repository](https://github.com/AndersonDesign1/graft) · [Changelog](https://github.com/AndersonDesign1/graft/blob/main/packages/cli/CHANGELOG.md) · [Security policy](https://github.com/AndersonDesign1/graft/blob/main/SECURITY.md)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
findConfig
|
|
4
|
+
} from "./chunk-2NH6KYTL.js";
|
|
5
|
+
|
|
6
|
+
// src/commands/add.ts
|
|
7
|
+
import { dirname } from "path";
|
|
8
|
+
import { GraftError } from "@usegraft/contracts";
|
|
9
|
+
import { applyPlan, listItemNames, planAdd } from "@usegraft/registry";
|
|
10
|
+
async function addCommand(options) {
|
|
11
|
+
if (options.names.length === 0) {
|
|
12
|
+
throw new GraftError({
|
|
13
|
+
code: "REGISTRY_ITEM_NOT_FOUND",
|
|
14
|
+
message: "graft add needs an item name.",
|
|
15
|
+
fix: `Usage: graft add <item>. Available items: ${listItemNames().join(", ") || "(none bundled)"}.`
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
const projectDir = dirname(findConfig(options.cwd));
|
|
19
|
+
const plan = planAdd(options.names, { targetDir: projectDir });
|
|
20
|
+
console.log(
|
|
21
|
+
`${options.dryRun ? "Would add" : "Adding"} ${plan.items.map((i) => `${i.name} (${i.type})`).join(", ")}:`
|
|
22
|
+
);
|
|
23
|
+
for (const file of plan.files) {
|
|
24
|
+
const state = file.identical ? "unchanged" : file.exists ? "overwrite" : "create";
|
|
25
|
+
console.log(` ${state.padEnd(9)} ${file.relPath}`);
|
|
26
|
+
}
|
|
27
|
+
console.log(` regenerate ${plan.barrel.relPath}`);
|
|
28
|
+
if (plan.mdxMap) console.log(` regenerate ${plan.mdxMap.relPath}`);
|
|
29
|
+
if (options.dryRun) {
|
|
30
|
+
if (plan.conflicts.length > 0) {
|
|
31
|
+
console.log(
|
|
32
|
+
`
|
|
33
|
+
${plan.conflicts.length} existing file(s) differ \u2014 re-run without --dry-run and with --overwrite to replace: ${plan.conflicts.join(", ")}`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
printFollowups(plan);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const result = applyPlan(plan, { overwrite: options.overwrite });
|
|
40
|
+
const parts = [`${result.written.length} written`];
|
|
41
|
+
if (result.skipped.length > 0) parts.push(`${result.skipped.length} unchanged`);
|
|
42
|
+
const regen = [plan.barrel.relPath, result.mdxMapPath].filter(Boolean).join(" + ");
|
|
43
|
+
console.log(
|
|
44
|
+
`
|
|
45
|
+
Added ${plan.items.map((i) => i.name).join(", ")} (${parts.join(", ")}); ${regen} regenerated.`
|
|
46
|
+
);
|
|
47
|
+
printFollowups(plan);
|
|
48
|
+
console.log(
|
|
49
|
+
"It's live on your next `graft compile` \u2014 no graft.config.ts edit needed (the graft/ barrel wires modules; the MDX map wires blocks)."
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
function printFollowups(plan) {
|
|
53
|
+
const deps = Object.entries(plan.npmDependencies);
|
|
54
|
+
if (deps.length > 0) {
|
|
55
|
+
console.log("\nEnsure these packages are installed:");
|
|
56
|
+
for (const [pkg, spec] of deps) console.log(` ${pkg}@${spec}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export {
|
|
60
|
+
addCommand
|
|
61
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
approvalsListCommand,
|
|
4
|
+
decideCommand,
|
|
5
|
+
formatApproval,
|
|
6
|
+
operatorName
|
|
7
|
+
} from "./chunk-3CP4IY73.js";
|
|
8
|
+
import "./chunk-2NH6KYTL.js";
|
|
9
|
+
export {
|
|
10
|
+
approvalsListCommand,
|
|
11
|
+
decideCommand,
|
|
12
|
+
formatApproval,
|
|
13
|
+
operatorName
|
|
14
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
loadProjectEnv
|
|
4
|
+
} from "./chunk-2NH6KYTL.js";
|
|
5
|
+
|
|
6
|
+
// src/commands/asset.ts
|
|
7
|
+
import { readFileSync, statSync } from "fs";
|
|
8
|
+
import {
|
|
9
|
+
contentTypeFor,
|
|
10
|
+
createStorage,
|
|
11
|
+
defaultKeyFor,
|
|
12
|
+
storageConfigFromEnv
|
|
13
|
+
} from "@usegraft/assets";
|
|
14
|
+
import { GraftError } from "@usegraft/contracts";
|
|
15
|
+
async function assetPutCommand(options) {
|
|
16
|
+
loadProjectEnv(options.cwd);
|
|
17
|
+
let body;
|
|
18
|
+
try {
|
|
19
|
+
statSync(options.file);
|
|
20
|
+
body = readFileSync(options.file);
|
|
21
|
+
} catch {
|
|
22
|
+
throw new GraftError({
|
|
23
|
+
code: "DOCUMENT_NOT_FOUND",
|
|
24
|
+
message: `File not found: ${options.file}`,
|
|
25
|
+
fix: "Pass a path to an existing file: graft asset put <file> [key].",
|
|
26
|
+
details: { file: options.file }
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
let storage;
|
|
30
|
+
try {
|
|
31
|
+
storage = createStorage(storageConfigFromEnv());
|
|
32
|
+
} catch (error) {
|
|
33
|
+
throw new GraftError({
|
|
34
|
+
code: "ENV_VAR_MISSING",
|
|
35
|
+
message: error instanceof Error ? error.message : String(error),
|
|
36
|
+
fix: "Set S3_ENDPOINT, S3_ACCESS_KEY, S3_SECRET_KEY, and S3_BUCKET in the project's .env (any parent directory works).",
|
|
37
|
+
details: { variables: ["S3_ENDPOINT", "S3_ACCESS_KEY", "S3_SECRET_KEY", "S3_BUCKET"] }
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
const key = options.key ?? defaultKeyFor(options.file);
|
|
41
|
+
const contentType = contentTypeFor(options.file);
|
|
42
|
+
await storage.put(key, body, contentType);
|
|
43
|
+
return { key, contentType, bytes: body.byteLength };
|
|
44
|
+
}
|
|
45
|
+
export {
|
|
46
|
+
assetPutCommand,
|
|
47
|
+
contentTypeFor,
|
|
48
|
+
defaultKeyFor
|
|
49
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
loadProjectEnv,
|
|
4
|
+
requireDatabaseUrl
|
|
5
|
+
} from "./chunk-2NH6KYTL.js";
|
|
6
|
+
|
|
7
|
+
// src/commands/branch.ts
|
|
8
|
+
import { GraftError } from "@usegraft/contracts";
|
|
9
|
+
async function branchListCommand(options) {
|
|
10
|
+
loadProjectEnv(options.cwd);
|
|
11
|
+
const url = requireDatabaseUrl();
|
|
12
|
+
const { createDb, listBranches } = await import("@usegraft/db");
|
|
13
|
+
const handle = createDb(url);
|
|
14
|
+
try {
|
|
15
|
+
return await listBranches(handle.db);
|
|
16
|
+
} finally {
|
|
17
|
+
await handle.close();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
async function branchCreateCommand(options) {
|
|
21
|
+
const backend = options.backend ?? "overlay";
|
|
22
|
+
if (backend !== "overlay" && backend !== "neon") {
|
|
23
|
+
throw new GraftError({
|
|
24
|
+
code: "BRANCH_INVALID",
|
|
25
|
+
message: `Unknown branch backend "${backend}".`,
|
|
26
|
+
fix: "Use --backend overlay (default; shared DB, instant) or --backend neon (physical fork; needs NEON_API_KEY + GRAFT_NEON_PROJECT_ID).",
|
|
27
|
+
details: { backend }
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
loadProjectEnv(options.cwd);
|
|
31
|
+
const url = requireDatabaseUrl();
|
|
32
|
+
const db = await import("@usegraft/db");
|
|
33
|
+
const handle = db.createDb(url);
|
|
34
|
+
try {
|
|
35
|
+
if (backend === "neon") {
|
|
36
|
+
const config = db.neonConfigFromEnv();
|
|
37
|
+
return await db.createNeonBranch(
|
|
38
|
+
handle.db,
|
|
39
|
+
{ name: options.name, from: options.from, databaseUrl: url },
|
|
40
|
+
config
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
return await db.createBranch(handle.db, { name: options.name, from: options.from });
|
|
44
|
+
} finally {
|
|
45
|
+
await handle.close();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async function branchDropCommand(options) {
|
|
49
|
+
loadProjectEnv(options.cwd);
|
|
50
|
+
const url = requireDatabaseUrl();
|
|
51
|
+
const db = await import("@usegraft/db");
|
|
52
|
+
const handle = db.createDb(url);
|
|
53
|
+
try {
|
|
54
|
+
const meta = await db.getBranch(handle.db, options.name);
|
|
55
|
+
if (meta.backend === "neon") {
|
|
56
|
+
await db.dropNeonBranch(handle.db, options.name, db.neonConfigFromEnv());
|
|
57
|
+
return { backend: "neon" };
|
|
58
|
+
}
|
|
59
|
+
const result = await db.dropBranch(handle.db, options.name, { purgeRows: true });
|
|
60
|
+
return { ...result, backend: "overlay" };
|
|
61
|
+
} finally {
|
|
62
|
+
await handle.close();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function formatBranch(branch) {
|
|
66
|
+
const parent = branch.parent ? ` \u2190 ${branch.parent}` : " (root)";
|
|
67
|
+
const backend = branch.backend === "neon" && branch.endpointHost ? `neon @ ${branch.endpointHost}` : branch.backend;
|
|
68
|
+
return `${branch.name}${parent} [${backend}, ${branch.status}, created ${branch.createdAt.toISOString().slice(0, 10)}]`;
|
|
69
|
+
}
|
|
70
|
+
export {
|
|
71
|
+
branchCreateCommand,
|
|
72
|
+
branchDropCommand,
|
|
73
|
+
branchListCommand,
|
|
74
|
+
formatBranch
|
|
75
|
+
};
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/config.ts
|
|
4
|
+
import { existsSync } from "fs";
|
|
5
|
+
import { dirname, join, resolve } from "path";
|
|
6
|
+
import { GraftError, STATIC_INDEX_DEFAULT_PATH } from "@usegraft/contracts";
|
|
7
|
+
import { createJiti } from "jiti";
|
|
8
|
+
var CONFIG_FILENAMES = [
|
|
9
|
+
"graft.config.ts",
|
|
10
|
+
"graft.config.mts",
|
|
11
|
+
"graft.config.js",
|
|
12
|
+
"graft.config.mjs"
|
|
13
|
+
];
|
|
14
|
+
function findConfig(cwd) {
|
|
15
|
+
let dir = resolve(cwd);
|
|
16
|
+
for (; ; ) {
|
|
17
|
+
for (const name of CONFIG_FILENAMES) {
|
|
18
|
+
const candidate = join(dir, name);
|
|
19
|
+
if (existsSync(candidate)) return candidate;
|
|
20
|
+
}
|
|
21
|
+
const parent = dirname(dir);
|
|
22
|
+
if (parent === dir) break;
|
|
23
|
+
dir = parent;
|
|
24
|
+
}
|
|
25
|
+
throw new GraftError({
|
|
26
|
+
code: "CONFIG_NOT_FOUND",
|
|
27
|
+
message: `No graft.config.{ts,js} found in ${resolve(cwd)} or any parent directory.`,
|
|
28
|
+
fix: "cd into a Graft project, or scaffold one here with `graft init`."
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function isCollection(value) {
|
|
32
|
+
return typeof value === "object" && value !== null && typeof value.name === "string" && typeof value.describe === "function" && "schema" in value;
|
|
33
|
+
}
|
|
34
|
+
function isFunction(value) {
|
|
35
|
+
return typeof value === "object" && value !== null && typeof value.name === "string" && typeof value.describe === "function" && typeof value.handler === "function" && "schema" in value;
|
|
36
|
+
}
|
|
37
|
+
async function loadConfig(configPath) {
|
|
38
|
+
const jiti = createJiti(configPath, { moduleCache: false });
|
|
39
|
+
let mod;
|
|
40
|
+
try {
|
|
41
|
+
mod = await jiti.import(configPath);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
throw new GraftError({
|
|
44
|
+
code: "CONFIG_INVALID",
|
|
45
|
+
message: `Failed to load ${configPath}: ${error instanceof Error ? error.message : String(error)}`,
|
|
46
|
+
fix: "Fix the error in graft.config.ts \u2014 it must be importable and export `collections` (a record of defineCollection results)."
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
const collections = mod.collections;
|
|
50
|
+
if (typeof collections !== "object" || collections === null || Array.isArray(collections)) {
|
|
51
|
+
throw new GraftError({
|
|
52
|
+
code: "CONFIG_INVALID",
|
|
53
|
+
message: `${configPath} does not export a \`collections\` record.`,
|
|
54
|
+
fix: 'Add `export const collections = { \u2026 }` where each value is a defineCollection result from "@usegraft/core".'
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
const entries = Object.entries(collections);
|
|
58
|
+
if (entries.length === 0) {
|
|
59
|
+
throw new GraftError({
|
|
60
|
+
code: "CONFIG_INVALID",
|
|
61
|
+
message: `${configPath} exports an empty \`collections\` record.`,
|
|
62
|
+
fix: "Define at least one collection with defineCollection and include it in the exported `collections`."
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
for (const [key, value] of entries) {
|
|
66
|
+
if (!isCollection(value)) {
|
|
67
|
+
throw new GraftError({
|
|
68
|
+
code: "CONFIG_INVALID",
|
|
69
|
+
message: `collections.${key} in ${configPath} is not a collection.`,
|
|
70
|
+
fix: `Create it with defineCollection from "@usegraft/core" (it must have a name, a schema, and describe()).`,
|
|
71
|
+
details: { key }
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
let functions = {};
|
|
76
|
+
if (mod.functions !== void 0) {
|
|
77
|
+
if (typeof mod.functions !== "object" || mod.functions === null || Array.isArray(mod.functions)) {
|
|
78
|
+
throw new GraftError({
|
|
79
|
+
code: "CONFIG_INVALID",
|
|
80
|
+
message: `${configPath} exports \`functions\` but it is not a record.`,
|
|
81
|
+
fix: "Export `functions` as a record of defineFunction results, e.g. `export const functions = { pageStats }`, or omit the export."
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
const fnEntries = Object.entries(mod.functions);
|
|
85
|
+
for (const [key, value] of fnEntries) {
|
|
86
|
+
if (!isFunction(value)) {
|
|
87
|
+
throw new GraftError({
|
|
88
|
+
code: "CONFIG_INVALID",
|
|
89
|
+
message: `functions.${key} in ${configPath} is not a function.`,
|
|
90
|
+
fix: `Create it with defineFunction from "@usegraft/core" (it must have a name, a schema, a handler, and describe()).`,
|
|
91
|
+
details: { key }
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
functions = mod.functions;
|
|
96
|
+
}
|
|
97
|
+
const projectDir = dirname(configPath);
|
|
98
|
+
const index = parseIndexConfig(mod.index, projectDir, configPath);
|
|
99
|
+
assertStaticSupports(index, collections, functions, configPath);
|
|
100
|
+
const mdxTrust = parseMdxTrust(mod.mdxTrust, configPath);
|
|
101
|
+
const contentDirSetting = typeof mod.contentDir === "string" ? mod.contentDir : "content";
|
|
102
|
+
const migrationsDirSetting = typeof mod.migrationsDir === "string" ? mod.migrationsDir : "migrations";
|
|
103
|
+
return {
|
|
104
|
+
configPath,
|
|
105
|
+
projectDir,
|
|
106
|
+
contentDir: resolve(projectDir, contentDirSetting),
|
|
107
|
+
migrationsDir: resolve(projectDir, migrationsDirSetting),
|
|
108
|
+
index,
|
|
109
|
+
collections,
|
|
110
|
+
functions,
|
|
111
|
+
mdxTrust
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function parseMdxTrust(value, configPath) {
|
|
115
|
+
if (value === void 0) return "restricted";
|
|
116
|
+
if (value === "restricted" || value === "full") return value;
|
|
117
|
+
throw new GraftError({
|
|
118
|
+
code: "CONFIG_INVALID",
|
|
119
|
+
message: `${configPath} exports \`mdxTrust\` as ${JSON.stringify(value)}, which is not "restricted" or "full".`,
|
|
120
|
+
fix: 'Export `mdxTrust = "restricted"` (the default: `{\u2026}` expressions and `import` are refused in authored bodies) or `mdxTrust = "full"` (every author has commit access, so code review is the control). Omit the export to keep the default.',
|
|
121
|
+
details: { mdxTrust: value }
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
function assertStaticSupports(index, collections, functions, configPath) {
|
|
125
|
+
if (index.driver !== "static") return;
|
|
126
|
+
const dbCollections = Object.values(collections).filter((collection) => collection.authority === "db-authoritative").map((collection) => collection.name);
|
|
127
|
+
const functionNames = Object.keys(functions);
|
|
128
|
+
if (dbCollections.length === 0 && functionNames.length === 0) return;
|
|
129
|
+
const needs = [
|
|
130
|
+
dbCollections.length > 0 ? `db-authoritative collection(s) ${dbCollections.map((n) => `"${n}"`).join(", ")}` : "",
|
|
131
|
+
functionNames.length > 0 ? `typed function(s) ${functionNames.map((n) => `"${n}"`).join(", ")}` : ""
|
|
132
|
+
].filter(Boolean);
|
|
133
|
+
throw new GraftError({
|
|
134
|
+
code: "NEEDS_DATABASE",
|
|
135
|
+
message: `${configPath} uses the static index, but declares ${needs.join(" and ")} \u2014 those live in Postgres, not in the compiled artifact.`,
|
|
136
|
+
fix: 'Upgrade this project to the Postgres tier: set DATABASE_URL in .env, change graft.config to `export const index = "postgres"`, run `graft db migrate`, then `graft compile`. To stay service-free instead, remove the db-authoritative collections and functions (a `graft add` primitive that brought them can be deleted from graft/).',
|
|
137
|
+
details: { dbCollections, functions: functionNames }
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
function parseIndexConfig(value, projectDir, configPath) {
|
|
141
|
+
if (value === void 0 || value === "postgres") return { driver: "postgres" };
|
|
142
|
+
if (value === "static") {
|
|
143
|
+
return { driver: "static", path: resolve(projectDir, STATIC_INDEX_DEFAULT_PATH) };
|
|
144
|
+
}
|
|
145
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
146
|
+
const driver = value.driver;
|
|
147
|
+
const path = value.path;
|
|
148
|
+
if (driver === "postgres") return { driver: "postgres" };
|
|
149
|
+
if (driver === "static" && (path === void 0 || typeof path === "string")) {
|
|
150
|
+
return { driver: "static", path: resolve(projectDir, path ?? STATIC_INDEX_DEFAULT_PATH) };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
throw new GraftError({
|
|
154
|
+
code: "CONFIG_INVALID",
|
|
155
|
+
message: `${configPath} exports an invalid \`index\` setting.`,
|
|
156
|
+
fix: `Use \`export const index = "static"\` (SQLite artifact, zero services), \`"postgres"\` (DATABASE_URL \u2014 the default when omitted), or { driver: "static", path: "\u2026" }.`,
|
|
157
|
+
details: { index: value }
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
function loadProjectEnv(startDir) {
|
|
161
|
+
let dir = resolve(startDir);
|
|
162
|
+
for (; ; ) {
|
|
163
|
+
const candidate = join(dir, ".env");
|
|
164
|
+
if (existsSync(candidate)) {
|
|
165
|
+
try {
|
|
166
|
+
process.loadEnvFile(candidate);
|
|
167
|
+
} catch {
|
|
168
|
+
}
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const parent = dirname(dir);
|
|
172
|
+
if (parent === dir) return;
|
|
173
|
+
dir = parent;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function requireDatabaseUrl() {
|
|
177
|
+
const url = process.env.DATABASE_URL;
|
|
178
|
+
if (url) return url;
|
|
179
|
+
throw new GraftError({
|
|
180
|
+
code: "ENV_VAR_MISSING",
|
|
181
|
+
message: "DATABASE_URL is not set.",
|
|
182
|
+
fix: "Add DATABASE_URL=postgres://\u2026 to the project's .env (any parent directory works) or export it in the environment.",
|
|
183
|
+
details: { variable: "DATABASE_URL" }
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export {
|
|
188
|
+
CONFIG_FILENAMES,
|
|
189
|
+
findConfig,
|
|
190
|
+
loadConfig,
|
|
191
|
+
loadProjectEnv,
|
|
192
|
+
requireDatabaseUrl
|
|
193
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
loadProjectEnv,
|
|
4
|
+
requireDatabaseUrl
|
|
5
|
+
} from "./chunk-2NH6KYTL.js";
|
|
6
|
+
|
|
7
|
+
// src/commands/approvals.ts
|
|
8
|
+
import { userInfo } from "os";
|
|
9
|
+
function operatorName() {
|
|
10
|
+
try {
|
|
11
|
+
return userInfo().username;
|
|
12
|
+
} catch {
|
|
13
|
+
return process.env.USERNAME ?? process.env.USER ?? "operator";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
async function approvalsListCommand(options) {
|
|
17
|
+
loadProjectEnv(options.cwd);
|
|
18
|
+
const url = requireDatabaseUrl();
|
|
19
|
+
const { createDb, listPendingApprovals } = await import("@usegraft/db");
|
|
20
|
+
const handle = createDb(url);
|
|
21
|
+
try {
|
|
22
|
+
return await listPendingApprovals(handle.db);
|
|
23
|
+
} finally {
|
|
24
|
+
await handle.close();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async function decideCommand(options) {
|
|
28
|
+
loadProjectEnv(options.cwd);
|
|
29
|
+
const url = requireDatabaseUrl();
|
|
30
|
+
const [{ createDb, decideApproval }, { GraftError }] = await Promise.all([
|
|
31
|
+
import("@usegraft/db"),
|
|
32
|
+
import("@usegraft/contracts")
|
|
33
|
+
]);
|
|
34
|
+
const handle = createDb(url);
|
|
35
|
+
try {
|
|
36
|
+
const row = await decideApproval(handle.db, options.id, options.decision, {
|
|
37
|
+
kind: "human",
|
|
38
|
+
id: operatorName()
|
|
39
|
+
});
|
|
40
|
+
if (!row) {
|
|
41
|
+
throw new GraftError({
|
|
42
|
+
code: "APPROVAL_INVALID",
|
|
43
|
+
message: `No PENDING approval "${options.id}" exists \u2014 it may already be decided, consumed, or mistyped.`,
|
|
44
|
+
fix: "Run `graft approvals` to see what is actually pending; only pending approvals can be approved or denied.",
|
|
45
|
+
details: { id: options.id }
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return row;
|
|
49
|
+
} finally {
|
|
50
|
+
await handle.close();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function formatApproval(row) {
|
|
54
|
+
const requester = row.requestedById ? `${row.requestedByKind}:${row.requestedById}` : row.requestedByKind;
|
|
55
|
+
return [
|
|
56
|
+
`${row.id}`,
|
|
57
|
+
` function: ${row.functionName} (branch ${row.branchId})`,
|
|
58
|
+
` requested: ${row.createdAt.toISOString()} by ${requester}`,
|
|
59
|
+
` input: ${JSON.stringify(row.input)}`
|
|
60
|
+
].join("\n");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export {
|
|
64
|
+
operatorName,
|
|
65
|
+
approvalsListCommand,
|
|
66
|
+
decideCommand,
|
|
67
|
+
formatApproval
|
|
68
|
+
};
|