@agilesyndrome/cf-genai-base 5.0.0 → 5.0.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/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/admin/routes.js +1 -1
- package/src/auth/index.js +1 -0
- package/src/auth/oauth-single.js +204 -0
- package/src/auth/subscriptions.js +19 -2
- package/src/auth/users.js +16 -7
- 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/core/security.js +22 -5
- package/src/runtime/worker.js +16 -3
package/src/cli/cli.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { backupD1, checkConfig, checkD1, migrateD1, refreshLocalD1, refreshStagingD1, restoreD1, statusD1 } from "./d1.js";
|
|
2
|
+
import { runUserCommand } from "./users.js";
|
|
3
|
+
import { runTenantCommand } from "./tenants.js";
|
|
4
|
+
import { runOperationalCommand } from "./operations.js";
|
|
5
|
+
import { runProjectCommand } from "./project.js";
|
|
6
|
+
import { runVersionCommand } from "./version.js";
|
|
7
|
+
import { siteStatus } from "./status.js";
|
|
8
|
+
|
|
9
|
+
const usage = `Usage:
|
|
10
|
+
cf-genai check|test|build|ci|ci:lint
|
|
11
|
+
cf-genai lint data-access
|
|
12
|
+
cf-genai dev [options]
|
|
13
|
+
cf-genai upgrade <base|auth|llm|messaging> <latest|VERSION>
|
|
14
|
+
cf-genai release [--confirm] [--first] [--add-trust] [--dry-run] [--bypass-lint] [--type patch|minor|major] [--version MAJOR.MINOR]
|
|
15
|
+
cf-genai release-status [--wait MINUTES] [--json]
|
|
16
|
+
cf-genai status [--env local|staging|production] [--json]
|
|
17
|
+
cf-genai version
|
|
18
|
+
cf-genai d1 refresh|backup|restore|migrate|status|check local|staging|production [options]
|
|
19
|
+
cf-genai admin status|features|users|tenants|scopes|groups|healthchecks|circuit-breakers [options]
|
|
20
|
+
cf-genai config check [options]
|
|
21
|
+
cf-genai user list|get|update [username] [options]
|
|
22
|
+
cf-genai tenant list|get|create|update [tenant-id] [options]
|
|
23
|
+
|
|
24
|
+
Options:
|
|
25
|
+
--database NAME D1 binding or database name (default: DB)
|
|
26
|
+
--production-database NAME Production D1 binding or database name
|
|
27
|
+
--production-env NAME Wrangler production environment (default: none)
|
|
28
|
+
--staging-env NAME Wrangler staging environment (default: staging)
|
|
29
|
+
--config PATH Wrangler config path (default: wrangler.jsonc)
|
|
30
|
+
--env VALUE Environment alias: local, staging, or prod
|
|
31
|
+
--output PATH Backup output file
|
|
32
|
+
--file PATH Restore input file
|
|
33
|
+
--wrangler COMMAND Wrangler command (default: npx wrangler)
|
|
34
|
+
--yes Confirm a remote staging refresh
|
|
35
|
+
--confirm-production Explicitly permit production migration or breaker changes
|
|
36
|
+
--confirm Confirm a destructive or release operation
|
|
37
|
+
--dry-run Show release checks without changing Git or npm
|
|
38
|
+
--version MAJOR.MINOR Explicit release target; patch is assigned as .0
|
|
39
|
+
--bypass-lint Skip the release preflight lint (use sparingly)
|
|
40
|
+
--json Return machine-readable output
|
|
41
|
+
--help Show this help
|
|
42
|
+
`;
|
|
43
|
+
|
|
44
|
+
function splitCommand(value = "npx wrangler") {
|
|
45
|
+
return value.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g)?.map((part) => part.replace(/^(["'])(.*)\1$/, "$2")) ?? ["npx", "wrangler"];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parseOptions(args, env = process.env) {
|
|
49
|
+
const options = {
|
|
50
|
+
config: env.CF_GENAI_WRANGLER_CONFIG || "wrangler.jsonc",
|
|
51
|
+
database: env.CF_GENAI_DATABASE || "DB",
|
|
52
|
+
productionEnv: env.CF_GENAI_PRODUCTION_ENV || "",
|
|
53
|
+
stagingEnv: env.CF_GENAI_STAGING_ENV || "staging",
|
|
54
|
+
wranglerCommand: splitCommand(env.CF_GENAI_WRANGLER || "npx wrangler"),
|
|
55
|
+
yes: false,
|
|
56
|
+
confirmProduction: false,
|
|
57
|
+
target: env.CF_GENAI_TARGET || "local",
|
|
58
|
+
roles: undefined,
|
|
59
|
+
scopes: undefined,
|
|
60
|
+
groups: undefined,
|
|
61
|
+
tenants: undefined,
|
|
62
|
+
name: undefined,
|
|
63
|
+
json: false,
|
|
64
|
+
};
|
|
65
|
+
const positional = [];
|
|
66
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
67
|
+
const arg = args[index];
|
|
68
|
+
if (arg === "--help" || arg === "-h") return { ...options, help: true, positional };
|
|
69
|
+
if (arg === "--yes") options.yes = true;
|
|
70
|
+
else if (arg === "--json") options.json = true
|
|
71
|
+
else if (arg === "--confirm-production") options.confirmProduction = true;
|
|
72
|
+
else if (arg.startsWith("--")) {
|
|
73
|
+
const [key, inline] = arg.slice(2).split("=", 2);
|
|
74
|
+
const names = {
|
|
75
|
+
database: "database", "production-database": "productionDatabase",
|
|
76
|
+
"production-env": "productionEnv", "staging-env": "stagingEnv",
|
|
77
|
+
config: "config", wrangler: "wrangler", target: "target", env: "target", roles: "roles", scopes: "scopes", groups: "groups", tenants: "tenants", name: "name", output: "output", file: "file",
|
|
78
|
+
};
|
|
79
|
+
const optionName = names[key];
|
|
80
|
+
if (!optionName) throw new Error(`Unknown option: ${arg}`);
|
|
81
|
+
const value = inline ?? args[++index];
|
|
82
|
+
if (value === undefined) throw new Error(`Missing value for ${arg}.`);
|
|
83
|
+
if (optionName === "wrangler") options.wranglerCommand = splitCommand(value);
|
|
84
|
+
else options[optionName] = value;
|
|
85
|
+
} else positional.push(arg);
|
|
86
|
+
}
|
|
87
|
+
return { ...options, positional, productionDatabase: options.productionDatabase || options.database };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function main(args = process.argv.slice(2), env = process.env) {
|
|
91
|
+
if (args[0]?.includes(":")) { const [domain, action] = args[0].split(":", 2); if (["user", "tenant", "healthcheck", "healthchecks", "circuit-breaker", "circuit-breakers", "circuit"].includes(domain)) args = [domain, action, ...args.slice(1)]; }
|
|
92
|
+
if (args[0] === "version") return runVersionCommand();
|
|
93
|
+
const projectCommand = args[0];
|
|
94
|
+
if (["check", "test", "build", "ci", "ci:lint", "lint", "dev", "upgrade", "release", "release-status"].includes(projectCommand)) {
|
|
95
|
+
const result = runProjectCommand(projectCommand, args.slice(1));
|
|
96
|
+
if (result === null) throw new Error(`Unknown command.\n\n${usage}`);
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
const options = parseOptions(args, env);
|
|
100
|
+
if (options.positional[0] === "status") return siteStatus(options);
|
|
101
|
+
if (options.help || options.positional.length === 0) {
|
|
102
|
+
console.log(usage);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
let [domain, action, target] = options.positional;
|
|
106
|
+
if (domain === "admin") { const adminAction = { user: "users", tenant: "tenants", healthcheck: "healthchecks", "circuit-breaker": "circuit-breakers", circuit: "circuit-breakers" }[action] || action; if (["local", "staging", "prod", "production"].includes(target)) options.target = target === "prod" ? "production" : target; if (adminAction === "tenants") return runTenantCommand({ action: options.positional[2] || "list", identifier: options.positional[3], name: options.name, options }); if (["healthchecks", "circuit-breakers"].includes(adminAction)) return runOperationalCommand({ domain: adminAction, action: options.positional[2] || "list", identifier: options.positional[3], value: options.positional[4], options }); return runOperationalCommand({ domain: "admin", action: adminAction, identifier: options.positional[3], value: options.positional[4], options }); }
|
|
107
|
+
if (domain === "backup" || domain === "restore") { target = action || options.target; action = domain; return runD1FileCommand(action, target, options); }
|
|
108
|
+
if (domain === "user") return runUserCommand({ action, username: target, options });
|
|
109
|
+
if (domain === "tenant") return runTenantCommand({ action, identifier: target, name: options.name, options });
|
|
110
|
+
if (["healthcheck", "healthchecks", "circuit-breaker", "circuit-breakers", "circuit"].includes(domain)) return runOperationalCommand({ domain, action, identifier: target, value: options.positional[3], options });
|
|
111
|
+
if (domain === "config" && action === "check") return checkConfig(options);
|
|
112
|
+
if (domain !== "d1" || !["refresh", "backup", "restore", "migrate", "status", "check"].includes(action)) throw new Error(`Unknown command.\n\n${usage}`);
|
|
113
|
+
target = target === "prod" ? "production" : target;
|
|
114
|
+
if (!["local", "staging", "production"].includes(target)) throw new Error("Target must be local, staging, or production.");
|
|
115
|
+
if (action === "backup" || action === "restore") return runD1FileCommand(action, target, options);
|
|
116
|
+
if (action === "refresh") {
|
|
117
|
+
if (target === "staging" && !options.yes) throw new Error("Staging refresh replaces remote data; rerun with --yes.");
|
|
118
|
+
if (target === "local") return refreshLocalD1({ database: options.database, productionDatabase: options.productionDatabase, options });
|
|
119
|
+
if (target === "staging") return refreshStagingD1({ database: options.database, productionDatabase: options.productionDatabase, options });
|
|
120
|
+
throw new Error(`Unknown command.\n\n${usage}`);
|
|
121
|
+
}
|
|
122
|
+
if (action === "migrate" && target === "production" && !options.confirmProduction) throw new Error("Production migration requires --confirm-production.");
|
|
123
|
+
if (action === "migrate") return migrateD1({ target, database: options.database, options });
|
|
124
|
+
if (action === "status") return statusD1({ target, database: options.database, options });
|
|
125
|
+
return checkD1({ target, database: options.database, options });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function runD1FileCommand(action, target, options) {
|
|
129
|
+
if (action === "backup") return backupD1({ target, database: options.database, options });
|
|
130
|
+
return restoreD1({ target, database: options.database, options });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export { parseOptions, usage };
|
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("'", "''") + "'"; }
|