@usegraft/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/add-UBAZMSP7.js +61 -0
- package/dist/approvals-Z3EOK5AM.js +68 -0
- package/dist/asset-FPEL463J.js +49 -0
- package/dist/branch-HRG46YIL.js +75 -0
- package/dist/chunk-BUPFXTUG.js +182 -0
- package/dist/chunk-GPA66AP5.js +22 -0
- package/dist/chunk-KZ3FDWUK.js +71 -0
- package/dist/chunk-LTFTDG6Z.js +172 -0
- package/dist/chunk-TUWL7WFD.js +256 -0
- package/dist/compilations-6P45MPAE.js +29 -0
- package/dist/compile-AAUC4G7H.js +11 -0
- package/dist/content-DY2YI3LT.js +44 -0
- package/dist/db-VGDWG4QA.js +48 -0
- package/dist/dev-MX7TQOJ2.js +113 -0
- package/dist/harden-HQHONZPH.js +43 -0
- package/dist/index.js +427 -0
- package/dist/init-3H2IP5US.js +117 -0
- package/dist/mcp-US3QFLOR.js +61 -0
- package/dist/merge-CZUNMMB5.js +175 -0
- package/dist/migrate-RRW4Z6RB.js +10 -0
- package/dist/serve-Q63UTTER.js +14 -0
- package/dist/studio-TNPOB665.js +103 -0
- package/package.json +46 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
discoverMigrations
|
|
4
|
+
} from "./chunk-LTFTDG6Z.js";
|
|
5
|
+
import {
|
|
6
|
+
findConfig,
|
|
7
|
+
loadConfig,
|
|
8
|
+
loadProjectEnv,
|
|
9
|
+
requireDatabaseUrl
|
|
10
|
+
} from "./chunk-BUPFXTUG.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
|
+
branchId: targetWrite,
|
|
148
|
+
gitSha
|
|
149
|
+
});
|
|
150
|
+
compiled = result.changes;
|
|
151
|
+
console.log(
|
|
152
|
+
`recompiled "${into}": +${compiled.added.length} ~${compiled.changed.length} -${compiled.removed.length} (${compiled.unchanged} unchanged)`
|
|
153
|
+
);
|
|
154
|
+
console.log(
|
|
155
|
+
`
|
|
156
|
+
Merge complete. Drop the branch when you're done with it: graft branch drop ${options.branch}`
|
|
157
|
+
);
|
|
158
|
+
} else {
|
|
159
|
+
console.log(`would recompile the working tree into "${into}"`);
|
|
160
|
+
console.log(
|
|
161
|
+
`
|
|
162
|
+
Dry run \u2014 nothing was written. git-merge the branch's commits first if you haven't, then run \`graft merge ${options.branch} --apply\`.`
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
return { replayed, dataMoved, compiled, didApply: options.apply === true };
|
|
166
|
+
} finally {
|
|
167
|
+
await src.close();
|
|
168
|
+
await target.close();
|
|
169
|
+
await control.close();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
export {
|
|
173
|
+
mergeCommand,
|
|
174
|
+
pendingLedgerRows
|
|
175
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
createNodeListener,
|
|
4
|
+
createServeRouter,
|
|
5
|
+
serveCommand,
|
|
6
|
+
startServe
|
|
7
|
+
} from "./chunk-TUWL7WFD.js";
|
|
8
|
+
import "./chunk-BUPFXTUG.js";
|
|
9
|
+
export {
|
|
10
|
+
createNodeListener,
|
|
11
|
+
createServeRouter,
|
|
12
|
+
serveCommand,
|
|
13
|
+
startServe
|
|
14
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
createNodeListener
|
|
4
|
+
} from "./chunk-TUWL7WFD.js";
|
|
5
|
+
import {
|
|
6
|
+
findConfig,
|
|
7
|
+
loadConfig,
|
|
8
|
+
loadProjectEnv,
|
|
9
|
+
requireDatabaseUrl
|
|
10
|
+
} from "./chunk-BUPFXTUG.js";
|
|
11
|
+
|
|
12
|
+
// src/commands/studio.ts
|
|
13
|
+
import { execFile } from "child_process";
|
|
14
|
+
import { createServer } from "http";
|
|
15
|
+
import { userInfo } from "os";
|
|
16
|
+
import { GraftError } from "@usegraft/contracts";
|
|
17
|
+
function isLoopback(host) {
|
|
18
|
+
return host === "127.0.0.1" || host === "localhost" || host === "::1";
|
|
19
|
+
}
|
|
20
|
+
function operatorName() {
|
|
21
|
+
try {
|
|
22
|
+
return userInfo().username;
|
|
23
|
+
} catch {
|
|
24
|
+
return process.env.USERNAME ?? process.env.USER ?? "operator";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function openBrowser(url) {
|
|
28
|
+
const platform = process.platform;
|
|
29
|
+
const cmd = platform === "win32" ? "cmd" : platform === "darwin" ? "open" : "xdg-open";
|
|
30
|
+
const args = platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
31
|
+
execFile(cmd, args, () => {
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
async function studioCommand(options) {
|
|
35
|
+
loadProjectEnv(options.cwd);
|
|
36
|
+
const config = await loadConfig(findConfig(options.cwd));
|
|
37
|
+
const url = requireDatabaseUrl();
|
|
38
|
+
const [{ createStudioHandler }, { createDb, resolveBranchHandle, scopeWriteBranch }] = await Promise.all([import("@usegraft/studio"), import("@usegraft/db")]);
|
|
39
|
+
const control = createDb(url);
|
|
40
|
+
const branchName = options.branchId ?? "main";
|
|
41
|
+
const branch = await resolveBranchHandle(control.db, branchName, { databaseUrl: url });
|
|
42
|
+
const writeBranch = scopeWriteBranch(branch.scope);
|
|
43
|
+
const host = options.host ?? process.env.HOST ?? "127.0.0.1";
|
|
44
|
+
const requestedPort = options.port ?? Number(process.env.GRAFT_STUDIO_PORT ?? 4983);
|
|
45
|
+
if (!Number.isInteger(requestedPort) || requestedPort < 0 || requestedPort > 65535) {
|
|
46
|
+
await branch.close();
|
|
47
|
+
await control.close();
|
|
48
|
+
throw new GraftError({
|
|
49
|
+
code: "INPUT_VALIDATION_FAILED",
|
|
50
|
+
message: `"${options.port ?? process.env.GRAFT_STUDIO_PORT}" is not a valid port.`,
|
|
51
|
+
fix: "Pass --port <0-65535> (0 picks a free port)."
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
const loopback = isLoopback(host);
|
|
55
|
+
const devToken = process.env.GRAFT_DEV_TOKEN;
|
|
56
|
+
if (!loopback && !devToken) {
|
|
57
|
+
console.warn(
|
|
58
|
+
"[graft studio] WARNING: binding beyond loopback with no GRAFT_DEV_TOKEN \u2014 set a bearer token before exposing Studio to a network."
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
const authorize = !loopback && devToken ? (request) => {
|
|
62
|
+
const header = request.headers.get("authorization") ?? "";
|
|
63
|
+
return header === `Bearer ${devToken}`;
|
|
64
|
+
} : !loopback ? () => false : void 0;
|
|
65
|
+
const handler = createStudioHandler({
|
|
66
|
+
db: branch.db,
|
|
67
|
+
collections: config.collections,
|
|
68
|
+
contentDir: config.contentDir,
|
|
69
|
+
defaultBranch: writeBranch,
|
|
70
|
+
decidedBy: operatorName,
|
|
71
|
+
authorize
|
|
72
|
+
});
|
|
73
|
+
const server = createServer(createNodeListener(handler));
|
|
74
|
+
await new Promise((resolve, reject) => {
|
|
75
|
+
server.once("error", reject);
|
|
76
|
+
server.listen(requestedPort, host, () => resolve());
|
|
77
|
+
});
|
|
78
|
+
const address = server.address();
|
|
79
|
+
const port = typeof address === "object" && address ? address.port : requestedPort;
|
|
80
|
+
const base = `http://${host}:${port}`;
|
|
81
|
+
console.log(
|
|
82
|
+
[
|
|
83
|
+
`graft studio \u2014 branch "${branchName}" (opt-in)`,
|
|
84
|
+
` ui GET ${base}/`,
|
|
85
|
+
` openapi GET ${base}/api/studio/v1/openapi.json`,
|
|
86
|
+
` Edit content, approve/deny \u2014 same ops as MCP/CLI.`
|
|
87
|
+
].join("\n")
|
|
88
|
+
);
|
|
89
|
+
if (loopback) openBrowser(`${base}/?branch=${encodeURIComponent(branchName)}`);
|
|
90
|
+
await new Promise((resolve) => {
|
|
91
|
+
const stop = () => resolve();
|
|
92
|
+
process.once("SIGINT", stop);
|
|
93
|
+
process.once("SIGTERM", stop);
|
|
94
|
+
});
|
|
95
|
+
await new Promise((resolve, reject) => {
|
|
96
|
+
server.close((err) => err ? reject(err) : resolve());
|
|
97
|
+
});
|
|
98
|
+
await branch.close();
|
|
99
|
+
await control.close();
|
|
100
|
+
}
|
|
101
|
+
export {
|
|
102
|
+
studioCommand
|
|
103
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@usegraft/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/AndersonDesign1/graft.git",
|
|
8
|
+
"directory": "packages/cli"
|
|
9
|
+
},
|
|
10
|
+
"bin": {
|
|
11
|
+
"graft": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"type": "module",
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"jiti": "^2.4.0",
|
|
22
|
+
"@usegraft/content-migrations": "0.1.0",
|
|
23
|
+
"@usegraft/compiler": "0.1.0",
|
|
24
|
+
"@usegraft/auth": "0.1.0",
|
|
25
|
+
"@usegraft/assets": "0.1.0",
|
|
26
|
+
"@usegraft/core": "0.1.0",
|
|
27
|
+
"@usegraft/contracts": "0.1.0",
|
|
28
|
+
"@usegraft/db": "0.1.0",
|
|
29
|
+
"@usegraft/mcp": "0.1.0",
|
|
30
|
+
"@usegraft/registry": "0.1.0",
|
|
31
|
+
"@usegraft/studio": "0.1.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@usegraft/sdk-core": "0.1.0"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=22.16"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsup",
|
|
41
|
+
"dev": "tsup --watch",
|
|
42
|
+
"typecheck": "tsc --noEmit",
|
|
43
|
+
"test": "vitest run --passWithNoTests",
|
|
44
|
+
"lint": "oxlint ."
|
|
45
|
+
}
|
|
46
|
+
}
|