@general-input/cli 0.1.2 → 0.2.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/dist/cli.js CHANGED
@@ -315,7 +315,7 @@ function buildClientLabel() {
315
315
  return `geni CLI on ${hostname()}`;
316
316
  }
317
317
  function sleep(ms) {
318
- return new Promise((resolve) => setTimeout(resolve, ms));
318
+ return new Promise((resolve20) => setTimeout(resolve20, ms));
319
319
  }
320
320
 
321
321
  // src/services/WorkspaceService.ts
@@ -384,7 +384,7 @@ import chalk4 from "chalk";
384
384
  // package.json
385
385
  var package_default = {
386
386
  name: "@general-input/cli",
387
- version: "0.1.2",
387
+ version: "0.2.0",
388
388
  type: "module",
389
389
  description: "The agent-facing CLI for General Input. Authenticate, manage workflows, run bash with operator credentials injected by the cloud.",
390
390
  license: "SEE LICENSE IN LICENSE",
@@ -416,8 +416,10 @@ var package_default = {
416
416
  build: "tsup",
417
417
  clean: "rm -rf dist",
418
418
  typecheck: "tsc --noEmit",
419
- lint: "eslint . --fix --max-warnings 0",
419
+ lint: "eslint . --max-warnings 0",
420
+ "lint:fix": "eslint . --fix --max-warnings 0",
420
421
  format: "prettier --write . --ignore-path=../../.prettierignore",
422
+ "format:check": "prettier --check . --ignore-path=../../.prettierignore",
421
423
  test: "vitest run",
422
424
  "test:watch": "vitest",
423
425
  prepublishOnly: "pnpm build"
@@ -824,8 +826,10 @@ var DiscoveryService = class {
824
826
  /**
825
827
  * Validate the service slug against the integration catalog (404
826
828
  * surfaces the same way `integration get` does, mapped to exit 4
827
- * by the command), then return the dashboard connect URL the
828
- * operator should be sent to.
829
+ * by the command), then return the dashboard integration-detail
830
+ * URL the operator should be sent to. That page already hosts the
831
+ * connect UI for every integration kind (OAuth start, API-key
832
+ * dialog, etc.), so we don't need a dedicated CLI-side route.
829
833
  *
830
834
  * Side effect: if `printUrlOnly` is false, opens the URL in the
831
835
  * operator's default browser. The CLI always prints the URL too,
@@ -835,7 +839,8 @@ var DiscoveryService = class {
835
839
  async connectCredential(args) {
836
840
  const { session, client } = await this.sessionContext.requireAuthed();
837
841
  await client.integrations.get(args.service);
838
- const url = `${this.configService.resolveDashboardUrl(session.server)}/credentials/connect?service=${encodeURIComponent(args.service)}`;
842
+ const dashboardUrl = this.configService.resolveDashboardUrl(session.server);
843
+ const url = `${dashboardUrl}/${encodeURIComponent(session.workspace.slug)}/integrations/${encodeURIComponent(args.service)}`;
839
844
  if (args.printUrlOnly) return { kind: "print-url", url };
840
845
  this.browserOpener.open(url);
841
846
  return { kind: "open-browser", url };
@@ -1054,6 +1059,270 @@ var ConfigService = class {
1054
1059
  }
1055
1060
  };
1056
1061
 
1062
+ // src/services/WorkflowAuthoringService.ts
1063
+ import { readFileSync as readFileSync2 } from "fs";
1064
+ import { basename } from "path";
1065
+
1066
+ // src/lib/resourceFiles.ts
1067
+ import {
1068
+ readdirSync,
1069
+ readFileSync,
1070
+ writeFileSync,
1071
+ mkdirSync,
1072
+ existsSync,
1073
+ statSync
1074
+ } from "fs";
1075
+ import { join, dirname, relative, sep } from "path";
1076
+ var RESOURCE_MARKER_FILE = ".geni-resource.json";
1077
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".turbo"]);
1078
+ function readMarker(dir) {
1079
+ const path = join(dir, RESOURCE_MARKER_FILE);
1080
+ if (!existsSync(path)) return null;
1081
+ try {
1082
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
1083
+ if (parsed && typeof parsed.resourceId === "string" && typeof parsed.resourceType === "string") {
1084
+ return {
1085
+ resourceId: parsed.resourceId,
1086
+ resourceType: parsed.resourceType
1087
+ };
1088
+ }
1089
+ return null;
1090
+ } catch {
1091
+ return null;
1092
+ }
1093
+ }
1094
+ function writeMarker(dir, marker) {
1095
+ mkdirSync(dir, { recursive: true });
1096
+ writeFileSync(
1097
+ join(dir, RESOURCE_MARKER_FILE),
1098
+ JSON.stringify(marker, null, 2) + "\n"
1099
+ );
1100
+ }
1101
+ function resolveResourceId(args) {
1102
+ if (args.id) return args.id;
1103
+ const marker = readMarker(args.dir);
1104
+ if (marker) return marker.resourceId;
1105
+ printError(
1106
+ `No resource id given and no ${RESOURCE_MARKER_FILE} found in ${args.dir}. Pass the id (\`geni resource <cmd> <id>\`) or run from a resource directory (one created by \`geni resource create\` / \`geni resource pull\`).`
1107
+ );
1108
+ exit(ExitCode.InvalidArgs);
1109
+ }
1110
+ function readLocalFiles(dir) {
1111
+ if (!existsSync(dir)) {
1112
+ printError(`Directory not found: ${dir}`);
1113
+ exit(ExitCode.InvalidArgs);
1114
+ }
1115
+ const files = [];
1116
+ walk(dir, dir, files);
1117
+ return files;
1118
+ }
1119
+ function walk(root, current, out) {
1120
+ for (const entry of readdirSync(current)) {
1121
+ if (entry.startsWith(".")) continue;
1122
+ const full = join(current, entry);
1123
+ const stat = statSync(full);
1124
+ if (stat.isDirectory()) {
1125
+ if (SKIP_DIRS.has(entry)) continue;
1126
+ walk(root, full, out);
1127
+ continue;
1128
+ }
1129
+ if (!stat.isFile()) continue;
1130
+ const rel = relative(root, full).split(sep).join("/");
1131
+ out.push(bufferToCliFile(rel, readFileSync(full)));
1132
+ }
1133
+ }
1134
+ function writeLocalFiles(dir, files) {
1135
+ for (const file of files) {
1136
+ const target = join(dir, file.path);
1137
+ mkdirSync(dirname(target), { recursive: true });
1138
+ writeFileSync(target, cliFileBytes(file));
1139
+ }
1140
+ }
1141
+ function bufferToCliFile(path, buf) {
1142
+ const asUtf8 = buf.toString("utf-8");
1143
+ if (Buffer.from(asUtf8, "utf-8").equals(buf)) {
1144
+ return { path, content: asUtf8, encoding: "utf8" };
1145
+ }
1146
+ return { path, content: buf.toString("base64"), encoding: "base64" };
1147
+ }
1148
+ function cliFileBytes(file) {
1149
+ return file.encoding === "base64" ? Buffer.from(file.content, "base64") : file.content;
1150
+ }
1151
+ var MIME_BY_EXT = {
1152
+ json: "application/json",
1153
+ csv: "text/csv",
1154
+ txt: "text/plain",
1155
+ md: "text/markdown",
1156
+ pdf: "application/pdf",
1157
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
1158
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
1159
+ pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
1160
+ png: "image/png",
1161
+ jpg: "image/jpeg",
1162
+ jpeg: "image/jpeg",
1163
+ gif: "image/gif",
1164
+ html: "text/html",
1165
+ xml: "application/xml",
1166
+ zip: "application/zip"
1167
+ };
1168
+ function guessMimeType(path) {
1169
+ const ext = path.split(".").pop()?.toLowerCase() ?? "";
1170
+ return MIME_BY_EXT[ext] ?? "application/octet-stream";
1171
+ }
1172
+ function dirHasResourceFiles(dir) {
1173
+ if (!existsSync(dir)) return false;
1174
+ return readdirSync(dir).some(
1175
+ (entry) => !entry.startsWith(".") && !SKIP_DIRS.has(entry)
1176
+ );
1177
+ }
1178
+
1179
+ // src/services/WorkflowAuthoringService.ts
1180
+ var WorkflowAuthoringService = class {
1181
+ constructor(sessionContext) {
1182
+ this.sessionContext = sessionContext;
1183
+ }
1184
+ sessionContext;
1185
+ /** Create a workflow in the cloud, then pull its scaffold files into `dir`. */
1186
+ async create(args) {
1187
+ const { client } = await this.sessionContext.requireAuthed();
1188
+ const detail = await client.workflows.create({
1189
+ workflowType: args.workflowType,
1190
+ name: args.name
1191
+ });
1192
+ const filesResponse = await client.workflows.files(detail.id);
1193
+ writeLocalFiles(args.dir, filesResponse.files);
1194
+ writeMarker(args.dir, {
1195
+ resourceId: detail.id,
1196
+ resourceType: detail.workflowType
1197
+ });
1198
+ return { detail, fileCount: filesResponse.files.length };
1199
+ }
1200
+ async list() {
1201
+ const { client } = await this.sessionContext.requireAuthed();
1202
+ return client.workflows.list();
1203
+ }
1204
+ async get(id) {
1205
+ const { client } = await this.sessionContext.requireAuthed();
1206
+ return client.workflows.get(id);
1207
+ }
1208
+ async setType(id, workflowType) {
1209
+ const { client } = await this.sessionContext.requireAuthed();
1210
+ return client.workflows.setType(id, { workflowType });
1211
+ }
1212
+ /** Download a workflow's live files into `dir` + write its marker. */
1213
+ async pull(args) {
1214
+ const { client } = await this.sessionContext.requireAuthed();
1215
+ const response = await client.workflows.files(args.id);
1216
+ writeLocalFiles(args.dir, response.files);
1217
+ writeMarker(args.dir, {
1218
+ resourceId: args.id,
1219
+ resourceType: response.workflowType
1220
+ });
1221
+ return {
1222
+ fileCount: response.files.length,
1223
+ workflowType: response.workflowType
1224
+ };
1225
+ }
1226
+ async validate(args) {
1227
+ const { client } = await this.sessionContext.requireAuthed();
1228
+ const files = readLocalFiles(args.dir);
1229
+ return client.workflows.validate(args.id, { files });
1230
+ }
1231
+ async publish(args) {
1232
+ const { client } = await this.sessionContext.requireAuthed();
1233
+ const files = readLocalFiles(args.dir);
1234
+ return client.workflows.publish(args.id, {
1235
+ files,
1236
+ changeSummary: args.changeSummary
1237
+ });
1238
+ }
1239
+ async getConfig(id) {
1240
+ const { client } = await this.sessionContext.requireAuthed();
1241
+ return client.workflows.getConfig(id);
1242
+ }
1243
+ async updateConfig(id, partial) {
1244
+ const { client } = await this.sessionContext.requireAuthed();
1245
+ return client.workflows.updateConfig(id, partial);
1246
+ }
1247
+ async test(id, triggerPayload) {
1248
+ const { client } = await this.sessionContext.requireAuthed();
1249
+ return client.workflows.test(id, { triggerPayload });
1250
+ }
1251
+ async getExecution(id, executionId) {
1252
+ const { client } = await this.sessionContext.requireAuthed();
1253
+ return client.workflows.getExecution(id, executionId);
1254
+ }
1255
+ async getExecutionLogs(id, executionId) {
1256
+ const { client } = await this.sessionContext.requireAuthed();
1257
+ return client.workflows.getExecutionLogs(id, executionId);
1258
+ }
1259
+ async getExecutionTrace(id, executionId) {
1260
+ const { client } = await this.sessionContext.requireAuthed();
1261
+ return client.workflows.getExecutionTrace(id, executionId);
1262
+ }
1263
+ async listExecutions(id, limit) {
1264
+ const { client } = await this.sessionContext.requireAuthed();
1265
+ return client.workflows.listExecutions(id, limit);
1266
+ }
1267
+ async triggerSample(id) {
1268
+ const { client } = await this.sessionContext.requireAuthed();
1269
+ return client.workflows.triggerSample(id);
1270
+ }
1271
+ /**
1272
+ * Upload a local file as a file-type test input: presign, PUT the
1273
+ * bytes, return the `store://` URI to pass in a test payload.
1274
+ */
1275
+ async stageInput(args) {
1276
+ const { client } = await this.sessionContext.requireAuthed();
1277
+ const bytes = readFileSync2(args.localPath);
1278
+ const filename = basename(args.localPath);
1279
+ const mimeType = guessMimeType(args.localPath);
1280
+ const { uploadUrl, storageUri } = await client.workflows.inputUploadUrl(
1281
+ args.id,
1282
+ { filename, mimeType }
1283
+ );
1284
+ const response = await fetch(uploadUrl, {
1285
+ method: "PUT",
1286
+ headers: { "Content-Type": mimeType },
1287
+ body: new Uint8Array(bytes)
1288
+ });
1289
+ if (!response.ok) {
1290
+ throw new Error(
1291
+ `Upload failed (HTTP ${response.status}) for ${filename}.`
1292
+ );
1293
+ }
1294
+ return { storageUri, filename };
1295
+ }
1296
+ async appBuildStatus(id) {
1297
+ const { client } = await this.sessionContext.requireAuthed();
1298
+ return client.workflows.appBuildStatus(id);
1299
+ }
1300
+ async appInvocations(id, opts) {
1301
+ const { client } = await this.sessionContext.requireAuthed();
1302
+ return client.workflows.appInvocations(id, opts);
1303
+ }
1304
+ async runHandler(id, body) {
1305
+ const { client } = await this.sessionContext.requireAuthed();
1306
+ return client.workflows.runHandler(id, body);
1307
+ }
1308
+ async appErrors(id, opts) {
1309
+ const { client } = await this.sessionContext.requireAuthed();
1310
+ return client.workflows.appErrors(id, opts);
1311
+ }
1312
+ async clearHandlerCache(id) {
1313
+ const { client } = await this.sessionContext.requireAuthed();
1314
+ return client.workflows.clearHandlerCache(id);
1315
+ }
1316
+ async spec(type) {
1317
+ const { client } = await this.sessionContext.requireAuthed();
1318
+ return client.workflows.spec(type);
1319
+ }
1320
+ async listTriggers(query) {
1321
+ const { client } = await this.sessionContext.requireAuthed();
1322
+ return client.workflows.listTriggers(query);
1323
+ }
1324
+ };
1325
+
1057
1326
  // src/clients/AuthApiClient.ts
1058
1327
  var AuthApiClient = class {
1059
1328
  constructor(http) {
@@ -1173,6 +1442,157 @@ var OperationsApiClient = class {
1173
1442
  }
1174
1443
  };
1175
1444
 
1445
+ // src/clients/WorkflowsApiClient.ts
1446
+ var WorkflowsApiClient = class {
1447
+ constructor(http) {
1448
+ this.http = http;
1449
+ }
1450
+ http;
1451
+ async list() {
1452
+ this.http.requireAuthed();
1453
+ return this.http.fetch("/cli/workflows");
1454
+ }
1455
+ async create(body) {
1456
+ this.http.requireAuthed();
1457
+ return this.http.fetch("/cli/workflows", { method: "POST", body });
1458
+ }
1459
+ async get(id) {
1460
+ this.http.requireAuthed();
1461
+ return this.http.fetch(`/cli/workflows/${encodeURIComponent(id)}`);
1462
+ }
1463
+ async setType(id, body) {
1464
+ this.http.requireAuthed();
1465
+ return this.http.fetch(`/cli/workflows/${encodeURIComponent(id)}/type`, {
1466
+ method: "PATCH",
1467
+ body
1468
+ });
1469
+ }
1470
+ async files(id) {
1471
+ this.http.requireAuthed();
1472
+ return this.http.fetch(`/cli/workflows/${encodeURIComponent(id)}/files`);
1473
+ }
1474
+ async validate(id, body) {
1475
+ this.http.requireAuthed();
1476
+ return this.http.fetch(
1477
+ `/cli/workflows/${encodeURIComponent(id)}/validate`,
1478
+ {
1479
+ method: "POST",
1480
+ body
1481
+ }
1482
+ );
1483
+ }
1484
+ async publish(id, body) {
1485
+ this.http.requireAuthed();
1486
+ return this.http.fetch(`/cli/workflows/${encodeURIComponent(id)}/publish`, {
1487
+ method: "POST",
1488
+ body
1489
+ });
1490
+ }
1491
+ async getConfig(id) {
1492
+ this.http.requireAuthed();
1493
+ return this.http.fetch(`/cli/workflows/${encodeURIComponent(id)}/config`);
1494
+ }
1495
+ async updateConfig(id, body) {
1496
+ this.http.requireAuthed();
1497
+ return this.http.fetch(`/cli/workflows/${encodeURIComponent(id)}/config`, {
1498
+ method: "PATCH",
1499
+ body
1500
+ });
1501
+ }
1502
+ async test(id, body) {
1503
+ this.http.requireAuthed();
1504
+ return this.http.fetch(`/cli/workflows/${encodeURIComponent(id)}/test`, {
1505
+ method: "POST",
1506
+ body
1507
+ });
1508
+ }
1509
+ async listExecutions(id, limit) {
1510
+ this.http.requireAuthed();
1511
+ const query = limit ? `?limit=${limit}` : "";
1512
+ return this.http.fetch(
1513
+ `/cli/workflows/${encodeURIComponent(id)}/executions${query}`
1514
+ );
1515
+ }
1516
+ async getExecution(id, executionId) {
1517
+ this.http.requireAuthed();
1518
+ return this.http.fetch(
1519
+ `/cli/workflows/${encodeURIComponent(id)}/executions/${encodeURIComponent(executionId)}`
1520
+ );
1521
+ }
1522
+ async getExecutionLogs(id, executionId) {
1523
+ this.http.requireAuthed();
1524
+ return this.http.fetch(
1525
+ `/cli/workflows/${encodeURIComponent(id)}/executions/${encodeURIComponent(executionId)}/logs`
1526
+ );
1527
+ }
1528
+ async getExecutionTrace(id, executionId) {
1529
+ this.http.requireAuthed();
1530
+ return this.http.fetch(
1531
+ `/cli/workflows/${encodeURIComponent(id)}/executions/${encodeURIComponent(executionId)}/trace`
1532
+ );
1533
+ }
1534
+ async triggerSample(id) {
1535
+ this.http.requireAuthed();
1536
+ return this.http.fetch(
1537
+ `/cli/workflows/${encodeURIComponent(id)}/trigger-sample`
1538
+ );
1539
+ }
1540
+ async inputUploadUrl(id, body) {
1541
+ this.http.requireAuthed();
1542
+ return this.http.fetch(
1543
+ `/cli/workflows/${encodeURIComponent(id)}/input-upload-url`,
1544
+ { method: "POST", body }
1545
+ );
1546
+ }
1547
+ async appBuildStatus(id) {
1548
+ this.http.requireAuthed();
1549
+ return this.http.fetch(
1550
+ `/cli/workflows/${encodeURIComponent(id)}/app/build-status`
1551
+ );
1552
+ }
1553
+ async appInvocations(id, opts) {
1554
+ this.http.requireAuthed();
1555
+ const params = new URLSearchParams();
1556
+ if (opts.handler) params.set("handler", opts.handler);
1557
+ if (opts.errors) params.set("errors", "true");
1558
+ if (opts.limit) params.set("limit", String(opts.limit));
1559
+ const query = params.toString();
1560
+ return this.http.fetch(
1561
+ `/cli/workflows/${encodeURIComponent(id)}/app/invocations${query ? `?${query}` : ""}`
1562
+ );
1563
+ }
1564
+ async runHandler(id, body) {
1565
+ this.http.requireAuthed();
1566
+ return this.http.fetch(
1567
+ `/cli/workflows/${encodeURIComponent(id)}/app/run-handler`,
1568
+ { method: "POST", body }
1569
+ );
1570
+ }
1571
+ async appErrors(id, opts) {
1572
+ this.http.requireAuthed();
1573
+ const query = opts.limit ? `?limit=${opts.limit}` : "";
1574
+ return this.http.fetch(
1575
+ `/cli/workflows/${encodeURIComponent(id)}/app/errors${query}`
1576
+ );
1577
+ }
1578
+ async clearHandlerCache(id) {
1579
+ this.http.requireAuthed();
1580
+ return this.http.fetch(
1581
+ `/cli/workflows/${encodeURIComponent(id)}/app/clear-cache`,
1582
+ { method: "POST" }
1583
+ );
1584
+ }
1585
+ async spec(type) {
1586
+ this.http.requireAuthed();
1587
+ return this.http.fetch(`/cli/workflows/spec/${encodeURIComponent(type)}`);
1588
+ }
1589
+ async listTriggers(query) {
1590
+ this.http.requireAuthed();
1591
+ const suffix = query ? `?q=${encodeURIComponent(query)}` : "";
1592
+ return this.http.fetch(`/cli/triggers${suffix}`);
1593
+ }
1594
+ };
1595
+
1176
1596
  // src/clients/ApiClientFactory.ts
1177
1597
  var ApiClientFactory = class {
1178
1598
  build(args) {
@@ -1183,7 +1603,8 @@ var ApiClientFactory = class {
1183
1603
  exec: new ExecApiClient(http),
1184
1604
  credentials: new CredentialsApiClient(http),
1185
1605
  integrations: new IntegrationsApiClient(http),
1186
- operations: new OperationsApiClient(http)
1606
+ operations: new OperationsApiClient(http),
1607
+ workflows: new WorkflowsApiClient(http)
1187
1608
  };
1188
1609
  }
1189
1610
  };
@@ -1297,9 +1718,9 @@ function isErrnoCode(err, expected) {
1297
1718
  }
1298
1719
 
1299
1720
  // src/clients/ConfigStore.ts
1300
- import { readFileSync } from "fs";
1721
+ import { readFileSync as readFileSync3 } from "fs";
1301
1722
  import { mkdir as mkdir2, writeFile as writeFile2, unlink as unlink2 } from "fs/promises";
1302
- import { dirname } from "path";
1723
+ import { dirname as dirname2 } from "path";
1303
1724
  var ConfigStore = class {
1304
1725
  constructor(filePath) {
1305
1726
  this.filePath = filePath;
@@ -1313,7 +1734,7 @@ var ConfigStore = class {
1313
1734
  loadSync() {
1314
1735
  let raw;
1315
1736
  try {
1316
- raw = readFileSync(this.filePath, "utf8");
1737
+ raw = readFileSync3(this.filePath, "utf8");
1317
1738
  } catch {
1318
1739
  return null;
1319
1740
  }
@@ -1331,7 +1752,7 @@ var ConfigStore = class {
1331
1752
  * config is non-secret, unlike the session file.
1332
1753
  */
1333
1754
  async save(config) {
1334
- await mkdir2(dirname(this.filePath), { recursive: true });
1755
+ await mkdir2(dirname2(this.filePath), { recursive: true });
1335
1756
  await writeFile2(this.filePath, JSON.stringify(config, null, 2) + "\n", {
1336
1757
  mode: 420
1337
1758
  });
@@ -1413,11 +1834,11 @@ var ChildProcessSpawner = class {
1413
1834
  process.on("SIGINT", forwardSignal);
1414
1835
  process.on("SIGTERM", forwardSignal);
1415
1836
  try {
1416
- const exitCode = await new Promise((resolve, reject) => {
1837
+ const exitCode = await new Promise((resolve20, reject) => {
1417
1838
  child.once("exit", (code, signal) => {
1418
- if (code !== null) resolve(code);
1419
- else if (signal !== null) resolve(128 + signalNumber(signal));
1420
- else resolve(1);
1839
+ if (code !== null) resolve20(code);
1840
+ else if (signal !== null) resolve20(128 + signalNumber(signal));
1841
+ else resolve20(1);
1421
1842
  });
1422
1843
  child.once("error", (err) => reject(err));
1423
1844
  });
@@ -1430,7 +1851,7 @@ var ChildProcessSpawner = class {
1430
1851
  }
1431
1852
  };
1432
1853
  function pipeWithScrubbing(source, dest, scrubber, onChunk) {
1433
- return new Promise((resolve) => {
1854
+ return new Promise((resolve20) => {
1434
1855
  let flushed = false;
1435
1856
  const emit = (chunk) => {
1436
1857
  if (chunk.length === 0) return;
@@ -1441,13 +1862,13 @@ function pipeWithScrubbing(source, dest, scrubber, onChunk) {
1441
1862
  if (flushed) return;
1442
1863
  flushed = true;
1443
1864
  emit(scrubber.redact("", { final: true }));
1444
- resolve();
1865
+ resolve20();
1445
1866
  };
1446
1867
  source.on("end", finishOnce);
1447
1868
  source.on("close", finishOnce);
1448
1869
  source.on("error", () => {
1449
1870
  flushed = true;
1450
- resolve();
1871
+ resolve20();
1451
1872
  });
1452
1873
  source.setEncoding("utf8");
1453
1874
  source.on("data", (chunk) => {
@@ -1468,15 +1889,15 @@ function signalNumber(signal) {
1468
1889
 
1469
1890
  // src/lib/paths.ts
1470
1891
  import { homedir } from "os";
1471
- import { join } from "path";
1892
+ import { join as join2 } from "path";
1472
1893
  function configDir() {
1473
- return process.env.GENI_CONFIG_DIR ?? join(homedir(), ".config", "geni");
1894
+ return process.env.GENI_CONFIG_DIR ?? join2(homedir(), ".config", "geni");
1474
1895
  }
1475
1896
  function sessionFilePath() {
1476
- return join(configDir(), "runner-session.json");
1897
+ return join2(configDir(), "runner-session.json");
1477
1898
  }
1478
1899
  function configFilePath() {
1479
- return join(configDir(), "config.json");
1900
+ return join2(configDir(), "config.json");
1480
1901
  }
1481
1902
 
1482
1903
  // src/dependencyInjection/clients.ts
@@ -1511,6 +1932,9 @@ var discoveryService = new DiscoveryService(
1511
1932
  browserOpener,
1512
1933
  configService
1513
1934
  );
1935
+ var workflowAuthoringService = new WorkflowAuthoringService(
1936
+ sessionContextService
1937
+ );
1514
1938
 
1515
1939
  // src/lib/cliErrors.ts
1516
1940
  function exitOnApiError(error, opts = {}) {
@@ -2132,10 +2556,10 @@ function registerCredentialConnect(parent) {
2132
2556
  return;
2133
2557
  }
2134
2558
  printInfo(`Opening ${chalk6.cyan(intent.url)}`);
2135
- printInfo("\u21B3 approve in your browser");
2559
+ printInfo(`\u21B3 connect ${service} in your browser, then come back here`);
2136
2560
  process.stdout.write(
2137
2561
  `
2138
- When you're done, re-run: geni credential list --service ${service}
2562
+ Once it's connected, re-run: geni credential list --service ${service}
2139
2563
  `
2140
2564
  );
2141
2565
  } catch (error) {
@@ -2416,52 +2840,1176 @@ function registerIntegrationCommands(program2) {
2416
2840
  registerIntegrationOperation(integration);
2417
2841
  }
2418
2842
 
2419
- // src/commands/config/get.ts
2420
- var UNSET_PLACEHOLDER = "(unset)";
2421
- function executeConfigGet(args) {
2422
- const file = configService.fileValues();
2423
- if (args.key) {
2424
- if (!isSettableConfigKey(args.key)) {
2843
+ // src/commands/resource/create.ts
2844
+ import { resolve } from "path";
2845
+ function registerResourceCreate(parent) {
2846
+ parent.command("create").description(
2847
+ "Create a resource (code or agent workflow, or app) and scaffold its starting files locally. Edit them, then validate and publish. Read the contract first with `geni resource spec <type>`."
2848
+ ).requiredOption(
2849
+ "--type <type>",
2850
+ 'Resource type: "code" (deterministic, fixed steps), "agent" (LLM-driven, variable-length tasks), or "app" (interactive React mini-app with server-side handlers).'
2851
+ ).option(
2852
+ "--name <name>",
2853
+ 'Human-friendly name in Title Case (e.g. "Daily Lead Finder"). Defaults to a generated name.'
2854
+ ).option(
2855
+ "--dir <path>",
2856
+ "Directory to scaffold the files into. Defaults to the current directory."
2857
+ ).option("--json", "Machine-readable output.").action(async (opts) => {
2858
+ if (opts.type !== "code" && opts.type !== "agent" && opts.type !== "app") {
2425
2859
  printError(
2426
- `Unknown config key "${args.key}". Valid keys: ${SETTABLE_CONFIG_KEYS.join(", ")}.`
2860
+ `--type must be "code", "agent", or "app" (got "${opts.type ?? ""}").`
2427
2861
  );
2428
2862
  exit(ExitCode.InvalidArgs);
2429
2863
  }
2430
- const value = file[args.key];
2431
- if (args.json) {
2432
- printJson({ [args.key]: value ?? null });
2864
+ const dir = resolve(opts.dir ?? ".");
2865
+ if (readMarker(dir)) {
2866
+ printError(
2867
+ `${dir} is already a resource directory (${RESOURCE_MARKER_FILE} present). Pick a fresh directory with --dir, or pull/edit the existing one.`
2868
+ );
2869
+ exit(ExitCode.InvalidArgs);
2870
+ }
2871
+ try {
2872
+ const { detail, fileCount } = await workflowAuthoringService.create({
2873
+ workflowType: opts.type,
2874
+ name: opts.name,
2875
+ dir
2876
+ });
2877
+ if (opts.json) {
2878
+ printJson({ resource: detail, dir, fileCount });
2879
+ return;
2880
+ }
2881
+ printSuccess(`Created ${detail.workflowType} resource "${detail.name}"`);
2882
+ printInfo(`id: ${detail.id}`);
2883
+ printInfo(`Scaffolded ${fileCount} file(s) into ${dir}`);
2884
+ printInfo(
2885
+ "Next: edit the files, then `geni resource validate`. See the contract with `geni resource spec " + detail.workflowType + "`."
2886
+ );
2887
+ } catch (error) {
2888
+ exitOnApiError(error);
2889
+ }
2890
+ });
2891
+ }
2892
+
2893
+ // src/commands/resource/list.ts
2894
+ async function executeResourceList(opts) {
2895
+ try {
2896
+ const { workflows } = await workflowAuthoringService.list();
2897
+ if (opts.json) {
2898
+ printJson({ workflows });
2433
2899
  return;
2434
2900
  }
2435
- process.stdout.write((value ?? UNSET_PLACEHOLDER) + "\n");
2436
- return;
2437
- }
2438
- if (args.json) {
2439
- const payload = {};
2440
- for (const k of SETTABLE_CONFIG_KEYS) payload[k] = file[k] ?? null;
2441
- printJson(payload);
2442
- return;
2901
+ if (workflows.length === 0) {
2902
+ process.stdout.write(
2903
+ 'No resources yet. Create one with `geni resource create --type code --name "..."`.\n'
2904
+ );
2905
+ return;
2906
+ }
2907
+ printTable(
2908
+ ["ID", "NAME", "TYPE", "ENABLED", "VALID"],
2909
+ workflows.map((workflow) => [
2910
+ workflow.id,
2911
+ workflow.name,
2912
+ workflow.workflowType,
2913
+ workflow.isEnabled ? "yes" : "no",
2914
+ workflow.isValid ? "yes" : "no"
2915
+ ]),
2916
+ { colorFn: dimColumn(0) }
2917
+ );
2918
+ } catch (error) {
2919
+ exitOnApiError(error);
2443
2920
  }
2444
- printTable(
2445
- ["KEY", "VALUE"],
2446
- SETTABLE_CONFIG_KEYS.map((k) => [k, file[k] ?? UNSET_PLACEHOLDER])
2447
- );
2448
2921
  }
2449
- function registerConfigGet(parent) {
2450
- parent.command("get").argument(
2451
- "[key]",
2452
- `Specific key to read (${SETTABLE_CONFIG_KEYS.join(" | ")}). Omit to list every settable key with its value.`
2453
- ).description(
2454
- "Print what's written to the persistent config file. Symmetric with `geni config set` \u2014 whatever you wrote is what you read. Unset keys render as `(unset)` in table output and `null` in --json. For the URL the CLI is actually hitting at runtime (which can differ if a runner-session is bound to a different server), run `geni auth status`."
2455
- ).option(
2456
- "--json",
2457
- "Machine-readable output. Unset keys are emitted as JSON `null`."
2458
- ).action(
2459
- (key, opts) => executeConfigGet({ key, json: opts.json })
2460
- );
2922
+ function registerResourceList(parent) {
2923
+ parent.command("list").description(
2924
+ "List the resources (workflows and apps) you can access in this workspace."
2925
+ ).option("--json", "Machine-readable output.").action((opts) => executeResourceList(opts));
2461
2926
  }
2462
2927
 
2463
- // src/commands/config/set.ts
2928
+ // src/commands/resource/get.ts
2929
+ import { resolve as resolve2 } from "path";
2930
+ function registerResourceGet(parent) {
2931
+ parent.command("get [id]").description(
2932
+ "Show a resource detail (type, enabled, valid). Omit the id to use the .geni-resource.json marker in --dir."
2933
+ ).option(
2934
+ "--dir <path>",
2935
+ "Resource directory for id resolution. Defaults to cwd."
2936
+ ).option("--json", "Machine-readable output.").action(async (id, opts) => {
2937
+ const dir = resolve2(opts.dir ?? ".");
2938
+ const workflowId = resolveResourceId({ id, dir });
2939
+ try {
2940
+ const detail = await workflowAuthoringService.get(workflowId);
2941
+ if (opts.json) {
2942
+ printJson(detail);
2943
+ return;
2944
+ }
2945
+ printInfo(`id: ${detail.id}`);
2946
+ printInfo(`name: ${detail.name}`);
2947
+ printInfo(`type: ${detail.workflowType}`);
2948
+ printInfo(`enabled: ${detail.isEnabled ? "yes" : "no"}`);
2949
+ printInfo(`valid: ${detail.isValid ? "yes" : "no"}`);
2950
+ printInfo(`updated: ${detail.updatedAt}`);
2951
+ } catch (error) {
2952
+ exitOnApiError(error, {
2953
+ notFoundMessage: `Resource "${workflowId}" not found in this workspace.`
2954
+ });
2955
+ }
2956
+ });
2957
+ }
2958
+
2959
+ // src/commands/resource/pull.ts
2960
+ import { resolve as resolve3 } from "path";
2961
+ function registerResourcePull(parent) {
2962
+ parent.command("pull <id>").description(
2963
+ "Download a workflow's live files into a directory (default cwd) and write its .geni-resource.json marker."
2964
+ ).option("--dir <path>", "Destination directory. Defaults to cwd.").option("--force", "Overwrite existing files in the directory.").action(async (id, opts) => {
2965
+ const dir = resolve3(opts.dir ?? ".");
2966
+ if (!opts.force && dirHasResourceFiles(dir)) {
2967
+ printError(
2968
+ `${dir} already contains files. Re-run with --force to overwrite, or pull into an empty --dir.`
2969
+ );
2970
+ exit(ExitCode.InvalidArgs);
2971
+ }
2972
+ try {
2973
+ const { fileCount, workflowType } = await workflowAuthoringService.pull(
2974
+ {
2975
+ id,
2976
+ dir
2977
+ }
2978
+ );
2979
+ printSuccess(
2980
+ `Pulled ${fileCount} file(s) for ${workflowType} workflow ${id}`
2981
+ );
2982
+ printInfo(`into ${dir}`);
2983
+ } catch (error) {
2984
+ exitOnApiError(error, {
2985
+ notFoundMessage: `Resource "${id}" not found in this workspace.`
2986
+ });
2987
+ }
2988
+ });
2989
+ }
2990
+
2991
+ // src/commands/resource/validate.ts
2992
+ import { resolve as resolve4 } from "path";
2993
+
2994
+ // src/lib/printValidationErrors.ts
2995
+ import chalk8 from "chalk";
2996
+ function printValidationErrors(errors) {
2997
+ for (const error of errors) {
2998
+ const where = error.path ? chalk8.dim(`${error.path}: `) : "";
2999
+ process.stderr.write(`${chalk8.red("\u2717")} ${where}${error.message}
3000
+ `);
3001
+ }
3002
+ }
3003
+
3004
+ // src/commands/resource/validate.ts
3005
+ function registerResourceValidate(parent) {
3006
+ parent.command("validate [id]").description(
3007
+ "Validate the local files against the spec, credential/const wiring, and (code) TypeScript, without publishing. Exit 9 on validation failure."
3008
+ ).option("--dir <path>", "Resource directory. Defaults to cwd.").option("--json", "Machine-readable output.").action(async (id, opts) => {
3009
+ const dir = resolve4(opts.dir ?? ".");
3010
+ const workflowId = resolveResourceId({ id, dir });
3011
+ try {
3012
+ const result = await workflowAuthoringService.validate({
3013
+ id: workflowId,
3014
+ dir
3015
+ });
3016
+ if (opts.json) {
3017
+ printJson(result);
3018
+ if (!result.valid) exit(ExitCode.ValidationFailed);
3019
+ return;
3020
+ }
3021
+ if (result.valid) {
3022
+ printSuccess("Valid. Ready to publish.");
3023
+ return;
3024
+ }
3025
+ printValidationErrors(result.errors);
3026
+ exit(ExitCode.ValidationFailed);
3027
+ } catch (error) {
3028
+ exitOnApiError(error, {
3029
+ notFoundMessage: `Resource "${workflowId}" not found in this workspace.`
3030
+ });
3031
+ }
3032
+ });
3033
+ }
3034
+
3035
+ // src/commands/resource/config/get.ts
3036
+ import { resolve as resolve5 } from "path";
3037
+ function registerConfigGet(parent) {
3038
+ parent.command("get [id]").description("Show the current credential / const / trigger bindings.").option("--dir <path>", "Resource directory. Defaults to cwd.").option("--json", "Machine-readable output.").action(async (id, opts) => {
3039
+ const dir = resolve5(opts.dir ?? ".");
3040
+ const workflowId = resolveResourceId({ id, dir });
3041
+ try {
3042
+ const snapshot = await workflowAuthoringService.getConfig(workflowId);
3043
+ if (opts.json) {
3044
+ printJson(snapshot);
3045
+ return;
3046
+ }
3047
+ printConfig(snapshot);
3048
+ } catch (error) {
3049
+ exitOnApiError(error, {
3050
+ notFoundMessage: `Resource "${workflowId}" not found in this workspace.`
3051
+ });
3052
+ }
3053
+ });
3054
+ }
3055
+ function printConfig(snapshot) {
3056
+ printInfo("credentials:");
3057
+ const creds = Object.entries(snapshot.credentials);
3058
+ if (creds.length === 0) process.stdout.write(" (none)\n");
3059
+ for (const [service, credId] of creds) {
3060
+ process.stdout.write(` ${service} = ${credId}
3061
+ `);
3062
+ }
3063
+ printInfo("consts:");
3064
+ const vars = Object.entries(snapshot.variables);
3065
+ if (vars.length === 0) process.stdout.write(" (none)\n");
3066
+ for (const [key, value] of vars) {
3067
+ process.stdout.write(` ${key} = ${JSON.stringify(value)}
3068
+ `);
3069
+ }
3070
+ printInfo("triggers:");
3071
+ const triggers = Object.entries(snapshot.triggers);
3072
+ if (triggers.length === 0) process.stdout.write(" (none)\n");
3073
+ for (const [triggerId, entry] of triggers) {
3074
+ process.stdout.write(` ${triggerId}: ${JSON.stringify(entry)}
3075
+ `);
3076
+ }
3077
+ }
3078
+
3079
+ // src/commands/resource/config/set.ts
3080
+ import { resolve as resolve6 } from "path";
2464
3081
  function registerConfigSet(parent) {
3082
+ parent.command("set [id]").description(
3083
+ "Wire config. Repeat each flag as needed. Credentials: --cred <service>=<credentialId>. Consts: --const <key>=<value> (value JSON-parsed, else string). Schedules: --schedule <triggerId>=<cron>, --timezone <triggerId>=<tz>."
3084
+ ).option("--dir <path>", "Resource directory. Defaults to cwd.").option(
3085
+ "--cred <service=credentialId>",
3086
+ "Assign a credential to a service slot. Repeatable.",
3087
+ collect2,
3088
+ []
3089
+ ).option(
3090
+ "--cred-clear <service>",
3091
+ "Clear a service slot. Repeatable.",
3092
+ collect2,
3093
+ []
3094
+ ).option(
3095
+ "--const <key=value>",
3096
+ "Set a const value (value is JSON-parsed when possible, else a string). Repeatable.",
3097
+ collect2,
3098
+ []
3099
+ ).option(
3100
+ "--schedule <triggerId=cron>",
3101
+ "Set a cron trigger schedule. Repeatable.",
3102
+ collect2,
3103
+ []
3104
+ ).option(
3105
+ "--timezone <triggerId=tz>",
3106
+ "Set a trigger timezone (IANA, e.g. America/New_York). Repeatable.",
3107
+ collect2,
3108
+ []
3109
+ ).option("--json", "Machine-readable output.").action(async (id, opts) => {
3110
+ const dir = resolve6(opts.dir ?? ".");
3111
+ const workflowId = resolveResourceId({ id, dir });
3112
+ const partial = buildPartial(opts);
3113
+ if (!partial.credentials && !partial.variables && !partial.triggers) {
3114
+ printError(
3115
+ "Nothing to set. Pass at least one of --cred, --cred-clear, --const, --schedule, --timezone."
3116
+ );
3117
+ exit(ExitCode.InvalidArgs);
3118
+ }
3119
+ try {
3120
+ const result = await workflowAuthoringService.updateConfig(
3121
+ workflowId,
3122
+ partial
3123
+ );
3124
+ if (opts.json) {
3125
+ printJson(result);
3126
+ if (!result.ok) exit(ExitCode.ValidationFailed);
3127
+ return;
3128
+ }
3129
+ if (!result.ok) {
3130
+ printValidationErrors(result.errors);
3131
+ exit(ExitCode.ValidationFailed);
3132
+ }
3133
+ printSuccess(`Updated config (${result.applied.length} change(s))`);
3134
+ for (const path of result.applied) printInfo(path);
3135
+ } catch (error) {
3136
+ exitOnApiError(error, {
3137
+ notFoundMessage: `Resource "${workflowId}" not found, or you lack edit access.`
3138
+ });
3139
+ }
3140
+ });
3141
+ }
3142
+ function buildPartial(opts) {
3143
+ const partial = {};
3144
+ const credentials = {};
3145
+ for (const pair of opts.cred) {
3146
+ const { key, value } = splitPair(pair, "--cred");
3147
+ credentials[key] = value;
3148
+ }
3149
+ for (const service of opts.credClear) {
3150
+ credentials[service] = null;
3151
+ }
3152
+ if (Object.keys(credentials).length > 0) partial.credentials = credentials;
3153
+ const variables = {};
3154
+ for (const pair of opts.const) {
3155
+ const { key, value } = splitPair(pair, "--const");
3156
+ variables[key] = coerceValue(value);
3157
+ }
3158
+ if (Object.keys(variables).length > 0) partial.variables = variables;
3159
+ const triggers = {};
3160
+ for (const pair of opts.schedule) {
3161
+ const { key, value } = splitPair(pair, "--schedule");
3162
+ triggers[key] = { ...triggers[key], schedule: value };
3163
+ }
3164
+ for (const pair of opts.timezone) {
3165
+ const { key, value } = splitPair(pair, "--timezone");
3166
+ triggers[key] = { ...triggers[key], timezone: value };
3167
+ }
3168
+ if (Object.keys(triggers).length > 0) partial.triggers = triggers;
3169
+ return partial;
3170
+ }
3171
+ function collect2(value, prev) {
3172
+ return [...prev, value];
3173
+ }
3174
+ function splitPair(pair, flag) {
3175
+ const index = pair.indexOf("=");
3176
+ if (index <= 0) {
3177
+ printError(`${flag} expects <key>=<value> (got "${pair}").`);
3178
+ exit(ExitCode.InvalidArgs);
3179
+ }
3180
+ return { key: pair.slice(0, index), value: pair.slice(index + 1) };
3181
+ }
3182
+ function coerceValue(raw) {
3183
+ try {
3184
+ return JSON.parse(raw);
3185
+ } catch {
3186
+ return raw;
3187
+ }
3188
+ }
3189
+
3190
+ // src/commands/resource/config/index.ts
3191
+ function registerResourceConfig(parent) {
3192
+ const config = parent.command("config").description(
3193
+ "Read or wire a workflow config: credentials, const values, trigger schedules."
3194
+ );
3195
+ registerConfigGet(config);
3196
+ registerConfigSet(config);
3197
+ }
3198
+
3199
+ // src/commands/resource/publish.ts
3200
+ import { resolve as resolve7 } from "path";
3201
+ function registerResourcePublish(parent) {
3202
+ parent.command("publish [id]").description(
3203
+ "Validate and promote the local files to the live version. Required before a test (`geni workflow test`) or handler run (`geni app run-handler`) reflects your edits. Exit 9 on validation failure (nothing published)."
3204
+ ).requiredOption(
3205
+ "--summary <text>",
3206
+ 'One plain-language sentence describing the change, shown to the user (e.g. "Now also posts to Slack when an invoice is paid"). No file names or jargon.'
3207
+ ).option("--dir <path>", "Resource directory. Defaults to cwd.").option("--json", "Machine-readable output.").action(async (id, opts) => {
3208
+ const dir = resolve7(opts.dir ?? ".");
3209
+ const workflowId = resolveResourceId({ id, dir });
3210
+ try {
3211
+ const result = await workflowAuthoringService.publish({
3212
+ id: workflowId,
3213
+ dir,
3214
+ changeSummary: opts.summary
3215
+ });
3216
+ if (opts.json) {
3217
+ printJson(result);
3218
+ if (!result.ok) exit(ExitCode.ValidationFailed);
3219
+ return;
3220
+ }
3221
+ if (!result.ok) {
3222
+ printValidationErrors(result.errors);
3223
+ exit(ExitCode.ValidationFailed);
3224
+ }
3225
+ printSuccess(
3226
+ `Published ${result.filesPersisted} file(s) to ${workflowId}`
3227
+ );
3228
+ for (const url of result.webhookUrls) {
3229
+ printInfo(`webhook: ${url}`);
3230
+ }
3231
+ printInfo(
3232
+ "Verify it: `geni workflow test` (workflows) or `geni app run-handler` (apps)."
3233
+ );
3234
+ } catch (error) {
3235
+ exitOnApiError(error, {
3236
+ notFoundMessage: `Resource "${workflowId}" not found, or you lack edit access.`
3237
+ });
3238
+ }
3239
+ });
3240
+ }
3241
+
3242
+ // src/commands/resource/spec.ts
3243
+ function registerResourceSpec(parent) {
3244
+ parent.command("spec <type>").description(
3245
+ "Print the resource-type contract (code | agent | app): required files, runtime API, allowed/forbidden patterns. Read this before editing files."
3246
+ ).action(async (type) => {
3247
+ try {
3248
+ const result = await workflowAuthoringService.spec(type);
3249
+ process.stdout.write(
3250
+ result.spec.endsWith("\n") ? result.spec : result.spec + "\n"
3251
+ );
3252
+ } catch (error) {
3253
+ exitOnApiError(error, {
3254
+ notFoundMessage: `Unknown resource type "${type}". Use code, agent, or app.`
3255
+ });
3256
+ }
3257
+ });
3258
+ }
3259
+
3260
+ // src/commands/resource/index.ts
3261
+ function registerResourceCommands(program2) {
3262
+ const resource = program2.command("resource").alias("resources").description(
3263
+ "Author resources (workflows and apps) locally against the cloud: scaffold and pull files, edit them, then validate, wire credentials, and publish. Run `geni resource spec <type>` to learn a type contract."
3264
+ ).action(() => executeResourceList({}));
3265
+ registerResourceCreate(resource);
3266
+ registerResourceList(resource);
3267
+ registerResourceGet(resource);
3268
+ registerResourcePull(resource);
3269
+ registerResourceValidate(resource);
3270
+ registerResourceConfig(resource);
3271
+ registerResourcePublish(resource);
3272
+ registerResourceSpec(resource);
3273
+ resource.addHelpText(
3274
+ "after",
3275
+ `
3276
+ Build loop (run \`geni resource spec <type>\` first):
3277
+ 1. geni resource create --type code --name "My Resource" scaffold files + the cloud resource
3278
+ 2. (edit the files with your editor)
3279
+ 3. geni resource validate spec + type check, no publish
3280
+ 4. geni resource config set ... wire credentials, consts, schedules
3281
+ 5. geni resource publish --summary "..." promote your files to live
3282
+ 6. geni workflow test (workflows) or geni app run-handler (apps) verify it
3283
+
3284
+ Commands accept the resource id as a positional, or infer it from the
3285
+ .geni-resource.json marker when run inside a resource directory.`
3286
+ );
3287
+ }
3288
+
3289
+ // src/commands/workflow/setType.ts
3290
+ import { resolve as resolve8 } from "path";
3291
+ function registerWorkflowSetType(parent) {
3292
+ parent.command("set-type <type> [id]").description(
3293
+ "Switch a workflow's execution type (code <-> agent). Then rewrite the files for the new type and delete the old type's. Read `geni resource spec <type>`."
3294
+ ).option("--dir <path>", "Resource directory. Defaults to cwd.").option("--json", "Machine-readable output.").action(
3295
+ async (type, id, opts) => {
3296
+ if (type !== "code" && type !== "agent") {
3297
+ printError(`type must be "code" or "agent" (got "${type}").`);
3298
+ exit(ExitCode.InvalidArgs);
3299
+ }
3300
+ const dir = resolve8(opts.dir ?? ".");
3301
+ const workflowId = resolveResourceId({ id, dir });
3302
+ try {
3303
+ const result = await workflowAuthoringService.setType(
3304
+ workflowId,
3305
+ type
3306
+ );
3307
+ if (readMarker(dir)) {
3308
+ writeMarker(dir, { resourceId: workflowId, resourceType: type });
3309
+ }
3310
+ if (opts.json) {
3311
+ printJson(result);
3312
+ return;
3313
+ }
3314
+ printSuccess(`Switched ${workflowId} to ${type}`);
3315
+ if (result.forbiddenFiles.length > 0) {
3316
+ printInfo(
3317
+ `Delete any leftover files from the previous type: ${result.forbiddenFiles.join(", ")}`
3318
+ );
3319
+ }
3320
+ printInfo(
3321
+ `Write the ${type} files, then validate. Read the contract: \`geni resource spec ${type}\`.`
3322
+ );
3323
+ } catch (error) {
3324
+ exitOnApiError(error, {
3325
+ notFoundMessage: `Workflow "${workflowId}" not found, or you lack edit access.`
3326
+ });
3327
+ }
3328
+ }
3329
+ );
3330
+ }
3331
+
3332
+ // src/commands/workflow/test.ts
3333
+ import { resolve as resolve9 } from "path";
3334
+ import { z as z3 } from "zod";
3335
+ var TERMINAL = /* @__PURE__ */ new Set(["completed", "failed", "cancelled", "awaiting_input"]);
3336
+ var POLL_INTERVAL_MS = 2e3;
3337
+ function registerWorkflowTest(parent) {
3338
+ parent.command("test [id]").description(
3339
+ "Run a cloud test execution of the published version, with an optional trigger payload, and wait for the result. Publish first to test your latest edits."
3340
+ ).option(
3341
+ "--payload <json>",
3342
+ `Trigger payload as a JSON object (e.g. '{"days":7}'). Defaults to {}.`
3343
+ ).option("--dir <path>", "Resource directory. Defaults to cwd.").option(
3344
+ "--timeout <seconds>",
3345
+ "How long to wait for the run to finish before giving up. Default 180."
3346
+ ).option("--json", "Machine-readable output (final execution + logs).").action(async (id, opts) => {
3347
+ const dir = resolve9(opts.dir ?? ".");
3348
+ const workflowId = resolveResourceId({ id, dir });
3349
+ const payload = parsePayload(opts.payload);
3350
+ const timeoutMs = parseTimeout(opts.timeout);
3351
+ try {
3352
+ const scheduled = await workflowAuthoringService.test(
3353
+ workflowId,
3354
+ payload
3355
+ );
3356
+ if (scheduled.blockers && scheduled.blockers.length > 0) {
3357
+ printError("Workflow isn't ready to run:");
3358
+ printValidationErrors(scheduled.blockers);
3359
+ printError(
3360
+ "Wire the missing credentials / set the schedule with `geni resource config set`, then retry."
3361
+ );
3362
+ exit(ExitCode.ValidationFailed);
3363
+ }
3364
+ if (!scheduled.executionId) {
3365
+ printError(
3366
+ "No execution was scheduled. Retry, or check the workflow config."
3367
+ );
3368
+ exit(ExitCode.InternalError);
3369
+ }
3370
+ const executionId = scheduled.executionId;
3371
+ if (!opts.json) printInfo(`Running test execution ${executionId} ...`);
3372
+ const detail = await pollUntilDone(workflowId, executionId, timeoutMs);
3373
+ const logs = await workflowAuthoringService.getExecutionLogs(
3374
+ workflowId,
3375
+ executionId
3376
+ );
3377
+ if (opts.json) {
3378
+ printJson({ execution: detail, logs: logs.logs });
3379
+ } else {
3380
+ printDetail(detail, logs.logs);
3381
+ }
3382
+ if (detail.status !== "completed") {
3383
+ exit(
3384
+ detail.status === "awaiting_input" ? ExitCode.Ok : ExitCode.GenericError
3385
+ );
3386
+ }
3387
+ } catch (error) {
3388
+ exitOnApiError(error, {
3389
+ notFoundMessage: `Workflow "${workflowId}" not found, or you lack run access.`
3390
+ });
3391
+ }
3392
+ });
3393
+ }
3394
+ async function pollUntilDone(workflowId, executionId, timeoutMs) {
3395
+ const deadline = Date.now() + timeoutMs;
3396
+ let detail = await workflowAuthoringService.getExecution(
3397
+ workflowId,
3398
+ executionId
3399
+ );
3400
+ while (!TERMINAL.has(detail.status)) {
3401
+ if (Date.now() > deadline) {
3402
+ printError(
3403
+ `Timed out after ${Math.round(timeoutMs / 1e3)}s waiting for ${executionId} (last status: ${detail.status}). It may still finish. Check \`geni workflow executions\`.`
3404
+ );
3405
+ exit(ExitCode.Timeout);
3406
+ }
3407
+ await sleep2(POLL_INTERVAL_MS);
3408
+ detail = await workflowAuthoringService.getExecution(
3409
+ workflowId,
3410
+ executionId
3411
+ );
3412
+ }
3413
+ return detail;
3414
+ }
3415
+ function printDetail(detail, logs) {
3416
+ if (detail.status === "completed") {
3417
+ printSuccess(`Execution ${detail.id} completed`);
3418
+ } else {
3419
+ printError(`Execution ${detail.id} ${detail.status}`);
3420
+ }
3421
+ if (detail.sandboxRuntimeMs !== null) {
3422
+ printInfo(`runtime: ${detail.sandboxRuntimeMs}ms`);
3423
+ }
3424
+ if (detail.error) printError(`error: ${detail.error}`);
3425
+ if (detail.run !== null && detail.run !== void 0) {
3426
+ process.stdout.write("--- output ---\n");
3427
+ process.stdout.write(JSON.stringify(detail.run, null, 2) + "\n");
3428
+ }
3429
+ if (logs && logs.trim().length > 0) {
3430
+ process.stdout.write("--- logs ---\n");
3431
+ process.stdout.write(logs.endsWith("\n") ? logs : logs + "\n");
3432
+ }
3433
+ }
3434
+ function parsePayload(raw) {
3435
+ if (!raw) return {};
3436
+ let parsed;
3437
+ try {
3438
+ parsed = JSON.parse(raw);
3439
+ } catch {
3440
+ printError(`--payload must be valid JSON. Got: ${raw}`);
3441
+ exit(ExitCode.InvalidArgs);
3442
+ }
3443
+ if (Array.isArray(parsed)) {
3444
+ printError("--payload must be a JSON object, not an array.");
3445
+ exit(ExitCode.InvalidArgs);
3446
+ }
3447
+ const result = z3.record(z3.string(), z3.unknown()).safeParse(parsed);
3448
+ if (!result.success) {
3449
+ printError(`--payload must be a JSON object (e.g. '{"days":7}').`);
3450
+ exit(ExitCode.InvalidArgs);
3451
+ }
3452
+ return result.data;
3453
+ }
3454
+ function parseTimeout(raw) {
3455
+ if (!raw) return 18e4;
3456
+ const seconds = Number.parseInt(raw, 10);
3457
+ if (!Number.isFinite(seconds) || seconds <= 0) {
3458
+ printError("--timeout must be a positive number of seconds.");
3459
+ exit(ExitCode.InvalidArgs);
3460
+ }
3461
+ return seconds * 1e3;
3462
+ }
3463
+ function sleep2(ms) {
3464
+ return new Promise((resolve20) => setTimeout(resolve20, ms));
3465
+ }
3466
+
3467
+ // src/commands/workflow/triggerSample.ts
3468
+ import { resolve as resolve10 } from "path";
3469
+ function registerWorkflowTriggerSample(parent) {
3470
+ parent.command("trigger-sample [id]").description(
3471
+ "Fetch live sample data from the workflow's configured poll trigger (the exact shape `input` receives). Wire the trigger's credential + inputs with `geni resource config set` first."
3472
+ ).option("--dir <path>", "Resource directory. Defaults to cwd.").option("--json", "Machine-readable output.").action(async (id, opts) => {
3473
+ const dir = resolve10(opts.dir ?? ".");
3474
+ const workflowId = resolveResourceId({ id, dir });
3475
+ try {
3476
+ const result = await workflowAuthoringService.triggerSample(workflowId);
3477
+ if (opts.json) {
3478
+ printJson(result);
3479
+ return;
3480
+ }
3481
+ process.stdout.write(
3482
+ result.result.endsWith("\n") ? result.result : result.result + "\n"
3483
+ );
3484
+ } catch (error) {
3485
+ exitOnApiError(error, {
3486
+ notFoundMessage: `Workflow "${workflowId}" not found in this workspace.`
3487
+ });
3488
+ }
3489
+ });
3490
+ }
3491
+
3492
+ // src/commands/workflow/stageInput.ts
3493
+ import { resolve as resolve11 } from "path";
3494
+ import { existsSync as existsSync2, statSync as statSync2 } from "fs";
3495
+ function registerWorkflowStageInput(parent) {
3496
+ parent.command("stage-input <file>").description(
3497
+ "Upload a local file as a file-type input test value. Prints a store:// URI to use as that field's value in `geni workflow test --payload`."
3498
+ ).option("--workflow <id>", "Workflow id (else resolved from --dir marker).").option(
3499
+ "--dir <path>",
3500
+ "Resource directory for id resolution. Defaults to cwd."
3501
+ ).option("--json", "Machine-readable output.").action(async (file, opts) => {
3502
+ const localPath = resolve11(file);
3503
+ if (!existsSync2(localPath) || !statSync2(localPath).isFile()) {
3504
+ printError(`File not found: ${localPath}`);
3505
+ exit(ExitCode.InvalidArgs);
3506
+ }
3507
+ const dir = resolve11(opts.dir ?? ".");
3508
+ const workflowId = resolveResourceId({ id: opts.workflow, dir });
3509
+ try {
3510
+ const { storageUri, filename } = await workflowAuthoringService.stageInput({
3511
+ id: workflowId,
3512
+ localPath
3513
+ });
3514
+ if (opts.json) {
3515
+ printJson({ storageUri, filename });
3516
+ return;
3517
+ }
3518
+ printSuccess(`Staged ${filename}`);
3519
+ printInfo(`store URI: ${storageUri}`);
3520
+ printInfo(
3521
+ `Pass it as the file field's value, e.g. \`geni workflow test --payload '{"<field>":"${storageUri}"}'\`.`
3522
+ );
3523
+ } catch (error) {
3524
+ exitOnApiError(error, {
3525
+ notFoundMessage: `Workflow "${workflowId}" not found, or you lack run access.`
3526
+ });
3527
+ }
3528
+ });
3529
+ }
3530
+
3531
+ // src/commands/workflow/executions.ts
3532
+ import { resolve as resolve12 } from "path";
3533
+ function registerWorkflowExecutions(parent) {
3534
+ parent.command("executions [id]").description("List a workflow recent executions (id, status, when).").option("--dir <path>", "Resource directory. Defaults to cwd.").option("--limit <n>", "Max rows (default 20, max 100).").option("--json", "Machine-readable output.").action(async (id, opts) => {
3535
+ const dir = resolve12(opts.dir ?? ".");
3536
+ const workflowId = resolveResourceId({ id, dir });
3537
+ const limit = opts.limit ? Number.parseInt(opts.limit, 10) : void 0;
3538
+ try {
3539
+ const { executions } = await workflowAuthoringService.listExecutions(
3540
+ workflowId,
3541
+ Number.isFinite(limit) ? limit : void 0
3542
+ );
3543
+ if (opts.json) {
3544
+ printJson({ executions });
3545
+ return;
3546
+ }
3547
+ if (executions.length === 0) {
3548
+ process.stdout.write("No executions yet. Run `geni workflow test`.\n");
3549
+ return;
3550
+ }
3551
+ printTable(
3552
+ ["ID", "STATUS", "TEST", "WHEN", "LABEL"],
3553
+ executions.map((execution) => [
3554
+ execution.id,
3555
+ execution.status,
3556
+ execution.isTest ? "yes" : "no",
3557
+ execution.createdAt,
3558
+ execution.name ?? execution.eventDescription
3559
+ ]),
3560
+ { colorFn: dimColumn(0) }
3561
+ );
3562
+ } catch (error) {
3563
+ exitOnApiError(error, {
3564
+ notFoundMessage: `Workflow "${workflowId}" not found in this workspace.`
3565
+ });
3566
+ }
3567
+ });
3568
+ }
3569
+
3570
+ // src/commands/workflow/execution.ts
3571
+ import { resolve as resolve14 } from "path";
3572
+
3573
+ // src/lib/executionTrace.ts
3574
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
3575
+ import { join as join3, resolve as resolve13 } from "path";
3576
+ function writeExecutionTraceBundle(args) {
3577
+ const { baseDir, trace } = args;
3578
+ const dir = resolve13(baseDir, trace.id);
3579
+ mkdirSync2(dir, { recursive: true });
3580
+ const files = [];
3581
+ const write = (name, content) => {
3582
+ const path = join3(dir, name);
3583
+ writeFileSync2(path, content);
3584
+ files.push(path);
3585
+ };
3586
+ if (trace.run !== null && trace.run !== void 0) {
3587
+ write("run.json", JSON.stringify(trace.run, null, 2));
3588
+ }
3589
+ if (trace.logs !== null) {
3590
+ write("logs.txt", trace.logs);
3591
+ }
3592
+ if (trace.input !== null && trace.input !== void 0) {
3593
+ write("input.json", JSON.stringify(trace.input, null, 2));
3594
+ }
3595
+ if (trace.thread.length > 0) {
3596
+ write("thread.json", JSON.stringify(trace.thread, null, 2));
3597
+ }
3598
+ return { dir, files };
3599
+ }
3600
+
3601
+ // src/commands/workflow/execution.ts
3602
+ function registerWorkflowExecution(parent) {
3603
+ parent.command("execution <executionId>").description(
3604
+ "Inspect one execution. Default shows status and output; add --logs, --input, or --thread for more, or --download to write the full trace to disk."
3605
+ ).option("--workflow <id>", "Workflow id (else resolved from --dir marker).").option(
3606
+ "--dir <path>",
3607
+ "Resource directory for id resolution. Defaults to cwd."
3608
+ ).option("--logs", "Include the execution logs.").option("--input", "Include the trigger input payload.").option(
3609
+ "--thread",
3610
+ "Include the step-by-step execution thread (every step the run took)."
3611
+ ).option(
3612
+ "--download [dir]",
3613
+ 'Write the full trace (run, logs, input, thread) to <dir>/<id>/. Defaults to "executions".'
3614
+ ).option("--json", "Machine-readable output.").action(async (executionId, opts) => {
3615
+ const dir = resolve14(opts.dir ?? ".");
3616
+ const workflowId = resolveResourceId({ id: opts.workflow, dir });
3617
+ const wantsTrace = opts.input || opts.thread || opts.download !== void 0;
3618
+ try {
3619
+ if (wantsTrace) {
3620
+ await runTraceMode({ workflowId, executionId, opts });
3621
+ return;
3622
+ }
3623
+ const detail = await workflowAuthoringService.getExecution(
3624
+ workflowId,
3625
+ executionId
3626
+ );
3627
+ const logs = opts.logs ? (await workflowAuthoringService.getExecutionLogs(
3628
+ workflowId,
3629
+ executionId
3630
+ )).logs : null;
3631
+ if (opts.json) {
3632
+ printJson({ execution: detail, logs });
3633
+ return;
3634
+ }
3635
+ printInfo(`id: ${detail.id}`);
3636
+ printInfo(`status: ${detail.status}`);
3637
+ printInfo(`test: ${detail.isTest ? "yes" : "no"}`);
3638
+ if (detail.sandboxRuntimeMs !== null) {
3639
+ printInfo(`runtime: ${detail.sandboxRuntimeMs}ms`);
3640
+ }
3641
+ if (detail.error) printError(`error: ${detail.error}`);
3642
+ printOutput(detail.run);
3643
+ if (opts.logs) printSection("logs", logs ?? "(no logs)");
3644
+ } catch (error) {
3645
+ exitOnApiError(error, {
3646
+ notFoundMessage: `Execution "${executionId}" not found for that workflow.`
3647
+ });
3648
+ }
3649
+ });
3650
+ }
3651
+ async function runTraceMode(args) {
3652
+ const { workflowId, executionId, opts } = args;
3653
+ const trace = await workflowAuthoringService.getExecutionTrace(
3654
+ workflowId,
3655
+ executionId
3656
+ );
3657
+ const download = opts.download !== void 0 ? writeExecutionTraceBundle({
3658
+ baseDir: typeof opts.download === "string" ? opts.download : "executions",
3659
+ trace
3660
+ }) : null;
3661
+ if (opts.json) {
3662
+ printJson({ execution: trace, download });
3663
+ return;
3664
+ }
3665
+ printInfo(`id: ${trace.id}`);
3666
+ printInfo(`status: ${trace.status}`);
3667
+ if (trace.error) printError(`error: ${trace.error}`);
3668
+ printOutput(trace.run);
3669
+ if (opts.input) {
3670
+ printSection("input", JSON.stringify(trace.input ?? null, null, 2));
3671
+ }
3672
+ if (opts.thread) {
3673
+ printSection(
3674
+ "thread",
3675
+ trace.thread.length > 0 ? JSON.stringify(trace.thread, null, 2) : "(no thread)"
3676
+ );
3677
+ }
3678
+ if (opts.logs) printSection("logs", trace.logs ?? "(no logs)");
3679
+ if (download) {
3680
+ printSuccess(
3681
+ `Wrote ${download.files.length} trace file(s) to ${download.dir}`
3682
+ );
3683
+ for (const file of download.files) printInfo(file);
3684
+ }
3685
+ }
3686
+ function printOutput(run2) {
3687
+ if (run2 === null || run2 === void 0) return;
3688
+ printSection("output", JSON.stringify(run2, null, 2));
3689
+ }
3690
+ function printSection(label, content) {
3691
+ process.stdout.write(`--- ${label} ---
3692
+ `);
3693
+ process.stdout.write(content + "\n");
3694
+ }
3695
+
3696
+ // src/commands/workflow/index.ts
3697
+ function registerWorkflowCommands(program2) {
3698
+ const workflow = program2.command("workflow").alias("workflows").description(
3699
+ "Workflow-only tools: run cloud test executions, sample triggers, switch type, inspect runs. Shared lifecycle (create / validate / publish / config) is under `geni resource`."
3700
+ );
3701
+ registerWorkflowSetType(workflow);
3702
+ registerWorkflowTest(workflow);
3703
+ registerWorkflowTriggerSample(workflow);
3704
+ registerWorkflowStageInput(workflow);
3705
+ registerWorkflowExecutions(workflow);
3706
+ registerWorkflowExecution(workflow);
3707
+ }
3708
+
3709
+ // src/commands/app/buildStatus.ts
3710
+ import { resolve as resolve15 } from "path";
3711
+ function registerAppBuildStatus(parent) {
3712
+ parent.command("build-status [id]").description(
3713
+ "Read the live app Vite build state: status, error, last built."
3714
+ ).option("--dir <path>", "Resource directory. Defaults to cwd.").option("--json", "Machine-readable output.").action(async (id, opts) => {
3715
+ const dir = resolve15(opts.dir ?? ".");
3716
+ const workflowId = resolveResourceId({ id, dir });
3717
+ try {
3718
+ const result = await workflowAuthoringService.appBuildStatus(workflowId);
3719
+ if (opts.json) {
3720
+ printJson(result);
3721
+ return;
3722
+ }
3723
+ printInfo(`status: ${result.buildStatus}`);
3724
+ printInfo(`last built: ${result.lastBuiltAt ?? "(never)"}`);
3725
+ if (result.buildError) printError(`error: ${result.buildError}`);
3726
+ } catch (error) {
3727
+ exitOnApiError(error, {
3728
+ notFoundMessage: `App "${workflowId}" not found in this workspace.`
3729
+ });
3730
+ }
3731
+ });
3732
+ }
3733
+
3734
+ // src/commands/app/runHandler.ts
3735
+ import { resolve as resolve16 } from "path";
3736
+ import { z as z4 } from "zod";
3737
+ function registerAppRunHandler(parent) {
3738
+ parent.command("run-handler <name> [id]").description(
3739
+ "Dispatch a handler against the live app with --input JSON (bypasses cache) and print the result. Publish first. Exits nonzero on handler error."
3740
+ ).option("--input <json>", "Handler input as a JSON object. Defaults to {}.").option(
3741
+ "--timeout <seconds>",
3742
+ "Server-side wait before timing out. Default 60, max 300."
3743
+ ).option("--dir <path>", "Resource directory. Defaults to cwd.").option("--json", "Machine-readable output.").action(
3744
+ async (name, id, opts) => {
3745
+ const dir = resolve16(opts.dir ?? ".");
3746
+ const workflowId = resolveResourceId({ id, dir });
3747
+ const input = parseInput(opts.input);
3748
+ const timeoutSeconds = parseTimeout2(opts.timeout);
3749
+ try {
3750
+ const result = await workflowAuthoringService.runHandler(workflowId, {
3751
+ handlerName: name,
3752
+ input,
3753
+ timeoutSeconds
3754
+ });
3755
+ if (opts.json) {
3756
+ printJson(result);
3757
+ if (!result.ok) exit(ExitCode.GenericError);
3758
+ return;
3759
+ }
3760
+ if (result.ok) {
3761
+ printSuccess(`Handler "${name}" ran`);
3762
+ process.stdout.write("--- result ---\n");
3763
+ process.stdout.write(JSON.stringify(result.result, null, 2) + "\n");
3764
+ return;
3765
+ }
3766
+ printError(
3767
+ `Handler "${name}" failed (${result.status}): ${result.message}`
3768
+ );
3769
+ exit(ExitCode.GenericError);
3770
+ } catch (error) {
3771
+ exitOnApiError(error, {
3772
+ notFoundMessage: `App "${workflowId}" not found, or you lack run access.`
3773
+ });
3774
+ }
3775
+ }
3776
+ );
3777
+ }
3778
+ function parseInput(raw) {
3779
+ if (!raw) return {};
3780
+ let parsed;
3781
+ try {
3782
+ parsed = JSON.parse(raw);
3783
+ } catch {
3784
+ printError(`--input must be valid JSON. Got: ${raw}`);
3785
+ exit(ExitCode.InvalidArgs);
3786
+ }
3787
+ if (Array.isArray(parsed)) {
3788
+ printError("--input must be a JSON object, not an array.");
3789
+ exit(ExitCode.InvalidArgs);
3790
+ }
3791
+ const result = z4.record(z4.string(), z4.unknown()).safeParse(parsed);
3792
+ if (!result.success) {
3793
+ printError(`--input must be a JSON object (e.g. '{"limit":50}').`);
3794
+ exit(ExitCode.InvalidArgs);
3795
+ }
3796
+ return result.data;
3797
+ }
3798
+ function parseTimeout2(raw) {
3799
+ if (!raw) return void 0;
3800
+ const seconds = Number.parseInt(raw, 10);
3801
+ if (!Number.isFinite(seconds) || seconds <= 0) {
3802
+ printError("--timeout must be a positive number of seconds.");
3803
+ exit(ExitCode.InvalidArgs);
3804
+ }
3805
+ return seconds;
3806
+ }
3807
+
3808
+ // src/commands/app/invocations.ts
3809
+ import { resolve as resolve17 } from "path";
3810
+ function registerAppInvocations(parent) {
3811
+ parent.command("invocations [id]").description(
3812
+ "Recent handler invocations: status, timing, cache hits, errors."
3813
+ ).option("--handler <name>", "Filter to one handler.").option("--errors", "Only failed invocations.").option("--limit <n>", "Max rows (default 20).").option("--dir <path>", "Resource directory. Defaults to cwd.").option("--json", "Machine-readable output.").action(async (id, opts) => {
3814
+ const dir = resolve17(opts.dir ?? ".");
3815
+ const workflowId = resolveResourceId({ id, dir });
3816
+ const limit = opts.limit ? Number.parseInt(opts.limit, 10) : void 0;
3817
+ try {
3818
+ const { invocations } = await workflowAuthoringService.appInvocations(
3819
+ workflowId,
3820
+ {
3821
+ handler: opts.handler,
3822
+ errors: opts.errors,
3823
+ limit: Number.isFinite(limit) ? limit : void 0
3824
+ }
3825
+ );
3826
+ if (opts.json) {
3827
+ printJson({ invocations });
3828
+ return;
3829
+ }
3830
+ if (invocations.length === 0) {
3831
+ process.stdout.write("No matching invocations.\n");
3832
+ return;
3833
+ }
3834
+ printTable(
3835
+ ["WHEN", "HANDLER", "STATUS", "MS", "CACHED", "ERROR"],
3836
+ invocations.map((invocation) => [
3837
+ invocation.createdAt,
3838
+ invocation.handlerName,
3839
+ invocation.status,
3840
+ String(invocation.durationMs),
3841
+ invocation.cacheHit ? "yes" : "no",
3842
+ invocation.errorMessage ?? ""
3843
+ ]),
3844
+ { colorFn: dimColumn(0) }
3845
+ );
3846
+ } catch (error) {
3847
+ exitOnApiError(error, {
3848
+ notFoundMessage: `App "${workflowId}" not found in this workspace.`
3849
+ });
3850
+ }
3851
+ });
3852
+ }
3853
+
3854
+ // src/commands/app/errors.ts
3855
+ import { resolve as resolve18 } from "path";
3856
+ function registerAppErrors(parent) {
3857
+ parent.command("errors [id]").description(
3858
+ "Recent browser-side errors the iframe reported (render/rejection/load)."
3859
+ ).option("--limit <n>", "Max rows (default 20).").option("--dir <path>", "Resource directory. Defaults to cwd.").option("--json", "Machine-readable output.").action(async (id, opts) => {
3860
+ const dir = resolve18(opts.dir ?? ".");
3861
+ const workflowId = resolveResourceId({ id, dir });
3862
+ const limit = opts.limit ? Number.parseInt(opts.limit, 10) : void 0;
3863
+ try {
3864
+ const { errors } = await workflowAuthoringService.appErrors(
3865
+ workflowId,
3866
+ {
3867
+ limit: Number.isFinite(limit) ? limit : void 0
3868
+ }
3869
+ );
3870
+ if (opts.json) {
3871
+ printJson({ errors });
3872
+ return;
3873
+ }
3874
+ if (errors.length === 0) {
3875
+ process.stdout.write("No errors reported from the iframe.\n");
3876
+ return;
3877
+ }
3878
+ for (const event of errors) {
3879
+ process.stdout.write(
3880
+ `[${event.eventTs}] ${event.kind}: ${event.message}
3881
+ `
3882
+ );
3883
+ if (event.stack) process.stdout.write(event.stack + "\n");
3884
+ }
3885
+ } catch (error) {
3886
+ exitOnApiError(error, {
3887
+ notFoundMessage: `App "${workflowId}" not found in this workspace.`
3888
+ });
3889
+ }
3890
+ });
3891
+ }
3892
+
3893
+ // src/commands/app/clearCache.ts
3894
+ import { resolve as resolve19 } from "path";
3895
+ function registerAppClearCache(parent) {
3896
+ parent.command("clear-cache [id]").description(
3897
+ "Clear the live app handler response cache (republishing also does this)."
3898
+ ).option("--dir <path>", "Resource directory. Defaults to cwd.").action(async (id, opts) => {
3899
+ const dir = resolve19(opts.dir ?? ".");
3900
+ const workflowId = resolveResourceId({ id, dir });
3901
+ try {
3902
+ await workflowAuthoringService.clearHandlerCache(workflowId);
3903
+ printSuccess("Handler cache cleared.");
3904
+ } catch (error) {
3905
+ exitOnApiError(error, {
3906
+ notFoundMessage: `App "${workflowId}" not found, or you lack edit access.`
3907
+ });
3908
+ }
3909
+ });
3910
+ }
3911
+
3912
+ // src/commands/app/index.ts
3913
+ function registerAppCommands(program2) {
3914
+ const app = program2.command("app").alias("apps").description(
3915
+ "App-only tools: build status, run/inspect handlers, browser errors, handler cache. Run against the live (published) app. Shared lifecycle is under `geni resource`."
3916
+ );
3917
+ registerAppBuildStatus(app);
3918
+ registerAppRunHandler(app);
3919
+ registerAppInvocations(app);
3920
+ registerAppErrors(app);
3921
+ registerAppClearCache(app);
3922
+ }
3923
+
3924
+ // src/commands/trigger/list.ts
3925
+ async function executeTriggerList(opts) {
3926
+ try {
3927
+ const { triggers } = await workflowAuthoringService.listTriggers(opts.query);
3928
+ if (opts.json) {
3929
+ printJson({ triggers });
3930
+ return;
3931
+ }
3932
+ if (triggers.length === 0) {
3933
+ process.stdout.write("No trigger providers match.\n");
3934
+ return;
3935
+ }
3936
+ printTable(
3937
+ ["TRIGGER ID", "TYPE", "SERVICE", "NAME"],
3938
+ triggers.map((trigger) => [
3939
+ trigger.type === "poll" ? trigger.triggerId : `(${trigger.type})`,
3940
+ trigger.type,
3941
+ trigger.service,
3942
+ trigger.name
3943
+ ]),
3944
+ { colorFn: dimColumn(0) }
3945
+ );
3946
+ } catch (error) {
3947
+ exitOnApiError(error);
3948
+ }
3949
+ }
3950
+ function registerTriggerList(parent) {
3951
+ parent.command("list").description(
3952
+ "List trigger providers. Poll triggers show a copyable triggerId; webhook/cron are built in and take no triggerId."
3953
+ ).option(
3954
+ "-q, --query <text>",
3955
+ 'Filter by name / service / description (e.g. "email", "reddit", "schedule").'
3956
+ ).option("--json", "Machine-readable output.").action((opts) => executeTriggerList(opts));
3957
+ }
3958
+
3959
+ // src/commands/trigger/index.ts
3960
+ function registerTriggerCommands(program2) {
3961
+ const trigger = program2.command("trigger").alias("triggers").description(
3962
+ "Discover trigger providers (webhook / cron / poll) that can start a workflow. Use a poll trigger id in workflow.json."
3963
+ ).action(() => executeTriggerList({}));
3964
+ registerTriggerList(trigger);
3965
+ }
3966
+
3967
+ // src/commands/config/get.ts
3968
+ var UNSET_PLACEHOLDER = "(unset)";
3969
+ function executeConfigGet(args) {
3970
+ const file = configService.fileValues();
3971
+ if (args.key) {
3972
+ if (!isSettableConfigKey(args.key)) {
3973
+ printError(
3974
+ `Unknown config key "${args.key}". Valid keys: ${SETTABLE_CONFIG_KEYS.join(", ")}.`
3975
+ );
3976
+ exit(ExitCode.InvalidArgs);
3977
+ }
3978
+ const value = file[args.key];
3979
+ if (args.json) {
3980
+ printJson({ [args.key]: value ?? null });
3981
+ return;
3982
+ }
3983
+ process.stdout.write((value ?? UNSET_PLACEHOLDER) + "\n");
3984
+ return;
3985
+ }
3986
+ if (args.json) {
3987
+ const payload = {};
3988
+ for (const k of SETTABLE_CONFIG_KEYS) payload[k] = file[k] ?? null;
3989
+ printJson(payload);
3990
+ return;
3991
+ }
3992
+ printTable(
3993
+ ["KEY", "VALUE"],
3994
+ SETTABLE_CONFIG_KEYS.map((k) => [k, file[k] ?? UNSET_PLACEHOLDER])
3995
+ );
3996
+ }
3997
+ function registerConfigGet2(parent) {
3998
+ parent.command("get").argument(
3999
+ "[key]",
4000
+ `Specific key to read (${SETTABLE_CONFIG_KEYS.join(" | ")}). Omit to list every settable key with its value.`
4001
+ ).description(
4002
+ "Print what's written to the persistent config file. Symmetric with `geni config set` \u2014 whatever you wrote is what you read. Unset keys render as `(unset)` in table output and `null` in --json. For the URL the CLI is actually hitting at runtime (which can differ if a runner-session is bound to a different server), run `geni auth status`."
4003
+ ).option(
4004
+ "--json",
4005
+ "Machine-readable output. Unset keys are emitted as JSON `null`."
4006
+ ).action(
4007
+ (key, opts) => executeConfigGet({ key, json: opts.json })
4008
+ );
4009
+ }
4010
+
4011
+ // src/commands/config/set.ts
4012
+ function registerConfigSet2(parent) {
2465
4013
  parent.command("set").argument("<key>", `Config key (${SETTABLE_CONFIG_KEYS.join(" | ")}).`).argument(
2466
4014
  "<value>",
2467
4015
  "New value. URL keys must be valid http:// or https:// URLs (validated on write)."
@@ -2537,8 +4085,8 @@ function registerConfigCommands(program2) {
2537
4085
  const config = program2.command("config").description(
2538
4086
  "Read and write the persistent CLI config (`~/.config/geni/config.json` by default; honors $GENI_CONFIG_DIR). Holds defaults the resolver consults when nothing more specific is set, useful for pointing the CLI at a self-hosted or local-dev server without re-passing `--server` or exporting an env var on every shell."
2539
4087
  ).action(() => executeConfigGet({}));
2540
- registerConfigGet(config);
2541
- registerConfigSet(config);
4088
+ registerConfigGet2(config);
4089
+ registerConfigSet2(config);
2542
4090
  registerConfigUnset(config);
2543
4091
  registerConfigPath(config);
2544
4092
  config.addHelpText(
@@ -2568,38 +4116,38 @@ logout (or use \`geni login --server <url>\`) to switch in one step.
2568
4116
 
2569
4117
  // src/lib/skills.ts
2570
4118
  import { homedir as homedir2 } from "os";
2571
- import { join as join2 } from "path";
4119
+ import { join as join4 } from "path";
2572
4120
  import {
2573
- mkdirSync,
2574
- writeFileSync,
2575
- existsSync,
2576
- readFileSync as readFileSync2,
4121
+ mkdirSync as mkdirSync3,
4122
+ writeFileSync as writeFileSync3,
4123
+ existsSync as existsSync3,
4124
+ readFileSync as readFileSync4,
2577
4125
  unlinkSync,
2578
4126
  rmSync
2579
4127
  } from "fs";
2580
4128
 
2581
4129
  // src/skills/geni.md
2582
- var geni_default = "---\nname: geni\ndescription: Use the operator's connected services (Slack, Gmail, GitHub, Stripe, anything they've authorized in General Input) to fulfill their request. Load this whenever the user wants you to take an action on their behalf against an external SaaS account, fetch data from one of their tools, or wire something up that crosses service boundaries.\n---\n\n# geni\n\n`geni` is a CLI that gives you credentialed access to the operator's\nconnected accounts. The cloud injects their tokens into a fresh bash\nsubprocess as env vars; you write the `curl`, you never see the\nsecret. Run `geni --help` for the full command surface.\n\n## How to talk about geni\n\nTreat geni as a teammate \u2014 the integration specialist who picks the\nright operation and runs the credentialed call. You're the one\nsynthesizing and replying.\n\nIn summaries of multi-step work, credit geni naturally: \"Geni pulled\nthe last 30 days of charges from Stripe; I rolled them into the report\nbelow.\" Skip the credit on trivial single calls; don't manufacture\nteam language (\"with geni's help\u2026\") to fill space. Lowercase `geni`\nin commands; capitalize \"Geni\" when personifying.\n\n## Request flow\n\nDiscovery first. Don't guess URLs, params, or env var names \u2014 the\noperation docs have them.\n\n1. `geni credential list [--service <slug>]` \u2014 find a connected\n credential id. If nothing's connected for the service, run\n `geni integration list -q \"<keyword>\"` and `geni credential\nconnect <service>` to prompt the operator to authorize.\n2. `geni integration operations <service> [-q \"<keyword>\"]` \u2014 find\n the operation you need.\n3. `geni integration operation <id> --format markdown` \u2014 read the\n HTTP shape, params, and **exact env var names** this call will set.\n4. `geni exec bash --cred <cred_id> --reason \"<what + why>\" -- '<curl>'`\n\n## Env vars\n\nRead the names off the operation docs; never derive them. Two flavors\nemitted per credential:\n\n- **Suffixed**: `<SERVICE>_<FIELD>_<id>` (e.g. `$SLACK_BOT_TOKEN_ABC`,\n `$SALESFORCE_INSTANCE_URL_KG`). The `<id>` is the credential id with\n `cred_` stripped, uppercased. Use these when fanning out across\n multiple credentials of the same service in one call.\n- **Canonical aliases**: well-known SDK names (`$GH_TOKEN`,\n `$SLACK_BOT_TOKEN`, `$OPENAI_API_KEY`, `$STRIPE_API_KEY`, \u2026) for\n tools that hardcode them.\n\nPlus `$PLATFORM_API_KEY` and `$PLATFORM_BASE_URL` on every exec \u2014 use\nthem to call General Input's first-party services (web search, image\ngen, weather, send-email) without `--cred`.\n\n## --reason is per-call\n\nEvery `geni exec bash --cred ...` requires `--reason`. It lands in\nthe operator's audit log and is shown to them. Be specific\n(\"Posting daily digest to #engineering\"), not generic (\"Slack call\").\nRe-state it on every call \u2014 the log is per-call, not per-session.\n\n## Output is scrubbed\n\nstdout and stderr pass through a streaming scrubber that replaces\nevery registered secret with `[REDACTED:credential_<id>]`. Don't try\nto exfiltrate via `echo $TOKEN | base64`; common encodings are\ncaught too. You don't need to see the secret to use it \u2014 the\nsubprocess can.\n\n## Exit codes worth knowing\n\n- `0` \u2014 success; `1\u2013125` \u2014 subprocess's own exit (curl, jq, etc.)\n- `4` \u2014 resource not found (bad cred id, slug, operation id)\n- `77` \u2014 server refused to resolve a credential. Don't retry; surface\n to the operator.\n- `78` \u2014 session expired. Tell the operator to run `geni login`.\n\n## Don't\n\n- Don't construct a curl without reading the operation docs first.\n Guessing at URLs and env var names is the most common failure mode.\n- Don't promise scheduled or recurring jobs. Workflows are coming\n soon but not shipped; offer the one-off equivalent now.\n- Use `--json` on list/get commands when you're going to parse the\n output. Table output is for humans.\n";
4130
+ var geni_default = "---\nname: geni\ndescription: Use the operator's connected services (Slack, Gmail, GitHub, Stripe, anything they've authorized in General Input) to fulfill their request, or build them a saved workflow (scheduled or event-driven automation). Load this whenever the user wants you to take an action on their behalf against an external SaaS account, fetch data from one of their tools, wire something up that crosses service boundaries, or set up a recurring/triggered automation.\n---\n\n# geni\n\n`geni` is a CLI that gives you credentialed access to the operator's\nconnected accounts. The cloud injects their tokens into a fresh bash\nsubprocess as env vars; you write the `curl`, you never see the\nsecret. Run `geni --help` for the full command surface.\n\n## How to talk about geni\n\nTreat geni as a teammate \u2014 the integration specialist who picks the\nright operation and runs the credentialed call. You're the one\nsynthesizing and replying.\n\nIn summaries of multi-step work, credit geni naturally: \"Geni pulled\nthe last 30 days of charges from Stripe; I rolled them into the report\nbelow.\" Skip the credit on trivial single calls; don't manufacture\nteam language (\"with geni's help\u2026\") to fill space. Lowercase `geni`\nin commands; capitalize \"Geni\" when personifying.\n\n## Request flow\n\nDiscovery first. Don't guess URLs, params, or env var names \u2014 the\noperation docs have them.\n\n1. `geni credential list [--service <slug>]` \u2014 find a connected\n credential id. If nothing's connected for the service, run\n `geni integration list -q \"<keyword>\"` to confirm the slug, then\n `geni credential connect <service>` (see \"Connecting a missing\n credential\" below).\n2. `geni integration operations <service> [-q \"<keyword>\"]` \u2014 find\n the operation you need.\n3. `geni integration operation <id> --format markdown` \u2014 read the\n HTTP shape, params, and **exact env var names** this call will set.\n4. `geni exec bash --cred <cred_id> --reason \"<what + why>\" -- '<curl>'`\n\n## Connecting a missing credential\n\n`geni credential connect <service>` opens the integration's\ndashboard page in the operator's browser. Tell the operator to\nfinish the connection there and come back \u2014 then re-run `geni\ncredential list --service <service>` to pick up the new id. Don't\npoll, don't loop. In non-interactive shells, add `--print-url` so\nthe URL prints to stdout instead.\n\n## Env vars\n\nRead the names off the operation docs; never derive them. Two flavors\nemitted per credential:\n\n- **Suffixed**: `<SERVICE>_<FIELD>_<id>` (e.g. `$SLACK_BOT_TOKEN_ABC`,\n `$SALESFORCE_INSTANCE_URL_KG`). The `<id>` is the credential id with\n `cred_` stripped, uppercased. Use these when fanning out across\n multiple credentials of the same service in one call.\n- **Canonical aliases**: well-known SDK names (`$GH_TOKEN`,\n `$SLACK_BOT_TOKEN`, `$OPENAI_API_KEY`, `$STRIPE_API_KEY`, \u2026) for\n tools that hardcode them.\n\nPlus `$PLATFORM_API_KEY` and `$PLATFORM_BASE_URL` on every exec \u2014 use\nthem to call General Input's first-party services (web search, image\ngen, weather, send-email) without `--cred`.\n\n## --reason is per-call\n\nEvery `geni exec bash --cred ...` requires `--reason`. It lands in\nthe operator's audit log and is shown to them. Be specific\n(\"Posting daily digest to #engineering\"), not generic (\"Slack call\").\nRe-state it on every call \u2014 the log is per-call, not per-session.\n\n## Output is scrubbed\n\nstdout and stderr pass through a streaming scrubber that replaces\nevery registered secret with `[REDACTED:credential_<id>]`. Don't try\nto exfiltrate via `echo $TOKEN | base64`; common encodings are\ncaught too. You don't need to see the secret to use it \u2014 the\nsubprocess can.\n\n## Building resources (workflows and apps)\n\n`geni exec bash` is for one-off actions. When the user wants something\nsaved and reusable, build a **resource**. A resource is one of three\ntypes: a `code` **workflow** (deterministic, fixed steps, runs on a\nschedule or trigger), an `agent` **workflow** (LLM-driven, variable-length\ntasks like research or triage), or an `app` (an interactive React mini-app\nwith server-side handlers, like a dashboard or internal tool). An app is\nnot a workflow, so the shared lifecycle lives under `geni resource` and\nthe type-specific tools under `geni workflow` and `geni app`.\n\nYou author the files locally with your own editor; the cloud validates,\nconfigures, publishes, and runs them. This is the same build loop as the\ngeni chat, just on the user's machine with your brain driving it. Same\nloop for all three types; apps add a build step and handler tools (see\n\"Building apps\" below).\n\n**Read the spec first.** `geni resource spec code` (or `agent`) prints\nthe full contract: required files, the runtime API, allowed and\nforbidden patterns. Don't write a single file before reading it.\n\nThe loop:\n\n1. `geni resource create --type code --name \"Daily Digest\"`: creates the\n resource in the cloud and scaffolds its files into the current\n directory, with a `.geni-resource.json` marker so later commands here\n resolve the id automatically.\n2. Edit the files (`workflow.json`, `main.ts`, `package.json`) with your\n normal Read/Write/Edit tools. **Don't run `npm install`, `tsc`, or\n `bun` locally**: `@general-input/core` only resolves server-side, so\n a local compile always fails on the missing module. That's not a real\n error.\n3. `geni resource validate`: server-side spec + wiring + TypeScript\n check. Exit 9 means invalid; fix what it reports and re-run. A green\n validate is the compile gate.\n4. `geni resource config set`: wire what the workflow needs. Find\n credential ids with `geni credential list`; set them with\n `--cred <service>=<credentialId>`. Set consts with `--const k=v`,\n schedules with `--schedule <triggerId>=<cron>`. `geni resource config\nget` shows the current state. Find trigger providers with\n `geni trigger list`.\n5. `geni resource publish --summary \"<one plain sentence>\"`: promote\n your local files to the live version. Required before a test reflects\n your latest edits.\n6. `geni workflow test --payload '{...}'`: run a real cloud test\n execution and wait for the result, output, and logs. Iterate from\n step 2 until it does what the user asked.\n\nWhen a run misbehaves, `geni workflow execution <id> --download` writes\nits full trace to `executions/<id>/` (run.json, logs.txt, input.json, and\nthread.json). For an agent workflow, read `thread.json` to see every step\nthe agent took. Use `--thread` or `--input` to print those inline instead.\n\nUse the same discovery tools as exec: `geni integration operations\n<service>` and `geni integration operation <id>` to learn an API's shape\nbefore you write the `fetch` call in `main.ts`.\n\nFor a poll-triggered workflow, `geni workflow trigger-sample` returns\nlive sample records so you see the exact shape `input` receives before\nwriting `main.ts` (wire the trigger's credential and inputs with\n`config set` first). To test a file-type input, `geni workflow\nstage-input <file>` uploads a local file and prints a `store://` URI to\npass as that field's value in `--payload`.\n\n## Building apps\n\nAn `app` workflow is a React SPA (rendered in the dashboard's iframe)\nthat calls server-side handlers. Create it with `geni resource create\n--type app`; read the contract with `geni resource spec app`. The shared\ncommands work the same (validate, config set for credential slots,\npublish), with these differences:\n\n- The scaffold is a whole Vite project. Edit only `app.json`, `src/App.tsx`,\n `src/pages/*`, your own `src/components/*`, and `handlers/*.ts`. Leave\n the platform-owned files alone.\n- `geni resource publish` builds the app; the result's `build` reports\n whether the Vite build passed. You can't preview the UI from the CLI,\n so drive correctness through handlers and the build.\n- `geni app run-handler <name> --input '{...}'` runs a handler\n against the live app (publish first). `geni app invocations`\n shows recent handler runs, `geni app errors` shows browser\n errors the iframe reported, `geni app build-status` shows the\n build, and `geni app clear-cache` flushes the handler cache.\n\nThe loop: create --type app -> edit handlers + pages -> validate ->\nconfig set (wire credential slots) -> publish (builds) -> app run-handler\nto verify -> tell the user to open it in their dashboard.\n\nWhen it works, tell the user it's in their dashboard, where they can see\nruns and turn its triggers on. Commands accept the workflow id as a\npositional or infer it from the marker in the current directory.\n\n## Exit codes worth knowing\n\n- `0` \u2014 success; `1\u2013125` \u2014 subprocess's own exit (curl, jq, etc.)\n- `4` \u2014 resource not found (bad cred id, slug, operation id)\n- `77` \u2014 server refused to resolve a credential. Don't retry; surface\n to the operator.\n- `78` \u2014 session expired. Tell the operator to run `geni login`.\n\n## Don't\n\n- Don't construct a curl without reading the operation docs first.\n Guessing at URLs and env var names is the most common failure mode.\n- Don't hand-write workflow files before reading `geni resource spec\n<type>`. Don't try to compile or `npm install` them locally; let\n `geni resource validate` be the gate.\n- Use `--json` on list/get commands when you're going to parse the\n output. Table output is for humans.\n";
2583
4131
 
2584
4132
  // src/lib/skills.ts
2585
4133
  var GENI_SKILL_NAME = "geni";
2586
4134
  var GENI_SKILL_MD = geni_default;
2587
4135
  var home = homedir2();
2588
- var claudeSkillsDir = join2(home, ".claude", "skills");
2589
- var claudeGeniDir = join2(claudeSkillsDir, GENI_SKILL_NAME);
2590
- var agentSkillsDir = join2(home, ".agents", "skills");
2591
- var agentSkillsGeniDir = join2(agentSkillsDir, GENI_SKILL_NAME);
4136
+ var claudeSkillsDir = join4(home, ".claude", "skills");
4137
+ var claudeGeniDir = join4(claudeSkillsDir, GENI_SKILL_NAME);
4138
+ var agentSkillsDir = join4(home, ".agents", "skills");
4139
+ var agentSkillsGeniDir = join4(agentSkillsDir, GENI_SKILL_NAME);
2592
4140
  var TARGETS = [
2593
4141
  {
2594
4142
  name: "Claude Code",
2595
- detect: () => existsSync(join2(home, ".claude")),
4143
+ detect: () => existsSync3(join4(home, ".claude")),
2596
4144
  skillDir: claudeGeniDir,
2597
- skillPath: join2(claudeGeniDir, "SKILL.md"),
4145
+ skillPath: join4(claudeGeniDir, "SKILL.md"),
2598
4146
  // Earlier versions of `geni skills install` wrote a loose .md file
2599
4147
  // at `~/.claude/skills/geni.md`, which Claude Code silently ignores.
2600
4148
  // Clean it up on install/uninstall so upgraders aren't left with a
2601
4149
  // stale sibling that confuses `doctor`.
2602
- legacyPaths: [join2(claudeSkillsDir, `${GENI_SKILL_NAME}.md`)]
4150
+ legacyPaths: [join4(claudeSkillsDir, `${GENI_SKILL_NAME}.md`)]
2603
4151
  },
2604
4152
  {
2605
4153
  name: "Codex CLI / Gemini CLI / VS Code Copilot",
@@ -2609,9 +4157,9 @@ var TARGETS = [
2609
4157
  // because it lives inside VS Code's user data with no clean
2610
4158
  // home-dir signal — Copilot users typically have one of the CLIs
2611
4159
  // installed too, and the install is idempotent if they don't.
2612
- detect: () => existsSync(join2(home, ".codex")) || existsSync(join2(home, ".gemini")) || existsSync(join2(home, ".agents")),
4160
+ detect: () => existsSync3(join4(home, ".codex")) || existsSync3(join4(home, ".gemini")) || existsSync3(join4(home, ".agents")),
2613
4161
  skillDir: agentSkillsGeniDir,
2614
- skillPath: join2(agentSkillsGeniDir, "SKILL.md"),
4162
+ skillPath: join4(agentSkillsGeniDir, "SKILL.md"),
2615
4163
  legacyPaths: []
2616
4164
  }
2617
4165
  ];
@@ -2629,12 +4177,12 @@ function installSkills() {
2629
4177
  if (!target.detect()) continue;
2630
4178
  const path = target.skillPath;
2631
4179
  try {
2632
- mkdirSync(target.skillDir, { recursive: true });
2633
- const previous = existsSync(path) ? readFileSync2(path, "utf-8") : null;
4180
+ mkdirSync3(target.skillDir, { recursive: true });
4181
+ const previous = existsSync3(path) ? readFileSync4(path, "utf-8") : null;
2634
4182
  const changed = previous !== GENI_SKILL_MD;
2635
- writeFileSync(path, GENI_SKILL_MD);
4183
+ writeFileSync3(path, GENI_SKILL_MD);
2636
4184
  for (const legacy of target.legacyPaths) {
2637
- if (existsSync(legacy)) unlinkSync(legacy);
4185
+ if (existsSync3(legacy)) unlinkSync(legacy);
2638
4186
  }
2639
4187
  results.push({
2640
4188
  name: target.name,
@@ -2658,8 +4206,8 @@ function uninstallSkills() {
2658
4206
  if (!target.detect()) continue;
2659
4207
  const dir = target.skillDir;
2660
4208
  const path = target.skillPath;
2661
- const legacies = target.legacyPaths.filter((p2) => existsSync(p2));
2662
- if (!existsSync(dir) && legacies.length === 0) {
4209
+ const legacies = target.legacyPaths.filter((p2) => existsSync3(p2));
4210
+ if (!existsSync3(dir) && legacies.length === 0) {
2663
4211
  results.push({ name: target.name, path, status: "absent" });
2664
4212
  continue;
2665
4213
  }
@@ -2744,10 +4292,10 @@ function registerSkillsCommands(program2) {
2744
4292
  }
2745
4293
 
2746
4294
  // src/commands/doctor.ts
2747
- import chalk8 from "chalk";
4295
+ import chalk9 from "chalk";
2748
4296
 
2749
4297
  // src/lib/preflight.ts
2750
- import { delimiter, join as join3 } from "path";
4298
+ import { delimiter, join as join5 } from "path";
2751
4299
  import { accessSync, constants } from "fs";
2752
4300
  var REQUIRED_RUNTIME_DEPS = ["bash", "curl", "jq"];
2753
4301
  function findOnPath(name) {
@@ -2755,7 +4303,7 @@ function findOnPath(name) {
2755
4303
  if (!path) return null;
2756
4304
  for (const dir of path.split(delimiter)) {
2757
4305
  if (dir.length === 0) continue;
2758
- const candidate = join3(dir, name);
4306
+ const candidate = join5(dir, name);
2759
4307
  try {
2760
4308
  accessSync(candidate, constants.X_OK);
2761
4309
  return candidate;
@@ -2795,7 +4343,7 @@ function installHint(deps) {
2795
4343
  }
2796
4344
 
2797
4345
  // src/commands/doctor.ts
2798
- import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
4346
+ import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
2799
4347
  function registerDoctorCommand(program2) {
2800
4348
  program2.command("doctor").description(
2801
4349
  "Diagnose your geni setup: required system tools, active session, network reach to the cloud, and skill installation across detected AI agents. Prints a checklist with \u2713/\u2717 for each."
@@ -2898,7 +4446,7 @@ function skillsCheck() {
2898
4446
  }
2899
4447
  const out = [];
2900
4448
  for (const target of targets) {
2901
- if (!existsSync2(target.path)) {
4449
+ if (!existsSync4(target.path)) {
2902
4450
  out.push({
2903
4451
  label: `${target.name} skill installed`,
2904
4452
  status: "fail",
@@ -2906,7 +4454,7 @@ function skillsCheck() {
2906
4454
  });
2907
4455
  continue;
2908
4456
  }
2909
- const onDisk = readFileSync3(target.path, "utf-8");
4457
+ const onDisk = readFileSync5(target.path, "utf-8");
2910
4458
  if (onDisk === GENI_SKILL_MD) {
2911
4459
  out.push({
2912
4460
  label: `${target.name} skill installed`,
@@ -2925,17 +4473,17 @@ function skillsCheck() {
2925
4473
  }
2926
4474
  function printReport(checks) {
2927
4475
  for (const check of checks) {
2928
- const mark = check.status === "pass" ? chalk8.green("\u2713") : check.status === "warn" ? chalk8.yellow("!") : chalk8.red("\u2717");
4476
+ const mark = check.status === "pass" ? chalk9.green("\u2713") : check.status === "warn" ? chalk9.yellow("!") : chalk9.red("\u2717");
2929
4477
  process.stdout.write(`${mark} ${check.label}
2930
4478
  `);
2931
- process.stdout.write(` ${chalk8.dim(check.detail)}
4479
+ process.stdout.write(` ${chalk9.dim(check.detail)}
2932
4480
  `);
2933
4481
  }
2934
4482
  const fails = checks.filter((c) => c.status === "fail").length;
2935
4483
  const warns = checks.filter((c) => c.status === "warn").length;
2936
4484
  process.stdout.write("\n");
2937
4485
  if (fails === 0 && warns === 0) {
2938
- process.stdout.write(chalk8.green("All checks passed.\n"));
4486
+ process.stdout.write(chalk9.green("All checks passed.\n"));
2939
4487
  } else {
2940
4488
  process.stdout.write(
2941
4489
  `${fails} failure${fails === 1 ? "" : "s"}, ${warns} warning${warns === 1 ? "" : "s"}.
@@ -2957,6 +4505,10 @@ registerWorkspaceCommands(program);
2957
4505
  registerExecCommands(program);
2958
4506
  registerCredentialCommands(program);
2959
4507
  registerIntegrationCommands(program);
4508
+ registerResourceCommands(program);
4509
+ registerWorkflowCommands(program);
4510
+ registerAppCommands(program);
4511
+ registerTriggerCommands(program);
2960
4512
  registerConfigCommands(program);
2961
4513
  registerSkillsCommands(program);
2962
4514
  registerDoctorCommand(program);