@canaryai/cli 0.1.12 → 0.1.14
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/dist/{feature-flag-MFTRYRYX.js → feature-flag-ESPSOSKG.js} +102 -16
- package/dist/feature-flag-ESPSOSKG.js.map +1 -0
- package/dist/index.js +22 -3
- package/dist/index.js.map +1 -1
- package/dist/{knobs-T3O4Z3ZB.js → knobs-HKONHY55.js} +148 -62
- package/dist/knobs-HKONHY55.js.map +1 -0
- package/dist/release-WOD3DAX4.js +249 -0
- package/dist/release-WOD3DAX4.js.map +1 -0
- package/package.json +1 -1
- package/dist/feature-flag-MFTRYRYX.js.map +0 -1
- package/dist/knobs-T3O4Z3ZB.js.map +0 -1
|
@@ -20,6 +20,63 @@ function getArgValue(argv, key) {
|
|
|
20
20
|
function hasFlag(argv, ...flags) {
|
|
21
21
|
return flags.some((flag) => argv.includes(flag));
|
|
22
22
|
}
|
|
23
|
+
function toLifecycleLabel(stage) {
|
|
24
|
+
switch (stage) {
|
|
25
|
+
case "deprecated":
|
|
26
|
+
return "deprecated";
|
|
27
|
+
case "ready_for_cleanup":
|
|
28
|
+
return "ready_for_cleanup";
|
|
29
|
+
default:
|
|
30
|
+
return "active";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function parseLifecycleStage(argv) {
|
|
34
|
+
const stage = getArgValue(argv, "--stage");
|
|
35
|
+
if (!stage || !["active", "deprecated", "ready_for_cleanup"].includes(stage)) {
|
|
36
|
+
console.error("Error: --stage is required and must be one of: active, deprecated, ready_for_cleanup");
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
return stage;
|
|
40
|
+
}
|
|
41
|
+
function formatValue(valueType, value) {
|
|
42
|
+
if (valueType === "json") {
|
|
43
|
+
return JSON.stringify(value);
|
|
44
|
+
}
|
|
45
|
+
return String(value);
|
|
46
|
+
}
|
|
47
|
+
function formatFinalValue(knob) {
|
|
48
|
+
if (knob.lifecycleStage === "active") {
|
|
49
|
+
return "(none)";
|
|
50
|
+
}
|
|
51
|
+
return formatValue(knob.valueType, knob.finalValue);
|
|
52
|
+
}
|
|
53
|
+
function parseTypedValue(raw, valueType) {
|
|
54
|
+
switch (valueType) {
|
|
55
|
+
case "boolean":
|
|
56
|
+
if (raw !== "true" && raw !== "false") {
|
|
57
|
+
console.error('Error: Boolean value must be "true" or "false".');
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
return raw === "true";
|
|
61
|
+
case "string":
|
|
62
|
+
return raw;
|
|
63
|
+
case "number": {
|
|
64
|
+
const num = parseFloat(raw);
|
|
65
|
+
if (Number.isNaN(num)) {
|
|
66
|
+
console.error(`Error: Invalid number: ${raw}`);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
return num;
|
|
70
|
+
}
|
|
71
|
+
case "json":
|
|
72
|
+
try {
|
|
73
|
+
return JSON.parse(raw);
|
|
74
|
+
} catch {
|
|
75
|
+
console.error(`Error: Invalid JSON: ${raw}`);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
23
80
|
async function resolveConfig(argv) {
|
|
24
81
|
const storedApiUrl = await readStoredApiUrl();
|
|
25
82
|
const env = getArgValue(argv, "--env");
|
|
@@ -54,14 +111,7 @@ async function apiRequest(apiUrl, token, method, path, body) {
|
|
|
54
111
|
const json = await res.json();
|
|
55
112
|
return json;
|
|
56
113
|
}
|
|
57
|
-
function
|
|
58
|
-
if (knob.valueType === "json") {
|
|
59
|
-
return JSON.stringify(knob.value);
|
|
60
|
-
}
|
|
61
|
-
return String(knob.value);
|
|
62
|
-
}
|
|
63
|
-
async function handleList(argv, apiUrl, token) {
|
|
64
|
-
const jsonOutput = hasFlag(argv, "--json");
|
|
114
|
+
async function fetchKnobs(apiUrl, token) {
|
|
65
115
|
const res = await fetch(`${apiUrl}/superadmin/knobs`, {
|
|
66
116
|
headers: { Authorization: `Bearer ${token}` }
|
|
67
117
|
});
|
|
@@ -75,7 +125,11 @@ async function handleList(argv, apiUrl, token) {
|
|
|
75
125
|
console.error(`Error: ${json.error}`);
|
|
76
126
|
process.exit(1);
|
|
77
127
|
}
|
|
78
|
-
|
|
128
|
+
return json.knobs ?? [];
|
|
129
|
+
}
|
|
130
|
+
async function handleList(argv, apiUrl, token) {
|
|
131
|
+
const jsonOutput = hasFlag(argv, "--json");
|
|
132
|
+
const knobs = await fetchKnobs(apiUrl, token);
|
|
79
133
|
if (jsonOutput) {
|
|
80
134
|
console.log(JSON.stringify(knobs, null, 2));
|
|
81
135
|
return;
|
|
@@ -86,7 +140,11 @@ async function handleList(argv, apiUrl, token) {
|
|
|
86
140
|
}
|
|
87
141
|
for (const knob of knobs) {
|
|
88
142
|
const desc = knob.description ? ` ${knob.description}` : "";
|
|
89
|
-
|
|
143
|
+
const lifecycle = `lifecycle=${toLifecycleLabel(knob.lifecycleStage)}`;
|
|
144
|
+
const finalValue = knob.lifecycleStage === "active" ? "" : ` final=${formatFinalValue(knob)}`;
|
|
145
|
+
console.log(
|
|
146
|
+
` ${knob.key} [${knob.valueType}] = ${formatValue(knob.valueType, knob.value)} (${lifecycle}${finalValue})${desc}`
|
|
147
|
+
);
|
|
90
148
|
}
|
|
91
149
|
}
|
|
92
150
|
async function handleGet(argv, apiUrl, token) {
|
|
@@ -97,20 +155,8 @@ async function handleGet(argv, apiUrl, token) {
|
|
|
97
155
|
process.exit(1);
|
|
98
156
|
}
|
|
99
157
|
const jsonOutput = hasFlag(argv, "--json");
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
});
|
|
103
|
-
if (res.status === 401) {
|
|
104
|
-
console.error("Error: Unauthorized. Your session may have expired.");
|
|
105
|
-
console.error("Run: canary login");
|
|
106
|
-
process.exit(1);
|
|
107
|
-
}
|
|
108
|
-
const json = await res.json();
|
|
109
|
-
if (!json.ok) {
|
|
110
|
-
console.error(`Error: ${json.error}`);
|
|
111
|
-
process.exit(1);
|
|
112
|
-
}
|
|
113
|
-
const knob = (json.knobs ?? []).find((k) => k.key === key);
|
|
158
|
+
const knobs = await fetchKnobs(apiUrl, token);
|
|
159
|
+
const knob = knobs.find((k) => k.key === key);
|
|
114
160
|
if (!knob) {
|
|
115
161
|
console.error(`Knob not found: ${key}`);
|
|
116
162
|
process.exit(1);
|
|
@@ -121,7 +167,9 @@ async function handleGet(argv, apiUrl, token) {
|
|
|
121
167
|
}
|
|
122
168
|
console.log(` Key: ${knob.key}`);
|
|
123
169
|
console.log(` Type: ${knob.valueType}`);
|
|
124
|
-
console.log(` Value: ${formatValue(knob)}`);
|
|
170
|
+
console.log(` Value: ${formatValue(knob.valueType, knob.value)}`);
|
|
171
|
+
console.log(` Lifecycle: ${toLifecycleLabel(knob.lifecycleStage)}`);
|
|
172
|
+
console.log(` Final value: ${formatFinalValue(knob)}`);
|
|
125
173
|
console.log(` Description: ${knob.description ?? "(none)"}`);
|
|
126
174
|
console.log(` Updated: ${knob.updatedAt ?? "(never)"}`);
|
|
127
175
|
}
|
|
@@ -147,36 +195,7 @@ async function handleSet(argv, apiUrl, token) {
|
|
|
147
195
|
console.error("Error: --type is required and must be one of: boolean, string, number, json");
|
|
148
196
|
process.exit(1);
|
|
149
197
|
}
|
|
150
|
-
|
|
151
|
-
switch (valueType) {
|
|
152
|
-
case "boolean":
|
|
153
|
-
if (rawValue !== "true" && rawValue !== "false") {
|
|
154
|
-
console.error('Error: Boolean value must be "true" or "false".');
|
|
155
|
-
process.exit(1);
|
|
156
|
-
}
|
|
157
|
-
value = rawValue === "true";
|
|
158
|
-
break;
|
|
159
|
-
case "string":
|
|
160
|
-
value = rawValue;
|
|
161
|
-
break;
|
|
162
|
-
case "number": {
|
|
163
|
-
const num = parseFloat(rawValue);
|
|
164
|
-
if (Number.isNaN(num)) {
|
|
165
|
-
console.error(`Error: Invalid number: ${rawValue}`);
|
|
166
|
-
process.exit(1);
|
|
167
|
-
}
|
|
168
|
-
value = num;
|
|
169
|
-
break;
|
|
170
|
-
}
|
|
171
|
-
case "json":
|
|
172
|
-
try {
|
|
173
|
-
value = JSON.parse(rawValue);
|
|
174
|
-
} catch {
|
|
175
|
-
console.error(`Error: Invalid JSON: ${rawValue}`);
|
|
176
|
-
process.exit(1);
|
|
177
|
-
}
|
|
178
|
-
break;
|
|
179
|
-
}
|
|
198
|
+
const value = parseTypedValue(rawValue, valueType);
|
|
180
199
|
const description = getArgValue(argv, "--description") ?? void 0;
|
|
181
200
|
const result = await apiRequest(apiUrl, token, "POST", "/superadmin/knobs", {
|
|
182
201
|
key,
|
|
@@ -188,7 +207,7 @@ async function handleSet(argv, apiUrl, token) {
|
|
|
188
207
|
console.error(`Error: ${result.error}`);
|
|
189
208
|
process.exit(1);
|
|
190
209
|
}
|
|
191
|
-
console.log(`Set knob: ${key} = ${formatValue(result.knob)}`);
|
|
210
|
+
console.log(`Set knob: ${key} = ${formatValue(valueType, result.knob?.value)}`);
|
|
192
211
|
}
|
|
193
212
|
async function handleDelete(argv, apiUrl, token) {
|
|
194
213
|
const key = argv[0];
|
|
@@ -226,7 +245,66 @@ async function handleToggle(argv, apiUrl, token) {
|
|
|
226
245
|
console.error(`Error: ${result.error}`);
|
|
227
246
|
process.exit(1);
|
|
228
247
|
}
|
|
229
|
-
|
|
248
|
+
const knob = result.knob;
|
|
249
|
+
console.log(`Toggled knob: ${key} -> ${formatValue(knob?.valueType ?? "boolean", knob?.value)}`);
|
|
250
|
+
}
|
|
251
|
+
async function handleLifecycle(argv, apiUrl, token) {
|
|
252
|
+
const key = argv[0];
|
|
253
|
+
if (!key || key.startsWith("--")) {
|
|
254
|
+
console.error("Error: Missing knob key.");
|
|
255
|
+
console.error(
|
|
256
|
+
"Usage: canary knobs lifecycle <key> --stage <active|deprecated|ready_for_cleanup> [--final-value <value>]"
|
|
257
|
+
);
|
|
258
|
+
process.exit(1);
|
|
259
|
+
}
|
|
260
|
+
const stage = parseLifecycleStage(argv);
|
|
261
|
+
const rawFinalValue = getArgValue(argv, "--final-value");
|
|
262
|
+
const clearFinalValue = hasFlag(argv, "--clear-final-value");
|
|
263
|
+
if (rawFinalValue !== void 0 && clearFinalValue) {
|
|
264
|
+
console.error("Error: use either --final-value or --clear-final-value, not both.");
|
|
265
|
+
process.exit(1);
|
|
266
|
+
}
|
|
267
|
+
const knobs = await fetchKnobs(apiUrl, token);
|
|
268
|
+
const knob = knobs.find((k) => k.key === key);
|
|
269
|
+
if (!knob) {
|
|
270
|
+
console.error(`Knob not found: ${key}`);
|
|
271
|
+
process.exit(1);
|
|
272
|
+
}
|
|
273
|
+
let finalValue = void 0;
|
|
274
|
+
if (stage === "active") {
|
|
275
|
+
if (rawFinalValue !== void 0) {
|
|
276
|
+
console.error("Error: active stage does not accept --final-value. Use --stage deprecated|ready_for_cleanup.");
|
|
277
|
+
process.exit(1);
|
|
278
|
+
}
|
|
279
|
+
finalValue = clearFinalValue ? void 0 : void 0;
|
|
280
|
+
} else {
|
|
281
|
+
if (clearFinalValue) {
|
|
282
|
+
console.error("Error: --clear-final-value can only be used with --stage active.");
|
|
283
|
+
process.exit(1);
|
|
284
|
+
}
|
|
285
|
+
if (rawFinalValue === void 0) {
|
|
286
|
+
console.error("Error: --final-value is required when stage is deprecated or ready_for_cleanup.");
|
|
287
|
+
process.exit(1);
|
|
288
|
+
}
|
|
289
|
+
finalValue = parseTypedValue(rawFinalValue, knob.valueType);
|
|
290
|
+
}
|
|
291
|
+
const result = await apiRequest(
|
|
292
|
+
apiUrl,
|
|
293
|
+
token,
|
|
294
|
+
"POST",
|
|
295
|
+
`/superadmin/knobs/${encodeURIComponent(key)}/lifecycle`,
|
|
296
|
+
{
|
|
297
|
+
stage,
|
|
298
|
+
finalValue
|
|
299
|
+
}
|
|
300
|
+
);
|
|
301
|
+
if (!result.ok || !result.knob) {
|
|
302
|
+
console.error(`Error: ${result.error ?? "Failed to update lifecycle"}`);
|
|
303
|
+
process.exit(1);
|
|
304
|
+
}
|
|
305
|
+
console.log(
|
|
306
|
+
`Updated lifecycle for ${key}: stage=${toLifecycleLabel(result.knob.lifecycleStage)}, final=${formatFinalValue(result.knob)}`
|
|
307
|
+
);
|
|
230
308
|
}
|
|
231
309
|
function printKnobsHelp() {
|
|
232
310
|
console.log(
|
|
@@ -240,14 +318,19 @@ function printKnobsHelp() {
|
|
|
240
318
|
" Set a knob value",
|
|
241
319
|
" delete <key> Delete a knob",
|
|
242
320
|
" toggle <key> Toggle a boolean knob",
|
|
321
|
+
" lifecycle <key> --stage <stage> [--final-value <value>]",
|
|
322
|
+
" Mark knob lifecycle + final value",
|
|
243
323
|
"",
|
|
244
324
|
"Types: boolean, string, number, json",
|
|
325
|
+
"Stages: active, deprecated, ready_for_cleanup",
|
|
245
326
|
"",
|
|
246
327
|
"Options:",
|
|
247
|
-
" --
|
|
248
|
-
" --
|
|
249
|
-
" --
|
|
250
|
-
" --
|
|
328
|
+
" --final-value <value> Final value for deprecated/ready_for_cleanup",
|
|
329
|
+
" --clear-final-value Clear final value (only valid with --stage active)",
|
|
330
|
+
" --env <env> Target environment (prod, dev, local)",
|
|
331
|
+
" --json Output as JSON (list/get only)",
|
|
332
|
+
" --api-url <url> API URL override (takes precedence over --env)",
|
|
333
|
+
" --token <key> API token override"
|
|
251
334
|
].join("\n")
|
|
252
335
|
);
|
|
253
336
|
}
|
|
@@ -274,6 +357,9 @@ async function runKnobs(argv) {
|
|
|
274
357
|
case "toggle":
|
|
275
358
|
await handleToggle(rest, apiUrl, token);
|
|
276
359
|
break;
|
|
360
|
+
case "lifecycle":
|
|
361
|
+
await handleLifecycle(rest, apiUrl, token);
|
|
362
|
+
break;
|
|
277
363
|
default:
|
|
278
364
|
console.error(`Unknown sub-command: ${subCommand}`);
|
|
279
365
|
printKnobsHelp();
|
|
@@ -283,4 +369,4 @@ async function runKnobs(argv) {
|
|
|
283
369
|
export {
|
|
284
370
|
runKnobs
|
|
285
371
|
};
|
|
286
|
-
//# sourceMappingURL=knobs-
|
|
372
|
+
//# sourceMappingURL=knobs-HKONHY55.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/knobs.ts"],"sourcesContent":["/**\n * CLI Knobs Management\n *\n * Allows superadmins to manage knobs (global config) via the CLI.\n */\n\nimport process from \"node:process\";\nimport { readStoredToken, readStoredApiUrl } from \"./auth.js\";\n\nconst ENV_URLS: Record<string, string> = {\n prod: \"https://api.trycanary.ai\",\n production: \"https://api.trycanary.ai\",\n dev: \"https://api.dev.trycanary.ai\",\n local: \"http://localhost:3000\",\n};\n\ntype LifecycleStage = \"active\" | \"deprecated\" | \"ready_for_cleanup\";\n\ntype SerializedKnob = {\n key: string;\n description: string | null;\n valueType: \"boolean\" | \"string\" | \"number\" | \"json\";\n value: unknown;\n lifecycleStage: LifecycleStage;\n finalValue: unknown;\n updatedAt: string | null;\n updatedBy: string | null;\n createdAt: string | null;\n};\n\ntype KnobListResponse = {\n ok: boolean;\n knobs?: SerializedKnob[];\n error?: string;\n};\n\ntype ApiResponse = {\n ok: boolean;\n error?: string;\n knob?: SerializedKnob;\n};\n\nfunction getArgValue(argv: string[], key: string): string | undefined {\n const index = argv.indexOf(key);\n if (index === -1 || index >= argv.length - 1) return undefined;\n return argv[index + 1];\n}\n\nfunction hasFlag(argv: string[], ...flags: string[]): boolean {\n return flags.some((flag) => argv.includes(flag));\n}\n\nfunction toLifecycleLabel(stage: LifecycleStage): string {\n switch (stage) {\n case \"deprecated\":\n return \"deprecated\";\n case \"ready_for_cleanup\":\n return \"ready_for_cleanup\";\n default:\n return \"active\";\n }\n}\n\nfunction parseLifecycleStage(argv: string[]): LifecycleStage {\n const stage = getArgValue(argv, \"--stage\");\n if (!stage || ![\"active\", \"deprecated\", \"ready_for_cleanup\"].includes(stage)) {\n console.error(\"Error: --stage is required and must be one of: active, deprecated, ready_for_cleanup\");\n process.exit(1);\n }\n return stage as LifecycleStage;\n}\n\nfunction formatValue(valueType: SerializedKnob[\"valueType\"], value: unknown): string {\n if (valueType === \"json\") {\n return JSON.stringify(value);\n }\n return String(value);\n}\n\nfunction formatFinalValue(knob: SerializedKnob): string {\n if (knob.lifecycleStage === \"active\") {\n return \"(none)\";\n }\n return formatValue(knob.valueType, knob.finalValue);\n}\n\nfunction parseTypedValue(raw: string, valueType: SerializedKnob[\"valueType\"]): unknown {\n switch (valueType) {\n case \"boolean\":\n if (raw !== \"true\" && raw !== \"false\") {\n console.error('Error: Boolean value must be \"true\" or \"false\".');\n process.exit(1);\n }\n return raw === \"true\";\n case \"string\":\n return raw;\n case \"number\": {\n const num = parseFloat(raw);\n if (Number.isNaN(num)) {\n console.error(`Error: Invalid number: ${raw}`);\n process.exit(1);\n }\n return num;\n }\n case \"json\":\n try {\n return JSON.parse(raw);\n } catch {\n console.error(`Error: Invalid JSON: ${raw}`);\n process.exit(1);\n }\n }\n}\n\nasync function resolveConfig(argv: string[]) {\n const storedApiUrl = await readStoredApiUrl();\n const env = getArgValue(argv, \"--env\");\n\n if (env && !ENV_URLS[env]) {\n console.error(`Unknown environment: ${env}`);\n console.error(\"Valid environments: prod, dev, local\");\n process.exit(1);\n }\n\n const apiUrl =\n getArgValue(argv, \"--api-url\") ??\n (env ? ENV_URLS[env] : undefined) ??\n process.env.CANARY_API_URL ??\n storedApiUrl ??\n \"https://api.trycanary.ai\";\n\n const token =\n getArgValue(argv, \"--token\") ?? process.env.CANARY_API_TOKEN ?? (await readStoredToken());\n\n if (!token) {\n console.error(\"Error: No API token found.\");\n console.error(\"Run: canary login\");\n process.exit(1);\n }\n\n return { apiUrl, token };\n}\n\nasync function apiRequest(\n apiUrl: string,\n token: string,\n method: string,\n path: string,\n body?: Record<string, unknown>\n): Promise<ApiResponse> {\n const res = await fetch(`${apiUrl}${path}`, {\n method,\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n },\n ...(body ? { body: JSON.stringify(body) } : {}),\n });\n\n if (res.status === 401) {\n console.error(\"Error: Unauthorized. Your session may have expired.\");\n console.error(\"Run: canary login\");\n process.exit(1);\n }\n\n const json = (await res.json()) as ApiResponse;\n return json;\n}\n\nasync function fetchKnobs(apiUrl: string, token: string): Promise<SerializedKnob[]> {\n const res = await fetch(`${apiUrl}/superadmin/knobs`, {\n headers: { Authorization: `Bearer ${token}` },\n });\n\n if (res.status === 401) {\n console.error(\"Error: Unauthorized. Your session may have expired.\");\n console.error(\"Run: canary login\");\n process.exit(1);\n }\n\n const json = (await res.json()) as KnobListResponse;\n\n if (!json.ok) {\n console.error(`Error: ${json.error}`);\n process.exit(1);\n }\n\n return json.knobs ?? [];\n}\n\nasync function handleList(argv: string[], apiUrl: string, token: string): Promise<void> {\n const jsonOutput = hasFlag(argv, \"--json\");\n const knobs = await fetchKnobs(apiUrl, token);\n\n if (jsonOutput) {\n console.log(JSON.stringify(knobs, null, 2));\n return;\n }\n\n if (knobs.length === 0) {\n console.log(\"No knobs found.\");\n return;\n }\n\n for (const knob of knobs) {\n const desc = knob.description ? ` ${knob.description}` : \"\";\n const lifecycle = `lifecycle=${toLifecycleLabel(knob.lifecycleStage)}`;\n const finalValue =\n knob.lifecycleStage === \"active\" ? \"\" : ` final=${formatFinalValue(knob)}`;\n console.log(\n ` ${knob.key} [${knob.valueType}] = ${formatValue(knob.valueType, knob.value)} (${lifecycle}${finalValue})${desc}`\n );\n }\n}\n\nasync function handleGet(argv: string[], apiUrl: string, token: string): Promise<void> {\n const key = argv[0];\n if (!key || key.startsWith(\"--\")) {\n console.error(\"Error: Missing knob key.\");\n console.error(\"Usage: canary knobs get <key>\");\n process.exit(1);\n }\n\n const jsonOutput = hasFlag(argv, \"--json\");\n const knobs = await fetchKnobs(apiUrl, token);\n\n const knob = knobs.find((k) => k.key === key);\n\n if (!knob) {\n console.error(`Knob not found: ${key}`);\n process.exit(1);\n }\n\n if (jsonOutput) {\n console.log(JSON.stringify(knob, null, 2));\n return;\n }\n\n console.log(` Key: ${knob.key}`);\n console.log(` Type: ${knob.valueType}`);\n console.log(` Value: ${formatValue(knob.valueType, knob.value)}`);\n console.log(` Lifecycle: ${toLifecycleLabel(knob.lifecycleStage)}`);\n console.log(` Final value: ${formatFinalValue(knob)}`);\n console.log(` Description: ${knob.description ?? \"(none)\"}`);\n console.log(` Updated: ${knob.updatedAt ?? \"(never)\"}`);\n}\n\nasync function handleSet(argv: string[], apiUrl: string, token: string): Promise<void> {\n const key = argv[0];\n if (!key || key.startsWith(\"--\")) {\n console.error(\"Error: Missing knob key.\");\n console.error(\n \"Usage: canary knobs set <key> <value> --type <boolean|string|number|json> [--description <text>]\"\n );\n process.exit(1);\n }\n\n const rawValue = argv[1];\n if (rawValue === undefined || rawValue.startsWith(\"--\")) {\n console.error(\"Error: Missing knob value.\");\n console.error(\n \"Usage: canary knobs set <key> <value> --type <boolean|string|number|json> [--description <text>]\"\n );\n process.exit(1);\n }\n\n const valueType = getArgValue(argv, \"--type\") as SerializedKnob[\"valueType\"] | undefined;\n if (!valueType || ![\"boolean\", \"string\", \"number\", \"json\"].includes(valueType)) {\n console.error(\"Error: --type is required and must be one of: boolean, string, number, json\");\n process.exit(1);\n }\n\n const value = parseTypedValue(rawValue, valueType);\n const description = getArgValue(argv, \"--description\") ?? undefined;\n\n const result = await apiRequest(apiUrl, token, \"POST\", \"/superadmin/knobs\", {\n key,\n valueType,\n value,\n description,\n });\n\n if (!result.ok) {\n console.error(`Error: ${result.error}`);\n process.exit(1);\n }\n\n console.log(`Set knob: ${key} = ${formatValue(valueType, result.knob?.value)}`);\n}\n\nasync function handleDelete(argv: string[], apiUrl: string, token: string): Promise<void> {\n const key = argv[0];\n if (!key || key.startsWith(\"--\")) {\n console.error(\"Error: Missing knob key.\");\n console.error(\"Usage: canary knobs delete <key>\");\n process.exit(1);\n }\n\n const result = await apiRequest(\n apiUrl,\n token,\n \"DELETE\",\n `/superadmin/knobs/${encodeURIComponent(key)}`\n );\n\n if (!result.ok) {\n console.error(`Error: ${result.error}`);\n process.exit(1);\n }\n\n console.log(`Deleted knob: ${key}`);\n}\n\nasync function handleToggle(argv: string[], apiUrl: string, token: string): Promise<void> {\n const key = argv[0];\n if (!key || key.startsWith(\"--\")) {\n console.error(\"Error: Missing knob key.\");\n console.error(\"Usage: canary knobs toggle <key>\");\n process.exit(1);\n }\n\n const result = await apiRequest(\n apiUrl,\n token,\n \"POST\",\n `/superadmin/knobs/${encodeURIComponent(key)}/toggle`\n );\n\n if (!result.ok) {\n console.error(`Error: ${result.error}`);\n process.exit(1);\n }\n\n const knob = result.knob;\n console.log(`Toggled knob: ${key} -> ${formatValue(knob?.valueType ?? \"boolean\", knob?.value)}`);\n}\n\nasync function handleLifecycle(argv: string[], apiUrl: string, token: string): Promise<void> {\n const key = argv[0];\n if (!key || key.startsWith(\"--\")) {\n console.error(\"Error: Missing knob key.\");\n console.error(\n \"Usage: canary knobs lifecycle <key> --stage <active|deprecated|ready_for_cleanup> [--final-value <value>]\"\n );\n process.exit(1);\n }\n\n const stage = parseLifecycleStage(argv);\n const rawFinalValue = getArgValue(argv, \"--final-value\");\n const clearFinalValue = hasFlag(argv, \"--clear-final-value\");\n\n if (rawFinalValue !== undefined && clearFinalValue) {\n console.error(\"Error: use either --final-value or --clear-final-value, not both.\");\n process.exit(1);\n }\n\n const knobs = await fetchKnobs(apiUrl, token);\n const knob = knobs.find((k) => k.key === key);\n\n if (!knob) {\n console.error(`Knob not found: ${key}`);\n process.exit(1);\n }\n\n let finalValue: unknown = undefined;\n\n if (stage === \"active\") {\n if (rawFinalValue !== undefined) {\n console.error(\"Error: active stage does not accept --final-value. Use --stage deprecated|ready_for_cleanup.\");\n process.exit(1);\n }\n finalValue = clearFinalValue ? undefined : undefined;\n } else {\n if (clearFinalValue) {\n console.error(\"Error: --clear-final-value can only be used with --stage active.\");\n process.exit(1);\n }\n if (rawFinalValue === undefined) {\n console.error(\"Error: --final-value is required when stage is deprecated or ready_for_cleanup.\");\n process.exit(1);\n }\n finalValue = parseTypedValue(rawFinalValue, knob.valueType);\n }\n\n const result = await apiRequest(\n apiUrl,\n token,\n \"POST\",\n `/superadmin/knobs/${encodeURIComponent(key)}/lifecycle`,\n {\n stage,\n finalValue,\n }\n );\n\n if (!result.ok || !result.knob) {\n console.error(`Error: ${result.error ?? \"Failed to update lifecycle\"}`);\n process.exit(1);\n }\n\n console.log(\n `Updated lifecycle for ${key}: stage=${toLifecycleLabel(result.knob.lifecycleStage)}, final=${formatFinalValue(result.knob)}`\n );\n}\n\nfunction printKnobsHelp(): void {\n console.log(\n [\n \"Usage: canary knobs <sub-command> [options]\",\n \"\",\n \"Sub-commands:\",\n \" list List all knobs\",\n \" get <key> Get a knob value\",\n \" set <key> <value> --type <type> [--description <text>]\",\n \" Set a knob value\",\n \" delete <key> Delete a knob\",\n \" toggle <key> Toggle a boolean knob\",\n \" lifecycle <key> --stage <stage> [--final-value <value>]\",\n \" Mark knob lifecycle + final value\",\n \"\",\n \"Types: boolean, string, number, json\",\n \"Stages: active, deprecated, ready_for_cleanup\",\n \"\",\n \"Options:\",\n \" --final-value <value> Final value for deprecated/ready_for_cleanup\",\n \" --clear-final-value Clear final value (only valid with --stage active)\",\n \" --env <env> Target environment (prod, dev, local)\",\n \" --json Output as JSON (list/get only)\",\n \" --api-url <url> API URL override (takes precedence over --env)\",\n \" --token <key> API token override\",\n ].join(\"\\n\")\n );\n}\n\nexport async function runKnobs(argv: string[]): Promise<void> {\n const [subCommand, ...rest] = argv;\n\n if (!subCommand || subCommand === \"help\" || hasFlag(argv, \"--help\", \"-h\")) {\n printKnobsHelp();\n return;\n }\n\n const { apiUrl, token } = await resolveConfig(argv);\n\n switch (subCommand) {\n case \"list\":\n await handleList(rest, apiUrl, token);\n break;\n case \"get\":\n await handleGet(rest, apiUrl, token);\n break;\n case \"set\":\n await handleSet(rest, apiUrl, token);\n break;\n case \"delete\":\n await handleDelete(rest, apiUrl, token);\n break;\n case \"toggle\":\n await handleToggle(rest, apiUrl, token);\n break;\n case \"lifecycle\":\n await handleLifecycle(rest, apiUrl, token);\n break;\n default:\n console.error(`Unknown sub-command: ${subCommand}`);\n printKnobsHelp();\n process.exit(1);\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readStoredApiUrl,
|
|
3
|
+
readStoredToken
|
|
4
|
+
} from "./chunk-2T64Z2NI.js";
|
|
5
|
+
import "./chunk-DGUM43GV.js";
|
|
6
|
+
|
|
7
|
+
// src/release.ts
|
|
8
|
+
import process from "process";
|
|
9
|
+
var ENV_URLS = {
|
|
10
|
+
prod: "https://api.trycanary.ai",
|
|
11
|
+
production: "https://api.trycanary.ai",
|
|
12
|
+
dev: "https://api.dev.trycanary.ai",
|
|
13
|
+
local: "http://localhost:3000"
|
|
14
|
+
};
|
|
15
|
+
var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
|
|
16
|
+
"completed",
|
|
17
|
+
"completed_with_errors",
|
|
18
|
+
"failed",
|
|
19
|
+
"canceled",
|
|
20
|
+
"timeout"
|
|
21
|
+
]);
|
|
22
|
+
function getArgValue(argv, key) {
|
|
23
|
+
const index = argv.indexOf(key);
|
|
24
|
+
if (index === -1 || index >= argv.length - 1) return void 0;
|
|
25
|
+
return argv[index + 1];
|
|
26
|
+
}
|
|
27
|
+
function hasFlag(argv, ...flags) {
|
|
28
|
+
return flags.some((flag) => argv.includes(flag));
|
|
29
|
+
}
|
|
30
|
+
async function resolveConfig(argv) {
|
|
31
|
+
const storedApiUrl = await readStoredApiUrl();
|
|
32
|
+
const env = getArgValue(argv, "--env");
|
|
33
|
+
if (env && !ENV_URLS[env]) {
|
|
34
|
+
console.error(`Unknown environment: ${env}`);
|
|
35
|
+
console.error("Valid environments: prod, dev, local");
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
const apiUrl = getArgValue(argv, "--api-url") ?? (env ? ENV_URLS[env] : void 0) ?? process.env.CANARY_API_URL ?? storedApiUrl ?? "https://api.trycanary.ai";
|
|
39
|
+
const token = getArgValue(argv, "--token") ?? process.env.CANARY_API_TOKEN ?? await readStoredToken();
|
|
40
|
+
if (!token) {
|
|
41
|
+
console.error("Error: No API token found.");
|
|
42
|
+
console.error("Set CANARY_API_TOKEN or run: canary login");
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
return { apiUrl, token };
|
|
46
|
+
}
|
|
47
|
+
async function handleTrigger(argv, apiUrl, token) {
|
|
48
|
+
const propertyId = getArgValue(argv, "--property-id");
|
|
49
|
+
if (!propertyId) {
|
|
50
|
+
console.error("Error: Missing --property-id <uuid>.");
|
|
51
|
+
console.error("Usage: canary release trigger --property-id <uuid> [--credential-ids <uuid,...>]");
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
const credentialIdsRaw = getArgValue(argv, "--credential-ids");
|
|
55
|
+
const body = { propertyId };
|
|
56
|
+
if (credentialIdsRaw) {
|
|
57
|
+
body.credentialIds = credentialIdsRaw.split(",").map((id) => id.trim());
|
|
58
|
+
}
|
|
59
|
+
const res = await fetch(`${apiUrl}/api/v1/release-qa/trigger`, {
|
|
60
|
+
method: "POST",
|
|
61
|
+
headers: {
|
|
62
|
+
Authorization: `Bearer ${token}`,
|
|
63
|
+
"Content-Type": "application/json"
|
|
64
|
+
},
|
|
65
|
+
body: JSON.stringify(body)
|
|
66
|
+
});
|
|
67
|
+
if (res.status === 401) {
|
|
68
|
+
console.error("Error: Unauthorized. Check your API token.");
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
const json = await res.json();
|
|
72
|
+
if (!json.ok) {
|
|
73
|
+
console.error(`Error: ${json.error}`);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
console.log(`Release QA run triggered`);
|
|
77
|
+
console.log(` Run ID: ${json.releaseRunId}`);
|
|
78
|
+
console.log(` Job ID: ${json.jobId}`);
|
|
79
|
+
}
|
|
80
|
+
async function fetchRunStatus(apiUrl, token, runId) {
|
|
81
|
+
const res = await fetch(`${apiUrl}/api/v1/release-qa/runs/${encodeURIComponent(runId)}`, {
|
|
82
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
83
|
+
});
|
|
84
|
+
if (res.status === 401) {
|
|
85
|
+
console.error("Error: Unauthorized. Check your API token.");
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
return await res.json();
|
|
89
|
+
}
|
|
90
|
+
async function handleStatus(argv, apiUrl, token) {
|
|
91
|
+
const runId = argv[0];
|
|
92
|
+
if (!runId || runId.startsWith("--")) {
|
|
93
|
+
console.error("Error: Missing run ID.");
|
|
94
|
+
console.error("Usage: canary release status <run-id>");
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
const jsonOutput = hasFlag(argv, "--json");
|
|
98
|
+
const json = await fetchRunStatus(apiUrl, token, runId);
|
|
99
|
+
if (!json.ok) {
|
|
100
|
+
console.error(`Error: ${json.error}`);
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
const run = json.run;
|
|
104
|
+
if (jsonOutput) {
|
|
105
|
+
console.log(JSON.stringify(run, null, 2));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
console.log(`Release QA Run: ${run.id}`);
|
|
109
|
+
console.log(` Status: ${run.status}`);
|
|
110
|
+
console.log(` Trigger: ${run.triggerSource}`);
|
|
111
|
+
console.log(` Commits analyzed: ${run.commitsAnalyzed}`);
|
|
112
|
+
console.log(` Testers: ${run.testersCompleted}/${run.testersSpawned} completed, ${run.testersFailed} failed`);
|
|
113
|
+
console.log(` Issues found: ${run.issuesFound}`);
|
|
114
|
+
console.log(` Regression tests: ${run.regressionTestsPassed}/${run.regressionTestsTotal} passed, ${run.regressionTestsFailed} failed`);
|
|
115
|
+
if (run.startedAt) console.log(` Started: ${run.startedAt}`);
|
|
116
|
+
if (run.finishedAt) console.log(` Finished: ${run.finishedAt}`);
|
|
117
|
+
}
|
|
118
|
+
function sleep(ms) {
|
|
119
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
120
|
+
}
|
|
121
|
+
async function handleRun(argv, apiUrl, token) {
|
|
122
|
+
const propertyId = getArgValue(argv, "--property-id");
|
|
123
|
+
if (!propertyId) {
|
|
124
|
+
console.error("Error: Missing --property-id <uuid>.");
|
|
125
|
+
console.error(
|
|
126
|
+
"Usage: canary release run --property-id <uuid> [--timeout 3600] [--poll-interval 30]"
|
|
127
|
+
);
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
const timeoutSec = parseInt(getArgValue(argv, "--timeout") ?? "3600", 10);
|
|
131
|
+
const pollIntervalSec = parseInt(getArgValue(argv, "--poll-interval") ?? "30", 10);
|
|
132
|
+
const credentialIdsRaw = getArgValue(argv, "--credential-ids");
|
|
133
|
+
const body = { propertyId };
|
|
134
|
+
if (credentialIdsRaw) {
|
|
135
|
+
body.credentialIds = credentialIdsRaw.split(",").map((id) => id.trim());
|
|
136
|
+
}
|
|
137
|
+
console.log("Triggering Release QA run...");
|
|
138
|
+
const triggerRes = await fetch(`${apiUrl}/api/v1/release-qa/trigger`, {
|
|
139
|
+
method: "POST",
|
|
140
|
+
headers: {
|
|
141
|
+
Authorization: `Bearer ${token}`,
|
|
142
|
+
"Content-Type": "application/json"
|
|
143
|
+
},
|
|
144
|
+
body: JSON.stringify(body)
|
|
145
|
+
});
|
|
146
|
+
if (triggerRes.status === 401) {
|
|
147
|
+
console.error("Error: Unauthorized. Check your API token.");
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
const triggerJson = await triggerRes.json();
|
|
151
|
+
if (!triggerJson.ok) {
|
|
152
|
+
console.error(`Error triggering run: ${triggerJson.error}`);
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
const runId = triggerJson.releaseRunId;
|
|
156
|
+
console.log(`Run started: ${runId}`);
|
|
157
|
+
const deadline = Date.now() + timeoutSec * 1e3;
|
|
158
|
+
let lastStatus = "";
|
|
159
|
+
while (Date.now() < deadline) {
|
|
160
|
+
await sleep(pollIntervalSec * 1e3);
|
|
161
|
+
let statusJson;
|
|
162
|
+
try {
|
|
163
|
+
statusJson = await fetchRunStatus(apiUrl, token, runId);
|
|
164
|
+
} catch (err) {
|
|
165
|
+
console.error(`Warning: Failed to fetch status, retrying... (${err})`);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (!statusJson.ok) {
|
|
169
|
+
console.error(`Error: ${statusJson.error}`);
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
const run = statusJson.run;
|
|
173
|
+
const statusLine = `[${run.status}] testers: ${run.testersCompleted}/${run.testersSpawned}, regressions: ${run.regressionTestsPassed}/${run.regressionTestsTotal} passed`;
|
|
174
|
+
if (run.status !== lastStatus) {
|
|
175
|
+
console.log(statusLine);
|
|
176
|
+
lastStatus = run.status;
|
|
177
|
+
} else {
|
|
178
|
+
console.log(statusLine);
|
|
179
|
+
}
|
|
180
|
+
if (TERMINAL_STATUSES.has(run.status)) {
|
|
181
|
+
console.log("");
|
|
182
|
+
console.log(`Run finished: ${run.status}`);
|
|
183
|
+
console.log(` Testers: ${run.testersCompleted}/${run.testersSpawned} completed, ${run.testersFailed} failed`);
|
|
184
|
+
console.log(` Regressions: ${run.regressionTestsPassed}/${run.regressionTestsTotal} passed, ${run.regressionTestsFailed} failed`);
|
|
185
|
+
console.log(` Issues found: ${run.issuesFound}`);
|
|
186
|
+
if (run.status === "completed") {
|
|
187
|
+
console.log("\nRelease QA PASSED");
|
|
188
|
+
process.exit(0);
|
|
189
|
+
} else {
|
|
190
|
+
console.error(`
|
|
191
|
+
Release QA FAILED (${run.status})`);
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
console.error(`
|
|
197
|
+
Timeout: Release QA did not complete within ${timeoutSec}s`);
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
200
|
+
function printReleaseHelp() {
|
|
201
|
+
console.log(
|
|
202
|
+
[
|
|
203
|
+
"Usage: canary release <sub-command> [options]",
|
|
204
|
+
"",
|
|
205
|
+
"Sub-commands:",
|
|
206
|
+
" trigger --property-id <uuid> [--credential-ids <uuid,...>]",
|
|
207
|
+
" Trigger a Release QA run",
|
|
208
|
+
" status <run-id> [--json] Check run status",
|
|
209
|
+
" run --property-id <uuid> [options] Trigger and poll until complete",
|
|
210
|
+
"",
|
|
211
|
+
"Run options:",
|
|
212
|
+
" --timeout <seconds> Max wait time (default: 3600)",
|
|
213
|
+
" --poll-interval <secs> Poll frequency (default: 30)",
|
|
214
|
+
" --credential-ids <ids> Comma-separated credential UUIDs",
|
|
215
|
+
"",
|
|
216
|
+
"Options:",
|
|
217
|
+
" --env <env> Target environment (prod, dev, local)",
|
|
218
|
+
" --api-url <url> API URL override",
|
|
219
|
+
" --token <key> API token override (or set CANARY_API_TOKEN)"
|
|
220
|
+
].join("\n")
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
async function runRelease(argv) {
|
|
224
|
+
const [subCommand, ...rest] = argv;
|
|
225
|
+
if (!subCommand || subCommand === "help" || hasFlag(argv, "--help", "-h")) {
|
|
226
|
+
printReleaseHelp();
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const { apiUrl, token } = await resolveConfig(argv);
|
|
230
|
+
switch (subCommand) {
|
|
231
|
+
case "trigger":
|
|
232
|
+
await handleTrigger(rest, apiUrl, token);
|
|
233
|
+
break;
|
|
234
|
+
case "status":
|
|
235
|
+
await handleStatus(rest, apiUrl, token);
|
|
236
|
+
break;
|
|
237
|
+
case "run":
|
|
238
|
+
await handleRun(rest, apiUrl, token);
|
|
239
|
+
break;
|
|
240
|
+
default:
|
|
241
|
+
console.error(`Unknown sub-command: ${subCommand}`);
|
|
242
|
+
printReleaseHelp();
|
|
243
|
+
process.exit(1);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
export {
|
|
247
|
+
runRelease
|
|
248
|
+
};
|
|
249
|
+
//# sourceMappingURL=release-WOD3DAX4.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/release.ts"],"sourcesContent":["/**\n * CLI Release QA Management\n *\n * Trigger, poll, and check status of Release QA runs from CI/CD pipelines.\n * Used by the scheduled-release GitHub Actions workflow to gate deployments.\n */\n\nimport process from \"node:process\";\nimport { readStoredToken, readStoredApiUrl } from \"./auth.js\";\n\nconst ENV_URLS: Record<string, string> = {\n prod: \"https://api.trycanary.ai\",\n production: \"https://api.trycanary.ai\",\n dev: \"https://api.dev.trycanary.ai\",\n local: \"http://localhost:3000\",\n};\n\ntype TriggerResponse = {\n ok: boolean;\n releaseRunId?: string;\n jobId?: string;\n error?: string;\n};\n\ntype RunStatusResponse = {\n ok: boolean;\n run?: {\n id: string;\n status: string;\n triggerSource: string;\n cutoffReason: string | null;\n fromSha: string | null;\n toSha: string | null;\n commitsAnalyzed: number;\n testersSpawned: number;\n testersCompleted: number;\n testersFailed: number;\n issuesFound: number;\n regressionTestsTotal: number;\n regressionTestsPassed: number;\n regressionTestsFailed: number;\n startedAt: string | null;\n finishedAt: string | null;\n createdAt: string;\n };\n error?: string;\n};\n\n/** Terminal statuses that stop polling */\nconst TERMINAL_STATUSES = new Set([\n \"completed\",\n \"completed_with_errors\",\n \"failed\",\n \"canceled\",\n \"timeout\",\n]);\n\nfunction getArgValue(argv: string[], key: string): string | undefined {\n const index = argv.indexOf(key);\n if (index === -1 || index >= argv.length - 1) return undefined;\n return argv[index + 1];\n}\n\nfunction hasFlag(argv: string[], ...flags: string[]): boolean {\n return flags.some((flag) => argv.includes(flag));\n}\n\nasync function resolveConfig(argv: string[]) {\n const storedApiUrl = await readStoredApiUrl();\n const env = getArgValue(argv, \"--env\");\n\n if (env && !ENV_URLS[env]) {\n console.error(`Unknown environment: ${env}`);\n console.error(\"Valid environments: prod, dev, local\");\n process.exit(1);\n }\n\n const apiUrl =\n getArgValue(argv, \"--api-url\") ??\n (env ? ENV_URLS[env] : undefined) ??\n process.env.CANARY_API_URL ??\n storedApiUrl ??\n \"https://api.trycanary.ai\";\n\n const token =\n getArgValue(argv, \"--token\") ?? process.env.CANARY_API_TOKEN ?? (await readStoredToken());\n\n if (!token) {\n console.error(\"Error: No API token found.\");\n console.error(\"Set CANARY_API_TOKEN or run: canary login\");\n process.exit(1);\n }\n\n return { apiUrl, token };\n}\n\nasync function handleTrigger(argv: string[], apiUrl: string, token: string): Promise<void> {\n const propertyId = getArgValue(argv, \"--property-id\");\n if (!propertyId) {\n console.error(\"Error: Missing --property-id <uuid>.\");\n console.error(\"Usage: canary release trigger --property-id <uuid> [--credential-ids <uuid,...>]\");\n process.exit(1);\n }\n\n const credentialIdsRaw = getArgValue(argv, \"--credential-ids\");\n const body: Record<string, unknown> = { propertyId };\n if (credentialIdsRaw) {\n body.credentialIds = credentialIdsRaw.split(\",\").map((id) => id.trim());\n }\n\n const res = await fetch(`${apiUrl}/api/v1/release-qa/trigger`, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(body),\n });\n\n if (res.status === 401) {\n console.error(\"Error: Unauthorized. Check your API token.\");\n process.exit(1);\n }\n\n const json = (await res.json()) as TriggerResponse;\n\n if (!json.ok) {\n console.error(`Error: ${json.error}`);\n process.exit(1);\n }\n\n console.log(`Release QA run triggered`);\n console.log(` Run ID: ${json.releaseRunId}`);\n console.log(` Job ID: ${json.jobId}`);\n}\n\nasync function fetchRunStatus(\n apiUrl: string,\n token: string,\n runId: string\n): Promise<RunStatusResponse> {\n const res = await fetch(`${apiUrl}/api/v1/release-qa/runs/${encodeURIComponent(runId)}`, {\n headers: { Authorization: `Bearer ${token}` },\n });\n\n if (res.status === 401) {\n console.error(\"Error: Unauthorized. Check your API token.\");\n process.exit(1);\n }\n\n return (await res.json()) as RunStatusResponse;\n}\n\nasync function handleStatus(argv: string[], apiUrl: string, token: string): Promise<void> {\n const runId = argv[0];\n if (!runId || runId.startsWith(\"--\")) {\n console.error(\"Error: Missing run ID.\");\n console.error(\"Usage: canary release status <run-id>\");\n process.exit(1);\n }\n\n const jsonOutput = hasFlag(argv, \"--json\");\n const json = await fetchRunStatus(apiUrl, token, runId);\n\n if (!json.ok) {\n console.error(`Error: ${json.error}`);\n process.exit(1);\n }\n\n const run = json.run!;\n\n if (jsonOutput) {\n console.log(JSON.stringify(run, null, 2));\n return;\n }\n\n console.log(`Release QA Run: ${run.id}`);\n console.log(` Status: ${run.status}`);\n console.log(` Trigger: ${run.triggerSource}`);\n console.log(` Commits analyzed: ${run.commitsAnalyzed}`);\n console.log(` Testers: ${run.testersCompleted}/${run.testersSpawned} completed, ${run.testersFailed} failed`);\n console.log(` Issues found: ${run.issuesFound}`);\n console.log(` Regression tests: ${run.regressionTestsPassed}/${run.regressionTestsTotal} passed, ${run.regressionTestsFailed} failed`);\n if (run.startedAt) console.log(` Started: ${run.startedAt}`);\n if (run.finishedAt) console.log(` Finished: ${run.finishedAt}`);\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nasync function handleRun(argv: string[], apiUrl: string, token: string): Promise<void> {\n const propertyId = getArgValue(argv, \"--property-id\");\n if (!propertyId) {\n console.error(\"Error: Missing --property-id <uuid>.\");\n console.error(\n \"Usage: canary release run --property-id <uuid> [--timeout 3600] [--poll-interval 30]\"\n );\n process.exit(1);\n }\n\n const timeoutSec = parseInt(getArgValue(argv, \"--timeout\") ?? \"3600\", 10);\n const pollIntervalSec = parseInt(getArgValue(argv, \"--poll-interval\") ?? \"30\", 10);\n\n const credentialIdsRaw = getArgValue(argv, \"--credential-ids\");\n const body: Record<string, unknown> = { propertyId };\n if (credentialIdsRaw) {\n body.credentialIds = credentialIdsRaw.split(\",\").map((id) => id.trim());\n }\n\n // Trigger\n console.log(\"Triggering Release QA run...\");\n const triggerRes = await fetch(`${apiUrl}/api/v1/release-qa/trigger`, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(body),\n });\n\n if (triggerRes.status === 401) {\n console.error(\"Error: Unauthorized. Check your API token.\");\n process.exit(1);\n }\n\n const triggerJson = (await triggerRes.json()) as TriggerResponse;\n\n if (!triggerJson.ok) {\n console.error(`Error triggering run: ${triggerJson.error}`);\n process.exit(1);\n }\n\n const runId = triggerJson.releaseRunId!;\n console.log(`Run started: ${runId}`);\n\n // Poll\n const deadline = Date.now() + timeoutSec * 1000;\n let lastStatus = \"\";\n\n while (Date.now() < deadline) {\n await sleep(pollIntervalSec * 1000);\n\n let statusJson: RunStatusResponse;\n try {\n statusJson = await fetchRunStatus(apiUrl, token, runId);\n } catch (err) {\n console.error(`Warning: Failed to fetch status, retrying... (${err})`);\n continue;\n }\n\n if (!statusJson.ok) {\n console.error(`Error: ${statusJson.error}`);\n process.exit(1);\n }\n\n const run = statusJson.run!;\n const statusLine = `[${run.status}] testers: ${run.testersCompleted}/${run.testersSpawned}, regressions: ${run.regressionTestsPassed}/${run.regressionTestsTotal} passed`;\n\n if (run.status !== lastStatus) {\n console.log(statusLine);\n lastStatus = run.status;\n } else {\n // Print progress on same status change in metrics\n console.log(statusLine);\n }\n\n if (TERMINAL_STATUSES.has(run.status)) {\n console.log(\"\");\n console.log(`Run finished: ${run.status}`);\n console.log(` Testers: ${run.testersCompleted}/${run.testersSpawned} completed, ${run.testersFailed} failed`);\n console.log(` Regressions: ${run.regressionTestsPassed}/${run.regressionTestsTotal} passed, ${run.regressionTestsFailed} failed`);\n console.log(` Issues found: ${run.issuesFound}`);\n\n if (run.status === \"completed\") {\n console.log(\"\\nRelease QA PASSED\");\n process.exit(0);\n } else {\n console.error(`\\nRelease QA FAILED (${run.status})`);\n process.exit(1);\n }\n }\n }\n\n console.error(`\\nTimeout: Release QA did not complete within ${timeoutSec}s`);\n process.exit(1);\n}\n\nfunction printReleaseHelp(): void {\n console.log(\n [\n \"Usage: canary release <sub-command> [options]\",\n \"\",\n \"Sub-commands:\",\n \" trigger --property-id <uuid> [--credential-ids <uuid,...>]\",\n \" Trigger a Release QA run\",\n \" status <run-id> [--json] Check run status\",\n \" run --property-id <uuid> [options] Trigger and poll until complete\",\n \"\",\n \"Run options:\",\n \" --timeout <seconds> Max wait time (default: 3600)\",\n \" --poll-interval <secs> Poll frequency (default: 30)\",\n \" --credential-ids <ids> Comma-separated credential UUIDs\",\n \"\",\n \"Options:\",\n \" --env <env> Target environment (prod, dev, local)\",\n \" --api-url <url> API URL override\",\n \" --token <key> API token override (or set CANARY_API_TOKEN)\",\n ].join(\"\\n\")\n );\n}\n\nexport async function runRelease(argv: string[]): Promise<void> {\n const [subCommand, ...rest] = argv;\n\n if (!subCommand || subCommand === \"help\" || hasFlag(argv, \"--help\", \"-h\")) {\n printReleaseHelp();\n return;\n }\n\n const { apiUrl, token } = await resolveConfig(argv);\n\n switch (subCommand) {\n case \"trigger\":\n await handleTrigger(rest, apiUrl, token);\n break;\n case \"status\":\n await handleStatus(rest, apiUrl, token);\n break;\n case \"run\":\n await handleRun(rest, apiUrl, token);\n break;\n default:\n console.error(`Unknown sub-command: ${subCommand}`);\n printReleaseHelp();\n process.exit(1);\n }\n}\n"],"mappings":"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","names":[]}
|