@computesdk/cli 1.0.9 → 1.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -587,7 +587,7 @@ import "dotenv/config";
587
587
  import { Command } from "commander";
588
588
  import * as p4 from "@clack/prompts";
589
589
  import pc6 from "picocolors";
590
- import { readFileSync as readFileSync4 } from "fs";
590
+ import { readFileSync as readFileSync5 } from "fs";
591
591
  import { fileURLToPath as fileURLToPath2 } from "url";
592
592
  import { dirname as dirname2, join as join5 } from "path";
593
593
  import { compute as compute4 } from "computesdk";
@@ -694,7 +694,7 @@ function buildProviderConfig(provider) {
694
694
  // src/actions.ts
695
695
  init_esm_shims();
696
696
  import pc from "picocolors";
697
- import { mkdirSync, writeFileSync } from "fs";
697
+ import { mkdirSync, readFileSync, writeFileSync } from "fs";
698
698
  import { basename, join } from "path";
699
699
 
700
700
  // src/actions-client.ts
@@ -739,6 +739,29 @@ var ActionsClient = class {
739
739
  });
740
740
  return this.parse(res);
741
741
  }
742
+ async put(path5, body = {}) {
743
+ const res = await this.fetchImpl(`${this.auth.baseUrl}${path5}`, {
744
+ method: "PUT",
745
+ headers: { ...this.headers(), "content-type": "application/json" },
746
+ body: JSON.stringify(body)
747
+ });
748
+ return this.parse(res);
749
+ }
750
+ async patch(path5, body = {}) {
751
+ const res = await this.fetchImpl(`${this.auth.baseUrl}${path5}`, {
752
+ method: "PATCH",
753
+ headers: { ...this.headers(), "content-type": "application/json" },
754
+ body: JSON.stringify(body)
755
+ });
756
+ return this.parse(res);
757
+ }
758
+ async del(path5) {
759
+ const res = await this.fetchImpl(`${this.auth.baseUrl}${path5}`, {
760
+ method: "DELETE",
761
+ headers: this.headers()
762
+ });
763
+ return this.parse(res);
764
+ }
742
765
  /**
743
766
  * One `data:` JSON payload per SSE event. `next()` resolves to the payload,
744
767
  * or null when the stream ends. Call `close()` to abort.
@@ -877,6 +900,18 @@ async function listRunJobs(c, runId) {
877
900
  steps: []
878
901
  }));
879
902
  }
903
+ async function fetchRunSummary(c, runId) {
904
+ try {
905
+ return await c.get(`/api/v1/actions/runs/${runId}/summary`);
906
+ } catch (e) {
907
+ if (!(e instanceof ActionsApiError && e.status === 404)) {
908
+ console.error(
909
+ pc.dim(`summary unavailable: ${e instanceof Error ? e.message : String(e)}`)
910
+ );
911
+ }
912
+ return null;
913
+ }
914
+ }
880
915
  function fail(error) {
881
916
  if (error instanceof ActionsApiError) {
882
917
  console.error(pc.red(`Error (${error.status}): ${error.message}`));
@@ -935,6 +970,7 @@ function formatRunDetail(run, runUrl) {
935
970
  lines.push(`title: ${run.title}`);
936
971
  lines.push(`started: ${run.startedAt} duration: ${formatDuration(run.durationMs)}`);
937
972
  if (run.cancellationReason) lines.push(`cancelled: ${run.cancellationReason}`);
973
+ if (run.providerOverride) lines.push(`dispatched to: ${run.providerOverride}`);
938
974
  if (run.supersededByRunId) lines.push(`superseded by: ${run.supersededByRunId}`);
939
975
  if (runUrl) lines.push(`url: ${runUrl}`);
940
976
  if (run.jobs.length > 0) {
@@ -954,6 +990,96 @@ function formatRunDetail(run, runUrl) {
954
990
  }
955
991
  return lines.join("\n");
956
992
  }
993
+ function formatRunInspection(run) {
994
+ const lines = [];
995
+ lines.push(`${pc.bold("run")} ${conclusionLabel(run.conclusion)}${run.runNumber === null ? "" : ` #${run.runNumber}`}`);
996
+ lines.push(pc.dim(run.id));
997
+ lines.push(`${safeTerm(run.ref)} ${shortSha(run.headSha)} ${safeTerm(run.event)}`);
998
+ lines.push(`queued: ${run.queuedAt} started: ${run.startedAt} finished: ${run.finishedAt}`);
999
+ if (run.cancellationReason) lines.push(`cancelled: ${run.cancellationReason}`);
1000
+ if (run.supersededByRunId) lines.push(`superseded by: ${run.supersededByRunId}`);
1001
+ if (run.blockedReason) lines.push(pc.red(`blocked: ${safeTerm(run.blockedReason)}`));
1002
+ if (run.concurrencyGroup) lines.push(`concurrency: ${safeTerm(run.concurrencyGroup)}`);
1003
+ if (run.providerOverride) lines.push(`dispatched to: ${safeTerm(run.providerOverride)}`);
1004
+ if (run.definitionStale) {
1005
+ const parsed = run.workflowParsedFromSha ? shortSha(run.workflowParsedFromSha) : "unknown";
1006
+ lines.push(pc.yellow(`definition: stored parse is from ${parsed}, not this head \u2014 context may be newer than the run`));
1007
+ }
1008
+ if (run.dispatchInputs && Object.keys(run.dispatchInputs).length > 0) {
1009
+ lines.push(`inputs: ${Object.entries(run.dispatchInputs).map(([k, v]) => `${safeTerm(k)}=${safeTerm(v)}`).join(" ")}`);
1010
+ }
1011
+ if (run.secrets) {
1012
+ const names = run.secrets.names.length > 0 ? run.secrets.names.map(safeTerm).join(", ") : "none";
1013
+ lines.push(`secrets (${run.secrets.access}${run.secrets.env ? ", exported to env" : ""}): ${names}`);
1014
+ }
1015
+ if (run.caches.length > 0) {
1016
+ lines.push("");
1017
+ lines.push(pc.bold("caches"));
1018
+ for (const cache of run.caches) {
1019
+ const action = [cache.restoredAt ? "restored" : null, cache.savedAt ? "saved" : null].filter(Boolean).join("+");
1020
+ lines.push(` ${safeTerm(cache.key)} ${pc.dim(`${action} ${safeTerm(cache.scopeRef)} ${cache.sizeBytes}B`)}`);
1021
+ }
1022
+ }
1023
+ if (run.jobs.length > 0) {
1024
+ lines.push("");
1025
+ lines.push(pc.bold("jobs"));
1026
+ for (const job of run.jobs) {
1027
+ const placement = job.provider ? `${safeTerm(job.provider)}${job.region ? `:${safeTerm(job.region)}` : ""}` : "-";
1028
+ lines.push(` ${safeTerm(job.name)} ${conclusionLabel(job.state)} ${placement}`);
1029
+ lines.push(pc.dim(` ${job.id}`));
1030
+ if (job.runsOn.length > 0 || job.resolvedRunsOn !== null) {
1031
+ const resolved = job.resolvedRunsOn !== null && job.resolvedRunsOn.join(",") !== job.runsOn.join(",") ? ` \u2192 ${job.resolvedRunsOn.map(safeTerm).join(", ")}` : "";
1032
+ lines.push(` runs-on: ${job.runsOn.map(safeTerm).join(", ") || "(none)"}${resolved}`);
1033
+ }
1034
+ if (job.runsOnHasExpression) lines.push(pc.dim(" runs-on resolves at runtime (expression)"));
1035
+ if (job.container) lines.push(` container: ${safeTerm(job.container)}`);
1036
+ if (job.runnerImage) lines.push(` image: ${safeTerm(job.runnerImage)}`);
1037
+ if (job.placementHint) {
1038
+ lines.push(` pinned: ${safeTerm(job.placementHint.label)}`);
1039
+ }
1040
+ if (job.concurrencyGroup) {
1041
+ lines.push(` concurrency: ${safeTerm(job.concurrencyGroup)}${job.concurrencyCancelInProgress ? " (cancel-in-progress)" : ""}`);
1042
+ }
1043
+ const overrides = [
1044
+ job.timeoutMinutes !== null ? `timeout ${job.timeoutMinutes}m` : null,
1045
+ job.fetchDepth !== null ? `fetch-depth ${job.fetchDepth}` : null
1046
+ ].filter(Boolean);
1047
+ if (overrides.length > 0) lines.push(` ${overrides.join(" ")}`);
1048
+ if (job.matrix) {
1049
+ lines.push(` matrix: ${Object.entries(job.matrix).map(([k, v]) => `${safeTerm(k)}=${safeTerm(v)}`).join(" ")}`);
1050
+ }
1051
+ if (job.failureReason) lines.push(pc.red(` failed: ${safeTerm(job.failureReason)}`));
1052
+ for (const attempt of job.placementAttempts) {
1053
+ lines.push(pc.dim(` placement failed on ${safeTerm(attempt.provider)}${attempt.region ? `:${safeTerm(attempt.region)}` : ""}: ${safeTerm(attempt.error)}`));
1054
+ }
1055
+ }
1056
+ }
1057
+ return lines.join("\n");
1058
+ }
1059
+ function formatRunSummary(summary) {
1060
+ const lines = [];
1061
+ lines.push(pc.bold(`failures \u2014 run ${summary.runId} ${conclusionLabel(summary.conclusion)}`));
1062
+ if (summary.failures.length === 0) {
1063
+ lines.push(" no failed jobs");
1064
+ return lines.join("\n");
1065
+ }
1066
+ for (const job of summary.failures) {
1067
+ const placement = job.provider ? `${safeTerm(job.provider)}${job.region ? `:${safeTerm(job.region)}` : ""}` : "-";
1068
+ lines.push(` ${job.id} ${safeTerm(job.name)} ${conclusionLabel(job.conclusion)} ${placement}`);
1069
+ if (job.failureReason) lines.push(pc.dim(` ${safeTerm(job.failureReason)}`));
1070
+ for (const step of job.failedSteps) {
1071
+ const exit = step.exitCode === null ? "" : ` (exit ${step.exitCode})`;
1072
+ lines.push(` step ${step.ordinal} ${safeTerm(step.name)}${exit}`);
1073
+ }
1074
+ if (job.excerpt) {
1075
+ const scope = job.excerpt.stepOrdinal === null ? "job log" : `step ${job.excerpt.stepOrdinal}`;
1076
+ const cut = job.excerpt.truncated ? ", truncated" : "";
1077
+ lines.push(pc.dim(` \u2500\u2500 ${scope} tail${cut} \u2500\u2500`));
1078
+ for (const line of job.excerpt.text.split("\n")) lines.push(` ${safeTerm(line)}`);
1079
+ }
1080
+ }
1081
+ return lines.join("\n");
1082
+ }
957
1083
  function parseInputs(pairs) {
958
1084
  const inputs = {};
959
1085
  for (const pair of pairs ?? []) {
@@ -1041,11 +1167,77 @@ async function* followRunLogs(client2, runId, jobs, step) {
1041
1167
  await new Promise((r) => setTimeout(r, 1e3));
1042
1168
  }
1043
1169
  }
1170
+ function safeTerm(s) {
1171
+ return s.replace(/[\u0000-\u001F\u007F-\u009F]/g, "\uFFFD");
1172
+ }
1173
+ function formatRunHistory(history) {
1174
+ const lines = [];
1175
+ const parts = Object.entries(history.conclusions).map(([c, n]) => `${n} ${c}`).join(", ");
1176
+ lines.push(`last ${history.runCount} runs: ${parts || "none"}`);
1177
+ if (history.jobs.length > 0) {
1178
+ lines.push("");
1179
+ lines.push(pc.bold("jobs"));
1180
+ for (const job of history.jobs) {
1181
+ const rate = job.runs > 0 ? Math.round(job.failedRuns / job.runs * 100) : 0;
1182
+ const platform = job.failedBeforeSteps > 0 ? pc.dim(` (${job.failedBeforeSteps} failed before steps)`) : "";
1183
+ const count = `${job.failedRuns}/${job.runs} runs failed`;
1184
+ const label = job.failedRuns > 0 ? pc.red(count) : pc.green(count);
1185
+ lines.push(` ${safeTerm(job.job)} ${label} ${rate}%${platform}`);
1186
+ for (const step of job.steps) {
1187
+ lines.push(pc.dim(` step "${safeTerm(step.step)}" failed in ${step.failedRuns} run${step.failedRuns === 1 ? "" : "s"}`));
1188
+ }
1189
+ }
1190
+ }
1191
+ const failed = history.runs.filter(
1192
+ (r) => r.conclusion === "failed" || r.failedJobs.length > 0
1193
+ );
1194
+ if (failed.length > 0) {
1195
+ lines.push("");
1196
+ lines.push(pc.bold("failed runs"));
1197
+ for (const run of failed) {
1198
+ const blame = run.failedJobs.length > 0 ? run.failedJobs.map((j) => safeTerm(j.step === null ? j.job : `${j.job}:${j.step}`)).join(", ") : pc.dim("(no failed job recorded)");
1199
+ lines.push(` ${shortSha(run.headSha)} ${pc.dim(run.startedAt)} ${blame}`);
1200
+ }
1201
+ }
1202
+ return lines.join("\n");
1203
+ }
1204
+ function formatProviderRow(p5) {
1205
+ const icon = p5.usable ? pc.green("\u25CF") : pc.gray("\u25CB");
1206
+ const name = p5.usable ? pc.white(p5.provider) : pc.gray(p5.provider);
1207
+ const flags = [
1208
+ p5.credential,
1209
+ p5.actCapable ? pc.green("act") : pc.gray("no-act"),
1210
+ p5.regions.length > 0 ? `regions: ${p5.regions.join(",")}` : "no region choice",
1211
+ p5.position === null ? pc.gray("not in provider order") : `order #${p5.position}`
1212
+ ].join(" ");
1213
+ return ` ${icon} ${name} ${flags}`;
1214
+ }
1215
+ function formatVerifyResult(r) {
1216
+ const ok = r.verified === true;
1217
+ const head = ok ? pc.green("verified") : pc.red("not verified");
1218
+ const detail = r.key.statusDetail ? pc.dim(` ${r.key.statusDetail}`) : "";
1219
+ const act = r.key.actCapable ? pc.green(" act-capable") : "";
1220
+ return `${head} ${r.key.provider}${act}${detail}`;
1221
+ }
1222
+ function formatRepoRow(r) {
1223
+ const head = r.enabled ? pc.green("enabled") : pc.dim("disabled");
1224
+ const via = r.authType === "github_app" ? "github-app" : r.authType;
1225
+ const workflows = r.workflowPaths.length > 0 ? `${r.workflowPaths.length} workflow${r.workflowPaths.length === 1 ? "" : "s"}` : "no workflows yet";
1226
+ const error = r.lastPollError ? pc.red(` poll error: ${safeTerm(r.lastPollError)}`) : "";
1227
+ return `${head} ${safeTerm(r.fullName)} ${pc.dim(`${safeTerm(via)} \xB7 ${safeTerm(r.defaultBranch)} \xB7 ${workflows}`)}${error}`;
1228
+ }
1229
+ function printDiscovery(d) {
1230
+ if (d.error) {
1231
+ console.log(pc.yellow(`discovery: ${safeTerm(d.error)} (the poll cron retries)`));
1232
+ return;
1233
+ }
1234
+ console.log(pc.dim(`discovery: ${d.workflowsFound ? "workflows found" : "no workflows yet"}${d.seeded ? ", cursors seeded" : ""}`));
1235
+ }
1044
1236
  function registerActionsCommands(program2) {
1045
1237
  const actions = program2.command("actions").alias("ci").description("Drive the benchmarks-platform Actions API");
1046
1238
  const common = (cmd) => cmd.option("--api-key <key>", "API key (default: $COMPUTE_API_KEY)").option("--base-url <url>", "API base URL (default: https://platform.computesdk.com)").option("--allow-untrusted-host", "send the API key to a non-computesdk, non-localhost --base-url").option("--json", "print machine-readable JSON");
1047
1239
  common(
1048
- actions.command("dispatch").description("Dispatch a workflow_dispatch run").argument("<repo>", "repository in owner/repo format").requiredOption("--workflow <path|name>", "workflow path or name").option("--ref <ref>", "git ref to run (default: the workflow's first watched ref)").option("--inputs <pairs...>", "workflow inputs as key=value")
1240
+ actions.command("dispatch").description("Dispatch a workflow_dispatch run").argument("<repo>", "repository in owner/repo format").requiredOption("--workflow <path|name>", "workflow path or name").option("--ref <ref>", "git ref to run (default: the workflow's first watched ref)").option("--inputs <pairs...>", "workflow inputs as key=value").option("--provider <id>", "place the run on one provider (e.g. namespace, vercel:sfo1) instead of the org provider order").option("--provider-region <region>", "region for --provider (same as --provider <id>:<region>)")
1049
1241
  ).action(async (repo, opts) => {
1050
1242
  try {
1051
1243
  const c = client(opts);
@@ -1062,9 +1254,20 @@ function registerActionsCommands(program2) {
1062
1254
  }
1063
1255
  const ref = opts.ref ?? workflow.refs[0];
1064
1256
  if (!ref) throw new Error("No --ref given and the workflow has no watched refs.");
1257
+ if (opts.providerRegion !== void 0 && opts.provider === void 0) {
1258
+ throw new Error("--provider-region requires --provider.");
1259
+ }
1065
1260
  const result = await c.post(
1066
1261
  "/api/v1/actions/dispatch",
1067
- { workflowId: workflow.id, ref, inputs: parseInputs(opts.inputs) }
1262
+ {
1263
+ workflowId: workflow.id,
1264
+ ref,
1265
+ inputs: parseInputs(opts.inputs),
1266
+ ...opts.provider !== void 0 && {
1267
+ provider: opts.provider,
1268
+ ...opts.providerRegion !== void 0 && { providerRegion: opts.providerRegion }
1269
+ }
1270
+ }
1068
1271
  );
1069
1272
  const org = await c.org();
1070
1273
  const url = `${c.baseUrl}/${org.slug}/actions/runs/${result.runId}`;
@@ -1096,6 +1299,29 @@ function registerActionsCommands(program2) {
1096
1299
  fail(e);
1097
1300
  }
1098
1301
  });
1302
+ common(
1303
+ actions.command("history").description("Recent run history for a workflow \u2014 per-job/step failure rates (is this failure flaky?)").argument("<repo>", "repository in owner/repo format").requiredOption("--workflow <path|name>", "workflow path or name").option("--branch <branch...>", "narrow the window to branches/refs").option("--job <job>", "narrow the rollup to one job name or workflow job id").option("--limit <n>", "runs in the window (default 10, max 50)")
1304
+ ).action(async (repo, opts) => {
1305
+ try {
1306
+ const c = client(opts);
1307
+ const { workflows } = await c.get(
1308
+ "/api/v1/actions/workflows",
1309
+ { repo }
1310
+ );
1311
+ const workflow = matchWorkflow(workflows, opts.workflow) ?? workflows.find((w) => w.path === opts.workflow);
1312
+ const workflowPath = workflow?.path ?? opts.workflow;
1313
+ const history = await c.get("/api/v1/actions/history", {
1314
+ repo,
1315
+ workflow: workflowPath,
1316
+ branch: opts.branch,
1317
+ job: opts.job,
1318
+ limit: opts.limit
1319
+ });
1320
+ output(opts, history, (h) => console.log(formatRunHistory(h)));
1321
+ } catch (e) {
1322
+ fail(e);
1323
+ }
1324
+ });
1099
1325
  common(
1100
1326
  actions.command("run").description("Show a run: jobs, provider:region placement, placement attempts").argument("<run-id>", "run ID")
1101
1327
  ).action(async (runId, opts) => {
@@ -1104,7 +1330,37 @@ function registerActionsCommands(program2) {
1104
1330
  const run = await c.get(`/api/v1/actions/runs/${runId}`);
1105
1331
  const org = await c.org();
1106
1332
  const url = `${c.baseUrl}/${org.slug}/actions/runs/${run.id}`;
1107
- output(opts, { ...run, url }, (r) => console.log(formatRunDetail(r, r.url)));
1333
+ const summary = run.conclusion === "failed" ? await fetchRunSummary(c, runId) : null;
1334
+ output(opts, { ...run, url, summary }, (r) => {
1335
+ console.log(formatRunDetail(r, r.url));
1336
+ if (summary && summary.failures.length > 0) {
1337
+ console.log("");
1338
+ console.log(formatRunSummary(summary));
1339
+ }
1340
+ });
1341
+ } catch (e) {
1342
+ fail(e);
1343
+ }
1344
+ });
1345
+ common(
1346
+ actions.command("summary").description("Show a run's failure digest: failed jobs, failed steps, bounded log tails").argument("<run-id>", "run ID")
1347
+ ).action(async (runId, opts) => {
1348
+ try {
1349
+ const summary = await client(opts).get(`/api/v1/actions/runs/${runId}/summary`);
1350
+ output(opts, summary, (s) => console.log(formatRunSummary(s)));
1351
+ } catch (e) {
1352
+ fail(e);
1353
+ }
1354
+ });
1355
+ common(
1356
+ actions.command("inspect").description("Inspect a run: resolved runner image, caches, secret names, concurrency, placement context").argument("<run-id>", "run ID")
1357
+ ).action(async (runId, opts) => {
1358
+ try {
1359
+ const c = client(opts);
1360
+ const inspection = await c.get(
1361
+ `/api/v1/actions/runs/${runId}/state`
1362
+ );
1363
+ output(opts, inspection, (r) => console.log(formatRunInspection(r)));
1108
1364
  } catch (e) {
1109
1365
  fail(e);
1110
1366
  }
@@ -1167,6 +1423,157 @@ function registerActionsCommands(program2) {
1167
1423
  fail(e);
1168
1424
  }
1169
1425
  });
1426
+ const providers = actions.command("providers").description("List the org's registered compute providers (regions, act/usable status)");
1427
+ common(providers).action(async (opts) => {
1428
+ try {
1429
+ const data = await client(opts).get("/api/v1/actions/providers");
1430
+ output(opts, data, (d) => {
1431
+ for (const p5 of d.providers) console.log(formatProviderRow(p5));
1432
+ });
1433
+ } catch (e) {
1434
+ fail(e);
1435
+ }
1436
+ });
1437
+ common(
1438
+ providers.command("configure").description("Save the org's credential for a provider (owner/admin API key)").argument("<provider>", "provider id (e.g. tensorlake, blaxel)").option("--key <key>", "provider API key (single-field credentials)").option("--field <pair...>", "credential fields as name=value (multi-field providers)").option("--verify", "run the placement probe after saving")
1439
+ ).action(async (provider, opts) => {
1440
+ try {
1441
+ const c = client(opts);
1442
+ let body;
1443
+ if (opts.field !== void 0) {
1444
+ if (opts.key !== void 0) throw new Error("Pass either --key or --field, not both.");
1445
+ body = { fields: parseInputs(opts.field) };
1446
+ } else if (opts.key !== void 0) {
1447
+ body = { key: opts.key };
1448
+ } else {
1449
+ throw new Error("Pass --key <key> or --field name=value.");
1450
+ }
1451
+ const saved = await c.put(
1452
+ `/api/v1/actions/providers/${provider}/key`,
1453
+ body
1454
+ );
1455
+ const verification = opts.verify ? await c.post(
1456
+ `/api/v1/actions/providers/${provider}/verify`
1457
+ ) : void 0;
1458
+ output(opts, verification ? { key: saved.key, verification } : saved, () => {
1459
+ console.log(`saved ${saved.key.provider} ${pc.dim(saved.key.keyHint)} ${saved.key.status}`);
1460
+ if (verification) console.log(formatVerifyResult(verification));
1461
+ });
1462
+ if (verification?.verified === false) process.exitCode = 1;
1463
+ } catch (e) {
1464
+ fail(e);
1465
+ }
1466
+ });
1467
+ common(
1468
+ providers.command("verify").description('Probe the stored provider key with a real sandbox ("Test" in Settings)').argument("<provider>", "provider id")
1469
+ ).action(async (provider, opts) => {
1470
+ try {
1471
+ const result = await client(opts).post(
1472
+ `/api/v1/actions/providers/${provider}/verify`
1473
+ );
1474
+ output(opts, result, (r) => console.log(formatVerifyResult(r)));
1475
+ if (result.verified === false) process.exitCode = 1;
1476
+ } catch (e) {
1477
+ fail(e);
1478
+ }
1479
+ });
1480
+ common(
1481
+ providers.command("remove").description("Delete the org's stored credential for a provider").argument("<provider>", "provider id")
1482
+ ).action(async (provider, opts) => {
1483
+ try {
1484
+ const result = await client(opts).del(
1485
+ `/api/v1/actions/providers/${provider}/key`
1486
+ );
1487
+ output(opts, result, (r) => {
1488
+ console.log(r.deleted ? `deleted ${r.provider}` : `no key stored for ${r.provider}`);
1489
+ });
1490
+ } catch (e) {
1491
+ fail(e);
1492
+ }
1493
+ });
1494
+ const repos = actions.command("repos").description("List the org's connected repositories");
1495
+ common(repos).action(async (opts) => {
1496
+ try {
1497
+ const data = await client(opts).get("/api/v1/actions/repos");
1498
+ output(opts, data, (d) => {
1499
+ for (const r of d.repos) console.log(formatRepoRow(r));
1500
+ });
1501
+ } catch (e) {
1502
+ fail(e);
1503
+ }
1504
+ });
1505
+ common(
1506
+ repos.command("connect").description("Connect a git remote (validated by a real ls-remote; lands enabled)").argument("<cloneUrl>", "http(s) or ssh git remote URL").option("--name <owner/repo>", "repo name override (default: derived from cloneUrl)").option("--branch <branch>", "default branch (default: main, else the remote's first)").option("--token <token>", "credential for private http(s) remotes").option("--username <user>", 'username \u2014 with --token, makes auth "basic" instead of "token"').option("--ssh-key-file <path>", "OpenSSH/PEM private key file for ssh remotes").option("--known-hosts-file <path>", "known_hosts pin for ssh remotes")
1507
+ ).action(async (cloneUrl, opts) => {
1508
+ try {
1509
+ let body;
1510
+ if (opts.sshKeyFile !== void 0) {
1511
+ if (opts.token !== void 0 || opts.username !== void 0) {
1512
+ throw new Error("Pass either --ssh-key-file or --token/--username, not both.");
1513
+ }
1514
+ body = {
1515
+ authType: "ssh",
1516
+ credential: readFileSync(opts.sshKeyFile, "utf8"),
1517
+ ...opts.knownHostsFile !== void 0 && {
1518
+ knownHosts: readFileSync(opts.knownHostsFile, "utf8")
1519
+ }
1520
+ };
1521
+ } else if (opts.username !== void 0) {
1522
+ if (opts.token === void 0) throw new Error("--username requires --token.");
1523
+ body = { authType: "basic", username: opts.username, credential: opts.token };
1524
+ } else if (opts.token !== void 0) {
1525
+ body = { authType: "token", credential: opts.token };
1526
+ } else {
1527
+ body = { authType: "none" };
1528
+ }
1529
+ if (opts.knownHostsFile !== void 0 && opts.sshKeyFile === void 0) {
1530
+ throw new Error("--known-hosts-file requires --ssh-key-file.");
1531
+ }
1532
+ const result = await client(opts).post("/api/v1/actions/repos", {
1533
+ cloneUrl,
1534
+ ...body,
1535
+ ...opts.name !== void 0 && { name: opts.name },
1536
+ ...opts.branch !== void 0 && { defaultBranch: opts.branch }
1537
+ });
1538
+ output(opts, result, (r) => {
1539
+ console.log(`connected ${pc.cyan(safeTerm(r.fullName))} ${pc.dim(`(${safeTerm(r.authType)}, ${safeTerm(r.defaultBranch)})`)}`);
1540
+ printDiscovery(r.discovery);
1541
+ });
1542
+ } catch (e) {
1543
+ fail(e);
1544
+ }
1545
+ });
1546
+ common(
1547
+ repos.command("enable").description("Enable a repo the org can already see (granted by a GitHub App install, or a connected remote)").argument("<repo>", "repository in owner/repo format").option("--repo-id <id>", "scope to one row when two installations grant the same name")
1548
+ ).action(async (repo, opts) => {
1549
+ try {
1550
+ const result = await client(opts).patch("/api/v1/actions/repos", {
1551
+ fullName: repo,
1552
+ enabled: true,
1553
+ ...opts.repoId !== void 0 && { repoId: opts.repoId }
1554
+ });
1555
+ output(opts, result, (r) => {
1556
+ console.log(`enabled ${pc.cyan(safeTerm(r.fullName))}`);
1557
+ if (r.discovery) printDiscovery(r.discovery);
1558
+ });
1559
+ } catch (e) {
1560
+ fail(e);
1561
+ }
1562
+ });
1563
+ common(
1564
+ repos.command("disable").description("Disable a repo \u2014 it stays connected but no runs are scheduled").argument("<repo>", "repository in owner/repo format").option("--repo-id <id>", "scope to one row when two installations grant the same name")
1565
+ ).action(async (repo, opts) => {
1566
+ try {
1567
+ const result = await client(opts).patch("/api/v1/actions/repos", {
1568
+ fullName: repo,
1569
+ enabled: false,
1570
+ ...opts.repoId !== void 0 && { repoId: opts.repoId }
1571
+ });
1572
+ output(opts, result, (r) => console.log(`disabled ${pc.cyan(safeTerm(r.fullName))}`));
1573
+ } catch (e) {
1574
+ fail(e);
1575
+ }
1576
+ });
1170
1577
  common(
1171
1578
  actions.command("cancel").description("Cancel a run").argument("<run-id>", "run ID")
1172
1579
  ).action(async (runId, opts) => {
@@ -1500,7 +1907,7 @@ init_workspace_cli();
1500
1907
  var __filename2 = fileURLToPath2(import.meta.url);
1501
1908
  var __dirname2 = dirname2(__filename2);
1502
1909
  var packageJson = JSON.parse(
1503
- readFileSync4(join5(__dirname2, "..", "package.json"), "utf-8")
1910
+ readFileSync5(join5(__dirname2, "..", "package.json"), "utf-8")
1504
1911
  );
1505
1912
  var isDevBuild = import.meta.url.includes("worktrees") || import.meta.url.includes("/packages/cli/");
1506
1913
  var VERSION = isDevBuild ? `${packageJson.version}-dev` : packageJson.version;