@runcofactor/cli 0.10.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/README.md +125 -0
- package/dist/api-catalog-GFGCLVS4.js +401 -0
- package/dist/api-catalog-GFGCLVS4.js.map +1 -0
- package/dist/attachment-SJC7D7FY.js +276 -0
- package/dist/attachment-SJC7D7FY.js.map +1 -0
- package/dist/auth-EN6NQCJY.js +31 -0
- package/dist/auth-EN6NQCJY.js.map +1 -0
- package/dist/bin.js +7 -0
- package/dist/bin.js.map +1 -0
- package/dist/chunk-3CZU7XCS.js +272 -0
- package/dist/chunk-3CZU7XCS.js.map +1 -0
- package/dist/chunk-4NRDONSC.js +1048 -0
- package/dist/chunk-4NRDONSC.js.map +1 -0
- package/dist/chunk-4PNOV7OV.js +51980 -0
- package/dist/chunk-4PNOV7OV.js.map +1 -0
- package/dist/chunk-52IO7ACJ.js +2941 -0
- package/dist/chunk-52IO7ACJ.js.map +1 -0
- package/dist/chunk-6LEZCKKV.js +663 -0
- package/dist/chunk-6LEZCKKV.js.map +1 -0
- package/dist/chunk-A4OFTUVV.js +170 -0
- package/dist/chunk-A4OFTUVV.js.map +1 -0
- package/dist/chunk-B2VISN2V.js +2459 -0
- package/dist/chunk-B2VISN2V.js.map +1 -0
- package/dist/chunk-CLAWT7BJ.js +271 -0
- package/dist/chunk-CLAWT7BJ.js.map +1 -0
- package/dist/chunk-GKY2KXO5.js +281 -0
- package/dist/chunk-GKY2KXO5.js.map +1 -0
- package/dist/chunk-I4QLI3JO.js +413 -0
- package/dist/chunk-I4QLI3JO.js.map +1 -0
- package/dist/chunk-IMPZAX6L.js +180 -0
- package/dist/chunk-IMPZAX6L.js.map +1 -0
- package/dist/chunk-J6SH7GQT.js +241 -0
- package/dist/chunk-J6SH7GQT.js.map +1 -0
- package/dist/chunk-JDBCZ3Z2.js +298 -0
- package/dist/chunk-JDBCZ3Z2.js.map +1 -0
- package/dist/chunk-JICSYFRJ.js +46 -0
- package/dist/chunk-JICSYFRJ.js.map +1 -0
- package/dist/chunk-JWHYXEA2.js +32 -0
- package/dist/chunk-JWHYXEA2.js.map +1 -0
- package/dist/chunk-N7HFALX3.js +88 -0
- package/dist/chunk-N7HFALX3.js.map +1 -0
- package/dist/chunk-Q4BO4Z5V.js +178 -0
- package/dist/chunk-Q4BO4Z5V.js.map +1 -0
- package/dist/chunk-QFDOWABX.js +195 -0
- package/dist/chunk-QFDOWABX.js.map +1 -0
- package/dist/chunk-SJJW77WN.js +605 -0
- package/dist/chunk-SJJW77WN.js.map +1 -0
- package/dist/chunk-VKVL7WBN.js +50 -0
- package/dist/chunk-VKVL7WBN.js.map +1 -0
- package/dist/chunk-WOHL4FTB.js +353 -0
- package/dist/chunk-WOHL4FTB.js.map +1 -0
- package/dist/chunk-XQXVG7RA.js +426 -0
- package/dist/chunk-XQXVG7RA.js.map +1 -0
- package/dist/chunk-ZD3KHESI.js +94 -0
- package/dist/chunk-ZD3KHESI.js.map +1 -0
- package/dist/credential-COLTBV6D.js +450 -0
- package/dist/credential-COLTBV6D.js.map +1 -0
- package/dist/credential-template-EEPYFG7R.js +291 -0
- package/dist/credential-template-EEPYFG7R.js.map +1 -0
- package/dist/debug-workflow-KOVH5GOP.js +259 -0
- package/dist/debug-workflow-KOVH5GOP.js.map +1 -0
- package/dist/dist-44PZR7BQ.js +423 -0
- package/dist/dist-44PZR7BQ.js.map +1 -0
- package/dist/docs-3SSORHB5.js +366 -0
- package/dist/docs-3SSORHB5.js.map +1 -0
- package/dist/doctor-OTSIWV6G.js +132 -0
- package/dist/doctor-OTSIWV6G.js.map +1 -0
- package/dist/feature-flag-T2LRZME3.js +283 -0
- package/dist/feature-flag-T2LRZME3.js.map +1 -0
- package/dist/file-type-S3UUBVCT.js +4205 -0
- package/dist/file-type-S3UUBVCT.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +982 -0
- package/dist/index.js.map +1 -0
- package/dist/init-TGWRECT2.js +208 -0
- package/dist/init-TGWRECT2.js.map +1 -0
- package/dist/install-DXSNWZKO.js +295 -0
- package/dist/install-DXSNWZKO.js.map +1 -0
- package/dist/issues-XIZOPX6T.js +564 -0
- package/dist/issues-XIZOPX6T.js.map +1 -0
- package/dist/knobs-NBINZIUB.js +295 -0
- package/dist/knobs-NBINZIUB.js.map +1 -0
- package/dist/list-3TDHK637.js +59 -0
- package/dist/list-3TDHK637.js.map +1 -0
- package/dist/local-HICVD6PU.js +100 -0
- package/dist/local-HICVD6PU.js.map +1 -0
- package/dist/login-UOIUXVUK.js +315 -0
- package/dist/login-UOIUXVUK.js.map +1 -0
- package/dist/mcp-54IJEOUA.js +1956 -0
- package/dist/mcp-54IJEOUA.js.map +1 -0
- package/dist/pdf-extract-ABWV3L5X.js +13 -0
- package/dist/pdf-extract-ABWV3L5X.js.map +1 -0
- package/dist/pdfjs-DQ2YA5S3.js +41561 -0
- package/dist/pdfjs-DQ2YA5S3.js.map +1 -0
- package/dist/property-UXLOLPTD.js +701 -0
- package/dist/property-UXLOLPTD.js.map +1 -0
- package/dist/property-skill-guide.md +189 -0
- package/dist/psql-BYUUNLGG.js +115 -0
- package/dist/psql-BYUUNLGG.js.map +1 -0
- package/dist/record-4TR2UIIS.js +233 -0
- package/dist/record-4TR2UIIS.js.map +1 -0
- package/dist/redis-35VBUYW7.js +116 -0
- package/dist/redis-35VBUYW7.js.map +1 -0
- package/dist/release-OOLTUBUT.js +354 -0
- package/dist/release-OOLTUBUT.js.map +1 -0
- package/dist/sandbox-XGSUU6G7.js +2046 -0
- package/dist/sandbox-XGSUU6G7.js.map +1 -0
- package/dist/scenarios-GWLJEGHA.js +280 -0
- package/dist/scenarios-GWLJEGHA.js.map +1 -0
- package/dist/session-MYFNZPRP.js +2414 -0
- package/dist/session-MYFNZPRP.js.map +1 -0
- package/dist/skill-KM34LDMO.js +1839 -0
- package/dist/skill-KM34LDMO.js.map +1 -0
- package/dist/skills/debug-run/SKILL.md +262 -0
- package/dist/skills/debug-run/history/1.0.0.md +244 -0
- package/dist/skills/debug-run/history/1.0.1.md +245 -0
- package/dist/skills/debug-run/history/1.0.2.md +254 -0
- package/dist/skills/debug-run/history/1.1.0.md +262 -0
- package/dist/skills/debug-run/version.json +3 -0
- package/dist/spec-RA3CFEKG.js +951 -0
- package/dist/spec-RA3CFEKG.js.map +1 -0
- package/dist/src-LB4HBTJQ.js +1516 -0
- package/dist/src-LB4HBTJQ.js.map +1 -0
- package/dist/src-N3TPKGTY.js +244 -0
- package/dist/src-N3TPKGTY.js.map +1 -0
- package/dist/src-UVDX5UP6.js +17 -0
- package/dist/src-UVDX5UP6.js.map +1 -0
- package/dist/start-PSEQSQA3.js +151 -0
- package/dist/start-PSEQSQA3.js.map +1 -0
- package/dist/tag-YY6YAABQ.js +22 -0
- package/dist/tag-YY6YAABQ.js.map +1 -0
- package/dist/task-IQC3YR5K.js +922 -0
- package/dist/task-IQC3YR5K.js.map +1 -0
- package/dist/test-suite-DZNJFXDW.js +394 -0
- package/dist/test-suite-DZNJFXDW.js.map +1 -0
- package/dist/trace-HXIW7U2D.js +14 -0
- package/dist/trace-HXIW7U2D.js.map +1 -0
- package/dist/triage-ORG45R5W.js +16 -0
- package/dist/triage-ORG45R5W.js.map +1 -0
- package/dist/upgrade-L2VYMP2V.js +61 -0
- package/dist/upgrade-L2VYMP2V.js.map +1 -0
- package/dist/workflow-XYFH5LTP.js +3708 -0
- package/dist/workflow-XYFH5LTP.js.map +1 -0
- package/dist/workflow-cache-NQJQFWMV.js +253 -0
- package/dist/workflow-cache-NQJQFWMV.js.map +1 -0
- package/dist/workflow-debug-BGD4UIXU.js +927 -0
- package/dist/workflow-debug-BGD4UIXU.js.map +1 -0
- package/dist/workflow-proposals-MTUDX46O.js +382 -0
- package/dist/workflow-proposals-MTUDX46O.js.map +1 -0
- package/package.json +61 -0
|
@@ -0,0 +1,2046 @@
|
|
|
1
|
+
import { createRequire as __cr } from "module"; const require = __cr(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
SANDBOX_GUIDE_SECTIONS,
|
|
4
|
+
printGuide
|
|
5
|
+
} from "./chunk-4NRDONSC.js";
|
|
6
|
+
import {
|
|
7
|
+
apiRequest,
|
|
8
|
+
selectProperty
|
|
9
|
+
} from "./chunk-WOHL4FTB.js";
|
|
10
|
+
import {
|
|
11
|
+
INSTANCE_SIZES,
|
|
12
|
+
INSTANCE_SIZE_SPEC,
|
|
13
|
+
parseInstanceSize
|
|
14
|
+
} from "./chunk-SJJW77WN.js";
|
|
15
|
+
import {
|
|
16
|
+
apiFetch,
|
|
17
|
+
exitOnFailure,
|
|
18
|
+
isTerminalFailure
|
|
19
|
+
} from "./chunk-3CZU7XCS.js";
|
|
20
|
+
import {
|
|
21
|
+
getArgValue,
|
|
22
|
+
hasFlag,
|
|
23
|
+
resolveConfig
|
|
24
|
+
} from "./chunk-A4OFTUVV.js";
|
|
25
|
+
import "./chunk-JWHYXEA2.js";
|
|
26
|
+
import "./chunk-VKVL7WBN.js";
|
|
27
|
+
|
|
28
|
+
// src/sandbox.ts
|
|
29
|
+
import process from "process";
|
|
30
|
+
import { readFileSync } from "fs";
|
|
31
|
+
async function fetchInstanceRoutes(apiUrl, token, instanceId) {
|
|
32
|
+
const res = await apiRequest(
|
|
33
|
+
apiUrl,
|
|
34
|
+
token,
|
|
35
|
+
"GET",
|
|
36
|
+
`/org/environments/instances/${instanceId}/routes`,
|
|
37
|
+
void 0,
|
|
38
|
+
{ timeoutMs: 3e4 }
|
|
39
|
+
);
|
|
40
|
+
return res.ok ? res.data.routes : [];
|
|
41
|
+
}
|
|
42
|
+
var STATUS_ICONS = {
|
|
43
|
+
completed: "[done]",
|
|
44
|
+
running: "[....]",
|
|
45
|
+
pending: "[ ]",
|
|
46
|
+
failed: "[FAIL]",
|
|
47
|
+
skipped: "[skip]"
|
|
48
|
+
};
|
|
49
|
+
function formatDate(iso) {
|
|
50
|
+
return new Date(iso).toLocaleString();
|
|
51
|
+
}
|
|
52
|
+
function pad(s, n) {
|
|
53
|
+
return s.padEnd(n);
|
|
54
|
+
}
|
|
55
|
+
var INSTANCE_SIZE_HELP_LINES = INSTANCE_SIZES.map((size) => {
|
|
56
|
+
const spec = INSTANCE_SIZE_SPEC[size];
|
|
57
|
+
return ` ${pad(size, 8)} ${spec.ec2InstanceType} \xB7 ${spec.vcpu} vCPU \xB7 ${spec.memoryMiB / 1024} GiB`;
|
|
58
|
+
});
|
|
59
|
+
function printHelp() {
|
|
60
|
+
console.log(
|
|
61
|
+
[
|
|
62
|
+
"canary sandbox: Manage sandbox templates and instances",
|
|
63
|
+
"",
|
|
64
|
+
"Sub-commands:",
|
|
65
|
+
" guide [section] Show sandbox authoring guide",
|
|
66
|
+
" list List templates and instances",
|
|
67
|
+
" template [id] Show template details",
|
|
68
|
+
" repo list --template <id> List repos linked to a template",
|
|
69
|
+
" repo add --template <id> --property-github-repo-id <uuid> [--role <r>] [--spec <r=owner/name@branch:subdir>]",
|
|
70
|
+
" repo update --template <id> --link-id <uuid> [--role <r>] [--branch <b>] [--subdirectory <s>] [--service-name <n>]",
|
|
71
|
+
" repo remove --template <id> --link-id <uuid>",
|
|
72
|
+
" checklist <template-id> Show checklist state",
|
|
73
|
+
" checklist skip <template-id> <item-id> Skip a checklist item",
|
|
74
|
+
" checklist complete <template-id> <item-id> [options] Mark item complete from CLI",
|
|
75
|
+
" artifacts <template-id> List artifacts",
|
|
76
|
+
" artifact get <template-id> <type> Get artifact content",
|
|
77
|
+
" artifact create <template-id> <type> <file> Upload artifact (use --path for file type)",
|
|
78
|
+
" create [property] [options] Create template from property (triggers provisioning)",
|
|
79
|
+
" provision <template-id> [options] Create instance from verified template",
|
|
80
|
+
" verify <template-id> Verify an authoring template (provision + probe + flip to verified)",
|
|
81
|
+
' set-size <template-id> <size> Set EC2 size for next provision (see "Instance sizes" below)',
|
|
82
|
+
" ami list <template-id> List per-template AMI history",
|
|
83
|
+
" ami rebake <template-id> [opts] Trigger a fresh AMI bake (--force, --wait)",
|
|
84
|
+
" ami use <template-id> <ami-row-id> Switch the template to a specific AMI (rollback)",
|
|
85
|
+
" build <instance-id> Build and start app services on sandbox",
|
|
86
|
+
" run-command <instance-id> <command...> Execute shell command on sandbox",
|
|
87
|
+
" --service <name> Run inside a Docker container instead of on the host VM",
|
|
88
|
+
" diagnostics <instance-id> Show sandbox diagnostics",
|
|
89
|
+
" logs <instance-id> [options] Show container logs",
|
|
90
|
+
" terminate <instance-id> Terminate an instance",
|
|
91
|
+
" config list <template-id> Show config schema and defaults",
|
|
92
|
+
" config set <template-id> <key> <value> Set a config default value",
|
|
93
|
+
" delete <template-id> Delete template and all instances",
|
|
94
|
+
"",
|
|
95
|
+
"Create options:",
|
|
96
|
+
" --property <name|id> Property (prompted if omitted, auto-selected if only one)",
|
|
97
|
+
" --branch <branch> Branch (default: from linked repo)",
|
|
98
|
+
" --name <name> Template name (default: repo name)",
|
|
99
|
+
" --subdirectory <path> Subdirectory (default: from linked repo rootDir)",
|
|
100
|
+
' --size <size> EC2 size for first authoring sandbox (default: medium; see "Instance sizes" below)',
|
|
101
|
+
"",
|
|
102
|
+
"Provision options:",
|
|
103
|
+
" --name <name> Instance name",
|
|
104
|
+
" --ttl <hours> Time to live in hours (default: 8)",
|
|
105
|
+
" --with-agent Start with sandbox agent",
|
|
106
|
+
"",
|
|
107
|
+
"Artifact get options:",
|
|
108
|
+
" --output <file> Write to file instead of stdout",
|
|
109
|
+
"",
|
|
110
|
+
"Logs options:",
|
|
111
|
+
" --service <name> Service name (default: all services)",
|
|
112
|
+
" --tail <n> Number of lines (default: 200)",
|
|
113
|
+
"",
|
|
114
|
+
"Guide sections:",
|
|
115
|
+
" goal, research, artifacts, actions, verify, checklist",
|
|
116
|
+
"",
|
|
117
|
+
"Instance sizes:",
|
|
118
|
+
...INSTANCE_SIZE_HELP_LINES,
|
|
119
|
+
"",
|
|
120
|
+
"Action sub-commands:",
|
|
121
|
+
" action list <template-id> List actions for a template",
|
|
122
|
+
" action get <template-id> <slug> Show action definition + script",
|
|
123
|
+
" action create <template-id> <slug> [opts] Create or update an action",
|
|
124
|
+
" action delete <template-id> <slug> Delete an action",
|
|
125
|
+
" action execute <instance-id> <slug> [opts] Execute action on a running instance",
|
|
126
|
+
" action executions <instance-id> [opts] List execution history",
|
|
127
|
+
"",
|
|
128
|
+
"Action create options:",
|
|
129
|
+
" --label <label> Display name (default: slug \u2192 Title Case)",
|
|
130
|
+
" --description <text> Description",
|
|
131
|
+
" --icon <icon> Phosphor icon name",
|
|
132
|
+
" --script <file> Path to script file (bash)",
|
|
133
|
+
" --input-schema <file> Path to JSON Schema file for input params",
|
|
134
|
+
" --output-schema <file> Path to JSON Schema file for output",
|
|
135
|
+
" --credential-output-config <file> JSON file with ActionCredentialOutputConfig (auto-creates a Canary credential from the action result)",
|
|
136
|
+
" --credential-template <id> Credential template this action provisions; login nodes using it auto-provision",
|
|
137
|
+
" --provisioning-mode <shared|unique> shared = provision once / reused (default); unique = one credential per run (script must accept a runId input param)",
|
|
138
|
+
" --timeout <ms> Timeout in ms (default: 55000)",
|
|
139
|
+
" --encrypt-script Encrypt script content at rest",
|
|
140
|
+
"",
|
|
141
|
+
"Action execute options:",
|
|
142
|
+
" --param <key>=<value> Set parameter (repeatable)",
|
|
143
|
+
" --params-file <file> JSON file with all params",
|
|
144
|
+
"",
|
|
145
|
+
"Global options:",
|
|
146
|
+
" --json Output raw JSON",
|
|
147
|
+
" --env <env> Environment (prod, dev, local)",
|
|
148
|
+
" -h, --help Show help"
|
|
149
|
+
].join("\n")
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
async function handleList(argv, apiUrl, token) {
|
|
153
|
+
const json = hasFlag(argv, "--json");
|
|
154
|
+
const [templatesRes, instancesRes] = await Promise.all([
|
|
155
|
+
apiRequest(
|
|
156
|
+
apiUrl,
|
|
157
|
+
token,
|
|
158
|
+
"GET",
|
|
159
|
+
"/org/environments/templates"
|
|
160
|
+
),
|
|
161
|
+
apiRequest(
|
|
162
|
+
apiUrl,
|
|
163
|
+
token,
|
|
164
|
+
"GET",
|
|
165
|
+
"/org/environments/instances"
|
|
166
|
+
)
|
|
167
|
+
]);
|
|
168
|
+
if (!templatesRes.ok) {
|
|
169
|
+
console.error(`Error: ${templatesRes.error}`);
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
if (!instancesRes.ok) {
|
|
173
|
+
console.error(`Error loading instances: ${instancesRes.error}`);
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
if (json) {
|
|
177
|
+
console.log(JSON.stringify({ templates: templatesRes.data, instances: instancesRes.data }, null, 2));
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const templates = templatesRes.data ?? [];
|
|
181
|
+
const instances = instancesRes.data ?? [];
|
|
182
|
+
console.log("Templates:");
|
|
183
|
+
if (templates.length === 0) {
|
|
184
|
+
console.log(" (none)");
|
|
185
|
+
} else {
|
|
186
|
+
console.log(` ${pad("ID", 38)} ${pad("Name", 24)} ${pad("Status", 14)} ${pad("Profile", 12)} Repository`);
|
|
187
|
+
for (const t of templates) {
|
|
188
|
+
console.log(` ${pad(t.id, 38)} ${pad(t.name, 24)} ${pad(t.status, 14)} ${pad(t.checklistProfile ?? "-", 12)} ${t.repositoryFullName}:${t.branch}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
console.log("");
|
|
192
|
+
console.log("Instances:");
|
|
193
|
+
if (instances.length === 0) {
|
|
194
|
+
console.log(" (none)");
|
|
195
|
+
} else {
|
|
196
|
+
console.log(` ${pad("ID", 38)} ${pad("Status", 14)} ${pad("Access", 10)} ${pad("Template", 38)} Expires`);
|
|
197
|
+
for (const i of instances) {
|
|
198
|
+
const access = i.isPrivate ? "private" : "public";
|
|
199
|
+
console.log(` ${pad(i.id, 38)} ${pad(i.status, 14)} ${pad(access, 10)} ${pad(i.templateId, 38)} ${i.expiresAt ? formatDate(i.expiresAt) : "-"}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
async function handleTemplate(argv, apiUrl, token) {
|
|
204
|
+
const [id] = argv.filter((a) => !a.startsWith("-"));
|
|
205
|
+
const json = hasFlag(argv, "--json");
|
|
206
|
+
if (!id) {
|
|
207
|
+
await handleList(argv, apiUrl, token);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const res = await apiRequest(
|
|
211
|
+
apiUrl,
|
|
212
|
+
token,
|
|
213
|
+
"GET",
|
|
214
|
+
`/org/environments/templates/${id}`
|
|
215
|
+
);
|
|
216
|
+
if (!res.ok) {
|
|
217
|
+
console.error(`Error: ${res.error}`);
|
|
218
|
+
process.exit(1);
|
|
219
|
+
}
|
|
220
|
+
if (json) {
|
|
221
|
+
console.log(JSON.stringify(res.data, null, 2));
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const t = res.data;
|
|
225
|
+
console.log(`Template: ${t.name}`);
|
|
226
|
+
console.log(` ID: ${t.id}`);
|
|
227
|
+
console.log(` Repository: ${t.repositoryFullName}:${t.branch}`);
|
|
228
|
+
if (t.subdirectory) console.log(` Subdir: ${t.subdirectory}`);
|
|
229
|
+
console.log(` Status: ${t.status}`);
|
|
230
|
+
console.log(` Profile: ${t.checklistProfile ?? "full-stack (default)"}`);
|
|
231
|
+
console.log(` Created: ${formatDate(t.createdAt)}`);
|
|
232
|
+
console.log(` Updated: ${formatDate(t.updatedAt)}`);
|
|
233
|
+
if (t.configSchema && t.configSchema.length > 0) {
|
|
234
|
+
console.log("");
|
|
235
|
+
console.log(" Config Schema:");
|
|
236
|
+
for (const c of t.configSchema) {
|
|
237
|
+
const flags = [c.required ? "required" : "optional", c.sensitive ? "sensitive" : ""].filter(Boolean).join(", ");
|
|
238
|
+
console.log(` ${c.key} (${flags})${c.description ? ` \u2014 ${c.description}` : ""}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
async function handleArtifacts(argv, apiUrl, token) {
|
|
243
|
+
const [templateId] = argv.filter((a) => !a.startsWith("-"));
|
|
244
|
+
const json = hasFlag(argv, "--json");
|
|
245
|
+
if (!templateId) {
|
|
246
|
+
console.error("Error: template ID required. Usage: canary sandbox artifacts <template-id>");
|
|
247
|
+
process.exit(1);
|
|
248
|
+
}
|
|
249
|
+
const res = await apiRequest(
|
|
250
|
+
apiUrl,
|
|
251
|
+
token,
|
|
252
|
+
"GET",
|
|
253
|
+
`/org/environments/templates/${templateId}/artifacts`
|
|
254
|
+
);
|
|
255
|
+
if (!res.ok) {
|
|
256
|
+
console.error(`Error: ${res.error}`);
|
|
257
|
+
process.exit(1);
|
|
258
|
+
}
|
|
259
|
+
if (json) {
|
|
260
|
+
console.log(JSON.stringify(res.data, null, 2));
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const artifacts = res.data ?? [];
|
|
264
|
+
if (artifacts.length === 0) {
|
|
265
|
+
console.log("No artifacts found.");
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
console.log(`${pad("Type", 22)} ${pad("Filename", 30)} ${pad("Task", 24)} Created`);
|
|
269
|
+
for (const a of artifacts) {
|
|
270
|
+
console.log(`${pad(a.artifactType, 22)} ${pad(a.filename, 30)} ${pad(a.taskId, 24)} ${formatDate(a.createdAt)}`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
async function handleChecklist(argv, apiUrl, token) {
|
|
274
|
+
const positional = argv.filter((a) => !a.startsWith("-"));
|
|
275
|
+
const json = hasFlag(argv, "--json");
|
|
276
|
+
if (positional[0] === "skip") {
|
|
277
|
+
await handleChecklistSkip(argv.slice(1), apiUrl, token);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (positional[0] === "complete") {
|
|
281
|
+
await handleChecklistComplete(argv.slice(1), apiUrl, token);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
const templateId = positional[0];
|
|
285
|
+
if (!templateId) {
|
|
286
|
+
console.error("Error: template ID required. Usage: canary sandbox checklist <template-id>");
|
|
287
|
+
process.exit(1);
|
|
288
|
+
}
|
|
289
|
+
const res = await apiRequest(
|
|
290
|
+
apiUrl,
|
|
291
|
+
token,
|
|
292
|
+
"GET",
|
|
293
|
+
`/org/environments/templates/${templateId}/checklist`
|
|
294
|
+
);
|
|
295
|
+
if (!res.ok) {
|
|
296
|
+
console.error(`Error: ${res.error}`);
|
|
297
|
+
process.exit(1);
|
|
298
|
+
}
|
|
299
|
+
if (json) {
|
|
300
|
+
console.log(JSON.stringify(res.data, null, 2));
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
const items = res.data ?? [];
|
|
304
|
+
if (items.length === 0) {
|
|
305
|
+
console.log("No checklist items.");
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
console.log("Checklist:");
|
|
309
|
+
for (const item of items) {
|
|
310
|
+
const icon = STATUS_ICONS[item.itemStatus.status] ?? "[????]";
|
|
311
|
+
const actionable = item.actionable ? " *" : "";
|
|
312
|
+
console.log(` ${icon} ${item.label}${actionable}`);
|
|
313
|
+
if (item.itemStatus.summary) {
|
|
314
|
+
console.log(` ${item.itemStatus.summary}`);
|
|
315
|
+
}
|
|
316
|
+
if (item.itemStatus.error) {
|
|
317
|
+
console.log(` ERROR: ${item.itemStatus.error}`);
|
|
318
|
+
}
|
|
319
|
+
if (item.artifacts.length > 0) {
|
|
320
|
+
console.log(` Artifacts: ${item.artifacts.map((a) => a.artifactType).join(", ")}`);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
console.log("");
|
|
324
|
+
console.log(" * = actionable (deps met, ready to run)");
|
|
325
|
+
}
|
|
326
|
+
async function handleChecklistSkip(argv, apiUrl, token) {
|
|
327
|
+
const positional = argv.filter((a) => !a.startsWith("-"));
|
|
328
|
+
const templateId = positional[0];
|
|
329
|
+
const itemId = positional[1];
|
|
330
|
+
if (!templateId || !itemId) {
|
|
331
|
+
console.error("Error: both template ID and item ID required. Usage: canary sandbox checklist skip <template-id> <item-id>");
|
|
332
|
+
process.exit(1);
|
|
333
|
+
}
|
|
334
|
+
const res = await apiRequest(
|
|
335
|
+
apiUrl,
|
|
336
|
+
token,
|
|
337
|
+
"POST",
|
|
338
|
+
`/org/environments/templates/${templateId}/checklist/${itemId}/skip`
|
|
339
|
+
);
|
|
340
|
+
if (!res.ok) {
|
|
341
|
+
console.error(`Error: ${res.error}`);
|
|
342
|
+
process.exit(1);
|
|
343
|
+
}
|
|
344
|
+
console.log(`Skipped checklist item: ${itemId}`);
|
|
345
|
+
}
|
|
346
|
+
async function handleChecklistComplete(argv, apiUrl, token) {
|
|
347
|
+
const positional = argv.filter((a) => !a.startsWith("-"));
|
|
348
|
+
const templateId = positional[0];
|
|
349
|
+
const itemId = positional[1];
|
|
350
|
+
const json = hasFlag(argv, "--json");
|
|
351
|
+
if (!templateId || !itemId) {
|
|
352
|
+
console.error("Error: both template ID and item ID required.");
|
|
353
|
+
console.error('Usage: canary sandbox checklist complete <template-id> <item-id> [--status success|failed] [--summary "..."]');
|
|
354
|
+
process.exit(1);
|
|
355
|
+
}
|
|
356
|
+
const status = getArgValue(argv, "--status") ?? "success";
|
|
357
|
+
const summary = getArgValue(argv, "--summary");
|
|
358
|
+
const configSchemaArg = getArgValue(argv, "--config-schema");
|
|
359
|
+
const healthCheckPath = getArgValue(argv, "--health-check-path");
|
|
360
|
+
let configSchema;
|
|
361
|
+
if (configSchemaArg) {
|
|
362
|
+
try {
|
|
363
|
+
configSchema = JSON.parse(configSchemaArg);
|
|
364
|
+
} catch {
|
|
365
|
+
try {
|
|
366
|
+
configSchema = JSON.parse(readFileSync(configSchemaArg, "utf-8"));
|
|
367
|
+
} catch (e) {
|
|
368
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
369
|
+
console.error(`Error reading config schema from "${configSchemaArg}": ${msg}`);
|
|
370
|
+
process.exit(1);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
const body = {
|
|
375
|
+
status: status === "failed" ? "failed" : "completed",
|
|
376
|
+
...summary ? { summary } : {},
|
|
377
|
+
...configSchema ? { configSchema } : {},
|
|
378
|
+
...healthCheckPath ? { healthCheckPath } : {}
|
|
379
|
+
};
|
|
380
|
+
const res = await apiRequest(
|
|
381
|
+
apiUrl,
|
|
382
|
+
token,
|
|
383
|
+
"POST",
|
|
384
|
+
`/org/environments/templates/${templateId}/checklist/${itemId}/complete`,
|
|
385
|
+
body
|
|
386
|
+
);
|
|
387
|
+
if (!res.ok) {
|
|
388
|
+
console.error(`Error: ${res.error}`);
|
|
389
|
+
process.exit(1);
|
|
390
|
+
}
|
|
391
|
+
if (json) {
|
|
392
|
+
console.log(JSON.stringify(res.data, null, 2));
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
console.log(`Completed checklist item: ${itemId} (${res.data?.status})`);
|
|
396
|
+
}
|
|
397
|
+
async function handleConfigList(argv, apiUrl, token) {
|
|
398
|
+
const positional = argv.filter((a) => !a.startsWith("-"));
|
|
399
|
+
const templateId = positional[0];
|
|
400
|
+
const json = hasFlag(argv, "--json");
|
|
401
|
+
if (!templateId) {
|
|
402
|
+
console.error("Error: template ID required. Usage: canary sandbox config list <template-id>");
|
|
403
|
+
process.exit(1);
|
|
404
|
+
}
|
|
405
|
+
const res = await apiRequest(
|
|
406
|
+
apiUrl,
|
|
407
|
+
token,
|
|
408
|
+
"GET",
|
|
409
|
+
`/org/environments/templates/${templateId}`
|
|
410
|
+
);
|
|
411
|
+
if (!res.ok) {
|
|
412
|
+
console.error(`Error: ${res.error}`);
|
|
413
|
+
process.exit(1);
|
|
414
|
+
}
|
|
415
|
+
const t = res.data;
|
|
416
|
+
const schema = t.configSchema ?? [];
|
|
417
|
+
const defaults = t.configDefaults ?? {};
|
|
418
|
+
if (json) {
|
|
419
|
+
console.log(JSON.stringify({ configSchema: schema, configDefaults: defaults }, null, 2));
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
if (schema.length === 0) {
|
|
423
|
+
console.log("No config schema defined for this template.");
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
console.log("Config Schema:");
|
|
427
|
+
for (const item of schema) {
|
|
428
|
+
const value = defaults[item.key];
|
|
429
|
+
const display = item.sensitive && value ? "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" : value ?? "(not set)";
|
|
430
|
+
const required = item.required ? " (required)" : "";
|
|
431
|
+
const sensitive = item.sensitive ? " [sensitive]" : "";
|
|
432
|
+
console.log(` ${item.key}${required}${sensitive}`);
|
|
433
|
+
if (item.description) console.log(` ${item.description}`);
|
|
434
|
+
console.log(` Value: ${display}`);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
async function handleConfigSet(argv, apiUrl, token) {
|
|
438
|
+
const positional = argv.filter((a) => !a.startsWith("-"));
|
|
439
|
+
const fromStdin = hasFlag(argv, "--value-stdin");
|
|
440
|
+
const templateId = positional[0];
|
|
441
|
+
const key = positional[1];
|
|
442
|
+
let value = positional[2];
|
|
443
|
+
if (!templateId || !key) {
|
|
444
|
+
console.error("Error: template ID and key required.");
|
|
445
|
+
console.error("Usage: canary sandbox config set <template-id> <key> <value>");
|
|
446
|
+
console.error(' For sensitive values: echo "sk-ant-..." | canary sandbox config set <template-id> <key> --value-stdin');
|
|
447
|
+
process.exit(1);
|
|
448
|
+
}
|
|
449
|
+
if (fromStdin) {
|
|
450
|
+
const chunks = [];
|
|
451
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
452
|
+
value = Buffer.concat(chunks).toString("utf-8").trim();
|
|
453
|
+
}
|
|
454
|
+
if (value === void 0) {
|
|
455
|
+
console.error("Error: value required. Pass as argument or use --value-stdin");
|
|
456
|
+
process.exit(1);
|
|
457
|
+
}
|
|
458
|
+
const getRes = await apiRequest(
|
|
459
|
+
apiUrl,
|
|
460
|
+
token,
|
|
461
|
+
"GET",
|
|
462
|
+
`/org/environments/templates/${templateId}`
|
|
463
|
+
);
|
|
464
|
+
if (!getRes.ok) {
|
|
465
|
+
console.error(`Error: ${getRes.error}`);
|
|
466
|
+
process.exit(1);
|
|
467
|
+
}
|
|
468
|
+
const currentDefaults = getRes.data?.configDefaults ?? {};
|
|
469
|
+
const updatedDefaults = { ...currentDefaults, [key]: value };
|
|
470
|
+
const res = await apiRequest(
|
|
471
|
+
apiUrl,
|
|
472
|
+
token,
|
|
473
|
+
"PATCH",
|
|
474
|
+
`/org/environments/templates/${templateId}/config`,
|
|
475
|
+
{
|
|
476
|
+
configDefaults: updatedDefaults
|
|
477
|
+
},
|
|
478
|
+
{ timeoutMs: 9e4 }
|
|
479
|
+
);
|
|
480
|
+
if (!res.ok) {
|
|
481
|
+
console.error(`Error: ${res.error}`);
|
|
482
|
+
process.exit(1);
|
|
483
|
+
}
|
|
484
|
+
console.log(`Config set: ${key}`);
|
|
485
|
+
}
|
|
486
|
+
async function handleSetSize(argv, apiUrl, token) {
|
|
487
|
+
const [templateId, sizeArg] = argv.filter((a) => !a.startsWith("-"));
|
|
488
|
+
if (!templateId || !sizeArg) {
|
|
489
|
+
console.error("Error: template ID and size required.");
|
|
490
|
+
console.error(`Usage: canary sandbox set-size <template-id> <${INSTANCE_SIZES.join("|")}>`);
|
|
491
|
+
process.exit(1);
|
|
492
|
+
}
|
|
493
|
+
const size = parseInstanceSize(sizeArg);
|
|
494
|
+
if (!size) {
|
|
495
|
+
console.error(`Error: invalid size '${sizeArg}'. Must be one of: ${INSTANCE_SIZES.join(", ")}`);
|
|
496
|
+
process.exit(1);
|
|
497
|
+
}
|
|
498
|
+
const res = await apiRequest(
|
|
499
|
+
apiUrl,
|
|
500
|
+
token,
|
|
501
|
+
"PATCH",
|
|
502
|
+
`/org/environments/templates/${templateId}/instance-size`,
|
|
503
|
+
{ instanceSize: size }
|
|
504
|
+
);
|
|
505
|
+
if (!res.ok) {
|
|
506
|
+
console.error(`Error: ${res.error}`);
|
|
507
|
+
process.exit(1);
|
|
508
|
+
}
|
|
509
|
+
console.log(`Instance size set to ${size}`);
|
|
510
|
+
}
|
|
511
|
+
async function handleCreate(argv, apiUrl, token) {
|
|
512
|
+
const positional = argv.filter((a) => !a.startsWith("-"));
|
|
513
|
+
const json = hasFlag(argv, "--json");
|
|
514
|
+
const propertyHint = positional[0] ?? getArgValue(argv, "--property");
|
|
515
|
+
const property = await selectProperty(apiUrl, token, propertyHint ?? void 0);
|
|
516
|
+
if (!property) process.exit(1);
|
|
517
|
+
const reposRes = await apiRequest(
|
|
518
|
+
apiUrl,
|
|
519
|
+
token,
|
|
520
|
+
"GET",
|
|
521
|
+
`/org/properties/${property.id}/github-repos`
|
|
522
|
+
);
|
|
523
|
+
if (!reposRes.ok || !reposRes.items?.length) {
|
|
524
|
+
console.error(`No GitHub repos linked to property "${property.name}". Link one first in the web app.`);
|
|
525
|
+
process.exit(1);
|
|
526
|
+
}
|
|
527
|
+
const linkedRepo = reposRes.items[0];
|
|
528
|
+
const branch = getArgValue(argv, "--branch") ?? linkedRepo.defaultBranch ?? "main";
|
|
529
|
+
const name = getArgValue(argv, "--name") ?? linkedRepo.repoName ?? linkedRepo.repoFullName.split("/").pop() ?? property.name;
|
|
530
|
+
const subdirectory = getArgValue(argv, "--subdirectory") ?? linkedRepo.rootDir ?? void 0;
|
|
531
|
+
const sizeArg = getArgValue(argv, "--size");
|
|
532
|
+
const instanceSize = sizeArg ? parseInstanceSize(sizeArg) : void 0;
|
|
533
|
+
if (sizeArg && !instanceSize) {
|
|
534
|
+
console.error(`Error: invalid --size '${sizeArg}'. Must be one of: ${INSTANCE_SIZES.join(", ")}`);
|
|
535
|
+
process.exit(1);
|
|
536
|
+
}
|
|
537
|
+
const body = {
|
|
538
|
+
name,
|
|
539
|
+
propertyId: property.id,
|
|
540
|
+
repositoryFullName: linkedRepo.repoFullName,
|
|
541
|
+
branch
|
|
542
|
+
};
|
|
543
|
+
if (subdirectory) body.subdirectory = subdirectory;
|
|
544
|
+
if (linkedRepo.agentHint) body.hints = linkedRepo.agentHint;
|
|
545
|
+
if (instanceSize) body.instanceSize = instanceSize;
|
|
546
|
+
const res = await apiRequest(
|
|
547
|
+
apiUrl,
|
|
548
|
+
token,
|
|
549
|
+
"POST",
|
|
550
|
+
"/org/environments/templates",
|
|
551
|
+
body
|
|
552
|
+
);
|
|
553
|
+
if (!res.ok) {
|
|
554
|
+
console.error(`Error: ${res.error}`);
|
|
555
|
+
process.exit(1);
|
|
556
|
+
}
|
|
557
|
+
if (json) {
|
|
558
|
+
console.log(JSON.stringify(res.data, null, 2));
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
console.log(`Template created: ${res.data.id}`);
|
|
562
|
+
console.log(` Property: ${property.name}`);
|
|
563
|
+
console.log(` Repository: ${res.data.repositoryFullName}:${res.data.branch}`);
|
|
564
|
+
console.log(` Status: ${res.data.status}`);
|
|
565
|
+
console.log("");
|
|
566
|
+
console.log("Authoring job enqueued. Poll status with:");
|
|
567
|
+
console.log(` canary sandbox template ${res.data.id}`);
|
|
568
|
+
}
|
|
569
|
+
async function handleProvision(argv, apiUrl, token) {
|
|
570
|
+
const positional = argv.filter((a) => !a.startsWith("-"));
|
|
571
|
+
const templateId = positional[0];
|
|
572
|
+
const json = hasFlag(argv, "--json");
|
|
573
|
+
if (!templateId) {
|
|
574
|
+
console.error("Error: template ID required. Usage: canary sandbox provision <template-id> [--ttl 8]");
|
|
575
|
+
process.exit(1);
|
|
576
|
+
}
|
|
577
|
+
const name = getArgValue(argv, "--name") ?? `instance-${Date.now()}`;
|
|
578
|
+
const ttl = parseInt(getArgValue(argv, "--ttl") ?? "8", 10);
|
|
579
|
+
const withAgent = hasFlag(argv, "--with-agent");
|
|
580
|
+
const isPrivate = hasFlag(argv, "--private");
|
|
581
|
+
const body = {
|
|
582
|
+
templateId,
|
|
583
|
+
name,
|
|
584
|
+
expiresInHours: ttl,
|
|
585
|
+
userConfig: {},
|
|
586
|
+
withAgent,
|
|
587
|
+
isPrivate
|
|
588
|
+
};
|
|
589
|
+
const res = await apiRequest(
|
|
590
|
+
apiUrl,
|
|
591
|
+
token,
|
|
592
|
+
"POST",
|
|
593
|
+
"/org/environments/instances",
|
|
594
|
+
body
|
|
595
|
+
);
|
|
596
|
+
if (!res.ok) {
|
|
597
|
+
console.error(`Error: ${res.error}`);
|
|
598
|
+
process.exit(1);
|
|
599
|
+
}
|
|
600
|
+
if (json) {
|
|
601
|
+
console.log(JSON.stringify(res.data, null, 2));
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
console.log(`Instance created: ${res.data.id}`);
|
|
605
|
+
console.log(` Status: ${res.data.status}`);
|
|
606
|
+
if (isPrivate) console.log(` Access: private (no public URL)`);
|
|
607
|
+
console.log(` Expires: ${res.data.expiresAt ? formatDate(res.data.expiresAt) : "-"}`);
|
|
608
|
+
}
|
|
609
|
+
function parseDockerPs(stdout) {
|
|
610
|
+
const lines = stdout.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
611
|
+
return lines.map((line) => {
|
|
612
|
+
const [name = "", status = ""] = line.split(" ");
|
|
613
|
+
const lower = status.toLowerCase();
|
|
614
|
+
let state = "other";
|
|
615
|
+
if (lower.startsWith("restarting")) state = "restarting";
|
|
616
|
+
else if (lower.startsWith("exited")) state = "exited";
|
|
617
|
+
else if (lower.startsWith("up") && lower.includes("unhealthy")) state = "up-unhealthy";
|
|
618
|
+
else if (lower.startsWith("up") && lower.includes("healthy")) state = "up-healthy";
|
|
619
|
+
else if (lower.startsWith("up")) state = "up";
|
|
620
|
+
return { name, status, state };
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
function isBrokenContainer(c) {
|
|
624
|
+
return c.state === "restarting" || c.state === "exited" || c.state === "up-unhealthy";
|
|
625
|
+
}
|
|
626
|
+
function containerNameToService(containerName) {
|
|
627
|
+
const withProject = containerName.match(/^workspace-(.+)-\d+$/);
|
|
628
|
+
if (withProject) return withProject[1];
|
|
629
|
+
const withoutProject = containerName.match(/^(.+)-\d+$/);
|
|
630
|
+
if (withoutProject) return withoutProject[1];
|
|
631
|
+
return containerName;
|
|
632
|
+
}
|
|
633
|
+
async function fetchServiceLogs(apiUrl, token, instanceId, service, tail) {
|
|
634
|
+
const res = await apiRequest(
|
|
635
|
+
apiUrl,
|
|
636
|
+
token,
|
|
637
|
+
"POST",
|
|
638
|
+
`/org/environments/instances/${instanceId}/docker/logs`,
|
|
639
|
+
{ service, tail }
|
|
640
|
+
);
|
|
641
|
+
if (!res.ok) return `(log fetch failed: ${res.error ?? "unknown"})`;
|
|
642
|
+
return res.data?.logs ?? "";
|
|
643
|
+
}
|
|
644
|
+
async function handleVerify(argv, apiUrl, token) {
|
|
645
|
+
const positional = argv.filter((a) => !a.startsWith("-"));
|
|
646
|
+
const templateId = positional[0];
|
|
647
|
+
const json = hasFlag(argv, "--json");
|
|
648
|
+
const keepInstance = hasFlag(argv, "--keep-instance");
|
|
649
|
+
const autoTerminateOnFailure = hasFlag(argv, "--auto-terminate-on-failure");
|
|
650
|
+
if (!templateId) {
|
|
651
|
+
console.error("Error: template ID required. Usage: canary sandbox verify <template-id>");
|
|
652
|
+
process.exit(1);
|
|
653
|
+
}
|
|
654
|
+
console.log(`Verifying template ${templateId}...`);
|
|
655
|
+
const verifyRes = await apiRequest(
|
|
656
|
+
apiUrl,
|
|
657
|
+
token,
|
|
658
|
+
"POST",
|
|
659
|
+
`/org/environments/templates/${templateId}/verify`,
|
|
660
|
+
{}
|
|
661
|
+
);
|
|
662
|
+
if (!verifyRes.ok) {
|
|
663
|
+
console.error(`Error: ${verifyRes.error}`);
|
|
664
|
+
process.exit(1);
|
|
665
|
+
}
|
|
666
|
+
const instanceId = verifyRes.data.instanceId;
|
|
667
|
+
console.log(` Verification instance: ${instanceId}`);
|
|
668
|
+
const POLL_INTERVAL_MS = 1e4;
|
|
669
|
+
const TIMEOUT_MS = 6e5;
|
|
670
|
+
const DETAIL_DUMP_INTERVAL_MS = 3e4;
|
|
671
|
+
const LOG_TAIL_LINES = 40;
|
|
672
|
+
const startTime = Date.now();
|
|
673
|
+
let finalStatus = "";
|
|
674
|
+
let publicUrl;
|
|
675
|
+
let lastDetailDumpAt = 0;
|
|
676
|
+
let routes = [];
|
|
677
|
+
let routesFetched = false;
|
|
678
|
+
while (Date.now() - startTime < TIMEOUT_MS) {
|
|
679
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
680
|
+
const diag = await apiRequest(
|
|
681
|
+
apiUrl,
|
|
682
|
+
token,
|
|
683
|
+
"GET",
|
|
684
|
+
`/org/environments/instances/${instanceId}/diagnostics`,
|
|
685
|
+
void 0,
|
|
686
|
+
{ timeoutMs: 6e4 }
|
|
687
|
+
);
|
|
688
|
+
if (!diag.ok) continue;
|
|
689
|
+
const now = Date.now();
|
|
690
|
+
const elapsed = Math.round((now - startTime) / 1e3);
|
|
691
|
+
const remaining = Math.max(0, Math.round((TIMEOUT_MS - (now - startTime)) / 1e3));
|
|
692
|
+
const status = String(diag.data.instance.status ?? "");
|
|
693
|
+
publicUrl = diag.data.instance.publicUrl;
|
|
694
|
+
const appOutput = "stdout" in diag.data.appHealth ? diag.data.appHealth.stdout : "";
|
|
695
|
+
const appUp = appOutput.length > 0 && !appOutput.includes("UNREACHABLE");
|
|
696
|
+
const dockerOutput = "stdout" in diag.data.docker ? diag.data.docker.stdout : "";
|
|
697
|
+
const containers = parseDockerPs(dockerOutput);
|
|
698
|
+
const broken = containers.filter(isBrokenContainer);
|
|
699
|
+
const ttl = remaining > 60 ? `${elapsed}s elapsed` : `${remaining}s remaining`;
|
|
700
|
+
process.stdout.write(
|
|
701
|
+
`\r [${ttl}] status: ${status}, app: ${appUp ? "up" : "waiting"}, containers: ${containers.length}, unhealthy: ${broken.length} `
|
|
702
|
+
);
|
|
703
|
+
if (status === "running" && appUp) {
|
|
704
|
+
process.stdout.write("\n");
|
|
705
|
+
finalStatus = "running";
|
|
706
|
+
break;
|
|
707
|
+
}
|
|
708
|
+
if (status === "failed" || status === "terminated") {
|
|
709
|
+
process.stdout.write("\n");
|
|
710
|
+
finalStatus = status;
|
|
711
|
+
break;
|
|
712
|
+
}
|
|
713
|
+
const shouldDump = !appUp && broken.length > 0 && now - lastDetailDumpAt >= DETAIL_DUMP_INTERVAL_MS;
|
|
714
|
+
if (shouldDump) {
|
|
715
|
+
lastDetailDumpAt = now;
|
|
716
|
+
process.stdout.write("\n");
|
|
717
|
+
console.log("");
|
|
718
|
+
console.log(` App unreachable after ${elapsed}s. Container state:`);
|
|
719
|
+
for (const c of containers) {
|
|
720
|
+
console.log(` ${c.name.padEnd(40)} ${c.status}`);
|
|
721
|
+
}
|
|
722
|
+
console.log("");
|
|
723
|
+
const MAX_LOG_CONTAINERS = 3;
|
|
724
|
+
const toFetch = broken.slice(0, MAX_LOG_CONTAINERS);
|
|
725
|
+
const overflow = broken.length - toFetch.length;
|
|
726
|
+
const fetched = await Promise.all(
|
|
727
|
+
toFetch.map(
|
|
728
|
+
(c) => fetchServiceLogs(apiUrl, token, instanceId, containerNameToService(c.name), LOG_TAIL_LINES).then((logs) => ({ c, logs }))
|
|
729
|
+
)
|
|
730
|
+
);
|
|
731
|
+
for (const { c, logs } of fetched) {
|
|
732
|
+
const tail = logs.trim().split("\n").slice(-LOG_TAIL_LINES).join("\n ");
|
|
733
|
+
console.log(` --- ${c.name} (${c.state}) ---`);
|
|
734
|
+
console.log(` ${tail || "(no logs)"}`);
|
|
735
|
+
console.log("");
|
|
736
|
+
}
|
|
737
|
+
if (overflow > 0) {
|
|
738
|
+
console.log(
|
|
739
|
+
` (+${overflow} more broken container${overflow === 1 ? "" : "s"} \u2014 re-run with: canary sandbox logs ${instanceId} --service <name>)`
|
|
740
|
+
);
|
|
741
|
+
console.log("");
|
|
742
|
+
}
|
|
743
|
+
if (!routesFetched) {
|
|
744
|
+
routes = await fetchInstanceRoutes(apiUrl, token, instanceId);
|
|
745
|
+
routesFetched = true;
|
|
746
|
+
}
|
|
747
|
+
const nginxProxy = containers.find((c) => containerNameToService(c.name) === "nginx-proxy");
|
|
748
|
+
if (nginxProxy?.state === "restarting" && routes.length > 0) {
|
|
749
|
+
const failedUpstreams = routes.map((r) => ({
|
|
750
|
+
route: r.route,
|
|
751
|
+
service: r.service,
|
|
752
|
+
container: broken.find((c) => containerNameToService(c.name) === r.service)
|
|
753
|
+
})).filter((x) => x.container);
|
|
754
|
+
if (failedUpstreams.length > 0) {
|
|
755
|
+
for (const fu of failedUpstreams) {
|
|
756
|
+
console.log(
|
|
757
|
+
` >> nginx-proxy is restart-looping because upstream '${fu.service}' (route '${fu.route}') is ${fu.container.state}. Read that service's logs above, not nginx's.`
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
console.log("");
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
if (finalStatus !== "running") {
|
|
766
|
+
console.error("");
|
|
767
|
+
console.error(`Verification failed \u2014 instance ended in status '${finalStatus || "timeout"}'.`);
|
|
768
|
+
if (publicUrl) console.error(` Instance URL: ${publicUrl}`);
|
|
769
|
+
console.error(` Instance ID: ${instanceId}`);
|
|
770
|
+
console.error(` Debug with: canary sandbox diagnostics ${instanceId}`);
|
|
771
|
+
console.error(` canary sandbox logs ${instanceId} --service <name>`);
|
|
772
|
+
console.error(` canary sandbox run-command ${instanceId} 'docker ps -a'`);
|
|
773
|
+
if (autoTerminateOnFailure) {
|
|
774
|
+
await apiRequest(apiUrl, token, "POST", `/org/environments/instances/${instanceId}/terminate`);
|
|
775
|
+
console.error(` Terminated (--auto-terminate-on-failure).`);
|
|
776
|
+
} else {
|
|
777
|
+
console.error(` Instance preserved by default for debugging. Pass --auto-terminate-on-failure in CI.`);
|
|
778
|
+
console.error(` Terminate manually with:`);
|
|
779
|
+
console.error(` canary sandbox terminate ${instanceId}`);
|
|
780
|
+
}
|
|
781
|
+
process.exit(1);
|
|
782
|
+
}
|
|
783
|
+
const evidenceSummary = `Verified via canary sandbox verify \u2014 fresh instance ${instanceId} came up healthy.`;
|
|
784
|
+
for (const itemId of ["server-running", "server-accessible"]) {
|
|
785
|
+
await apiRequest(
|
|
786
|
+
apiUrl,
|
|
787
|
+
token,
|
|
788
|
+
"POST",
|
|
789
|
+
`/org/environments/templates/${templateId}/checklist/${itemId}/complete`,
|
|
790
|
+
{ status: "completed", summary: evidenceSummary }
|
|
791
|
+
).catch(() => {
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
const completeRes = await apiRequest(
|
|
795
|
+
apiUrl,
|
|
796
|
+
token,
|
|
797
|
+
"POST",
|
|
798
|
+
`/org/environments/templates/${templateId}/checklist/template-verified/complete`,
|
|
799
|
+
{
|
|
800
|
+
status: "completed",
|
|
801
|
+
summary: evidenceSummary
|
|
802
|
+
}
|
|
803
|
+
);
|
|
804
|
+
if (!completeRes.ok) {
|
|
805
|
+
console.error(`Failed to record verification: ${completeRes.error}`);
|
|
806
|
+
console.error(`Instance ${instanceId} left running for inspection.`);
|
|
807
|
+
process.exit(1);
|
|
808
|
+
}
|
|
809
|
+
if (!keepInstance) {
|
|
810
|
+
await apiRequest(apiUrl, token, "POST", `/org/environments/instances/${instanceId}/terminate`);
|
|
811
|
+
console.log(` Terminated verification instance.`);
|
|
812
|
+
} else {
|
|
813
|
+
console.log(` --keep-instance: instance ${instanceId} left running${publicUrl ? ` at ${publicUrl}` : ""}.`);
|
|
814
|
+
}
|
|
815
|
+
const templateAfter = await apiRequest(
|
|
816
|
+
apiUrl,
|
|
817
|
+
token,
|
|
818
|
+
"GET",
|
|
819
|
+
`/org/environments/templates/${templateId}`
|
|
820
|
+
).catch(() => ({ ok: false }));
|
|
821
|
+
const templateStatus = templateAfter.data?.status;
|
|
822
|
+
const verified = templateStatus === "verified";
|
|
823
|
+
if (json) {
|
|
824
|
+
console.log(JSON.stringify({ templateId, verified, status: templateStatus, instanceId, publicUrl }, null, 2));
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
if (verified) {
|
|
828
|
+
console.log("Template verified.");
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
console.log(`Verification probe passed (instance ${instanceId} came up healthy),`);
|
|
832
|
+
console.log(`but template status is still '${templateStatus ?? "unknown"}'.`);
|
|
833
|
+
const checklistRes = await apiRequest(
|
|
834
|
+
apiUrl,
|
|
835
|
+
token,
|
|
836
|
+
"GET",
|
|
837
|
+
`/org/environments/templates/${templateId}/checklist`
|
|
838
|
+
).catch(() => ({ ok: false }));
|
|
839
|
+
const pending = (checklistRes.data ?? []).filter((it) => {
|
|
840
|
+
const s = it.itemStatus?.status ?? "pending";
|
|
841
|
+
return s !== "completed" && s !== "skipped";
|
|
842
|
+
});
|
|
843
|
+
if (pending.length > 0) {
|
|
844
|
+
console.log("Pending checklist items must be completed or skipped:");
|
|
845
|
+
for (const item of pending) {
|
|
846
|
+
console.log(` - ${item.label} (${item.id})`);
|
|
847
|
+
}
|
|
848
|
+
console.log("");
|
|
849
|
+
console.log('Complete: canary sandbox checklist complete <template-id> <item-id> --status success --summary "..."');
|
|
850
|
+
console.log("Skip: canary sandbox checklist skip <template-id> <item-id>");
|
|
851
|
+
}
|
|
852
|
+
process.exit(1);
|
|
853
|
+
}
|
|
854
|
+
async function handleArtifactGet(argv, apiUrl, token) {
|
|
855
|
+
const positional = argv.filter((a) => !a.startsWith("-"));
|
|
856
|
+
const templateId = positional[0];
|
|
857
|
+
const type = positional[1];
|
|
858
|
+
const outputFile = getArgValue(argv, "--output");
|
|
859
|
+
if (!templateId || !type) {
|
|
860
|
+
console.error("Error: template ID and artifact type required. Usage: canary sandbox artifact get <template-id> <type>");
|
|
861
|
+
process.exit(1);
|
|
862
|
+
}
|
|
863
|
+
const res = await apiRequest(
|
|
864
|
+
apiUrl,
|
|
865
|
+
token,
|
|
866
|
+
"GET",
|
|
867
|
+
`/org/environments/templates/${templateId}/artifacts/${type}`
|
|
868
|
+
);
|
|
869
|
+
if (!res.ok) {
|
|
870
|
+
console.error(`Error: ${res.error}`);
|
|
871
|
+
process.exit(1);
|
|
872
|
+
}
|
|
873
|
+
if (outputFile) {
|
|
874
|
+
const { writeFileSync } = await import("fs");
|
|
875
|
+
writeFileSync(outputFile, res.data.content ?? "", "utf-8");
|
|
876
|
+
console.log(`Written to ${outputFile}`);
|
|
877
|
+
} else {
|
|
878
|
+
process.stdout.write(res.data.content ?? "");
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
async function handleArtifactCreate(argv, apiUrl, token) {
|
|
882
|
+
const positional = argv.filter((a) => !a.startsWith("-"));
|
|
883
|
+
const templateId = positional[0];
|
|
884
|
+
const type = positional[1];
|
|
885
|
+
const filePath = positional[2];
|
|
886
|
+
const json = hasFlag(argv, "--json");
|
|
887
|
+
if (!templateId || !type || !filePath) {
|
|
888
|
+
console.error("Error: all args required. Usage: canary sandbox artifact create <template-id> <type> <file>");
|
|
889
|
+
process.exit(1);
|
|
890
|
+
}
|
|
891
|
+
const WELL_KNOWN_TYPES = ["docker-compose", "dockerfile", "startup-script", "setup-script", "seed-script", "environment-config", "file"];
|
|
892
|
+
if (!WELL_KNOWN_TYPES.includes(type)) {
|
|
893
|
+
console.error(`Error: invalid artifact type "${type}". Valid types: ${WELL_KNOWN_TYPES.join(", ")}`);
|
|
894
|
+
process.exit(1);
|
|
895
|
+
}
|
|
896
|
+
const destinationPath = getArgValue(argv, "--path");
|
|
897
|
+
if (type === "file" && !destinationPath) {
|
|
898
|
+
console.error("Error: file artifacts require --path <destination>. Example: --path /workspace/.canary/vite.sandbox.config.ts");
|
|
899
|
+
process.exit(1);
|
|
900
|
+
}
|
|
901
|
+
if (destinationPath && !destinationPath.startsWith("/workspace/.canary/")) {
|
|
902
|
+
const suggestedName = destinationPath.split("/").filter(Boolean).pop() ?? "yourfile";
|
|
903
|
+
console.error(
|
|
904
|
+
`Error: --path must be under /workspace/.canary/ (the repo tree is user-owned \u2014 sandbox files live in .canary/).
|
|
905
|
+
try: --path /workspace/.canary/${suggestedName}`
|
|
906
|
+
);
|
|
907
|
+
process.exit(1);
|
|
908
|
+
}
|
|
909
|
+
let content;
|
|
910
|
+
try {
|
|
911
|
+
content = readFileSync(filePath, "utf-8");
|
|
912
|
+
} catch (err) {
|
|
913
|
+
console.error(`Error reading file: ${err}`);
|
|
914
|
+
process.exit(1);
|
|
915
|
+
}
|
|
916
|
+
const filename = filePath.split("/").pop() ?? filePath;
|
|
917
|
+
const res = await apiRequest(
|
|
918
|
+
apiUrl,
|
|
919
|
+
token,
|
|
920
|
+
"PUT",
|
|
921
|
+
`/org/environments/templates/${templateId}/artifacts`,
|
|
922
|
+
{
|
|
923
|
+
artifactType: type,
|
|
924
|
+
filename,
|
|
925
|
+
content,
|
|
926
|
+
...destinationPath ? { destinationPath } : {}
|
|
927
|
+
},
|
|
928
|
+
{ timeoutMs: 9e4 }
|
|
929
|
+
);
|
|
930
|
+
if (!res.ok) {
|
|
931
|
+
console.error(`Error: ${res.error}`);
|
|
932
|
+
process.exit(1);
|
|
933
|
+
}
|
|
934
|
+
if (json) {
|
|
935
|
+
console.log(JSON.stringify(res.data, null, 2));
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
const kind = res.data?.kind;
|
|
939
|
+
const marker = kind === "replaced" ? " (replaced)" : kind === "created" ? " (created)" : "";
|
|
940
|
+
console.log(`Artifact uploaded: ${type} (${filename}, ${content.length} bytes)${marker}`);
|
|
941
|
+
if (kind === "replaced" && res.data?.previousPath) {
|
|
942
|
+
console.log(` previous destination removed: ${res.data.previousPath}`);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
async function handleBuild(argv, apiUrl, token) {
|
|
946
|
+
const positional = argv.filter((a) => !a.startsWith("-"));
|
|
947
|
+
const json = hasFlag(argv, "--json");
|
|
948
|
+
const noWait = hasFlag(argv, "--no-wait");
|
|
949
|
+
const instanceId = positional[0];
|
|
950
|
+
if (!instanceId) {
|
|
951
|
+
console.error("Error: instance ID required. Usage: canary sandbox build <instance-id>");
|
|
952
|
+
process.exit(1);
|
|
953
|
+
}
|
|
954
|
+
console.log("Building sandbox (this takes a few minutes)...");
|
|
955
|
+
const KICKOFF_TIMEOUT_MS = 8e3;
|
|
956
|
+
let buildCompleted = false;
|
|
957
|
+
let buildResult = null;
|
|
958
|
+
const result = await apiFetch(`${apiUrl}/org/environments/instances/${instanceId}/build`, {
|
|
959
|
+
method: "POST",
|
|
960
|
+
headers: {
|
|
961
|
+
Authorization: `Bearer ${token}`,
|
|
962
|
+
"Content-Type": "application/json"
|
|
963
|
+
},
|
|
964
|
+
token,
|
|
965
|
+
timeoutMs: KICKOFF_TIMEOUT_MS
|
|
966
|
+
});
|
|
967
|
+
if (result.ok) {
|
|
968
|
+
const data = await result.response.json().catch(() => null);
|
|
969
|
+
if (data?.ok && data.data) {
|
|
970
|
+
buildCompleted = true;
|
|
971
|
+
buildResult = data.data;
|
|
972
|
+
} else if (data && !data.ok) {
|
|
973
|
+
console.error(`Error: ${data.error ?? "build failed"}`);
|
|
974
|
+
process.exit(1);
|
|
975
|
+
}
|
|
976
|
+
} else {
|
|
977
|
+
const { failure } = result;
|
|
978
|
+
const isExpectedAsync = failure.kind === "network" || failure.kind === "http-error" && (failure.status === 502 || failure.status === 504);
|
|
979
|
+
if (!isExpectedAsync) {
|
|
980
|
+
if (isTerminalFailure(failure)) exitOnFailure(failure);
|
|
981
|
+
console.error(`Error: ${failure.message}`);
|
|
982
|
+
process.exit(1);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
if (buildCompleted && buildResult) {
|
|
986
|
+
if (json) {
|
|
987
|
+
console.log(JSON.stringify(buildResult, null, 2));
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
if (buildResult.stdout) {
|
|
991
|
+
process.stdout.write(buildResult.stdout);
|
|
992
|
+
if (!buildResult.stdout.endsWith("\n")) process.stdout.write("\n");
|
|
993
|
+
}
|
|
994
|
+
if (buildResult.stderr) {
|
|
995
|
+
process.stderr.write(`STDERR: ${buildResult.stderr}`);
|
|
996
|
+
if (!buildResult.stderr.endsWith("\n")) process.stderr.write("\n");
|
|
997
|
+
}
|
|
998
|
+
if (buildResult.exitCode === 0) {
|
|
999
|
+
console.log("Build complete.");
|
|
1000
|
+
} else {
|
|
1001
|
+
console.error(`Build failed with exit code ${buildResult.exitCode}`);
|
|
1002
|
+
process.exit(1);
|
|
1003
|
+
}
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
if (noWait) {
|
|
1007
|
+
console.log("Build started. Use diagnostics to monitor progress:");
|
|
1008
|
+
console.log(` canary sandbox diagnostics ${instanceId}`);
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
console.log("Build started, watching for services...");
|
|
1012
|
+
const BUILD_POLL_TIMEOUT_MS = 3e5;
|
|
1013
|
+
const POLL_INTERVAL_MS = 1e4;
|
|
1014
|
+
const startTime = Date.now();
|
|
1015
|
+
while (Date.now() - startTime < BUILD_POLL_TIMEOUT_MS) {
|
|
1016
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
1017
|
+
const diag = await apiRequest(
|
|
1018
|
+
apiUrl,
|
|
1019
|
+
token,
|
|
1020
|
+
"GET",
|
|
1021
|
+
`/org/environments/instances/${instanceId}/diagnostics`,
|
|
1022
|
+
void 0,
|
|
1023
|
+
{ timeoutMs: 6e4 }
|
|
1024
|
+
);
|
|
1025
|
+
if (!diag.ok) continue;
|
|
1026
|
+
const elapsed = Math.round((Date.now() - startTime) / 1e3);
|
|
1027
|
+
const dockerOutput = "stdout" in diag.data.docker ? diag.data.docker.stdout : "";
|
|
1028
|
+
const appOutput = "stdout" in diag.data.appHealth ? diag.data.appHealth.stdout : "";
|
|
1029
|
+
const appUp = appOutput.length > 0 && !appOutput.includes("UNREACHABLE");
|
|
1030
|
+
const containerCount = (dockerOutput.match(/\n/g) || []).length + (dockerOutput ? 1 : 0);
|
|
1031
|
+
const healthyCount = (dockerOutput.match(/\(healthy\)/g) || []).length;
|
|
1032
|
+
process.stdout.write(`\r [${elapsed}s] containers: ${containerCount}, healthy: ${healthyCount}, app: ${appUp ? "up" : "waiting"} `);
|
|
1033
|
+
if (appUp) {
|
|
1034
|
+
process.stdout.write("\n");
|
|
1035
|
+
console.log("Build complete.");
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
process.stdout.write("\n");
|
|
1040
|
+
console.error("Build timed out waiting for the app to respond. Check diagnostics and logs:");
|
|
1041
|
+
console.error(` canary sandbox diagnostics ${instanceId}`);
|
|
1042
|
+
console.error(` canary sandbox logs ${instanceId} --service <name>`);
|
|
1043
|
+
process.exit(1);
|
|
1044
|
+
}
|
|
1045
|
+
async function handleRunCommand(argv, apiUrl, token) {
|
|
1046
|
+
const json = hasFlag(argv, "--json");
|
|
1047
|
+
const service = getArgValue(argv, "--service");
|
|
1048
|
+
const timeoutFlag = getArgValue(argv, "--timeout");
|
|
1049
|
+
const filtered = argv.filter((a, i, arr) => {
|
|
1050
|
+
if (a === "--json") return false;
|
|
1051
|
+
if (a === "--service" || a === "--timeout") return false;
|
|
1052
|
+
if (i > 0 && (arr[i - 1] === "--service" || arr[i - 1] === "--timeout")) return false;
|
|
1053
|
+
return true;
|
|
1054
|
+
});
|
|
1055
|
+
const firstNonFlag = filtered.findIndex((a) => !a.startsWith("-"));
|
|
1056
|
+
const instanceId = firstNonFlag >= 0 ? filtered[firstNonFlag] : void 0;
|
|
1057
|
+
const command = firstNonFlag >= 0 ? filtered.slice(firstNonFlag + 1).join(" ") : "";
|
|
1058
|
+
if (!instanceId || !command) {
|
|
1059
|
+
console.error("Error: instance ID and command required. Usage: canary sandbox run-command <instance-id> [--service <name>] [--timeout <ms>] <command...>");
|
|
1060
|
+
process.exit(1);
|
|
1061
|
+
}
|
|
1062
|
+
const timeoutMs = timeoutFlag ? Math.max(parseInt(timeoutFlag, 10), 1e3) : 6e4;
|
|
1063
|
+
const res = await apiRequest(
|
|
1064
|
+
apiUrl,
|
|
1065
|
+
token,
|
|
1066
|
+
"POST",
|
|
1067
|
+
`/org/environments/instances/${instanceId}/run-command`,
|
|
1068
|
+
{ command, ...service ? { service } : {} },
|
|
1069
|
+
{ timeoutMs }
|
|
1070
|
+
);
|
|
1071
|
+
if (!res.ok) {
|
|
1072
|
+
console.error(`Error: ${res.error}`);
|
|
1073
|
+
process.exit(1);
|
|
1074
|
+
}
|
|
1075
|
+
if (json) {
|
|
1076
|
+
console.log(JSON.stringify(res.data, null, 2));
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
if (res.data.stdout) {
|
|
1080
|
+
process.stdout.write(res.data.stdout);
|
|
1081
|
+
if (!res.data.stdout.endsWith("\n")) process.stdout.write("\n");
|
|
1082
|
+
}
|
|
1083
|
+
if (res.data.stderr) {
|
|
1084
|
+
process.stderr.write(`STDERR: ${res.data.stderr}`);
|
|
1085
|
+
if (!res.data.stderr.endsWith("\n")) process.stderr.write("\n");
|
|
1086
|
+
}
|
|
1087
|
+
if (res.data.exitCode !== 0) {
|
|
1088
|
+
console.log(`Exit code: ${res.data.exitCode}`);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
async function handleDiagnostics(argv, apiUrl, token) {
|
|
1092
|
+
const positional = argv.filter((a) => !a.startsWith("-"));
|
|
1093
|
+
const instanceId = positional[0];
|
|
1094
|
+
const json = hasFlag(argv, "--json");
|
|
1095
|
+
if (!instanceId) {
|
|
1096
|
+
console.error("Error: instance ID required. Usage: canary sandbox diagnostics <instance-id>");
|
|
1097
|
+
process.exit(1);
|
|
1098
|
+
}
|
|
1099
|
+
const res = await apiRequest(
|
|
1100
|
+
apiUrl,
|
|
1101
|
+
token,
|
|
1102
|
+
"GET",
|
|
1103
|
+
`/org/environments/instances/${instanceId}/diagnostics`,
|
|
1104
|
+
void 0,
|
|
1105
|
+
{ timeoutMs: 6e4 }
|
|
1106
|
+
);
|
|
1107
|
+
if (!res.ok) {
|
|
1108
|
+
console.error(`Error: ${res.error}`);
|
|
1109
|
+
process.exit(1);
|
|
1110
|
+
}
|
|
1111
|
+
const routes = await fetchInstanceRoutes(apiUrl, token, instanceId);
|
|
1112
|
+
const data = { ...res.data, routes };
|
|
1113
|
+
if (json) {
|
|
1114
|
+
console.log(JSON.stringify(data, null, 2));
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
const d = data;
|
|
1118
|
+
console.log("=== Instance ===");
|
|
1119
|
+
for (const [key, value] of Object.entries(d.instance)) {
|
|
1120
|
+
console.log(` ${key}: ${value}`);
|
|
1121
|
+
}
|
|
1122
|
+
if (d.provision) {
|
|
1123
|
+
console.log("");
|
|
1124
|
+
console.log("=== Provisioning ===");
|
|
1125
|
+
console.log(` phase: ${d.provision.phase}`);
|
|
1126
|
+
console.log(` jobStatus: ${d.provision.jobStatus}`);
|
|
1127
|
+
if (d.provision.startedAt) console.log(` startedAt: ${d.provision.startedAt}`);
|
|
1128
|
+
if (d.provision.elapsedSeconds !== null) console.log(` elapsed: ${d.provision.elapsedSeconds}s`);
|
|
1129
|
+
if (d.provision.lastError) console.log(` lastError: ${d.provision.lastError}`);
|
|
1130
|
+
}
|
|
1131
|
+
const sections = [
|
|
1132
|
+
["Docker Containers", d.docker],
|
|
1133
|
+
["Git Status", d.git],
|
|
1134
|
+
["Disk Usage", d.disk],
|
|
1135
|
+
["Agent Health", d.agentHealth],
|
|
1136
|
+
["App Health", d.appHealth]
|
|
1137
|
+
];
|
|
1138
|
+
for (const [title, result] of sections) {
|
|
1139
|
+
console.log("");
|
|
1140
|
+
console.log(`=== ${title} ===`);
|
|
1141
|
+
if ("error" in result) {
|
|
1142
|
+
console.log(` Error: ${result.error}`);
|
|
1143
|
+
} else {
|
|
1144
|
+
if (result.stdout) console.log(result.stdout);
|
|
1145
|
+
if (result.stderr) console.log(` STDERR: ${result.stderr}`);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
async function handleLogs(argv, apiUrl, token) {
|
|
1150
|
+
const positional = argv.filter((a) => !a.startsWith("-"));
|
|
1151
|
+
const instanceId = positional[0];
|
|
1152
|
+
const json = hasFlag(argv, "--json");
|
|
1153
|
+
if (!instanceId) {
|
|
1154
|
+
console.error("Error: instance ID required. Usage: canary sandbox logs <instance-id>");
|
|
1155
|
+
process.exit(1);
|
|
1156
|
+
}
|
|
1157
|
+
const service = getArgValue(argv, "--service");
|
|
1158
|
+
const tail = getArgValue(argv, "--tail") ?? "200";
|
|
1159
|
+
const res = await apiRequest(
|
|
1160
|
+
apiUrl,
|
|
1161
|
+
token,
|
|
1162
|
+
"POST",
|
|
1163
|
+
`/org/environments/instances/${instanceId}/docker/logs`,
|
|
1164
|
+
{
|
|
1165
|
+
...service ? { service } : {},
|
|
1166
|
+
tail: parseInt(tail, 10)
|
|
1167
|
+
}
|
|
1168
|
+
);
|
|
1169
|
+
if (!res.ok) {
|
|
1170
|
+
console.error(`Error: ${res.error}`);
|
|
1171
|
+
process.exit(1);
|
|
1172
|
+
}
|
|
1173
|
+
if (json) {
|
|
1174
|
+
console.log(JSON.stringify(res.data, null, 2));
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
process.stdout.write(res.data.logs ?? "(no logs)");
|
|
1178
|
+
}
|
|
1179
|
+
async function handleTerminate(argv, apiUrl, token) {
|
|
1180
|
+
const positional = argv.filter((a) => !a.startsWith("-"));
|
|
1181
|
+
const instanceId = positional[0];
|
|
1182
|
+
if (!instanceId) {
|
|
1183
|
+
console.error("Error: instance ID required. Usage: canary sandbox terminate <instance-id>");
|
|
1184
|
+
process.exit(1);
|
|
1185
|
+
}
|
|
1186
|
+
const res = await apiRequest(
|
|
1187
|
+
apiUrl,
|
|
1188
|
+
token,
|
|
1189
|
+
"POST",
|
|
1190
|
+
`/org/environments/instances/${instanceId}/terminate`
|
|
1191
|
+
);
|
|
1192
|
+
if (!res.ok) {
|
|
1193
|
+
console.error(`Error: ${res.error}`);
|
|
1194
|
+
process.exit(1);
|
|
1195
|
+
}
|
|
1196
|
+
console.log(`Instance ${instanceId} termination initiated.`);
|
|
1197
|
+
}
|
|
1198
|
+
async function handleDelete(argv, apiUrl, token) {
|
|
1199
|
+
const positional = argv.filter((a) => !a.startsWith("-"));
|
|
1200
|
+
const templateId = positional[0];
|
|
1201
|
+
if (!templateId) {
|
|
1202
|
+
console.error("Error: template ID required. Usage: canary sandbox delete <template-id>");
|
|
1203
|
+
process.exit(1);
|
|
1204
|
+
}
|
|
1205
|
+
const res = await apiRequest(
|
|
1206
|
+
apiUrl,
|
|
1207
|
+
token,
|
|
1208
|
+
"DELETE",
|
|
1209
|
+
`/org/environments/templates/${templateId}`
|
|
1210
|
+
);
|
|
1211
|
+
if (!res.ok) {
|
|
1212
|
+
console.error(`Error: ${res.error}`);
|
|
1213
|
+
process.exit(1);
|
|
1214
|
+
}
|
|
1215
|
+
console.log(`Template ${templateId} deleted.`);
|
|
1216
|
+
}
|
|
1217
|
+
async function handleActionList(argv, apiUrl, token) {
|
|
1218
|
+
const positional = argv.filter((a) => !a.startsWith("-"));
|
|
1219
|
+
const templateId = positional[0];
|
|
1220
|
+
const json = hasFlag(argv, "--json");
|
|
1221
|
+
if (!templateId) {
|
|
1222
|
+
console.error("Error: template ID required. Usage: canary sandbox action list <template-id>");
|
|
1223
|
+
process.exit(1);
|
|
1224
|
+
}
|
|
1225
|
+
const res = await apiRequest(
|
|
1226
|
+
apiUrl,
|
|
1227
|
+
token,
|
|
1228
|
+
"GET",
|
|
1229
|
+
`/org/environments/templates/${templateId}/actions`
|
|
1230
|
+
);
|
|
1231
|
+
if (!res.ok) {
|
|
1232
|
+
console.error(`Error: ${res.error}`);
|
|
1233
|
+
process.exit(1);
|
|
1234
|
+
}
|
|
1235
|
+
if (json) {
|
|
1236
|
+
console.log(JSON.stringify(res.data, null, 2));
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
const actions = res.data ?? [];
|
|
1240
|
+
if (actions.length === 0) {
|
|
1241
|
+
console.log("No actions defined for this template.");
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1244
|
+
console.log(`Actions (${actions.length}):`);
|
|
1245
|
+
for (const a of actions) {
|
|
1246
|
+
const status = a.enabled ? "" : " [disabled]";
|
|
1247
|
+
console.log(` ${pad(a.slug, 30)} ${pad(a.label, 25)}${status}`);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
async function handleActionGet(argv, apiUrl, token) {
|
|
1251
|
+
const positional = argv.filter((a2) => !a2.startsWith("-"));
|
|
1252
|
+
const templateId = positional[0];
|
|
1253
|
+
const slug = positional[1];
|
|
1254
|
+
const json = hasFlag(argv, "--json");
|
|
1255
|
+
if (!templateId || !slug) {
|
|
1256
|
+
console.error("Error: template ID and slug required. Usage: canary sandbox action get <template-id> <slug>");
|
|
1257
|
+
process.exit(1);
|
|
1258
|
+
}
|
|
1259
|
+
const res = await apiRequest(
|
|
1260
|
+
apiUrl,
|
|
1261
|
+
token,
|
|
1262
|
+
"GET",
|
|
1263
|
+
`/org/environments/templates/${templateId}/actions/${slug}`
|
|
1264
|
+
);
|
|
1265
|
+
if (!res.ok) {
|
|
1266
|
+
console.error(`Error: ${res.error}`);
|
|
1267
|
+
process.exit(1);
|
|
1268
|
+
}
|
|
1269
|
+
if (json) {
|
|
1270
|
+
console.log(JSON.stringify(res.data, null, 2));
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
const a = res.data;
|
|
1274
|
+
console.log(`Action: ${a.slug}`);
|
|
1275
|
+
console.log(` Label: ${a.label}`);
|
|
1276
|
+
console.log(` Description: ${a.description ?? "(none)"}`);
|
|
1277
|
+
console.log(` Icon: ${a.icon ?? "(none)"}`);
|
|
1278
|
+
console.log(` Enabled: ${a.enabled}`);
|
|
1279
|
+
console.log(` Timeout: ${a.timeoutMs}ms`);
|
|
1280
|
+
console.log(` Input Schema:`);
|
|
1281
|
+
console.log(` ${JSON.stringify(a.inputSchema, null, 2).split("\n").join("\n ")}`);
|
|
1282
|
+
if (a.outputSchema) {
|
|
1283
|
+
console.log(` Output Schema:`);
|
|
1284
|
+
console.log(` ${JSON.stringify(a.outputSchema, null, 2).split("\n").join("\n ")}`);
|
|
1285
|
+
}
|
|
1286
|
+
console.log(` Script:`);
|
|
1287
|
+
console.log(` ${a.scriptContent.split("\n").join("\n ")}`);
|
|
1288
|
+
}
|
|
1289
|
+
async function handleActionCreate(argv, apiUrl, token) {
|
|
1290
|
+
const positional = argv.filter((a) => !a.startsWith("-"));
|
|
1291
|
+
const templateId = positional[0];
|
|
1292
|
+
const slug = positional[1];
|
|
1293
|
+
const json = hasFlag(argv, "--json");
|
|
1294
|
+
if (!templateId || !slug) {
|
|
1295
|
+
console.error("Error: template ID and slug required. Usage: canary sandbox action create <template-id> <slug> [options]");
|
|
1296
|
+
process.exit(1);
|
|
1297
|
+
}
|
|
1298
|
+
const scriptPath = getArgValue(argv, "--script");
|
|
1299
|
+
const inputSchemaPath = getArgValue(argv, "--input-schema");
|
|
1300
|
+
const outputSchemaPath = getArgValue(argv, "--output-schema");
|
|
1301
|
+
const credentialOutputConfigPath = getArgValue(argv, "--credential-output-config");
|
|
1302
|
+
const label = getArgValue(argv, "--label") ?? slug.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
1303
|
+
const description = getArgValue(argv, "--description");
|
|
1304
|
+
const icon = getArgValue(argv, "--icon");
|
|
1305
|
+
const timeoutStr = getArgValue(argv, "--timeout");
|
|
1306
|
+
const encryptScript = hasFlag(argv, "--encrypt-script");
|
|
1307
|
+
const credentialTemplateId = getArgValue(argv, "--credential-template");
|
|
1308
|
+
const provisioningMode = getArgValue(argv, "--provisioning-mode");
|
|
1309
|
+
if (provisioningMode && provisioningMode !== "shared" && provisioningMode !== "unique") {
|
|
1310
|
+
console.error("Error: --provisioning-mode must be 'shared' or 'unique'");
|
|
1311
|
+
process.exit(1);
|
|
1312
|
+
}
|
|
1313
|
+
if (!scriptPath) {
|
|
1314
|
+
console.error("Error: --script <file> is required");
|
|
1315
|
+
process.exit(1);
|
|
1316
|
+
}
|
|
1317
|
+
if (!inputSchemaPath) {
|
|
1318
|
+
console.error("Error: --input-schema <file> is required");
|
|
1319
|
+
process.exit(1);
|
|
1320
|
+
}
|
|
1321
|
+
let scriptContent;
|
|
1322
|
+
try {
|
|
1323
|
+
scriptContent = readFileSync(scriptPath, "utf-8");
|
|
1324
|
+
} catch (err) {
|
|
1325
|
+
console.error(`Error reading script file: ${err}`);
|
|
1326
|
+
process.exit(1);
|
|
1327
|
+
}
|
|
1328
|
+
let inputSchema;
|
|
1329
|
+
try {
|
|
1330
|
+
inputSchema = JSON.parse(readFileSync(inputSchemaPath, "utf-8"));
|
|
1331
|
+
} catch (err) {
|
|
1332
|
+
console.error(`Error reading input schema file: ${err}`);
|
|
1333
|
+
process.exit(1);
|
|
1334
|
+
}
|
|
1335
|
+
let outputSchema;
|
|
1336
|
+
if (outputSchemaPath) {
|
|
1337
|
+
try {
|
|
1338
|
+
outputSchema = JSON.parse(readFileSync(outputSchemaPath, "utf-8"));
|
|
1339
|
+
} catch (err) {
|
|
1340
|
+
console.error(`Error reading output schema file: ${err}`);
|
|
1341
|
+
process.exit(1);
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
let credentialOutputConfig;
|
|
1345
|
+
if (credentialOutputConfigPath) {
|
|
1346
|
+
try {
|
|
1347
|
+
credentialOutputConfig = JSON.parse(readFileSync(credentialOutputConfigPath, "utf-8"));
|
|
1348
|
+
} catch (err) {
|
|
1349
|
+
console.error(`Error reading credential output config file: ${err}`);
|
|
1350
|
+
process.exit(1);
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
const body = {
|
|
1354
|
+
label,
|
|
1355
|
+
scriptContent,
|
|
1356
|
+
inputSchema,
|
|
1357
|
+
...description ? { description } : {},
|
|
1358
|
+
...icon ? { icon } : {},
|
|
1359
|
+
...outputSchema ? { outputSchema } : {},
|
|
1360
|
+
...credentialOutputConfig ? { credentialOutputConfig } : {},
|
|
1361
|
+
...credentialTemplateId ? { credentialTemplateId } : {},
|
|
1362
|
+
...provisioningMode ? { provisioningMode } : {},
|
|
1363
|
+
...timeoutStr ? { timeoutMs: parseInt(timeoutStr, 10) } : {},
|
|
1364
|
+
...encryptScript ? { scriptEncrypted: true } : {}
|
|
1365
|
+
};
|
|
1366
|
+
const res = await apiRequest(
|
|
1367
|
+
apiUrl,
|
|
1368
|
+
token,
|
|
1369
|
+
"PUT",
|
|
1370
|
+
`/org/environments/templates/${templateId}/actions/${slug}`,
|
|
1371
|
+
body
|
|
1372
|
+
);
|
|
1373
|
+
if (!res.ok) {
|
|
1374
|
+
console.error(`Error: ${res.error}`);
|
|
1375
|
+
process.exit(1);
|
|
1376
|
+
}
|
|
1377
|
+
if (json) {
|
|
1378
|
+
console.log(JSON.stringify(res.data, null, 2));
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
1381
|
+
console.log(`Action '${slug}' saved (${scriptContent.length} bytes script, label: "${label}")`);
|
|
1382
|
+
}
|
|
1383
|
+
async function handleActionDelete(argv, apiUrl, token) {
|
|
1384
|
+
const positional = argv.filter((a) => !a.startsWith("-"));
|
|
1385
|
+
const templateId = positional[0];
|
|
1386
|
+
const slug = positional[1];
|
|
1387
|
+
if (!templateId || !slug) {
|
|
1388
|
+
console.error("Error: template ID and slug required. Usage: canary sandbox action delete <template-id> <slug>");
|
|
1389
|
+
process.exit(1);
|
|
1390
|
+
}
|
|
1391
|
+
const res = await apiRequest(
|
|
1392
|
+
apiUrl,
|
|
1393
|
+
token,
|
|
1394
|
+
"DELETE",
|
|
1395
|
+
`/org/environments/templates/${templateId}/actions/${slug}`
|
|
1396
|
+
);
|
|
1397
|
+
if (!res.ok) {
|
|
1398
|
+
console.error(`Error: ${res.error}`);
|
|
1399
|
+
process.exit(1);
|
|
1400
|
+
}
|
|
1401
|
+
console.log(`Action '${slug}' deleted.`);
|
|
1402
|
+
}
|
|
1403
|
+
async function handleActionExecute(argv, apiUrl, token) {
|
|
1404
|
+
const positional = argv.filter((a) => !a.startsWith("-"));
|
|
1405
|
+
const instanceId = positional[0];
|
|
1406
|
+
const slug = positional[1];
|
|
1407
|
+
const json = hasFlag(argv, "--json");
|
|
1408
|
+
if (!instanceId || !slug) {
|
|
1409
|
+
console.error("Error: instance ID and slug required. Usage: canary sandbox action execute <instance-id> <slug> [options]");
|
|
1410
|
+
process.exit(1);
|
|
1411
|
+
}
|
|
1412
|
+
let params = {};
|
|
1413
|
+
const paramsFile = getArgValue(argv, "--params-file");
|
|
1414
|
+
if (paramsFile) {
|
|
1415
|
+
try {
|
|
1416
|
+
params = JSON.parse(readFileSync(paramsFile, "utf-8"));
|
|
1417
|
+
} catch (err) {
|
|
1418
|
+
console.error(`Error reading params file: ${err}`);
|
|
1419
|
+
process.exit(1);
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1423
|
+
if (argv[i] === "--param" && argv[i + 1]) {
|
|
1424
|
+
const [key, ...valueParts] = argv[i + 1].split("=");
|
|
1425
|
+
if (key && valueParts.length > 0) {
|
|
1426
|
+
params[key] = valueParts.join("=");
|
|
1427
|
+
}
|
|
1428
|
+
i++;
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
const res = await apiRequest(
|
|
1432
|
+
apiUrl,
|
|
1433
|
+
token,
|
|
1434
|
+
"POST",
|
|
1435
|
+
`/org/environments/instances/${instanceId}/actions/${slug}/execute`,
|
|
1436
|
+
{
|
|
1437
|
+
params,
|
|
1438
|
+
source: "cli"
|
|
1439
|
+
}
|
|
1440
|
+
);
|
|
1441
|
+
if (!res.ok) {
|
|
1442
|
+
console.error(`Error: ${res.error}`);
|
|
1443
|
+
process.exit(1);
|
|
1444
|
+
}
|
|
1445
|
+
if (json) {
|
|
1446
|
+
console.log(JSON.stringify(res.data, null, 2));
|
|
1447
|
+
return;
|
|
1448
|
+
}
|
|
1449
|
+
const exec = res.data;
|
|
1450
|
+
const statusIcon = exec.status === "completed" ? "[done]" : exec.status === "failed" ? "[FAIL]" : `[${exec.status}]`;
|
|
1451
|
+
console.log(`${statusIcon} Action '${slug}' ${exec.status} (${exec.durationMs ?? 0}ms, exit ${exec.exitCode ?? "?"})`);
|
|
1452
|
+
if (exec.result) {
|
|
1453
|
+
console.log(`Result: ${JSON.stringify(exec.result, null, 2)}`);
|
|
1454
|
+
}
|
|
1455
|
+
if (exec.credentialId) {
|
|
1456
|
+
console.log(`Credential: ${exec.credentialId} (linked in property vault)`);
|
|
1457
|
+
}
|
|
1458
|
+
if (exec.status === "failed" && exec.stderr) {
|
|
1459
|
+
const tail = exec.stderr.slice(-1e4).trim().split("\n").slice(-20).join("\n");
|
|
1460
|
+
console.log(`Stderr (last 20 lines):
|
|
1461
|
+
${tail}`);
|
|
1462
|
+
}
|
|
1463
|
+
if (exec.error) {
|
|
1464
|
+
console.log(`Error: ${exec.error}`);
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
async function handleActionExecutions(argv, apiUrl, token) {
|
|
1468
|
+
const positional = argv.filter((a) => !a.startsWith("-"));
|
|
1469
|
+
const instanceId = positional[0];
|
|
1470
|
+
const json = hasFlag(argv, "--json");
|
|
1471
|
+
if (!instanceId) {
|
|
1472
|
+
console.error("Error: instance ID required. Usage: canary sandbox action executions <instance-id>");
|
|
1473
|
+
process.exit(1);
|
|
1474
|
+
}
|
|
1475
|
+
const res = await apiRequest(
|
|
1476
|
+
apiUrl,
|
|
1477
|
+
token,
|
|
1478
|
+
"GET",
|
|
1479
|
+
`/org/environments/instances/${instanceId}/actions/executions`
|
|
1480
|
+
);
|
|
1481
|
+
if (!res.ok) {
|
|
1482
|
+
console.error(`Error: ${res.error}`);
|
|
1483
|
+
process.exit(1);
|
|
1484
|
+
}
|
|
1485
|
+
if (json) {
|
|
1486
|
+
console.log(JSON.stringify(res.data, null, 2));
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
const execs = res.data ?? [];
|
|
1490
|
+
if (execs.length === 0) {
|
|
1491
|
+
console.log("No executions yet.");
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1494
|
+
console.log("Executions:");
|
|
1495
|
+
for (const e of execs) {
|
|
1496
|
+
const icon = STATUS_ICONS[e.status] ?? `[${e.status}]`;
|
|
1497
|
+
const dur = e.durationMs !== null && e.durationMs !== void 0 ? `${e.durationMs}ms` : "?ms";
|
|
1498
|
+
console.log(` ${icon} ${pad(e.id.slice(0, 8), 10)} ${pad(e.source, 6)} ${pad(e.status, 12)} ${pad(dur, 8)} ${formatDate(e.createdAt)}`);
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
function parseRepoSpec(spec) {
|
|
1502
|
+
let rest = spec;
|
|
1503
|
+
let role;
|
|
1504
|
+
const eq = rest.indexOf("=");
|
|
1505
|
+
if (eq > -1 && !rest.slice(0, eq).includes("/")) {
|
|
1506
|
+
role = rest.slice(0, eq);
|
|
1507
|
+
rest = rest.slice(eq + 1);
|
|
1508
|
+
}
|
|
1509
|
+
let subdirectory;
|
|
1510
|
+
const colon = rest.indexOf(":");
|
|
1511
|
+
if (colon > -1) {
|
|
1512
|
+
subdirectory = rest.slice(colon + 1);
|
|
1513
|
+
rest = rest.slice(0, colon);
|
|
1514
|
+
}
|
|
1515
|
+
let branch;
|
|
1516
|
+
const at = rest.indexOf("@");
|
|
1517
|
+
if (at > -1) {
|
|
1518
|
+
branch = rest.slice(at + 1);
|
|
1519
|
+
rest = rest.slice(0, at);
|
|
1520
|
+
}
|
|
1521
|
+
return { role, repoFullName: rest, branch, subdirectory };
|
|
1522
|
+
}
|
|
1523
|
+
async function handleRepoList(argv, apiUrl, token) {
|
|
1524
|
+
const json = hasFlag(argv, "--json");
|
|
1525
|
+
const templateId = getArgValue(argv, "--template");
|
|
1526
|
+
if (!templateId) {
|
|
1527
|
+
console.error("Error: --template is required.");
|
|
1528
|
+
process.exit(1);
|
|
1529
|
+
}
|
|
1530
|
+
const result = await apiRequest(
|
|
1531
|
+
apiUrl,
|
|
1532
|
+
token,
|
|
1533
|
+
"GET",
|
|
1534
|
+
`/org/environments/templates/${templateId}/repos`
|
|
1535
|
+
);
|
|
1536
|
+
if (!result.ok) {
|
|
1537
|
+
console.error(`Error: ${result.error ?? "Unknown error"}`);
|
|
1538
|
+
process.exit(1);
|
|
1539
|
+
}
|
|
1540
|
+
if (json) {
|
|
1541
|
+
console.log(JSON.stringify(result.items ?? [], null, 2));
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
1544
|
+
const items = result.items ?? [];
|
|
1545
|
+
if (items.length === 0) {
|
|
1546
|
+
console.log("No repos linked to this template.");
|
|
1547
|
+
return;
|
|
1548
|
+
}
|
|
1549
|
+
for (const r of items) {
|
|
1550
|
+
const role = r.role ? ` [${r.role}]` : "";
|
|
1551
|
+
const subdir = r.subdirectory ? `:${r.subdirectory}` : "";
|
|
1552
|
+
console.log(` ${r.serviceName} \u2192 ${r.propertyGithubRepoId}@${r.branch}${subdir}${role} (link=${r.id})`);
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
async function handleRepoAdd(argv, apiUrl, token) {
|
|
1556
|
+
const json = hasFlag(argv, "--json");
|
|
1557
|
+
const templateId = getArgValue(argv, "--template");
|
|
1558
|
+
const propertyGithubRepoId = getArgValue(argv, "--property-github-repo-id");
|
|
1559
|
+
const spec = getArgValue(argv, "--spec");
|
|
1560
|
+
const role = getArgValue(argv, "--role");
|
|
1561
|
+
const branch = getArgValue(argv, "--branch");
|
|
1562
|
+
const subdirectory = getArgValue(argv, "--subdirectory");
|
|
1563
|
+
const serviceName = getArgValue(argv, "--service-name");
|
|
1564
|
+
if (!templateId || !propertyGithubRepoId) {
|
|
1565
|
+
console.error("Error: --template and --property-github-repo-id are required.");
|
|
1566
|
+
console.error("Tip: link the repo to the property first via `canary property repo link`, then pass its id here.");
|
|
1567
|
+
process.exit(1);
|
|
1568
|
+
}
|
|
1569
|
+
const parsed = spec ? parseRepoSpec(spec) : {};
|
|
1570
|
+
const body = { propertyGithubRepoId };
|
|
1571
|
+
const finalRole = role ?? parsed.role;
|
|
1572
|
+
const finalBranch = branch ?? parsed.branch;
|
|
1573
|
+
const finalSubdir = subdirectory ?? parsed.subdirectory;
|
|
1574
|
+
if (finalRole !== void 0) body.role = finalRole;
|
|
1575
|
+
if (finalBranch !== void 0) body.branch = finalBranch;
|
|
1576
|
+
if (finalSubdir !== void 0) body.subdirectory = finalSubdir;
|
|
1577
|
+
if (serviceName !== void 0) body.serviceName = serviceName;
|
|
1578
|
+
const result = await apiRequest(
|
|
1579
|
+
apiUrl,
|
|
1580
|
+
token,
|
|
1581
|
+
"POST",
|
|
1582
|
+
`/org/environments/templates/${templateId}/repos`,
|
|
1583
|
+
body
|
|
1584
|
+
);
|
|
1585
|
+
if (!result.ok) {
|
|
1586
|
+
if (json) console.log(JSON.stringify(result, null, 2));
|
|
1587
|
+
else console.error(`Error: ${result.error ?? "Unknown error"}`);
|
|
1588
|
+
process.exit(1);
|
|
1589
|
+
}
|
|
1590
|
+
if (json) console.log(JSON.stringify(result.data, null, 2));
|
|
1591
|
+
else console.log(`Linked repo to template ${templateId} (link=${result.data?.id})`);
|
|
1592
|
+
}
|
|
1593
|
+
async function handleRepoUpdate(argv, apiUrl, token) {
|
|
1594
|
+
const json = hasFlag(argv, "--json");
|
|
1595
|
+
const templateId = getArgValue(argv, "--template");
|
|
1596
|
+
const linkId = getArgValue(argv, "--link-id");
|
|
1597
|
+
const role = getArgValue(argv, "--role");
|
|
1598
|
+
const branch = getArgValue(argv, "--branch");
|
|
1599
|
+
const subdirectory = getArgValue(argv, "--subdirectory");
|
|
1600
|
+
const serviceName = getArgValue(argv, "--service-name");
|
|
1601
|
+
if (!templateId || !linkId) {
|
|
1602
|
+
console.error("Error: --template and --link-id are required.");
|
|
1603
|
+
process.exit(1);
|
|
1604
|
+
}
|
|
1605
|
+
const body = {};
|
|
1606
|
+
if (role !== void 0) body.role = role;
|
|
1607
|
+
if (branch !== void 0) body.branch = branch;
|
|
1608
|
+
if (subdirectory !== void 0) body.subdirectory = subdirectory;
|
|
1609
|
+
if (serviceName !== void 0) body.serviceName = serviceName;
|
|
1610
|
+
const result = await apiRequest(
|
|
1611
|
+
apiUrl,
|
|
1612
|
+
token,
|
|
1613
|
+
"PATCH",
|
|
1614
|
+
`/org/environments/templates/${templateId}/repos/${linkId}`,
|
|
1615
|
+
body
|
|
1616
|
+
);
|
|
1617
|
+
if (!result.ok) {
|
|
1618
|
+
if (json) console.log(JSON.stringify(result, null, 2));
|
|
1619
|
+
else console.error(`Error: ${result.error ?? "Unknown error"}`);
|
|
1620
|
+
process.exit(1);
|
|
1621
|
+
}
|
|
1622
|
+
if (json) console.log(JSON.stringify(result.data, null, 2));
|
|
1623
|
+
else console.log(`Updated repo link ${linkId}`);
|
|
1624
|
+
}
|
|
1625
|
+
async function handleRepoRemove(argv, apiUrl, token) {
|
|
1626
|
+
const json = hasFlag(argv, "--json");
|
|
1627
|
+
const templateId = getArgValue(argv, "--template");
|
|
1628
|
+
const linkId = getArgValue(argv, "--link-id");
|
|
1629
|
+
if (!templateId || !linkId) {
|
|
1630
|
+
console.error("Error: --template and --link-id are required.");
|
|
1631
|
+
process.exit(1);
|
|
1632
|
+
}
|
|
1633
|
+
const result = await apiRequest(
|
|
1634
|
+
apiUrl,
|
|
1635
|
+
token,
|
|
1636
|
+
"DELETE",
|
|
1637
|
+
`/org/environments/templates/${templateId}/repos/${linkId}`
|
|
1638
|
+
);
|
|
1639
|
+
if (!result.ok) {
|
|
1640
|
+
if (json) console.log(JSON.stringify(result, null, 2));
|
|
1641
|
+
else console.error(`Error: ${result.error ?? "Unknown error"}`);
|
|
1642
|
+
process.exit(1);
|
|
1643
|
+
}
|
|
1644
|
+
if (json) console.log(JSON.stringify(result, null, 2));
|
|
1645
|
+
else console.log(`Removed repo link ${linkId} from template ${templateId}`);
|
|
1646
|
+
}
|
|
1647
|
+
async function handleRepo(argv, apiUrl, token) {
|
|
1648
|
+
const [action, ...rest] = argv;
|
|
1649
|
+
switch (action) {
|
|
1650
|
+
case "list":
|
|
1651
|
+
await handleRepoList(rest, apiUrl, token);
|
|
1652
|
+
break;
|
|
1653
|
+
case "add":
|
|
1654
|
+
await handleRepoAdd(rest, apiUrl, token);
|
|
1655
|
+
break;
|
|
1656
|
+
case "update":
|
|
1657
|
+
await handleRepoUpdate(rest, apiUrl, token);
|
|
1658
|
+
break;
|
|
1659
|
+
case "remove":
|
|
1660
|
+
case "rm":
|
|
1661
|
+
await handleRepoRemove(rest, apiUrl, token);
|
|
1662
|
+
break;
|
|
1663
|
+
default:
|
|
1664
|
+
console.error(
|
|
1665
|
+
`Unknown repo action: ${action ?? "(none)"}. Use list/add/update/remove.
|
|
1666
|
+
Examples:
|
|
1667
|
+
canary sandbox repo list --template <template-id>
|
|
1668
|
+
canary sandbox repo add --template <template-id> --property-github-repo-id <uuid> --role frontend
|
|
1669
|
+
canary sandbox repo add --template <template-id> --property-github-repo-id <uuid> --spec frontend=owner/web@main:/apps/web`
|
|
1670
|
+
);
|
|
1671
|
+
process.exit(1);
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
var AMI_TERMINAL_STATUSES = /* @__PURE__ */ new Set(["ready", "failed", "superseded", "deregistered"]);
|
|
1675
|
+
var formatHashShort = (hash) => hash.slice(0, 8);
|
|
1676
|
+
var AMI_COL = {
|
|
1677
|
+
status: 12,
|
|
1678
|
+
hash: 8,
|
|
1679
|
+
built: 20,
|
|
1680
|
+
size: 8,
|
|
1681
|
+
amiId: 22
|
|
1682
|
+
};
|
|
1683
|
+
function printAmiTable(rows, currentAmiId) {
|
|
1684
|
+
if (rows.length === 0) {
|
|
1685
|
+
console.log("No AMI history yet \u2014 trigger one with `canary sandbox ami rebake <template-id>`.");
|
|
1686
|
+
return;
|
|
1687
|
+
}
|
|
1688
|
+
const header = ` ${"STATUS".padEnd(AMI_COL.status)} ${"HASH".padEnd(AMI_COL.hash)} ${"BUILT".padEnd(AMI_COL.built)} ${"SIZE".padEnd(AMI_COL.size)} ${"AMI-ID".padEnd(AMI_COL.amiId)} ROW-ID`;
|
|
1689
|
+
const lines = [header];
|
|
1690
|
+
const markerFor = (row) => {
|
|
1691
|
+
if (currentAmiId && row.id === currentAmiId) return "*";
|
|
1692
|
+
if (row.status === "ready" || row.status === "superseded") return "R";
|
|
1693
|
+
return " ";
|
|
1694
|
+
};
|
|
1695
|
+
for (const row of rows) {
|
|
1696
|
+
const marker = markerFor(row);
|
|
1697
|
+
const built = row.builtAt ? new Date(row.builtAt).toISOString().slice(0, 19) + "Z" : "\u2014";
|
|
1698
|
+
lines.push(
|
|
1699
|
+
`${marker} ${row.status.padEnd(AMI_COL.status)} ${formatHashShort(row.artifactHash).padEnd(AMI_COL.hash)} ${built.padEnd(AMI_COL.built)} ${row.instanceSize.padEnd(AMI_COL.size)} ${(row.amiId ?? "\u2014").padEnd(AMI_COL.amiId)} ${row.id}`
|
|
1700
|
+
);
|
|
1701
|
+
}
|
|
1702
|
+
console.log(lines.join("\n"));
|
|
1703
|
+
console.log("");
|
|
1704
|
+
console.log(" '*' = current 'R' = ready/superseded (eligible for `canary sandbox ami use`)");
|
|
1705
|
+
}
|
|
1706
|
+
async function handleAmiList(argv, apiUrl, token) {
|
|
1707
|
+
const positional = argv.filter((a) => !a.startsWith("-"));
|
|
1708
|
+
const templateId = positional[0];
|
|
1709
|
+
const json = hasFlag(argv, "--json");
|
|
1710
|
+
if (!templateId) {
|
|
1711
|
+
console.error("Error: template ID required. Usage: canary sandbox ami list <template-id>");
|
|
1712
|
+
process.exit(1);
|
|
1713
|
+
}
|
|
1714
|
+
const res = await apiRequest(
|
|
1715
|
+
apiUrl,
|
|
1716
|
+
token,
|
|
1717
|
+
"GET",
|
|
1718
|
+
`/org/environments/templates/${templateId}/amis`
|
|
1719
|
+
);
|
|
1720
|
+
if (!res.ok) {
|
|
1721
|
+
if (json) console.log(JSON.stringify(res, null, 2));
|
|
1722
|
+
else console.error(`Error: ${res.error ?? "Unknown error"}`);
|
|
1723
|
+
process.exit(1);
|
|
1724
|
+
}
|
|
1725
|
+
if (json) {
|
|
1726
|
+
console.log(JSON.stringify(res, null, 2));
|
|
1727
|
+
return;
|
|
1728
|
+
}
|
|
1729
|
+
console.log(`Template: ${res.templateId}`);
|
|
1730
|
+
console.log(`Current AMI row: ${res.currentAmiId ?? "\u2014 (using base AMI)"}`);
|
|
1731
|
+
if (res.pendingAmiBakeHash) {
|
|
1732
|
+
console.log(`Pending bake hash: ${formatHashShort(res.pendingAmiBakeHash)}`);
|
|
1733
|
+
}
|
|
1734
|
+
console.log("");
|
|
1735
|
+
printAmiTable(res.amis ?? [], res.currentAmiId);
|
|
1736
|
+
}
|
|
1737
|
+
var POLL_TRANSIENT_FAILURE_LIMIT = 3;
|
|
1738
|
+
async function pollAmiBuild(apiUrl, token, templateId, jobId, pollIntervalMs = 5e3, timeoutMs = 45 * 60 * 1e3) {
|
|
1739
|
+
const startedAt = Date.now();
|
|
1740
|
+
let lastSeenStatus = null;
|
|
1741
|
+
let consecutiveFailures = 0;
|
|
1742
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
1743
|
+
const res = await apiRequest(
|
|
1744
|
+
apiUrl,
|
|
1745
|
+
token,
|
|
1746
|
+
"GET",
|
|
1747
|
+
`/org/environments/templates/${templateId}/amis`
|
|
1748
|
+
);
|
|
1749
|
+
if (!res.ok) {
|
|
1750
|
+
consecutiveFailures += 1;
|
|
1751
|
+
if (consecutiveFailures >= POLL_TRANSIENT_FAILURE_LIMIT) {
|
|
1752
|
+
console.error(
|
|
1753
|
+
`Aborting wait after ${consecutiveFailures} consecutive poll failures: ${res.error ?? "Unknown"}`
|
|
1754
|
+
);
|
|
1755
|
+
return null;
|
|
1756
|
+
}
|
|
1757
|
+
console.error(
|
|
1758
|
+
` \u21B3 poll failure ${consecutiveFailures}/${POLL_TRANSIENT_FAILURE_LIMIT} \u2014 ${res.error ?? "Unknown"}; retrying`
|
|
1759
|
+
);
|
|
1760
|
+
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
1761
|
+
continue;
|
|
1762
|
+
}
|
|
1763
|
+
consecutiveFailures = 0;
|
|
1764
|
+
const matching = (res.amis ?? []).find((r) => r.buildJobId === jobId);
|
|
1765
|
+
if (matching && AMI_TERMINAL_STATUSES.has(matching.status)) {
|
|
1766
|
+
return matching;
|
|
1767
|
+
}
|
|
1768
|
+
if (matching && matching.status !== lastSeenStatus) {
|
|
1769
|
+
lastSeenStatus = matching.status;
|
|
1770
|
+
console.log(` \u21B3 ${(/* @__PURE__ */ new Date()).toISOString()} \u2014 status: ${matching.status}`);
|
|
1771
|
+
}
|
|
1772
|
+
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
1773
|
+
}
|
|
1774
|
+
console.error(`Timed out after ${Math.round(timeoutMs / 1e3)}s waiting for bake to finish.`);
|
|
1775
|
+
return null;
|
|
1776
|
+
}
|
|
1777
|
+
async function handleAmiRebake(argv, apiUrl, token) {
|
|
1778
|
+
const positional = argv.filter((a) => !a.startsWith("-"));
|
|
1779
|
+
const templateId = positional[0];
|
|
1780
|
+
const json = hasFlag(argv, "--json");
|
|
1781
|
+
const force = hasFlag(argv, "--force");
|
|
1782
|
+
const wait = hasFlag(argv, "--wait");
|
|
1783
|
+
if (!templateId) {
|
|
1784
|
+
console.error("Error: template ID required.");
|
|
1785
|
+
console.error("Usage: canary sandbox ami rebake <template-id> [--force] [--wait]");
|
|
1786
|
+
process.exit(1);
|
|
1787
|
+
}
|
|
1788
|
+
const res = await apiRequest(
|
|
1789
|
+
apiUrl,
|
|
1790
|
+
token,
|
|
1791
|
+
"POST",
|
|
1792
|
+
`/org/environments/templates/${templateId}/amis/rebake`,
|
|
1793
|
+
{ force }
|
|
1794
|
+
);
|
|
1795
|
+
if (!res.ok) {
|
|
1796
|
+
if (json) console.log(JSON.stringify(res, null, 2));
|
|
1797
|
+
else console.error(`Error: ${res.error ?? "Unknown error"}`);
|
|
1798
|
+
process.exit(1);
|
|
1799
|
+
}
|
|
1800
|
+
if (json) {
|
|
1801
|
+
console.log(JSON.stringify(res, null, 2));
|
|
1802
|
+
return;
|
|
1803
|
+
}
|
|
1804
|
+
if (res.reason === "already-baked") {
|
|
1805
|
+
console.log(`AMI for hash ${formatHashShort(res.artifactHash ?? "")} already exists (${res.status}).`);
|
|
1806
|
+
console.log(` Row id: ${res.amiRowId}`);
|
|
1807
|
+
console.log(` Use \`canary sandbox ami use ${templateId} ${res.amiRowId}\` to switch to it,`);
|
|
1808
|
+
console.log(" or pass --force to bake a fresh copy anyway.");
|
|
1809
|
+
return;
|
|
1810
|
+
}
|
|
1811
|
+
if (res.reason === "coalesced") {
|
|
1812
|
+
console.log("Another rebake was already queued for this template; nothing to do.");
|
|
1813
|
+
if (wait) {
|
|
1814
|
+
console.log(
|
|
1815
|
+
" (--wait skipped: coalesced response has no job handle to follow. Watch via `canary task list --status in_progress` or re-run rebake when the in-flight one finishes.)"
|
|
1816
|
+
);
|
|
1817
|
+
}
|
|
1818
|
+
return;
|
|
1819
|
+
}
|
|
1820
|
+
console.log(`Bake job enqueued.`);
|
|
1821
|
+
console.log(` Job id: ${res.jobId}`);
|
|
1822
|
+
console.log(` Hash: ${formatHashShort(res.artifactHash ?? "")}`);
|
|
1823
|
+
if (wait) {
|
|
1824
|
+
if (!res.jobId) {
|
|
1825
|
+
console.error("Cannot --wait: API did not return a jobId for the enqueued bake.");
|
|
1826
|
+
process.exit(1);
|
|
1827
|
+
}
|
|
1828
|
+
console.log("");
|
|
1829
|
+
console.log("Polling for completion (Ctrl+C to detach; bake continues server-side)\u2026");
|
|
1830
|
+
const final = await pollAmiBuild(apiUrl, token, templateId, res.jobId);
|
|
1831
|
+
if (final) {
|
|
1832
|
+
console.log("");
|
|
1833
|
+
if (final.status === "ready") {
|
|
1834
|
+
console.log(`Bake complete: AMI ${final.amiId} (row ${final.id})`);
|
|
1835
|
+
} else {
|
|
1836
|
+
console.error(`Bake ended in status: ${final.status}`);
|
|
1837
|
+
if (final.buildError) console.error(` ${final.buildError}`);
|
|
1838
|
+
process.exit(1);
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
async function handleAmiUse(argv, apiUrl, token) {
|
|
1844
|
+
const positional = argv.filter((a) => !a.startsWith("-"));
|
|
1845
|
+
const templateId = positional[0];
|
|
1846
|
+
const amiRowId = positional[1];
|
|
1847
|
+
const json = hasFlag(argv, "--json");
|
|
1848
|
+
if (!templateId || !amiRowId) {
|
|
1849
|
+
console.error("Error: template ID and AMI row id required.");
|
|
1850
|
+
console.error("Usage: canary sandbox ami use <template-id> <ami-row-id>");
|
|
1851
|
+
console.error("Find row ids with `canary sandbox ami list <template-id>`.");
|
|
1852
|
+
process.exit(1);
|
|
1853
|
+
}
|
|
1854
|
+
const res = await apiRequest(
|
|
1855
|
+
apiUrl,
|
|
1856
|
+
token,
|
|
1857
|
+
"PATCH",
|
|
1858
|
+
`/org/environments/templates/${templateId}/current-ami`,
|
|
1859
|
+
{ amiRowId }
|
|
1860
|
+
);
|
|
1861
|
+
if (!res.ok) {
|
|
1862
|
+
if (json) console.log(JSON.stringify(res, null, 2));
|
|
1863
|
+
else console.error(`Error: ${res.error ?? "Unknown error"}`);
|
|
1864
|
+
process.exit(1);
|
|
1865
|
+
}
|
|
1866
|
+
if (json) {
|
|
1867
|
+
console.log(JSON.stringify(res, null, 2));
|
|
1868
|
+
return;
|
|
1869
|
+
}
|
|
1870
|
+
if (res.alreadyCurrent) {
|
|
1871
|
+
console.log(`Row ${amiRowId} is already the current AMI for template ${templateId}.`);
|
|
1872
|
+
return;
|
|
1873
|
+
}
|
|
1874
|
+
console.log(`Current AMI updated.`);
|
|
1875
|
+
console.log(` Template: ${res.templateId}`);
|
|
1876
|
+
console.log(` Row: ${res.currentAmiRowId}`);
|
|
1877
|
+
if (res.currentAmiId) {
|
|
1878
|
+
console.log(` AMI: ${res.currentAmiId}`);
|
|
1879
|
+
}
|
|
1880
|
+
if (res.promotedFromSuperseded) {
|
|
1881
|
+
console.log(" Note: chosen row was superseded; promoted back to ready.");
|
|
1882
|
+
}
|
|
1883
|
+
if (res.demotedPriorAmiRowId) {
|
|
1884
|
+
console.log(` Demoted: ${res.demotedPriorAmiRowId} (prior current \u2192 superseded)`);
|
|
1885
|
+
}
|
|
1886
|
+
if (res.priorDemoteSkipped) {
|
|
1887
|
+
console.log(
|
|
1888
|
+
" Warning: prior current row was not in `ready` status \u2014 demote skipped (see server logs)."
|
|
1889
|
+
);
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
async function handleAmi(argv, apiUrl, token) {
|
|
1893
|
+
const [action, ...rest] = argv;
|
|
1894
|
+
switch (action) {
|
|
1895
|
+
case "list":
|
|
1896
|
+
await handleAmiList(rest, apiUrl, token);
|
|
1897
|
+
break;
|
|
1898
|
+
case "rebake":
|
|
1899
|
+
await handleAmiRebake(rest, apiUrl, token);
|
|
1900
|
+
break;
|
|
1901
|
+
case "use":
|
|
1902
|
+
await handleAmiUse(rest, apiUrl, token);
|
|
1903
|
+
break;
|
|
1904
|
+
default:
|
|
1905
|
+
console.error(
|
|
1906
|
+
`Unknown ami action: ${action ?? "(none)"}. Use list/rebake/use.
|
|
1907
|
+
Examples:
|
|
1908
|
+
canary sandbox ami list <template-id>
|
|
1909
|
+
canary sandbox ami rebake <template-id> [--force] [--wait]
|
|
1910
|
+
canary sandbox ami use <template-id> <ami-row-id>`
|
|
1911
|
+
);
|
|
1912
|
+
process.exit(1);
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
async function runSandbox(argv) {
|
|
1916
|
+
const [subCommand, ...rest] = argv;
|
|
1917
|
+
if (!subCommand || subCommand === "help" || hasFlag(argv, "--help", "-h")) {
|
|
1918
|
+
printHelp();
|
|
1919
|
+
return;
|
|
1920
|
+
}
|
|
1921
|
+
if (subCommand === "guide") {
|
|
1922
|
+
const section = rest.find((a) => !a.startsWith("-"));
|
|
1923
|
+
const json = hasFlag(rest, "--json");
|
|
1924
|
+
printGuide(SANDBOX_GUIDE_SECTIONS, section, json);
|
|
1925
|
+
return;
|
|
1926
|
+
}
|
|
1927
|
+
const { apiUrl, token } = await resolveConfig(argv);
|
|
1928
|
+
const globalFlags = /* @__PURE__ */ new Set(["--env", "--api-url", "--token"]);
|
|
1929
|
+
const args = rest.filter((arg, i) => {
|
|
1930
|
+
if (globalFlags.has(arg)) return false;
|
|
1931
|
+
if (i > 0 && globalFlags.has(rest[i - 1])) return false;
|
|
1932
|
+
return true;
|
|
1933
|
+
});
|
|
1934
|
+
switch (subCommand) {
|
|
1935
|
+
case "list":
|
|
1936
|
+
await handleList(args, apiUrl, token);
|
|
1937
|
+
break;
|
|
1938
|
+
case "template":
|
|
1939
|
+
await handleTemplate(args, apiUrl, token);
|
|
1940
|
+
break;
|
|
1941
|
+
case "repo":
|
|
1942
|
+
await handleRepo(args, apiUrl, token);
|
|
1943
|
+
break;
|
|
1944
|
+
case "artifacts":
|
|
1945
|
+
await handleArtifacts(args, apiUrl, token);
|
|
1946
|
+
break;
|
|
1947
|
+
case "checklist":
|
|
1948
|
+
await handleChecklist(args, apiUrl, token);
|
|
1949
|
+
break;
|
|
1950
|
+
case "create":
|
|
1951
|
+
await handleCreate(args, apiUrl, token);
|
|
1952
|
+
break;
|
|
1953
|
+
case "provision":
|
|
1954
|
+
await handleProvision(args, apiUrl, token);
|
|
1955
|
+
break;
|
|
1956
|
+
case "verify":
|
|
1957
|
+
await handleVerify(args, apiUrl, token);
|
|
1958
|
+
break;
|
|
1959
|
+
case "set-size":
|
|
1960
|
+
await handleSetSize(args, apiUrl, token);
|
|
1961
|
+
break;
|
|
1962
|
+
case "ami":
|
|
1963
|
+
await handleAmi(args, apiUrl, token);
|
|
1964
|
+
break;
|
|
1965
|
+
case "artifact": {
|
|
1966
|
+
const [action, ...actionRest] = args;
|
|
1967
|
+
if (action === "get") {
|
|
1968
|
+
await handleArtifactGet(actionRest, apiUrl, token);
|
|
1969
|
+
} else if (action === "create") {
|
|
1970
|
+
await handleArtifactCreate(actionRest, apiUrl, token);
|
|
1971
|
+
} else {
|
|
1972
|
+
console.error(`Unknown artifact action: ${action}. Use "get" or "create".`);
|
|
1973
|
+
process.exit(1);
|
|
1974
|
+
}
|
|
1975
|
+
break;
|
|
1976
|
+
}
|
|
1977
|
+
case "config": {
|
|
1978
|
+
const [configAction, ...configRest] = args;
|
|
1979
|
+
if (configAction === "list") {
|
|
1980
|
+
await handleConfigList(configRest, apiUrl, token);
|
|
1981
|
+
} else if (configAction === "set") {
|
|
1982
|
+
await handleConfigSet(configRest, apiUrl, token);
|
|
1983
|
+
} else {
|
|
1984
|
+
console.error(`Unknown config action: ${configAction}. Use "list" or "set".`);
|
|
1985
|
+
process.exit(1);
|
|
1986
|
+
}
|
|
1987
|
+
break;
|
|
1988
|
+
}
|
|
1989
|
+
case "action": {
|
|
1990
|
+
const [actionSub, ...actionRest] = args;
|
|
1991
|
+
switch (actionSub) {
|
|
1992
|
+
case "list":
|
|
1993
|
+
await handleActionList(actionRest, apiUrl, token);
|
|
1994
|
+
break;
|
|
1995
|
+
case "get":
|
|
1996
|
+
await handleActionGet(actionRest, apiUrl, token);
|
|
1997
|
+
break;
|
|
1998
|
+
case "create":
|
|
1999
|
+
await handleActionCreate(actionRest, apiUrl, token);
|
|
2000
|
+
break;
|
|
2001
|
+
case "delete":
|
|
2002
|
+
await handleActionDelete(actionRest, apiUrl, token);
|
|
2003
|
+
break;
|
|
2004
|
+
case "execute":
|
|
2005
|
+
await handleActionExecute(actionRest, apiUrl, token);
|
|
2006
|
+
break;
|
|
2007
|
+
case "executions":
|
|
2008
|
+
await handleActionExecutions(actionRest, apiUrl, token);
|
|
2009
|
+
break;
|
|
2010
|
+
default:
|
|
2011
|
+
console.error(`Unknown action sub-command: ${actionSub}. Use "list", "get", "create", "delete", "execute", or "executions".`);
|
|
2012
|
+
process.exit(1);
|
|
2013
|
+
}
|
|
2014
|
+
break;
|
|
2015
|
+
}
|
|
2016
|
+
case "build":
|
|
2017
|
+
await handleBuild(args, apiUrl, token);
|
|
2018
|
+
break;
|
|
2019
|
+
case "run-command":
|
|
2020
|
+
await handleRunCommand(args, apiUrl, token);
|
|
2021
|
+
break;
|
|
2022
|
+
case "diagnostics":
|
|
2023
|
+
await handleDiagnostics(args, apiUrl, token);
|
|
2024
|
+
break;
|
|
2025
|
+
case "logs":
|
|
2026
|
+
await handleLogs(args, apiUrl, token);
|
|
2027
|
+
break;
|
|
2028
|
+
case "terminate":
|
|
2029
|
+
await handleTerminate(args, apiUrl, token);
|
|
2030
|
+
break;
|
|
2031
|
+
case "delete":
|
|
2032
|
+
await handleDelete(args, apiUrl, token);
|
|
2033
|
+
break;
|
|
2034
|
+
default:
|
|
2035
|
+
console.error(`Unknown sub-command: ${subCommand}`);
|
|
2036
|
+
printHelp();
|
|
2037
|
+
process.exit(1);
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
export {
|
|
2041
|
+
containerNameToService,
|
|
2042
|
+
isBrokenContainer,
|
|
2043
|
+
parseDockerPs,
|
|
2044
|
+
runSandbox
|
|
2045
|
+
};
|
|
2046
|
+
//# sourceMappingURL=sandbox-XGSUU6G7.js.map
|