@0dai-dev/cli 4.1.0 → 4.3.4
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 +30 -5
- package/bin/0dai.js +308 -60
- package/lib/commands/audit.js +13 -0
- package/lib/commands/auth.js +404 -122
- package/lib/commands/boneyard.js +44 -0
- package/lib/commands/ci.js +329 -0
- package/lib/commands/compliance.js +20 -0
- package/lib/commands/doctor.js +79 -14
- package/lib/commands/experience.js +5 -1
- package/lib/commands/feedback.js +92 -5
- package/lib/commands/gh.js +506 -0
- package/lib/commands/graph.js +78 -10
- package/lib/commands/heatmap.js +17 -0
- package/lib/commands/import_claude_code_agents.js +367 -0
- package/lib/commands/init.js +553 -53
- package/lib/commands/loop.js +108 -0
- package/lib/commands/mcp.js +410 -0
- package/lib/commands/models.js +42 -12
- package/lib/commands/paste.js +114 -0
- package/lib/commands/persona-simulate.js +19 -0
- package/lib/commands/play.js +173 -0
- package/lib/commands/provider.js +87 -0
- package/lib/commands/quota.js +76 -0
- package/lib/commands/receipt.js +53 -0
- package/lib/commands/report.js +29 -2
- package/lib/commands/run.js +44 -4
- package/lib/commands/runner.js +527 -0
- package/lib/commands/session.js +1 -7
- package/lib/commands/ssh.js +416 -0
- package/lib/commands/standup.js +40 -0
- package/lib/commands/status.js +131 -36
- package/lib/commands/swarm.js +97 -4
- package/lib/commands/tui.js +117 -0
- package/lib/commands/usage.js +87 -0
- package/lib/commands/vault.js +246 -0
- package/lib/commands/workspace.js +1 -0
- package/lib/onboarding.js +30 -10
- package/lib/shared.js +153 -96
- package/lib/tui/index.mjs +34994 -0
- package/lib/utils/auth.js +1 -0
- package/lib/utils/canonical-counts.js +54 -0
- package/lib/utils/diff-preview.js +192 -0
- package/lib/utils/identity.js +76 -18
- package/lib/utils/mcp-auth.js +607 -0
- package/lib/utils/model_ratings.js +77 -0
- package/lib/utils/plan.js +37 -2
- package/lib/vault/cipher.js +125 -0
- package/lib/vault/identity.js +122 -0
- package/lib/vault/index.js +184 -0
- package/lib/vault/storage.js +84 -0
- package/lib/wizard.js +19 -12
- package/package.json +13 -5
- package/scripts/build-tui.js +77 -0
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const shared = require("../shared");
|
|
4
|
+
const { T, R, D, E, G, W, fs, path, spawnSync, log, findRepoScript } = shared;
|
|
5
|
+
|
|
6
|
+
function readJson(filePath) {
|
|
7
|
+
try {
|
|
8
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
9
|
+
} catch (err) {
|
|
10
|
+
return { error: err.message };
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function commandExists(name) {
|
|
15
|
+
const result = spawnSync("bash", ["-lc", `command -v ${name} >/dev/null 2>&1`], {
|
|
16
|
+
encoding: "utf8",
|
|
17
|
+
timeout: 3000,
|
|
18
|
+
});
|
|
19
|
+
return result.status === 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function runText(cmd, args, timeoutMs = 10000) {
|
|
23
|
+
if (!commandExists(cmd)) {
|
|
24
|
+
return { ok: false, error: `${cmd} not installed`, stdout: "" };
|
|
25
|
+
}
|
|
26
|
+
const result = spawnSync(cmd, args, {
|
|
27
|
+
encoding: "utf8",
|
|
28
|
+
timeout: timeoutMs,
|
|
29
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
30
|
+
});
|
|
31
|
+
if (result.error) {
|
|
32
|
+
return { ok: false, error: result.error.message, stdout: result.stdout || "" };
|
|
33
|
+
}
|
|
34
|
+
if (typeof result.status === "number" && result.status !== 0) {
|
|
35
|
+
return {
|
|
36
|
+
ok: false,
|
|
37
|
+
error: String(result.stderr || result.stdout || `${cmd} exited ${result.status}`).trim(),
|
|
38
|
+
stdout: result.stdout || "",
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return { ok: true, error: "", stdout: result.stdout || "" };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function parseDropletList(stdout) {
|
|
45
|
+
return stdout.split(/\r?\n/).filter(Boolean).map((line) => {
|
|
46
|
+
const parts = line.trim().split(/\s+/);
|
|
47
|
+
return {
|
|
48
|
+
id: parts[0] || "",
|
|
49
|
+
name: parts[1] || "",
|
|
50
|
+
size_slug: parts[2] || "",
|
|
51
|
+
memory_mb: Number(parts[3] || 0),
|
|
52
|
+
vcpus: Number(parts[4] || 0),
|
|
53
|
+
disk_gb: Number(parts[5] || 0),
|
|
54
|
+
public_ip: parts[6] || "",
|
|
55
|
+
private_ip: parts[7] || "",
|
|
56
|
+
status: parts[8] || "",
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function parseAccount(stdout) {
|
|
62
|
+
const parts = stdout.trim().split(/\s+/);
|
|
63
|
+
return {
|
|
64
|
+
email: parts[0] || "",
|
|
65
|
+
droplet_limit: Number(parts[1] || 0),
|
|
66
|
+
status: parts[2] || "",
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function parseRunners(stdout) {
|
|
71
|
+
return stdout.split(/\r?\n/).filter(Boolean).map((line) => {
|
|
72
|
+
const [name, status, busy, labels] = line.split("\t");
|
|
73
|
+
return {
|
|
74
|
+
name: name || "",
|
|
75
|
+
status: status || "",
|
|
76
|
+
busy: busy === "true",
|
|
77
|
+
labels: labels ? labels.split(",").filter(Boolean) : [],
|
|
78
|
+
};
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function liveSnapshot(config) {
|
|
83
|
+
const account = runText("doctl", ["account", "get", "--format", "Email,DropletLimit,Status", "--no-header"]);
|
|
84
|
+
const droplets = runText("doctl", [
|
|
85
|
+
"compute", "droplet", "list",
|
|
86
|
+
"--format", "ID,Name,SizeSlug,Memory,VCPUs,Disk,PublicIPv4,PrivateIPv4,Status",
|
|
87
|
+
"--no-header",
|
|
88
|
+
]);
|
|
89
|
+
const pools = runText("doctl", [
|
|
90
|
+
"compute", "droplet-autoscale", "list",
|
|
91
|
+
"--format", "ID,Name,Region,Status,Min Instance,Max Instance,Target Instance,Avg CPU Util,Avg Mem Util,Target CPU Util,Target Mem Util",
|
|
92
|
+
"--no-header",
|
|
93
|
+
]);
|
|
94
|
+
const repo = config.github_repo || "iGeezmo/0dai";
|
|
95
|
+
const runners = runText("gh", [
|
|
96
|
+
"api", `repos/${repo}/actions/runners`, "--paginate",
|
|
97
|
+
"--jq", '.runners[] | [.name,.status,.busy,([.labels[].name]|join(","))] | @tsv',
|
|
98
|
+
]);
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
account: account.ok ? parseAccount(account.stdout) : null,
|
|
102
|
+
account_error: account.ok ? "" : account.error,
|
|
103
|
+
droplets: droplets.ok ? parseDropletList(droplets.stdout) : [],
|
|
104
|
+
droplets_error: droplets.ok ? "" : droplets.error,
|
|
105
|
+
autoscale_pools_raw: pools.ok ? pools.stdout.trim() : "",
|
|
106
|
+
autoscale_pools_error: pools.ok ? "" : pools.error,
|
|
107
|
+
github_runners: runners.ok ? parseRunners(runners.stdout) : [],
|
|
108
|
+
github_runners_error: runners.ok ? "" : runners.error,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function configuredHostRows(config, live) {
|
|
113
|
+
const liveByName = new Map((live.droplets || []).map((d) => [d.name, d]));
|
|
114
|
+
return (config.project_hosts || []).map((host) => ({
|
|
115
|
+
...host,
|
|
116
|
+
live: liveByName.get(host.name) || null,
|
|
117
|
+
}));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function configuredPoolRows(config, live) {
|
|
121
|
+
const runners = live.github_runners || [];
|
|
122
|
+
return (config.runner_pools || []).map((pool) => {
|
|
123
|
+
const memberNames = new Set(pool.member_names || []);
|
|
124
|
+
const distinctiveLabels = (pool.labels || []).filter((label) => !["self-hosted", "Linux", "X64"].includes(label));
|
|
125
|
+
const matchingRunners = runners.filter((runner) => {
|
|
126
|
+
if (memberNames.has(runner.name)) return true;
|
|
127
|
+
if (!distinctiveLabels.length) return false;
|
|
128
|
+
const runnerLabels = new Set(runner.labels || []);
|
|
129
|
+
return distinctiveLabels.every((label) => runnerLabels.has(label));
|
|
130
|
+
});
|
|
131
|
+
return { ...pool, matching_runners: matchingRunners };
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function buildRecommendations(config, live, target = process.cwd()) {
|
|
136
|
+
const recommendations = [];
|
|
137
|
+
const requestedLimit = Number((config.digitalocean || {}).requested_droplet_limit_min || 0);
|
|
138
|
+
const currentLimit = Number((live.account || {}).droplet_limit || (config.digitalocean || {}).current_droplet_limit || 0);
|
|
139
|
+
const liveDropletCount = (live.droplets || []).length;
|
|
140
|
+
const dropletLimitBlocksNewPool = Boolean(currentLimit && liveDropletCount >= currentLimit);
|
|
141
|
+
if (requestedLimit && currentLimit && currentLimit < requestedLimit) {
|
|
142
|
+
recommendations.push({
|
|
143
|
+
priority: "p0",
|
|
144
|
+
action: "request_droplet_limit_increase",
|
|
145
|
+
reason: `current DO droplet limit ${currentLimit} is below target ${requestedLimit}`,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const plannedPools = (config.runner_pools || [])
|
|
150
|
+
.filter((pool) => pool.provider === "digitalocean-autoscale-pool" && pool.status !== "future")
|
|
151
|
+
.sort((a, b) => Number(a.activation_priority || 100) - Number(b.activation_priority || 100));
|
|
152
|
+
const hasLivePool = Boolean(String(live.autoscale_pools_raw || "").trim());
|
|
153
|
+
const hasBootstrap = fs.existsSync(path.join(target, "ops", "runner", "bootstrap-ephemeral.sh"));
|
|
154
|
+
const hasScaler = fs.existsSync(path.join(target, "scripts", "do_ephemeral_runner_scaler.py"));
|
|
155
|
+
const zeroIdleProbe = (config.live_verification || {}).autoscale_zero_idle_probe || {};
|
|
156
|
+
const nativeAutoscaleRejectsMinZero = zeroIdleProbe.dynamic_min_instances_zero === "rejected"
|
|
157
|
+
|| zeroIdleProbe.static_target_instances_zero === "rejected";
|
|
158
|
+
|
|
159
|
+
const legacy = config.legacy_runner_pools || [];
|
|
160
|
+
if (legacy.length) {
|
|
161
|
+
recommendations.push({
|
|
162
|
+
priority: hasLivePool ? "p1" : "p2",
|
|
163
|
+
action: hasLivePool ? "drain_legacy_app_host_runners_after_smoke" : "keep_legacy_app_host_runners_until_ephemeral_smoke",
|
|
164
|
+
reason: hasLivePool
|
|
165
|
+
? "0dai-app-fra1 should stop carrying routine CI after dedicated runner validation"
|
|
166
|
+
: "legacy runners are rollback capacity while no DO autoscale pool exists",
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
for (const pool of plannedPools.slice(0, 1)) {
|
|
170
|
+
if (!hasLivePool) {
|
|
171
|
+
if (!hasBootstrap) {
|
|
172
|
+
recommendations.push({
|
|
173
|
+
priority: "p1",
|
|
174
|
+
action: "build_ephemeral_runner_bootstrap_before_pool_create",
|
|
175
|
+
reason: `${pool.pool_name || pool.id} is planned but no bootstrap entrypoint exists`,
|
|
176
|
+
});
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (!hasScaler) {
|
|
180
|
+
recommendations.push({
|
|
181
|
+
priority: "p1",
|
|
182
|
+
action: "add_queue_aware_runner_scaler_before_pool_create",
|
|
183
|
+
reason: `${pool.pool_name || pool.id} is planned but queue-aware DO target planner is missing`,
|
|
184
|
+
});
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (dropletLimitBlocksNewPool) {
|
|
188
|
+
recommendations.push({
|
|
189
|
+
priority: "p1",
|
|
190
|
+
action: "wait_for_droplet_limit_before_pool_create",
|
|
191
|
+
reason: `${pool.pool_name || pool.id} cannot be created while ${liveDropletCount}/${currentLimit} droplet slots are used`,
|
|
192
|
+
});
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (nativeAutoscaleRejectsMinZero && Number(pool.min_instances || 0) === 0) {
|
|
196
|
+
recommendations.push({
|
|
197
|
+
priority: "p1",
|
|
198
|
+
action: "choose_runner_zero_idle_strategy_before_pool_create",
|
|
199
|
+
reason: `${pool.pool_name || pool.id} is configured for min=0, but live DO API rejected zero-idle autoscale; choose native min=1 or custom create/delete runners`,
|
|
200
|
+
});
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
recommendations.push({
|
|
204
|
+
priority: "p1",
|
|
205
|
+
action: "create_and_smoke_ephemeral_runner_pool",
|
|
206
|
+
reason: `${pool.pool_name || pool.id} is planned, bootstrap/scaler exist, but no DO autoscale pool exists`,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const futurePools = (config.runner_pools || [])
|
|
212
|
+
.filter((pool) => pool.provider === "digitalocean-autoscale-pool" && pool.status === "future");
|
|
213
|
+
if (futurePools.length) {
|
|
214
|
+
recommendations.push({
|
|
215
|
+
priority: "p2",
|
|
216
|
+
action: "defer_future_specialized_profiles_until_base_pool_smoke_passes",
|
|
217
|
+
reason: `${futurePools.length} future pool profile(s) require separate image/toolchain validation`,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const warmPools = configuredPoolRows(config, live).filter((pool) => pool.status === "active-transitional");
|
|
222
|
+
for (const pool of warmPools) {
|
|
223
|
+
const online = (pool.matching_runners || []).filter((runner) => runner.status === "online").length;
|
|
224
|
+
if (online === 0) {
|
|
225
|
+
recommendations.push({
|
|
226
|
+
priority: "p0",
|
|
227
|
+
action: "restore_warm_runner",
|
|
228
|
+
reason: `${pool.id} has no online runner`,
|
|
229
|
+
});
|
|
230
|
+
} else {
|
|
231
|
+
recommendations.push({
|
|
232
|
+
priority: "p2",
|
|
233
|
+
action: "keep_warm_runner_until_ephemeral_pool_passes_smoke",
|
|
234
|
+
reason: `${pool.id} is online and provides rollback capacity`,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const rank = { p0: 0, p1: 1, p2: 2 };
|
|
240
|
+
return recommendations.sort((a, b) => (rank[a.priority] ?? 99) - (rank[b.priority] ?? 99));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function printHuman(config, live, recommendations = [], command = "status") {
|
|
244
|
+
console.log(`\n ${T}0dai runner architecture${R}`);
|
|
245
|
+
console.log(` strategy: ${config.strategy || "unknown"}`);
|
|
246
|
+
console.log(` persistent CLI: ${(config.placement || {}).persistent_cli || "unknown"}`);
|
|
247
|
+
console.log(` disposable compute: ${(config.placement || {}).disposable_compute || "unknown"}`);
|
|
248
|
+
|
|
249
|
+
if (live.account) {
|
|
250
|
+
const desired = (config.digitalocean || {}).requested_droplet_limit_min || 0;
|
|
251
|
+
const limitColor = desired && live.account.droplet_limit < desired ? W : G;
|
|
252
|
+
console.log(`\n DO account: ${live.account.email || "unknown"} limit=${limitColor}${live.account.droplet_limit}${R} status=${live.account.status || "unknown"}`);
|
|
253
|
+
if (desired && live.account.droplet_limit < desired) {
|
|
254
|
+
console.log(` ${W}limit below target: request at least ${desired}${R}`);
|
|
255
|
+
}
|
|
256
|
+
} else if (live.account_error) {
|
|
257
|
+
console.log(`\n ${E}DO account unavailable:${R} ${live.account_error}`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
console.log("\n Project hosts");
|
|
261
|
+
for (const row of configuredHostRows(config, live)) {
|
|
262
|
+
const liveText = row.live ? `${row.live.size_slug} ${row.live.status}` : "not found";
|
|
263
|
+
console.log(` - ${row.name.padEnd(20)} role=${row.role || ""} live=${liveText} cli=${row.persistent_cli ? "persistent" : "none"} autoscale=${row.autoscale ? "yes" : "no"}`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
console.log("\n Runner pools");
|
|
267
|
+
for (const row of configuredPoolRows(config, live)) {
|
|
268
|
+
const count = row.matching_runners ? row.matching_runners.length : 0;
|
|
269
|
+
const online = (row.matching_runners || []).filter((r) => r.status === "online").length;
|
|
270
|
+
const busy = (row.matching_runners || []).filter((r) => r.busy).length;
|
|
271
|
+
console.log(` - ${row.id.padEnd(22)} ${row.status || ""} ${row.provider || ""} size=${row.size_slug || ""} runners=${online}/${count} online busy=${busy}`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (live.github_runners_error) {
|
|
275
|
+
console.log(`\n ${E}GitHub runners unavailable:${R} ${live.github_runners_error}`);
|
|
276
|
+
} else {
|
|
277
|
+
console.log("\n GitHub runners");
|
|
278
|
+
for (const runner of live.github_runners) {
|
|
279
|
+
const busy = runner.busy ? `${W}busy${R}` : "idle";
|
|
280
|
+
console.log(` - ${runner.name.padEnd(22)} ${runner.status.padEnd(8)} ${busy} ${D}${runner.labels.join(",")}${R}`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (live.autoscale_pools_raw) {
|
|
285
|
+
console.log(`\n DO autoscale pools:\n${live.autoscale_pools_raw}`);
|
|
286
|
+
} else if (live.autoscale_pools_error) {
|
|
287
|
+
console.log(`\n ${D}DO autoscale pools unavailable: ${live.autoscale_pools_error}${R}`);
|
|
288
|
+
} else {
|
|
289
|
+
console.log(`\n ${D}DO autoscale pools: none${R}`);
|
|
290
|
+
}
|
|
291
|
+
if (command === "plan") {
|
|
292
|
+
console.log("\n Recommended next actions");
|
|
293
|
+
for (const rec of recommendations) {
|
|
294
|
+
const color = rec.priority === "p0" ? E : rec.priority === "p1" ? W : D;
|
|
295
|
+
console.log(` - ${color}${rec.priority}${R} ${rec.action}: ${rec.reason}`);
|
|
296
|
+
}
|
|
297
|
+
if (!recommendations.length) {
|
|
298
|
+
console.log(` ${G}no immediate runner architecture actions${R}`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
console.log("");
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function runQueueController(target, configPath, forwarded) {
|
|
305
|
+
const script = findRepoScript(target, "runner_queue_controller.py");
|
|
306
|
+
if (!script) {
|
|
307
|
+
log("runner queue controller not found");
|
|
308
|
+
console.log(` ${D}expected: scripts/runner_queue_controller.py${R}`);
|
|
309
|
+
process.exitCode = 1;
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
const result = spawnSync("python3", [script, "queue-status", "--config", configPath, ...forwarded], {
|
|
313
|
+
encoding: "utf8",
|
|
314
|
+
timeout: 30000,
|
|
315
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
316
|
+
});
|
|
317
|
+
if (result.error) {
|
|
318
|
+
log(`runner queue controller failed: ${result.error.message}`);
|
|
319
|
+
process.exitCode = 1;
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
323
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
324
|
+
if (typeof result.status === "number" && result.status !== 0) {
|
|
325
|
+
process.exitCode = result.status;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function runBurstPlanner(target, configPath, forwarded) {
|
|
330
|
+
const script = findRepoScript(target, "runner_burst_planner.py");
|
|
331
|
+
if (!script) {
|
|
332
|
+
log("runner burst planner not found");
|
|
333
|
+
console.log(` ${D}expected: scripts/runner_burst_planner.py${R}`);
|
|
334
|
+
process.exitCode = 1;
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
const result = spawnSync("python3", [script, "burst-plan", "--config", configPath, ...forwarded], {
|
|
338
|
+
encoding: "utf8",
|
|
339
|
+
timeout: 30000,
|
|
340
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
341
|
+
});
|
|
342
|
+
if (result.error) {
|
|
343
|
+
log(`runner burst planner failed: ${result.error.message}`);
|
|
344
|
+
process.exitCode = 1;
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
348
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
349
|
+
if (typeof result.status === "number" && result.status !== 0) {
|
|
350
|
+
process.exitCode = result.status;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function runRouteDryRunController(target, configPath, forwarded) {
|
|
355
|
+
const script = findRepoScript(target, "runner_route_dry_run.py");
|
|
356
|
+
if (!script) {
|
|
357
|
+
log("runner route dry-run controller not found");
|
|
358
|
+
console.log(` ${D}expected: scripts/runner_route_dry_run.py${R}`);
|
|
359
|
+
process.exitCode = 1;
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
const result = spawnSync("python3", [script, "route-dry-run", "--config", configPath, ...forwarded], {
|
|
363
|
+
encoding: "utf8",
|
|
364
|
+
timeout: 30000,
|
|
365
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
366
|
+
});
|
|
367
|
+
if (result.error) {
|
|
368
|
+
log(`runner route dry-run controller failed: ${result.error.message}`);
|
|
369
|
+
process.exitCode = 1;
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
373
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
374
|
+
if (typeof result.status === "number" && result.status !== 0) {
|
|
375
|
+
process.exitCode = result.status;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function runLabelAuditController(target, configPath, forwarded) {
|
|
380
|
+
const script = findRepoScript(target, "runner_label_audit.py");
|
|
381
|
+
if (!script) {
|
|
382
|
+
log("runner label audit controller not found");
|
|
383
|
+
console.log(` ${D}expected: scripts/runner_label_audit.py${R}`);
|
|
384
|
+
process.exitCode = 1;
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
const result = spawnSync("python3", [script, "label-audit", "--config", configPath, ...forwarded], {
|
|
388
|
+
encoding: "utf8",
|
|
389
|
+
timeout: 30000,
|
|
390
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
391
|
+
});
|
|
392
|
+
if (result.error) {
|
|
393
|
+
log(`runner label audit controller failed: ${result.error.message}`);
|
|
394
|
+
process.exitCode = 1;
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
398
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
399
|
+
if (typeof result.status === "number" && result.status !== 0) {
|
|
400
|
+
process.exitCode = result.status;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function runJitApplyController(target, configPath, jitCommand, forwarded) {
|
|
405
|
+
const script = findRepoScript(target, "runner_jit_apply_controller.py");
|
|
406
|
+
if (!script) {
|
|
407
|
+
log("runner JIT apply controller not found");
|
|
408
|
+
console.log(` ${D}expected: scripts/runner_jit_apply_controller.py${R}`);
|
|
409
|
+
process.exitCode = 1;
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
const result = spawnSync("python3", [script, jitCommand, "--config", configPath, ...forwarded], {
|
|
413
|
+
encoding: "utf8",
|
|
414
|
+
timeout: 30000,
|
|
415
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
416
|
+
});
|
|
417
|
+
if (result.error) {
|
|
418
|
+
log(`runner JIT apply controller failed: ${result.error.message}`);
|
|
419
|
+
process.exitCode = 1;
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
423
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
424
|
+
if (typeof result.status === "number" && result.status !== 0) {
|
|
425
|
+
process.exitCode = result.status;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function resolveRunnerManifestPath(target) {
|
|
430
|
+
const candidates = [
|
|
431
|
+
path.join(target, "ai", "meta", "manifest", "runner-pools.json"),
|
|
432
|
+
path.join(target, "ai", "manifest", "runner-pools.json"),
|
|
433
|
+
];
|
|
434
|
+
return candidates.find((candidate) => fs.existsSync(candidate)) || candidates[0];
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function cmdRunner(target, sub, args) {
|
|
438
|
+
const command = sub && !sub.startsWith("-") ? sub : "status";
|
|
439
|
+
const forwarded = command === sub ? args.slice(2) : args.slice(1);
|
|
440
|
+
const asJson = forwarded.includes("--json") || args.includes("--json");
|
|
441
|
+
const offline = forwarded.includes("--offline") || args.includes("--offline");
|
|
442
|
+
const configPath = resolveRunnerManifestPath(target);
|
|
443
|
+
if (!fs.existsSync(configPath)) {
|
|
444
|
+
log("runner-pools manifest not found");
|
|
445
|
+
console.log(` ${D}expected: ai/meta/manifest/runner-pools.json${R}`);
|
|
446
|
+
process.exitCode = 1;
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
const config = readJson(configPath);
|
|
450
|
+
if (config.error) {
|
|
451
|
+
log(`invalid runner-pools manifest: ${config.error}`);
|
|
452
|
+
process.exitCode = 1;
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (command === "queue-status") {
|
|
457
|
+
runQueueController(target, configPath, forwarded);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (command === "burst-plan") {
|
|
462
|
+
runBurstPlanner(target, configPath, forwarded);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (command === "route-dry-run" || command === "burst-route") {
|
|
467
|
+
runRouteDryRunController(target, configPath, forwarded);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (command === "label-audit") {
|
|
472
|
+
runLabelAuditController(target, configPath, forwarded);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (command === "burst-apply" || command === "burst-preflight" || command === "burst-reap" || command === "burst-reap-plan") {
|
|
477
|
+
const jitCommand = {
|
|
478
|
+
"burst-apply": "burst-apply",
|
|
479
|
+
"burst-preflight": "preflight",
|
|
480
|
+
"burst-reap": "reap",
|
|
481
|
+
"burst-reap-plan": "reap-plan",
|
|
482
|
+
}[command];
|
|
483
|
+
runJitApplyController(target, configPath, jitCommand, forwarded);
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (command !== "status" && command !== "plan") {
|
|
488
|
+
console.log("Usage: 0dai runner [status|plan|queue-status|label-audit|burst-plan|route-dry-run|burst-apply|burst-preflight|burst-reap|burst-reap-plan] [--json] [--offline]");
|
|
489
|
+
process.exitCode = 1;
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const live = offline ? {
|
|
494
|
+
account: null,
|
|
495
|
+
account_error: "offline mode",
|
|
496
|
+
droplets: [],
|
|
497
|
+
droplets_error: "offline mode",
|
|
498
|
+
autoscale_pools_raw: "",
|
|
499
|
+
autoscale_pools_error: "offline mode",
|
|
500
|
+
github_runners: [],
|
|
501
|
+
github_runners_error: "offline mode",
|
|
502
|
+
} : liveSnapshot(config);
|
|
503
|
+
|
|
504
|
+
const recommendations = buildRecommendations(config, live, target);
|
|
505
|
+
const payload = {
|
|
506
|
+
schema_version: 1,
|
|
507
|
+
command,
|
|
508
|
+
config_path: configPath,
|
|
509
|
+
strategy: config.strategy || "",
|
|
510
|
+
placement: config.placement || {},
|
|
511
|
+
digitalocean: config.digitalocean || {},
|
|
512
|
+
project_hosts: configuredHostRows(config, live),
|
|
513
|
+
runner_pools: configuredPoolRows(config, live),
|
|
514
|
+
legacy_runner_pools: config.legacy_runner_pools || [],
|
|
515
|
+
rules: config.rules || [],
|
|
516
|
+
recommendations,
|
|
517
|
+
live,
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
if (asJson) {
|
|
521
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
printHuman(config, live, recommendations, command);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
module.exports = { cmdRunner };
|
package/lib/commands/session.js
CHANGED
|
@@ -1,18 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
const shared = require("../shared");
|
|
3
|
-
const { log, fs, path
|
|
3
|
+
const { log, fs, path } = shared;
|
|
4
4
|
|
|
5
5
|
function cmdSession(target, sub, args) {
|
|
6
6
|
const sessFile = path.join(target, "ai", "sessions", "active.json");
|
|
7
7
|
const sessDir = path.dirname(sessFile);
|
|
8
8
|
|
|
9
9
|
if (sub === "save") {
|
|
10
|
-
const gate = requirePlan("pro", "Session Roaming", target);
|
|
11
|
-
if (gate) {
|
|
12
|
-
log(gate.error);
|
|
13
|
-
log(gate.hint);
|
|
14
|
-
return;
|
|
15
|
-
}
|
|
16
10
|
fs.mkdirSync(sessDir, { recursive: true });
|
|
17
11
|
const goal = args.find((_, i) => args[i - 1] === "--goal") || "";
|
|
18
12
|
const summary = args.find((_, i) => args[i - 1] === "--summary") || "";
|