@agilesyndrome/cf-genai-base 5.0.0 → 5.0.1
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/CLI.md +121 -0
- package/CONTRACT.md +2 -2
- package/README.md +49 -0
- package/bin/cf-genai.js +11 -0
- package/changelog.md +10 -0
- package/package.json +14 -4
- package/src/auth/index.js +1 -0
- package/src/auth/oauth-single.js +193 -0
- package/src/cli/cli.js +133 -0
- package/src/cli/d1.js +165 -0
- package/src/cli/operations.js +34 -0
- package/src/cli/project.js +560 -0
- package/src/cli/status.js +46 -0
- package/src/cli/tenants.js +16 -0
- package/src/cli/users.js +34 -0
- package/src/cli/version.js +54 -0
package/src/cli/d1.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
export const INTERNAL_TABLE_SQL = `
|
|
7
|
+
SELECT name
|
|
8
|
+
FROM sqlite_schema
|
|
9
|
+
WHERE type = 'table'
|
|
10
|
+
AND name NOT LIKE 'sqlite_%'
|
|
11
|
+
AND name NOT LIKE '_cf_%'
|
|
12
|
+
AND name <> 'd1_migrations'
|
|
13
|
+
ORDER BY name;
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
export function shellCommand(value = "npx wrangler") {
|
|
17
|
+
return value.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g)?.map((part) => part.replace(/^(["'])(.*)\1$/, "$2")) ?? ["npx", "wrangler"];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function targetArgs(target, options = {}) {
|
|
21
|
+
if (target === "local") return ["--local"];
|
|
22
|
+
const args = ["--remote"];
|
|
23
|
+
const environment = target === "staging" ? options.stagingEnv : options.productionEnv;
|
|
24
|
+
if (environment) args.push("--env", environment);
|
|
25
|
+
return args;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function extractRows(value) {
|
|
29
|
+
if (Array.isArray(value)) return value.flatMap(extractRows);
|
|
30
|
+
if (!value || typeof value !== "object") return [];
|
|
31
|
+
const rows = [];
|
|
32
|
+
for (const [key, child] of Object.entries(value)) {
|
|
33
|
+
if (key === "results" && Array.isArray(child)) rows.push(...child);
|
|
34
|
+
rows.push(...extractRows(child));
|
|
35
|
+
}
|
|
36
|
+
return rows;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function parseJsonRows(output) {
|
|
40
|
+
try {
|
|
41
|
+
return extractRows(JSON.parse(output));
|
|
42
|
+
} catch {
|
|
43
|
+
throw new Error("Wrangler returned invalid JSON while inspecting D1.");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function tableNamesFromJson(output) {
|
|
48
|
+
return parseJsonRows(output)
|
|
49
|
+
.map((row) => row?.name)
|
|
50
|
+
.filter((name) => typeof name === "string" && name.length > 0);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function clearSql(tableNames) {
|
|
54
|
+
const identifiers = tableNames.map((name) => `"${name.replaceAll('"', '""')}"`);
|
|
55
|
+
return [
|
|
56
|
+
"PRAGMA defer_foreign_keys = ON;",
|
|
57
|
+
...identifiers.map((name) => `DELETE FROM ${name};`),
|
|
58
|
+
"PRAGMA defer_foreign_keys = OFF;",
|
|
59
|
+
"",
|
|
60
|
+
].join("\n");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function stripInternalRows(sql) {
|
|
64
|
+
return sql
|
|
65
|
+
.split("\n")
|
|
66
|
+
.filter((line) => !/^\s*INSERT\s+INTO\s+["`]?((d1_migrations)|(sqlite_[^"`\s]*)|(_cf_[^"`\s]*))["`]?/i.test(line))
|
|
67
|
+
.join("\n");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function run(wrangler, args, options = {}) {
|
|
71
|
+
const result = spawnSync(wrangler[0], [...wrangler.slice(1), ...args], {
|
|
72
|
+
cwd: options.cwd,
|
|
73
|
+
env: options.env,
|
|
74
|
+
encoding: "utf8",
|
|
75
|
+
stdio: options.capture ? ["ignore", "pipe", "pipe"] : "inherit",
|
|
76
|
+
});
|
|
77
|
+
if (result.error) throw result.error;
|
|
78
|
+
if (result.status !== 0) throw new Error(`${wrangler.join(" ")} exited with status ${result.status ?? "unknown"}.`);
|
|
79
|
+
return result.stdout ?? "";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function execute(wrangler, database, target, sqlFile, options) {
|
|
83
|
+
run(wrangler, ["d1", "execute", database, ...targetArgs(target, options), "--file", sqlFile, "--yes", "--config", options.config], options);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function executeJson(wrangler, database, target, command, options) {
|
|
87
|
+
return run(wrangler, ["d1", "execute", database, ...targetArgs(target, options), "--command", command, "--json", "--config", options.config], { ...options, capture: true });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function migrations(wrangler, database, target, options) {
|
|
91
|
+
run(wrangler, ["d1", "migrations", "apply", database, ...targetArgs(target, options), "--config", options.config], options);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function refreshD1(target, { database, productionDatabase, options }) {
|
|
95
|
+
const directory = await mkdtemp(join(tmpdir(), "cf-genai-d1-refresh-"));
|
|
96
|
+
const exportPath = join(directory, "production-data.sql");
|
|
97
|
+
const importPath = join(directory, "application-data.sql");
|
|
98
|
+
try {
|
|
99
|
+
run(options.wranglerCommand, [
|
|
100
|
+
"d1", "export", productionDatabase, "--remote",
|
|
101
|
+
...(options.productionEnv ? ["--env", options.productionEnv] : []),
|
|
102
|
+
"--no-schema", "--output", exportPath, "--skip-confirmation", "--config", options.config,
|
|
103
|
+
], options);
|
|
104
|
+
migrations(options.wranglerCommand, database, target, options);
|
|
105
|
+
const tableOutput = executeJson(options.wranglerCommand, database, target, INTERNAL_TABLE_SQL, options);
|
|
106
|
+
const tables = tableNamesFromJson(tableOutput);
|
|
107
|
+
if (tables.length === 0) throw new Error(`No application tables found in ${target} D1 database.`);
|
|
108
|
+
const exported = await readFile(exportPath, "utf8");
|
|
109
|
+
await writeFile(importPath, `${clearSql(tables)}${stripInternalRows(exported)}`);
|
|
110
|
+
execute(options.wranglerCommand, database, target, importPath, options);
|
|
111
|
+
await mkdir(options.cwd || process.cwd(), { recursive: true });
|
|
112
|
+
await writeFile(join(options.cwd || process.cwd(), ".cf-genai-last-prod-refresh"), new Date().toISOString() + "\n");
|
|
113
|
+
} finally {
|
|
114
|
+
await rm(directory, { recursive: true, force: true });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function refreshLocalD1(args) {
|
|
119
|
+
return refreshD1("local", args);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function refreshStagingD1(args) {
|
|
123
|
+
return refreshD1("staging", args);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function migrateD1({ target, database, options }) {
|
|
127
|
+
migrations(options.wranglerCommand, database, target, options);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function statusD1({ target, database, options }) {
|
|
131
|
+
if (target !== "local") {
|
|
132
|
+
run(options.wranglerCommand, ["d1", "info", database, ...(target === "staging" && options.stagingEnv ? ["--env", options.stagingEnv] : []), ...(target === "production" && options.productionEnv ? ["--env", options.productionEnv] : []), "--config", options.config], options);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const output = executeJson(options.wranglerCommand, database, target, "SELECT name, type FROM sqlite_schema WHERE type IN ('table', 'index') ORDER BY type, name;", options);
|
|
136
|
+
console.log(output);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function checkD1({ target, database, options }) {
|
|
140
|
+
const output = executeJson(options.wranglerCommand, database, target, "PRAGMA foreign_key_check; PRAGMA integrity_check;", options);
|
|
141
|
+
const rows = parseJsonRows(output);
|
|
142
|
+
const failures = rows.filter((row) => !(Object.values(row ?? {}).length === 1 && Object.values(row)[0] === "ok"));
|
|
143
|
+
if (failures.length > 0) throw new Error(`D1 integrity checks failed: ${JSON.stringify(failures)}`);
|
|
144
|
+
console.log(`${target} D1 integrity checks passed.`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function backupD1({ target, database, options }) {
|
|
148
|
+
if (!options.output) throw new Error("Backup requires --output PATH.");
|
|
149
|
+
if (target === "production" && !options.confirmProduction) throw new Error("Production backup requires --confirm-production.");
|
|
150
|
+
run(options.wranglerCommand, ["d1", "export", database, ...targetArgs(target, options), "--output", options.output, "--skip-confirmation", "--config", options.config], options);
|
|
151
|
+
console.log("Backed up " + database + " (" + target + ") to " + options.output);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export async function restoreD1({ target, database, options }) {
|
|
155
|
+
if (!options.file) throw new Error("Restore requires --file PATH.");
|
|
156
|
+
try { await readFile(options.file); } catch { throw new Error("Restore file not found: " + options.file); }
|
|
157
|
+
if (target !== "local" && !options.yes) throw new Error("Remote restore replaces data; rerun with --yes.");
|
|
158
|
+
if (target === "production" && !options.confirmProduction) throw new Error("Production restore requires --confirm-production.");
|
|
159
|
+
execute(options.wranglerCommand, database, target, options.file, options);
|
|
160
|
+
console.log("Restored " + database + " (" + target + ") from " + options.file);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function checkConfig(options) {
|
|
164
|
+
run(options.wranglerCommand, ["deploy", "--dry-run", "--config", options.config], options);
|
|
165
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { executeJson, parseJsonRows } from "./d1.js";
|
|
2
|
+
|
|
3
|
+
function query(sql, options) { return parseJsonRows(executeJson(options.wranglerCommand, options.database, normalizeTarget(options.target), sql, options)); }
|
|
4
|
+
function normalizeTarget(target) { return target === "prod" ? "production" : target || "local"; }
|
|
5
|
+
function output(value) { console.log(JSON.stringify(value, null, 2)); }
|
|
6
|
+
|
|
7
|
+
export function runOperationalCommand({ domain, action, identifier, value, options }) {
|
|
8
|
+
const target = normalizeTarget(options.target);
|
|
9
|
+
if (!["local", "staging", "production"].includes(target)) throw new Error("Environment must be local, staging, or prod/production.");
|
|
10
|
+
if (domain === "admin" && action === "status") return output(query("SELECT feature,MAX(CASE state WHEN 'red' THEN 2 WHEN 'yellow' THEN 1 ELSE 0 END) AS severity,COUNT(*) AS healthchecks FROM core_healthchecks GROUP BY feature ORDER BY feature;", { ...options, target }));
|
|
11
|
+
if (domain === "admin" && action === "features") return output(query("SELECT feature,COUNT(*) AS healthchecks FROM core_healthchecks GROUP BY feature ORDER BY feature;", { ...options, target }));
|
|
12
|
+
if (domain === "admin" && action === "users") return output(query("SELECT id,email,display_name,provider,subject,is_admin,created_at,updated_at FROM auth_users ORDER BY email COLLATE NOCASE;", { ...options, target }));
|
|
13
|
+
if (domain === "admin" && action === "scopes") return output(query("SELECT name,label,description,system FROM auth_scopes ORDER BY name;", { ...options, target }));
|
|
14
|
+
if (domain === "admin" && action === "groups") return output(query("SELECT name,display_name,description,created_at,updated_at FROM auth_groups ORDER BY display_name COLLATE NOCASE;", { ...options, target }));
|
|
15
|
+
if ((domain === "healthcheck" || domain === "healthchecks" || (domain === "admin" && action === "healthchecks")) && action === "list") return output(query("SELECT id,feature,component,display_name,state,metadata_json,created_at,updated_at FROM core_healthchecks ORDER BY feature,component;", { ...options, target }));
|
|
16
|
+
if ((domain === "healthcheck" || domain === "healthchecks" || (domain === "admin" && action === "healthchecks")) && action === "get") { if (!identifier) throw new Error("A healthcheck id is required."); return output(query("SELECT id,feature,component,display_name,state,metadata_json,created_at,updated_at FROM core_healthchecks WHERE id=" + sql(identifier) + " LIMIT 1;", { ...options, target })); }
|
|
17
|
+
if ((domain === "healthcheck" || domain === "healthchecks" || (domain === "admin" && action === "healthchecks")) && ["set", "update"].includes(action)) { if (!identifier || !value) throw new Error("healthcheck:set requires an id and state (red, yellow, or green)."); if (!["red", "yellow", "green"].includes(value)) throw new Error("Healthcheck state must be red, yellow, or green."); query("UPDATE core_healthchecks SET state=" + sql(value) + ",updated_at=CURRENT_TIMESTAMP WHERE id=" + sql(identifier) + ";", { ...options, target }); return output({ id: identifier, state: value, environment: target, changed_by: "human:cli" }); }
|
|
18
|
+
if ((domain === "circuit-breaker" || domain === "circuit-breakers" || domain === "circuit" || (domain === "admin" && action === "circuit-breakers")) && action === "list") return output(query("SELECT id,feature,name,display_name,state,healthcheck_mode,allow_self_healing,metadata_json,created_at,updated_at FROM core_circuit_breakers ORDER BY feature,name;", { ...options, target }));
|
|
19
|
+
if ((domain === "circuit-breaker" || domain === "circuit-breakers" || domain === "circuit" || (domain === "admin" && action === "circuit-breakers")) && action === "get") {
|
|
20
|
+
if (!identifier) throw new Error("A circuit-breaker id is required.");
|
|
21
|
+
return output(query("SELECT id,feature,name,display_name,state,healthcheck_mode,allow_self_healing,metadata_json,created_at,updated_at FROM core_circuit_breakers WHERE id=" + sql(identifier) + " LIMIT 1;", { ...options, target }));
|
|
22
|
+
}
|
|
23
|
+
if ((domain === "circuit-breaker" || domain === "circuit-breakers" || domain === "circuit" || (domain === "admin" && action === "circuit-breakers")) && (action === "set" || action === "update")) {
|
|
24
|
+
if (!identifier || !value) throw new Error("circuit-breaker:set requires an id and state (off, tripped, or on).");
|
|
25
|
+
if (!["off", "tripped", "on"].includes(value)) throw new Error("Circuit-breaker state must be off, tripped, or on.");
|
|
26
|
+
if (target === "production" && !options.confirmProduction) throw new Error("Production circuit-breaker changes require --confirm-production.");
|
|
27
|
+
const rows = query("SELECT id,state FROM core_circuit_breakers WHERE id=" + sql(identifier) + " LIMIT 1;", { ...options, target });
|
|
28
|
+
if (!rows[0]) throw new Error("Circuit breaker not found: " + identifier);
|
|
29
|
+
query("UPDATE core_circuit_breakers SET state=" + sql(value) + ",updated_at=CURRENT_TIMESTAMP WHERE id=" + sql(identifier) + ";", { ...options, target });
|
|
30
|
+
return output({ id: identifier, state: value, environment: target, changed_by: "human:cli" });
|
|
31
|
+
}
|
|
32
|
+
throw new Error("Unknown operational command.");
|
|
33
|
+
}
|
|
34
|
+
function sql(value) { return "'" + String(value).replaceAll("'", "''") + "'"; }
|