@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 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.
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ findConfig
4
+ } from "./chunk-BUPFXTUG.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,68 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ loadProjectEnv,
4
+ requireDatabaseUrl
5
+ } from "./chunk-BUPFXTUG.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(
37
+ handle.db,
38
+ options.id,
39
+ options.decision,
40
+ options.decidedBy ?? operatorName()
41
+ );
42
+ if (!row) {
43
+ throw new GraftError({
44
+ code: "APPROVAL_INVALID",
45
+ message: `No PENDING approval "${options.id}" exists \u2014 it may already be decided, consumed, or mistyped.`,
46
+ fix: "Run `graft approvals` to see what is actually pending; only pending approvals can be approved or denied.",
47
+ details: { id: options.id }
48
+ });
49
+ }
50
+ return row;
51
+ } finally {
52
+ await handle.close();
53
+ }
54
+ }
55
+ function formatApproval(row) {
56
+ const requester = row.requestedById ? `${row.requestedByKind}:${row.requestedById}` : row.requestedByKind;
57
+ return [
58
+ `${row.id}`,
59
+ ` function: ${row.functionName} (branch ${row.branchId})`,
60
+ ` requested: ${row.createdAt.toISOString()} by ${requester}`,
61
+ ` input: ${JSON.stringify(row.input)}`
62
+ ].join("\n");
63
+ }
64
+ export {
65
+ approvalsListCommand,
66
+ decideCommand,
67
+ formatApproval
68
+ };
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ loadProjectEnv
4
+ } from "./chunk-BUPFXTUG.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-BUPFXTUG.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,182 @@
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 } 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 contentDirSetting = typeof mod.contentDir === "string" ? mod.contentDir : "content";
101
+ const migrationsDirSetting = typeof mod.migrationsDir === "string" ? mod.migrationsDir : "migrations";
102
+ return {
103
+ configPath,
104
+ projectDir,
105
+ contentDir: resolve(projectDir, contentDirSetting),
106
+ migrationsDir: resolve(projectDir, migrationsDirSetting),
107
+ index,
108
+ collections,
109
+ functions
110
+ };
111
+ }
112
+ function assertStaticSupports(index, collections, functions, configPath) {
113
+ if (index.driver !== "static") return;
114
+ const dbCollections = Object.values(collections).filter((collection) => collection.authority === "db-authoritative").map((collection) => collection.name);
115
+ const functionNames = Object.keys(functions);
116
+ if (dbCollections.length === 0 && functionNames.length === 0) return;
117
+ const needs = [
118
+ dbCollections.length > 0 ? `db-authoritative collection(s) ${dbCollections.map((n) => `"${n}"`).join(", ")}` : "",
119
+ functionNames.length > 0 ? `typed function(s) ${functionNames.map((n) => `"${n}"`).join(", ")}` : ""
120
+ ].filter(Boolean);
121
+ throw new GraftError({
122
+ code: "NEEDS_DATABASE",
123
+ message: `${configPath} uses the static index, but declares ${needs.join(" and ")} \u2014 those live in Postgres, not in the compiled artifact.`,
124
+ 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/).',
125
+ details: { dbCollections, functions: functionNames }
126
+ });
127
+ }
128
+ var STATIC_INDEX_DEFAULT = ".graft/index.db";
129
+ function parseIndexConfig(value, projectDir, configPath) {
130
+ if (value === void 0 || value === "postgres") return { driver: "postgres" };
131
+ if (value === "static") {
132
+ return { driver: "static", path: resolve(projectDir, STATIC_INDEX_DEFAULT) };
133
+ }
134
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
135
+ const driver = value.driver;
136
+ const path = value.path;
137
+ if (driver === "postgres") return { driver: "postgres" };
138
+ if (driver === "static" && (path === void 0 || typeof path === "string")) {
139
+ return { driver: "static", path: resolve(projectDir, path ?? STATIC_INDEX_DEFAULT) };
140
+ }
141
+ }
142
+ throw new GraftError({
143
+ code: "CONFIG_INVALID",
144
+ message: `${configPath} exports an invalid \`index\` setting.`,
145
+ fix: `Use \`export const index = "static"\` (SQLite artifact, zero services), \`"postgres"\` (DATABASE_URL \u2014 the default when omitted), or { driver: "static", path: "\u2026" }.`,
146
+ details: { index: value }
147
+ });
148
+ }
149
+ function loadProjectEnv(startDir) {
150
+ let dir = resolve(startDir);
151
+ for (; ; ) {
152
+ const candidate = join(dir, ".env");
153
+ if (existsSync(candidate)) {
154
+ try {
155
+ process.loadEnvFile(candidate);
156
+ } catch {
157
+ }
158
+ return;
159
+ }
160
+ const parent = dirname(dir);
161
+ if (parent === dir) return;
162
+ dir = parent;
163
+ }
164
+ }
165
+ function requireDatabaseUrl() {
166
+ const url = process.env.DATABASE_URL;
167
+ if (url) return url;
168
+ throw new GraftError({
169
+ code: "ENV_VAR_MISSING",
170
+ message: "DATABASE_URL is not set.",
171
+ fix: "Add DATABASE_URL=postgres://\u2026 to the project's .env (any parent directory works) or export it in the environment.",
172
+ details: { variable: "DATABASE_URL" }
173
+ });
174
+ }
175
+
176
+ export {
177
+ CONFIG_FILENAMES,
178
+ findConfig,
179
+ loadConfig,
180
+ loadProjectEnv,
181
+ requireDatabaseUrl
182
+ };
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/report.ts
4
+ function printGraftError(error) {
5
+ console.error(`error ${error.code}: ${error.message}`);
6
+ if (error.fix) console.error(` fix: ${error.fix}`);
7
+ if (error.details) console.error(` details: ${JSON.stringify(error.details)}`);
8
+ }
9
+ function formatCompileResult(result) {
10
+ const { added, changed, removed, unchanged } = result.changes;
11
+ const lines = [
12
+ `Compiled ${result.count} doc(s) @ ${result.gitSha?.slice(0, 7) ?? "no-git"}: +${added.length} added, ~${changed.length} changed, -${removed.length} removed, ${unchanged} unchanged`
13
+ ];
14
+ for (const key of [...added, ...changed]) lines.push(` upserted ${key}`);
15
+ for (const key of removed) lines.push(` removed ${key}`);
16
+ return lines.join("\n");
17
+ }
18
+
19
+ export {
20
+ printGraftError,
21
+ formatCompileResult
22
+ };
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ formatCompileResult
4
+ } from "./chunk-GPA66AP5.js";
5
+ import {
6
+ findConfig,
7
+ loadConfig,
8
+ loadProjectEnv,
9
+ requireDatabaseUrl
10
+ } from "./chunk-BUPFXTUG.js";
11
+
12
+ // src/commands/compile.ts
13
+ import { GraftError } from "@usegraft/contracts";
14
+ function assertNoStaticBranch(branchId) {
15
+ if (branchId === void 0 || branchId === "main") return;
16
+ throw new GraftError({
17
+ code: "NEEDS_DATABASE",
18
+ message: `--branch ${branchId} needs the Postgres index: this project runs in static mode, where a "branch" is just your git branch.`,
19
+ fix: `Check out the git branch and compile (each checkout builds its own artifact), or upgrade to Postgres for DB branching: set DATABASE_URL and \`export const index = "postgres"\` in graft.config.`,
20
+ details: { branchId }
21
+ });
22
+ }
23
+ async function compileCommand(options) {
24
+ loadProjectEnv(options.cwd);
25
+ const config = await loadConfig(findConfig(options.cwd));
26
+ if (config.index.driver === "static") {
27
+ assertNoStaticBranch(options.branchId);
28
+ const { compileStatic } = await import("@usegraft/compiler");
29
+ const result = await compileStatic({
30
+ contentDir: config.contentDir,
31
+ collections: config.collections,
32
+ indexPath: config.index.path
33
+ });
34
+ console.log(formatCompileResult(result));
35
+ return result;
36
+ }
37
+ const url = requireDatabaseUrl();
38
+ const [{ compile }, { createDb, resolveBranchHandle, scopeWriteBranch }] = await Promise.all([
39
+ import("@usegraft/compiler"),
40
+ import("@usegraft/db")
41
+ ]);
42
+ const control = createDb(url);
43
+ try {
44
+ const branch = await resolveBranchHandle(control.db, options.branchId ?? "main", {
45
+ databaseUrl: url
46
+ });
47
+ try {
48
+ if (branch.scope.kind === "physical") {
49
+ console.log(`neon branch "${branch.name}" \u2014 compiling into its own database`);
50
+ }
51
+ const result = await compile({
52
+ db: branch.db,
53
+ contentDir: config.contentDir,
54
+ collections: config.collections,
55
+ branchId: scopeWriteBranch(branch.scope),
56
+ pruneUnknown: options.pruneUnknown
57
+ });
58
+ console.log(formatCompileResult(result));
59
+ return result;
60
+ } finally {
61
+ await branch.close();
62
+ }
63
+ } finally {
64
+ await control.close();
65
+ }
66
+ }
67
+
68
+ export {
69
+ assertNoStaticBranch,
70
+ compileCommand
71
+ };