@general-input/cli 0.1.3 → 0.3.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.3",
387
+ version: "0.3.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"
@@ -1057,6 +1059,302 @@ var ConfigService = class {
1057
1059
  }
1058
1060
  };
1059
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, 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 listResourceRelPaths(dir) {
1111
+ if (!existsSync(dir)) {
1112
+ printError(`Directory not found: ${dir}`);
1113
+ exit(ExitCode.InvalidArgs);
1114
+ }
1115
+ const out = [];
1116
+ walk(dir, dir, out);
1117
+ return out;
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
+ out.push(relative(root, full).split(sep).join("/"));
1131
+ }
1132
+ }
1133
+ var MIME_BY_EXT = {
1134
+ json: "application/json",
1135
+ csv: "text/csv",
1136
+ txt: "text/plain",
1137
+ md: "text/markdown",
1138
+ pdf: "application/pdf",
1139
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
1140
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
1141
+ pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
1142
+ png: "image/png",
1143
+ jpg: "image/jpeg",
1144
+ jpeg: "image/jpeg",
1145
+ gif: "image/gif",
1146
+ html: "text/html",
1147
+ xml: "application/xml",
1148
+ zip: "application/zip"
1149
+ };
1150
+ function guessMimeType(path) {
1151
+ const ext = path.split(".").pop()?.toLowerCase() ?? "";
1152
+ return MIME_BY_EXT[ext] ?? "application/octet-stream";
1153
+ }
1154
+ function dirHasResourceFiles(dir) {
1155
+ if (!existsSync(dir)) return false;
1156
+ return readdirSync(dir).some(
1157
+ (entry) => !entry.startsWith(".") && !SKIP_DIRS.has(entry)
1158
+ );
1159
+ }
1160
+
1161
+ // src/lib/bundleArchive.ts
1162
+ import { mkdirSync as mkdirSync2 } from "fs";
1163
+ import { Readable } from "stream";
1164
+ import { create, extract } from "tar";
1165
+ async function packResourceDir(dir) {
1166
+ const files = listResourceRelPaths(dir);
1167
+ const stream = create({ gzip: true, cwd: dir, portable: true }, files);
1168
+ const chunks = [];
1169
+ for await (const chunk of stream) {
1170
+ chunks.push(Buffer.from(chunk));
1171
+ }
1172
+ return Buffer.concat(chunks);
1173
+ }
1174
+ async function extractBundle(dir, buffer) {
1175
+ mkdirSync2(dir, { recursive: true });
1176
+ await new Promise((resolve20, reject) => {
1177
+ const unpack = extract({
1178
+ cwd: dir,
1179
+ filter: (path) => !SKIP_DIRS.has(path.split("/")[0])
1180
+ });
1181
+ unpack.on("close", resolve20);
1182
+ unpack.on("error", reject);
1183
+ Readable.from(buffer).on("error", reject).pipe(unpack);
1184
+ });
1185
+ }
1186
+
1187
+ // src/services/WorkflowAuthoringService.ts
1188
+ var WorkflowAuthoringService = class {
1189
+ constructor(sessionContext) {
1190
+ this.sessionContext = sessionContext;
1191
+ }
1192
+ sessionContext;
1193
+ /** Create a workflow in the cloud, then pull its scaffold files into `dir`. */
1194
+ async create(args) {
1195
+ const { client } = await this.sessionContext.requireAuthed();
1196
+ const detail = await client.workflows.create({
1197
+ workflowType: args.workflowType,
1198
+ name: args.name
1199
+ });
1200
+ const { downloadUrl } = await client.workflows.bundleUrl(detail.id);
1201
+ const fileCount = await this.downloadBundleInto(downloadUrl, args.dir);
1202
+ writeMarker(args.dir, {
1203
+ resourceId: detail.id,
1204
+ resourceType: detail.workflowType
1205
+ });
1206
+ return { detail, fileCount };
1207
+ }
1208
+ async list() {
1209
+ const { client } = await this.sessionContext.requireAuthed();
1210
+ return client.workflows.list();
1211
+ }
1212
+ async get(id) {
1213
+ const { client } = await this.sessionContext.requireAuthed();
1214
+ return client.workflows.get(id);
1215
+ }
1216
+ async setType(id, workflowType) {
1217
+ const { client } = await this.sessionContext.requireAuthed();
1218
+ return client.workflows.setType(id, { workflowType });
1219
+ }
1220
+ /** Download a workflow's live files into `dir` + write its marker. */
1221
+ async pull(args) {
1222
+ const { client } = await this.sessionContext.requireAuthed();
1223
+ const { workflowType, downloadUrl } = await client.workflows.bundleUrl(
1224
+ args.id
1225
+ );
1226
+ const fileCount = await this.downloadBundleInto(downloadUrl, args.dir);
1227
+ writeMarker(args.dir, {
1228
+ resourceId: args.id,
1229
+ resourceType: workflowType
1230
+ });
1231
+ return { fileCount, workflowType };
1232
+ }
1233
+ async validate(args) {
1234
+ const { client } = await this.sessionContext.requireAuthed();
1235
+ const bundleKey = await this.uploadBundle(args.id, args.dir);
1236
+ return client.workflows.validate(args.id, { bundleKey });
1237
+ }
1238
+ async publish(args) {
1239
+ const { client } = await this.sessionContext.requireAuthed();
1240
+ const bundleKey = await this.uploadBundle(args.id, args.dir);
1241
+ return client.workflows.publish(args.id, {
1242
+ bundleKey,
1243
+ changeSummary: args.changeSummary
1244
+ });
1245
+ }
1246
+ /** Tar `dir`, PUT it to a presigned URL, return the staged `bundleKey`. */
1247
+ async uploadBundle(id, dir) {
1248
+ const { client } = await this.sessionContext.requireAuthed();
1249
+ const buffer = await packResourceDir(dir);
1250
+ const { uploadUrl, bundleKey, contentType } = await client.workflows.bundleUploadUrl(id);
1251
+ const response = await fetch(uploadUrl, {
1252
+ method: "PUT",
1253
+ headers: { "Content-Type": contentType },
1254
+ body: new Uint8Array(buffer)
1255
+ });
1256
+ if (!response.ok) {
1257
+ throw new Error(`Bundle upload failed (HTTP ${response.status}).`);
1258
+ }
1259
+ return bundleKey;
1260
+ }
1261
+ /** GET a presigned bundle URL, extract it into `dir`, return the file count. */
1262
+ async downloadBundleInto(downloadUrl, dir) {
1263
+ const response = await fetch(downloadUrl);
1264
+ if (!response.ok) {
1265
+ throw new Error(`Bundle download failed (HTTP ${response.status}).`);
1266
+ }
1267
+ const buffer = Buffer.from(await response.arrayBuffer());
1268
+ await extractBundle(dir, buffer);
1269
+ return listResourceRelPaths(dir).length;
1270
+ }
1271
+ async getConfig(id) {
1272
+ const { client } = await this.sessionContext.requireAuthed();
1273
+ return client.workflows.getConfig(id);
1274
+ }
1275
+ async updateConfig(id, partial) {
1276
+ const { client } = await this.sessionContext.requireAuthed();
1277
+ return client.workflows.updateConfig(id, partial);
1278
+ }
1279
+ async test(id, triggerPayload) {
1280
+ const { client } = await this.sessionContext.requireAuthed();
1281
+ return client.workflows.test(id, { triggerPayload });
1282
+ }
1283
+ async getExecution(id, executionId) {
1284
+ const { client } = await this.sessionContext.requireAuthed();
1285
+ return client.workflows.getExecution(id, executionId);
1286
+ }
1287
+ async getExecutionLogs(id, executionId) {
1288
+ const { client } = await this.sessionContext.requireAuthed();
1289
+ return client.workflows.getExecutionLogs(id, executionId);
1290
+ }
1291
+ async getExecutionTrace(id, executionId) {
1292
+ const { client } = await this.sessionContext.requireAuthed();
1293
+ return client.workflows.getExecutionTrace(id, executionId);
1294
+ }
1295
+ async listExecutions(id, limit) {
1296
+ const { client } = await this.sessionContext.requireAuthed();
1297
+ return client.workflows.listExecutions(id, limit);
1298
+ }
1299
+ async triggerSample(id) {
1300
+ const { client } = await this.sessionContext.requireAuthed();
1301
+ return client.workflows.triggerSample(id);
1302
+ }
1303
+ /**
1304
+ * Upload a local file as a file-type test input: presign, PUT the
1305
+ * bytes, return the `store://` URI to pass in a test payload.
1306
+ */
1307
+ async stageInput(args) {
1308
+ const { client } = await this.sessionContext.requireAuthed();
1309
+ const bytes = readFileSync2(args.localPath);
1310
+ const filename = basename(args.localPath);
1311
+ const mimeType = guessMimeType(args.localPath);
1312
+ const { uploadUrl, storageUri } = await client.workflows.inputUploadUrl(
1313
+ args.id,
1314
+ { filename, mimeType }
1315
+ );
1316
+ const response = await fetch(uploadUrl, {
1317
+ method: "PUT",
1318
+ headers: { "Content-Type": mimeType },
1319
+ body: new Uint8Array(bytes)
1320
+ });
1321
+ if (!response.ok) {
1322
+ throw new Error(
1323
+ `Upload failed (HTTP ${response.status}) for ${filename}.`
1324
+ );
1325
+ }
1326
+ return { storageUri, filename };
1327
+ }
1328
+ async appBuildStatus(id) {
1329
+ const { client } = await this.sessionContext.requireAuthed();
1330
+ return client.workflows.appBuildStatus(id);
1331
+ }
1332
+ async appInvocations(id, opts) {
1333
+ const { client } = await this.sessionContext.requireAuthed();
1334
+ return client.workflows.appInvocations(id, opts);
1335
+ }
1336
+ async runHandler(id, body) {
1337
+ const { client } = await this.sessionContext.requireAuthed();
1338
+ return client.workflows.runHandler(id, body);
1339
+ }
1340
+ async appErrors(id, opts) {
1341
+ const { client } = await this.sessionContext.requireAuthed();
1342
+ return client.workflows.appErrors(id, opts);
1343
+ }
1344
+ async clearHandlerCache(id) {
1345
+ const { client } = await this.sessionContext.requireAuthed();
1346
+ return client.workflows.clearHandlerCache(id);
1347
+ }
1348
+ async spec(type) {
1349
+ const { client } = await this.sessionContext.requireAuthed();
1350
+ return client.workflows.spec(type);
1351
+ }
1352
+ async listTriggers(query) {
1353
+ const { client } = await this.sessionContext.requireAuthed();
1354
+ return client.workflows.listTriggers(query);
1355
+ }
1356
+ };
1357
+
1060
1358
  // src/clients/AuthApiClient.ts
1061
1359
  var AuthApiClient = class {
1062
1360
  constructor(http) {
@@ -1176,6 +1474,166 @@ var OperationsApiClient = class {
1176
1474
  }
1177
1475
  };
1178
1476
 
1477
+ // src/clients/WorkflowsApiClient.ts
1478
+ var WorkflowsApiClient = class {
1479
+ constructor(http) {
1480
+ this.http = http;
1481
+ }
1482
+ http;
1483
+ async list() {
1484
+ this.http.requireAuthed();
1485
+ return this.http.fetch("/cli/workflows");
1486
+ }
1487
+ async create(body) {
1488
+ this.http.requireAuthed();
1489
+ return this.http.fetch("/cli/workflows", { method: "POST", body });
1490
+ }
1491
+ async get(id) {
1492
+ this.http.requireAuthed();
1493
+ return this.http.fetch(`/cli/workflows/${encodeURIComponent(id)}`);
1494
+ }
1495
+ async setType(id, body) {
1496
+ this.http.requireAuthed();
1497
+ return this.http.fetch(`/cli/workflows/${encodeURIComponent(id)}/type`, {
1498
+ method: "PATCH",
1499
+ body
1500
+ });
1501
+ }
1502
+ async bundleUrl(id) {
1503
+ this.http.requireAuthed();
1504
+ return this.http.fetch(
1505
+ `/cli/workflows/${encodeURIComponent(id)}/bundle-url`
1506
+ );
1507
+ }
1508
+ async bundleUploadUrl(id) {
1509
+ this.http.requireAuthed();
1510
+ return this.http.fetch(
1511
+ `/cli/workflows/${encodeURIComponent(id)}/bundle-upload-url`,
1512
+ { method: "POST" }
1513
+ );
1514
+ }
1515
+ async validate(id, body) {
1516
+ this.http.requireAuthed();
1517
+ return this.http.fetch(
1518
+ `/cli/workflows/${encodeURIComponent(id)}/validate`,
1519
+ {
1520
+ method: "POST",
1521
+ body
1522
+ }
1523
+ );
1524
+ }
1525
+ async publish(id, body) {
1526
+ this.http.requireAuthed();
1527
+ return this.http.fetch(`/cli/workflows/${encodeURIComponent(id)}/publish`, {
1528
+ method: "POST",
1529
+ body
1530
+ });
1531
+ }
1532
+ async getConfig(id) {
1533
+ this.http.requireAuthed();
1534
+ return this.http.fetch(`/cli/workflows/${encodeURIComponent(id)}/config`);
1535
+ }
1536
+ async updateConfig(id, body) {
1537
+ this.http.requireAuthed();
1538
+ return this.http.fetch(`/cli/workflows/${encodeURIComponent(id)}/config`, {
1539
+ method: "PATCH",
1540
+ body
1541
+ });
1542
+ }
1543
+ async test(id, body) {
1544
+ this.http.requireAuthed();
1545
+ return this.http.fetch(`/cli/workflows/${encodeURIComponent(id)}/test`, {
1546
+ method: "POST",
1547
+ body
1548
+ });
1549
+ }
1550
+ async listExecutions(id, limit) {
1551
+ this.http.requireAuthed();
1552
+ const query = limit ? `?limit=${limit}` : "";
1553
+ return this.http.fetch(
1554
+ `/cli/workflows/${encodeURIComponent(id)}/executions${query}`
1555
+ );
1556
+ }
1557
+ async getExecution(id, executionId) {
1558
+ this.http.requireAuthed();
1559
+ return this.http.fetch(
1560
+ `/cli/workflows/${encodeURIComponent(id)}/executions/${encodeURIComponent(executionId)}`
1561
+ );
1562
+ }
1563
+ async getExecutionLogs(id, executionId) {
1564
+ this.http.requireAuthed();
1565
+ return this.http.fetch(
1566
+ `/cli/workflows/${encodeURIComponent(id)}/executions/${encodeURIComponent(executionId)}/logs`
1567
+ );
1568
+ }
1569
+ async getExecutionTrace(id, executionId) {
1570
+ this.http.requireAuthed();
1571
+ return this.http.fetch(
1572
+ `/cli/workflows/${encodeURIComponent(id)}/executions/${encodeURIComponent(executionId)}/trace`
1573
+ );
1574
+ }
1575
+ async triggerSample(id) {
1576
+ this.http.requireAuthed();
1577
+ return this.http.fetch(
1578
+ `/cli/workflows/${encodeURIComponent(id)}/trigger-sample`
1579
+ );
1580
+ }
1581
+ async inputUploadUrl(id, body) {
1582
+ this.http.requireAuthed();
1583
+ return this.http.fetch(
1584
+ `/cli/workflows/${encodeURIComponent(id)}/input-upload-url`,
1585
+ { method: "POST", body }
1586
+ );
1587
+ }
1588
+ async appBuildStatus(id) {
1589
+ this.http.requireAuthed();
1590
+ return this.http.fetch(
1591
+ `/cli/workflows/${encodeURIComponent(id)}/app/build-status`
1592
+ );
1593
+ }
1594
+ async appInvocations(id, opts) {
1595
+ this.http.requireAuthed();
1596
+ const params = new URLSearchParams();
1597
+ if (opts.handler) params.set("handler", opts.handler);
1598
+ if (opts.errors) params.set("errors", "true");
1599
+ if (opts.limit) params.set("limit", String(opts.limit));
1600
+ const query = params.toString();
1601
+ return this.http.fetch(
1602
+ `/cli/workflows/${encodeURIComponent(id)}/app/invocations${query ? `?${query}` : ""}`
1603
+ );
1604
+ }
1605
+ async runHandler(id, body) {
1606
+ this.http.requireAuthed();
1607
+ return this.http.fetch(
1608
+ `/cli/workflows/${encodeURIComponent(id)}/app/run-handler`,
1609
+ { method: "POST", body }
1610
+ );
1611
+ }
1612
+ async appErrors(id, opts) {
1613
+ this.http.requireAuthed();
1614
+ const query = opts.limit ? `?limit=${opts.limit}` : "";
1615
+ return this.http.fetch(
1616
+ `/cli/workflows/${encodeURIComponent(id)}/app/errors${query}`
1617
+ );
1618
+ }
1619
+ async clearHandlerCache(id) {
1620
+ this.http.requireAuthed();
1621
+ return this.http.fetch(
1622
+ `/cli/workflows/${encodeURIComponent(id)}/app/clear-cache`,
1623
+ { method: "POST" }
1624
+ );
1625
+ }
1626
+ async spec(type) {
1627
+ this.http.requireAuthed();
1628
+ return this.http.fetch(`/cli/workflows/spec/${encodeURIComponent(type)}`);
1629
+ }
1630
+ async listTriggers(query) {
1631
+ this.http.requireAuthed();
1632
+ const suffix = query ? `?q=${encodeURIComponent(query)}` : "";
1633
+ return this.http.fetch(`/cli/triggers${suffix}`);
1634
+ }
1635
+ };
1636
+
1179
1637
  // src/clients/ApiClientFactory.ts
1180
1638
  var ApiClientFactory = class {
1181
1639
  build(args) {
@@ -1186,7 +1644,8 @@ var ApiClientFactory = class {
1186
1644
  exec: new ExecApiClient(http),
1187
1645
  credentials: new CredentialsApiClient(http),
1188
1646
  integrations: new IntegrationsApiClient(http),
1189
- operations: new OperationsApiClient(http)
1647
+ operations: new OperationsApiClient(http),
1648
+ workflows: new WorkflowsApiClient(http)
1190
1649
  };
1191
1650
  }
1192
1651
  };
@@ -1300,7 +1759,7 @@ function isErrnoCode(err, expected) {
1300
1759
  }
1301
1760
 
1302
1761
  // src/clients/ConfigStore.ts
1303
- import { readFileSync } from "fs";
1762
+ import { readFileSync as readFileSync3 } from "fs";
1304
1763
  import { mkdir as mkdir2, writeFile as writeFile2, unlink as unlink2 } from "fs/promises";
1305
1764
  import { dirname } from "path";
1306
1765
  var ConfigStore = class {
@@ -1316,7 +1775,7 @@ var ConfigStore = class {
1316
1775
  loadSync() {
1317
1776
  let raw;
1318
1777
  try {
1319
- raw = readFileSync(this.filePath, "utf8");
1778
+ raw = readFileSync3(this.filePath, "utf8");
1320
1779
  } catch {
1321
1780
  return null;
1322
1781
  }
@@ -1416,11 +1875,11 @@ var ChildProcessSpawner = class {
1416
1875
  process.on("SIGINT", forwardSignal);
1417
1876
  process.on("SIGTERM", forwardSignal);
1418
1877
  try {
1419
- const exitCode = await new Promise((resolve, reject) => {
1878
+ const exitCode = await new Promise((resolve20, reject) => {
1420
1879
  child.once("exit", (code, signal) => {
1421
- if (code !== null) resolve(code);
1422
- else if (signal !== null) resolve(128 + signalNumber(signal));
1423
- else resolve(1);
1880
+ if (code !== null) resolve20(code);
1881
+ else if (signal !== null) resolve20(128 + signalNumber(signal));
1882
+ else resolve20(1);
1424
1883
  });
1425
1884
  child.once("error", (err) => reject(err));
1426
1885
  });
@@ -1433,7 +1892,7 @@ var ChildProcessSpawner = class {
1433
1892
  }
1434
1893
  };
1435
1894
  function pipeWithScrubbing(source, dest, scrubber, onChunk) {
1436
- return new Promise((resolve) => {
1895
+ return new Promise((resolve20) => {
1437
1896
  let flushed = false;
1438
1897
  const emit = (chunk) => {
1439
1898
  if (chunk.length === 0) return;
@@ -1444,13 +1903,13 @@ function pipeWithScrubbing(source, dest, scrubber, onChunk) {
1444
1903
  if (flushed) return;
1445
1904
  flushed = true;
1446
1905
  emit(scrubber.redact("", { final: true }));
1447
- resolve();
1906
+ resolve20();
1448
1907
  };
1449
1908
  source.on("end", finishOnce);
1450
1909
  source.on("close", finishOnce);
1451
1910
  source.on("error", () => {
1452
1911
  flushed = true;
1453
- resolve();
1912
+ resolve20();
1454
1913
  });
1455
1914
  source.setEncoding("utf8");
1456
1915
  source.on("data", (chunk) => {
@@ -1471,15 +1930,15 @@ function signalNumber(signal) {
1471
1930
 
1472
1931
  // src/lib/paths.ts
1473
1932
  import { homedir } from "os";
1474
- import { join } from "path";
1933
+ import { join as join2 } from "path";
1475
1934
  function configDir() {
1476
- return process.env.GENI_CONFIG_DIR ?? join(homedir(), ".config", "geni");
1935
+ return process.env.GENI_CONFIG_DIR ?? join2(homedir(), ".config", "geni");
1477
1936
  }
1478
1937
  function sessionFilePath() {
1479
- return join(configDir(), "runner-session.json");
1938
+ return join2(configDir(), "runner-session.json");
1480
1939
  }
1481
1940
  function configFilePath() {
1482
- return join(configDir(), "config.json");
1941
+ return join2(configDir(), "config.json");
1483
1942
  }
1484
1943
 
1485
1944
  // src/dependencyInjection/clients.ts
@@ -1514,6 +1973,9 @@ var discoveryService = new DiscoveryService(
1514
1973
  browserOpener,
1515
1974
  configService
1516
1975
  );
1976
+ var workflowAuthoringService = new WorkflowAuthoringService(
1977
+ sessionContextService
1978
+ );
1517
1979
 
1518
1980
  // src/lib/cliErrors.ts
1519
1981
  function exitOnApiError(error, opts = {}) {
@@ -2135,9 +2597,7 @@ function registerCredentialConnect(parent) {
2135
2597
  return;
2136
2598
  }
2137
2599
  printInfo(`Opening ${chalk6.cyan(intent.url)}`);
2138
- printInfo(
2139
- `\u21B3 connect ${service} in your browser, then come back here`
2140
- );
2600
+ printInfo(`\u21B3 connect ${service} in your browser, then come back here`);
2141
2601
  process.stdout.write(
2142
2602
  `
2143
2603
  Once it's connected, re-run: geni credential list --service ${service}
@@ -2421,37 +2881,1161 @@ function registerIntegrationCommands(program2) {
2421
2881
  registerIntegrationOperation(integration);
2422
2882
  }
2423
2883
 
2424
- // src/commands/config/get.ts
2425
- var UNSET_PLACEHOLDER = "(unset)";
2426
- function executeConfigGet(args) {
2427
- const file = configService.fileValues();
2428
- if (args.key) {
2429
- if (!isSettableConfigKey(args.key)) {
2884
+ // src/commands/resource/create.ts
2885
+ import { resolve } from "path";
2886
+ function registerResourceCreate(parent) {
2887
+ parent.command("create").description(
2888
+ "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>`."
2889
+ ).requiredOption(
2890
+ "--type <type>",
2891
+ 'Resource type: "code" (deterministic, fixed steps), "agent" (LLM-driven, variable-length tasks), or "app" (interactive React mini-app with server-side handlers).'
2892
+ ).option(
2893
+ "--name <name>",
2894
+ 'Human-friendly name in Title Case (e.g. "Daily Lead Finder"). Defaults to a generated name.'
2895
+ ).option(
2896
+ "--dir <path>",
2897
+ "Directory to scaffold the files into. Defaults to the current directory."
2898
+ ).option("--json", "Machine-readable output.").action(async (opts) => {
2899
+ if (opts.type !== "code" && opts.type !== "agent" && opts.type !== "app") {
2430
2900
  printError(
2431
- `Unknown config key "${args.key}". Valid keys: ${SETTABLE_CONFIG_KEYS.join(", ")}.`
2901
+ `--type must be "code", "agent", or "app" (got "${opts.type ?? ""}").`
2432
2902
  );
2433
2903
  exit(ExitCode.InvalidArgs);
2434
2904
  }
2435
- const value = file[args.key];
2436
- if (args.json) {
2437
- printJson({ [args.key]: value ?? null });
2905
+ const dir = resolve(opts.dir ?? ".");
2906
+ if (readMarker(dir)) {
2907
+ printError(
2908
+ `${dir} is already a resource directory (${RESOURCE_MARKER_FILE} present). Pick a fresh directory with --dir, or pull/edit the existing one.`
2909
+ );
2910
+ exit(ExitCode.InvalidArgs);
2911
+ }
2912
+ try {
2913
+ const { detail, fileCount } = await workflowAuthoringService.create({
2914
+ workflowType: opts.type,
2915
+ name: opts.name,
2916
+ dir
2917
+ });
2918
+ if (opts.json) {
2919
+ printJson({ resource: detail, dir, fileCount });
2920
+ return;
2921
+ }
2922
+ printSuccess(`Created ${detail.workflowType} resource "${detail.name}"`);
2923
+ printInfo(`id: ${detail.id}`);
2924
+ printInfo(`Scaffolded ${fileCount} file(s) into ${dir}`);
2925
+ printInfo(
2926
+ "Next: edit the files, then `geni resource validate`. See the contract with `geni resource spec " + detail.workflowType + "`."
2927
+ );
2928
+ } catch (error) {
2929
+ exitOnApiError(error);
2930
+ }
2931
+ });
2932
+ }
2933
+
2934
+ // src/commands/resource/list.ts
2935
+ async function executeResourceList(opts) {
2936
+ try {
2937
+ const { workflows } = await workflowAuthoringService.list();
2938
+ if (opts.json) {
2939
+ printJson({ workflows });
2438
2940
  return;
2439
2941
  }
2440
- process.stdout.write((value ?? UNSET_PLACEHOLDER) + "\n");
2441
- return;
2442
- }
2443
- if (args.json) {
2444
- const payload = {};
2445
- for (const k of SETTABLE_CONFIG_KEYS) payload[k] = file[k] ?? null;
2446
- printJson(payload);
2447
- return;
2942
+ if (workflows.length === 0) {
2943
+ process.stdout.write(
2944
+ 'No resources yet. Create one with `geni resource create --type code --name "..."`.\n'
2945
+ );
2946
+ return;
2947
+ }
2948
+ printTable(
2949
+ ["ID", "NAME", "TYPE", "ENABLED", "VALID"],
2950
+ workflows.map((workflow) => [
2951
+ workflow.id,
2952
+ workflow.name,
2953
+ workflow.workflowType,
2954
+ workflow.isEnabled ? "yes" : "no",
2955
+ workflow.isValid ? "yes" : "no"
2956
+ ]),
2957
+ { colorFn: dimColumn(0) }
2958
+ );
2959
+ } catch (error) {
2960
+ exitOnApiError(error);
2961
+ }
2962
+ }
2963
+ function registerResourceList(parent) {
2964
+ parent.command("list").description(
2965
+ "List the resources (workflows and apps) you can access in this workspace."
2966
+ ).option("--json", "Machine-readable output.").action((opts) => executeResourceList(opts));
2967
+ }
2968
+
2969
+ // src/commands/resource/get.ts
2970
+ import { resolve as resolve2 } from "path";
2971
+ function registerResourceGet(parent) {
2972
+ parent.command("get [id]").description(
2973
+ "Show a resource detail (type, enabled, valid). Omit the id to use the .geni-resource.json marker in --dir."
2974
+ ).option(
2975
+ "--dir <path>",
2976
+ "Resource directory for id resolution. Defaults to cwd."
2977
+ ).option("--json", "Machine-readable output.").action(async (id, opts) => {
2978
+ const dir = resolve2(opts.dir ?? ".");
2979
+ const workflowId = resolveResourceId({ id, dir });
2980
+ try {
2981
+ const detail = await workflowAuthoringService.get(workflowId);
2982
+ if (opts.json) {
2983
+ printJson(detail);
2984
+ return;
2985
+ }
2986
+ printInfo(`id: ${detail.id}`);
2987
+ printInfo(`name: ${detail.name}`);
2988
+ printInfo(`type: ${detail.workflowType}`);
2989
+ printInfo(`enabled: ${detail.isEnabled ? "yes" : "no"}`);
2990
+ printInfo(`valid: ${detail.isValid ? "yes" : "no"}`);
2991
+ printInfo(`updated: ${detail.updatedAt}`);
2992
+ } catch (error) {
2993
+ exitOnApiError(error, {
2994
+ notFoundMessage: `Resource "${workflowId}" not found in this workspace.`
2995
+ });
2996
+ }
2997
+ });
2998
+ }
2999
+
3000
+ // src/commands/resource/pull.ts
3001
+ import { resolve as resolve3 } from "path";
3002
+ function registerResourcePull(parent) {
3003
+ parent.command("pull <id>").description(
3004
+ "Download a workflow's live files into a directory (default cwd) and write its .geni-resource.json marker."
3005
+ ).option("--dir <path>", "Destination directory. Defaults to cwd.").option("--force", "Overwrite existing files in the directory.").action(async (id, opts) => {
3006
+ const dir = resolve3(opts.dir ?? ".");
3007
+ if (!opts.force && dirHasResourceFiles(dir)) {
3008
+ printError(
3009
+ `${dir} already contains files. Re-run with --force to overwrite, or pull into an empty --dir.`
3010
+ );
3011
+ exit(ExitCode.InvalidArgs);
3012
+ }
3013
+ try {
3014
+ const { fileCount, workflowType } = await workflowAuthoringService.pull(
3015
+ {
3016
+ id,
3017
+ dir
3018
+ }
3019
+ );
3020
+ printSuccess(
3021
+ `Pulled ${fileCount} file(s) for ${workflowType} workflow ${id}`
3022
+ );
3023
+ printInfo(`into ${dir}`);
3024
+ } catch (error) {
3025
+ exitOnApiError(error, {
3026
+ notFoundMessage: `Resource "${id}" not found in this workspace.`
3027
+ });
3028
+ }
3029
+ });
3030
+ }
3031
+
3032
+ // src/commands/resource/validate.ts
3033
+ import { resolve as resolve4 } from "path";
3034
+
3035
+ // src/lib/printValidationErrors.ts
3036
+ import chalk8 from "chalk";
3037
+ function printValidationErrors(errors) {
3038
+ for (const error of errors) {
3039
+ const where = error.path ? chalk8.dim(`${error.path}: `) : "";
3040
+ process.stderr.write(`${chalk8.red("\u2717")} ${where}${error.message}
3041
+ `);
3042
+ }
3043
+ }
3044
+
3045
+ // src/commands/resource/validate.ts
3046
+ function registerResourceValidate(parent) {
3047
+ parent.command("validate [id]").description(
3048
+ "Validate the local files against the spec, credential/const wiring, and (code) TypeScript, without publishing. Exit 9 on validation failure."
3049
+ ).option("--dir <path>", "Resource directory. Defaults to cwd.").option("--json", "Machine-readable output.").action(async (id, opts) => {
3050
+ const dir = resolve4(opts.dir ?? ".");
3051
+ const workflowId = resolveResourceId({ id, dir });
3052
+ try {
3053
+ const result = await workflowAuthoringService.validate({
3054
+ id: workflowId,
3055
+ dir
3056
+ });
3057
+ if (opts.json) {
3058
+ printJson(result);
3059
+ if (!result.valid) exit(ExitCode.ValidationFailed);
3060
+ return;
3061
+ }
3062
+ if (result.valid) {
3063
+ printSuccess("Valid. Ready to publish.");
3064
+ return;
3065
+ }
3066
+ printValidationErrors(result.errors);
3067
+ exit(ExitCode.ValidationFailed);
3068
+ } catch (error) {
3069
+ exitOnApiError(error, {
3070
+ notFoundMessage: `Resource "${workflowId}" not found in this workspace.`
3071
+ });
3072
+ }
3073
+ });
3074
+ }
3075
+
3076
+ // src/commands/resource/config/get.ts
3077
+ import { resolve as resolve5 } from "path";
3078
+ function registerConfigGet(parent) {
3079
+ 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) => {
3080
+ const dir = resolve5(opts.dir ?? ".");
3081
+ const workflowId = resolveResourceId({ id, dir });
3082
+ try {
3083
+ const snapshot = await workflowAuthoringService.getConfig(workflowId);
3084
+ if (opts.json) {
3085
+ printJson(snapshot);
3086
+ return;
3087
+ }
3088
+ printConfig(snapshot);
3089
+ } catch (error) {
3090
+ exitOnApiError(error, {
3091
+ notFoundMessage: `Resource "${workflowId}" not found in this workspace.`
3092
+ });
3093
+ }
3094
+ });
3095
+ }
3096
+ function printConfig(snapshot) {
3097
+ printInfo("credentials:");
3098
+ const creds = Object.entries(snapshot.credentials);
3099
+ if (creds.length === 0) process.stdout.write(" (none)\n");
3100
+ for (const [service, credId] of creds) {
3101
+ process.stdout.write(` ${service} = ${credId}
3102
+ `);
3103
+ }
3104
+ printInfo("consts:");
3105
+ const vars = Object.entries(snapshot.variables);
3106
+ if (vars.length === 0) process.stdout.write(" (none)\n");
3107
+ for (const [key, value] of vars) {
3108
+ process.stdout.write(` ${key} = ${JSON.stringify(value)}
3109
+ `);
3110
+ }
3111
+ printInfo("triggers:");
3112
+ const triggers = Object.entries(snapshot.triggers);
3113
+ if (triggers.length === 0) process.stdout.write(" (none)\n");
3114
+ for (const [triggerId, entry] of triggers) {
3115
+ process.stdout.write(` ${triggerId}: ${JSON.stringify(entry)}
3116
+ `);
3117
+ }
3118
+ }
3119
+
3120
+ // src/commands/resource/config/set.ts
3121
+ import { resolve as resolve6 } from "path";
3122
+ function registerConfigSet(parent) {
3123
+ parent.command("set [id]").description(
3124
+ "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>."
3125
+ ).option("--dir <path>", "Resource directory. Defaults to cwd.").option(
3126
+ "--cred <service=credentialId>",
3127
+ "Assign a credential to a service slot. Repeatable.",
3128
+ collect2,
3129
+ []
3130
+ ).option(
3131
+ "--cred-clear <service>",
3132
+ "Clear a service slot. Repeatable.",
3133
+ collect2,
3134
+ []
3135
+ ).option(
3136
+ "--const <key=value>",
3137
+ "Set a const value (value is JSON-parsed when possible, else a string). Repeatable.",
3138
+ collect2,
3139
+ []
3140
+ ).option(
3141
+ "--schedule <triggerId=cron>",
3142
+ "Set a cron trigger schedule. Repeatable.",
3143
+ collect2,
3144
+ []
3145
+ ).option(
3146
+ "--timezone <triggerId=tz>",
3147
+ "Set a trigger timezone (IANA, e.g. America/New_York). Repeatable.",
3148
+ collect2,
3149
+ []
3150
+ ).option("--json", "Machine-readable output.").action(async (id, opts) => {
3151
+ const dir = resolve6(opts.dir ?? ".");
3152
+ const workflowId = resolveResourceId({ id, dir });
3153
+ const partial = buildPartial(opts);
3154
+ if (!partial.credentials && !partial.variables && !partial.triggers) {
3155
+ printError(
3156
+ "Nothing to set. Pass at least one of --cred, --cred-clear, --const, --schedule, --timezone."
3157
+ );
3158
+ exit(ExitCode.InvalidArgs);
3159
+ }
3160
+ try {
3161
+ const result = await workflowAuthoringService.updateConfig(
3162
+ workflowId,
3163
+ partial
3164
+ );
3165
+ if (opts.json) {
3166
+ printJson(result);
3167
+ if (!result.ok) exit(ExitCode.ValidationFailed);
3168
+ return;
3169
+ }
3170
+ if (!result.ok) {
3171
+ printValidationErrors(result.errors);
3172
+ exit(ExitCode.ValidationFailed);
3173
+ }
3174
+ printSuccess(`Updated config (${result.applied.length} change(s))`);
3175
+ for (const path of result.applied) printInfo(path);
3176
+ } catch (error) {
3177
+ exitOnApiError(error, {
3178
+ notFoundMessage: `Resource "${workflowId}" not found, or you lack edit access.`
3179
+ });
3180
+ }
3181
+ });
3182
+ }
3183
+ function buildPartial(opts) {
3184
+ const partial = {};
3185
+ const credentials = {};
3186
+ for (const pair of opts.cred) {
3187
+ const { key, value } = splitPair(pair, "--cred");
3188
+ credentials[key] = value;
3189
+ }
3190
+ for (const service of opts.credClear) {
3191
+ credentials[service] = null;
3192
+ }
3193
+ if (Object.keys(credentials).length > 0) partial.credentials = credentials;
3194
+ const variables = {};
3195
+ for (const pair of opts.const) {
3196
+ const { key, value } = splitPair(pair, "--const");
3197
+ variables[key] = coerceValue(value);
3198
+ }
3199
+ if (Object.keys(variables).length > 0) partial.variables = variables;
3200
+ const triggers = {};
3201
+ for (const pair of opts.schedule) {
3202
+ const { key, value } = splitPair(pair, "--schedule");
3203
+ triggers[key] = { ...triggers[key], schedule: value };
3204
+ }
3205
+ for (const pair of opts.timezone) {
3206
+ const { key, value } = splitPair(pair, "--timezone");
3207
+ triggers[key] = { ...triggers[key], timezone: value };
3208
+ }
3209
+ if (Object.keys(triggers).length > 0) partial.triggers = triggers;
3210
+ return partial;
3211
+ }
3212
+ function collect2(value, prev) {
3213
+ return [...prev, value];
3214
+ }
3215
+ function splitPair(pair, flag) {
3216
+ const index = pair.indexOf("=");
3217
+ if (index <= 0) {
3218
+ printError(`${flag} expects <key>=<value> (got "${pair}").`);
3219
+ exit(ExitCode.InvalidArgs);
3220
+ }
3221
+ return { key: pair.slice(0, index), value: pair.slice(index + 1) };
3222
+ }
3223
+ function coerceValue(raw) {
3224
+ try {
3225
+ return JSON.parse(raw);
3226
+ } catch {
3227
+ return raw;
3228
+ }
3229
+ }
3230
+
3231
+ // src/commands/resource/config/index.ts
3232
+ function registerResourceConfig(parent) {
3233
+ const config = parent.command("config").description(
3234
+ "Read or wire a workflow config: credentials, const values, trigger schedules."
3235
+ );
3236
+ registerConfigGet(config);
3237
+ registerConfigSet(config);
3238
+ }
3239
+
3240
+ // src/commands/resource/publish.ts
3241
+ import { resolve as resolve7 } from "path";
3242
+ function registerResourcePublish(parent) {
3243
+ parent.command("publish [id]").description(
3244
+ "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)."
3245
+ ).requiredOption(
3246
+ "--summary <text>",
3247
+ '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.'
3248
+ ).option("--dir <path>", "Resource directory. Defaults to cwd.").option("--json", "Machine-readable output.").action(async (id, opts) => {
3249
+ const dir = resolve7(opts.dir ?? ".");
3250
+ const workflowId = resolveResourceId({ id, dir });
3251
+ try {
3252
+ const result = await workflowAuthoringService.publish({
3253
+ id: workflowId,
3254
+ dir,
3255
+ changeSummary: opts.summary
3256
+ });
3257
+ if (opts.json) {
3258
+ printJson(result);
3259
+ if (!result.ok) exit(ExitCode.ValidationFailed);
3260
+ return;
3261
+ }
3262
+ if (!result.ok) {
3263
+ printValidationErrors(result.errors);
3264
+ exit(ExitCode.ValidationFailed);
3265
+ }
3266
+ printSuccess(
3267
+ `Published ${result.filesPersisted} file(s) to ${workflowId}`
3268
+ );
3269
+ for (const url of result.webhookUrls) {
3270
+ printInfo(`webhook: ${url}`);
3271
+ }
3272
+ printInfo(
3273
+ "Verify it: `geni workflow test` (workflows) or `geni app run-handler` (apps)."
3274
+ );
3275
+ } catch (error) {
3276
+ exitOnApiError(error, {
3277
+ notFoundMessage: `Resource "${workflowId}" not found, or you lack edit access.`
3278
+ });
3279
+ }
3280
+ });
3281
+ }
3282
+
3283
+ // src/commands/resource/spec.ts
3284
+ function registerResourceSpec(parent) {
3285
+ parent.command("spec <type>").description(
3286
+ "Print the resource-type contract (code | agent | app): required files, runtime API, allowed/forbidden patterns. Read this before editing files."
3287
+ ).action(async (type) => {
3288
+ try {
3289
+ const result = await workflowAuthoringService.spec(type);
3290
+ process.stdout.write(
3291
+ result.spec.endsWith("\n") ? result.spec : result.spec + "\n"
3292
+ );
3293
+ } catch (error) {
3294
+ exitOnApiError(error, {
3295
+ notFoundMessage: `Unknown resource type "${type}". Use code, agent, or app.`
3296
+ });
3297
+ }
3298
+ });
3299
+ }
3300
+
3301
+ // src/commands/resource/index.ts
3302
+ function registerResourceCommands(program2) {
3303
+ const resource = program2.command("resource").alias("resources").description(
3304
+ "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."
3305
+ ).action(() => executeResourceList({}));
3306
+ registerResourceCreate(resource);
3307
+ registerResourceList(resource);
3308
+ registerResourceGet(resource);
3309
+ registerResourcePull(resource);
3310
+ registerResourceValidate(resource);
3311
+ registerResourceConfig(resource);
3312
+ registerResourcePublish(resource);
3313
+ registerResourceSpec(resource);
3314
+ resource.addHelpText(
3315
+ "after",
3316
+ `
3317
+ Build loop (run \`geni resource spec <type>\` first):
3318
+ 1. geni resource create --type code --name "My Resource" scaffold files + the cloud resource
3319
+ 2. (edit the files with your editor)
3320
+ 3. geni resource validate spec + type check, no publish
3321
+ 4. geni resource config set ... wire credentials, consts, schedules
3322
+ 5. geni resource publish --summary "..." promote your files to live
3323
+ 6. geni workflow test (workflows) or geni app run-handler (apps) verify it
3324
+
3325
+ Commands accept the resource id as a positional, or infer it from the
3326
+ .geni-resource.json marker when run inside a resource directory.`
3327
+ );
3328
+ }
3329
+
3330
+ // src/commands/workflow/setType.ts
3331
+ import { resolve as resolve8 } from "path";
3332
+ function registerWorkflowSetType(parent) {
3333
+ parent.command("set-type <type> [id]").description(
3334
+ "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>`."
3335
+ ).option("--dir <path>", "Resource directory. Defaults to cwd.").option("--json", "Machine-readable output.").action(
3336
+ async (type, id, opts) => {
3337
+ if (type !== "code" && type !== "agent") {
3338
+ printError(`type must be "code" or "agent" (got "${type}").`);
3339
+ exit(ExitCode.InvalidArgs);
3340
+ }
3341
+ const dir = resolve8(opts.dir ?? ".");
3342
+ const workflowId = resolveResourceId({ id, dir });
3343
+ try {
3344
+ const result = await workflowAuthoringService.setType(
3345
+ workflowId,
3346
+ type
3347
+ );
3348
+ if (readMarker(dir)) {
3349
+ writeMarker(dir, { resourceId: workflowId, resourceType: type });
3350
+ }
3351
+ if (opts.json) {
3352
+ printJson(result);
3353
+ return;
3354
+ }
3355
+ printSuccess(`Switched ${workflowId} to ${type}`);
3356
+ if (result.forbiddenFiles.length > 0) {
3357
+ printInfo(
3358
+ `Delete any leftover files from the previous type: ${result.forbiddenFiles.join(", ")}`
3359
+ );
3360
+ }
3361
+ printInfo(
3362
+ `Write the ${type} files, then validate. Read the contract: \`geni resource spec ${type}\`.`
3363
+ );
3364
+ } catch (error) {
3365
+ exitOnApiError(error, {
3366
+ notFoundMessage: `Workflow "${workflowId}" not found, or you lack edit access.`
3367
+ });
3368
+ }
3369
+ }
3370
+ );
3371
+ }
3372
+
3373
+ // src/commands/workflow/test.ts
3374
+ import { resolve as resolve9 } from "path";
3375
+ import { z as z3 } from "zod";
3376
+ var TERMINAL = /* @__PURE__ */ new Set(["completed", "failed", "cancelled", "awaiting_input"]);
3377
+ var POLL_INTERVAL_MS = 2e3;
3378
+ function registerWorkflowTest(parent) {
3379
+ parent.command("test [id]").description(
3380
+ "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."
3381
+ ).option(
3382
+ "--payload <json>",
3383
+ `Trigger payload as a JSON object (e.g. '{"days":7}'). Defaults to {}.`
3384
+ ).option("--dir <path>", "Resource directory. Defaults to cwd.").option(
3385
+ "--timeout <seconds>",
3386
+ "How long to wait for the run to finish before giving up. Default 180."
3387
+ ).option("--json", "Machine-readable output (final execution + logs).").action(async (id, opts) => {
3388
+ const dir = resolve9(opts.dir ?? ".");
3389
+ const workflowId = resolveResourceId({ id, dir });
3390
+ const payload = parsePayload(opts.payload);
3391
+ const timeoutMs = parseTimeout(opts.timeout);
3392
+ try {
3393
+ const scheduled = await workflowAuthoringService.test(
3394
+ workflowId,
3395
+ payload
3396
+ );
3397
+ if (scheduled.blockers && scheduled.blockers.length > 0) {
3398
+ printError("Workflow isn't ready to run:");
3399
+ printValidationErrors(scheduled.blockers);
3400
+ printError(
3401
+ "Wire the missing credentials / set the schedule with `geni resource config set`, then retry."
3402
+ );
3403
+ exit(ExitCode.ValidationFailed);
3404
+ }
3405
+ if (!scheduled.executionId) {
3406
+ printError(
3407
+ "No execution was scheduled. Retry, or check the workflow config."
3408
+ );
3409
+ exit(ExitCode.InternalError);
3410
+ }
3411
+ const executionId = scheduled.executionId;
3412
+ if (!opts.json) printInfo(`Running test execution ${executionId} ...`);
3413
+ const detail = await pollUntilDone(workflowId, executionId, timeoutMs);
3414
+ const logs = await workflowAuthoringService.getExecutionLogs(
3415
+ workflowId,
3416
+ executionId
3417
+ );
3418
+ if (opts.json) {
3419
+ printJson({ execution: detail, logs: logs.logs });
3420
+ } else {
3421
+ printDetail(detail, logs.logs);
3422
+ }
3423
+ if (detail.status !== "completed") {
3424
+ exit(
3425
+ detail.status === "awaiting_input" ? ExitCode.Ok : ExitCode.GenericError
3426
+ );
3427
+ }
3428
+ } catch (error) {
3429
+ exitOnApiError(error, {
3430
+ notFoundMessage: `Workflow "${workflowId}" not found, or you lack run access.`
3431
+ });
3432
+ }
3433
+ });
3434
+ }
3435
+ async function pollUntilDone(workflowId, executionId, timeoutMs) {
3436
+ const deadline = Date.now() + timeoutMs;
3437
+ let detail = await workflowAuthoringService.getExecution(
3438
+ workflowId,
3439
+ executionId
3440
+ );
3441
+ while (!TERMINAL.has(detail.status)) {
3442
+ if (Date.now() > deadline) {
3443
+ printError(
3444
+ `Timed out after ${Math.round(timeoutMs / 1e3)}s waiting for ${executionId} (last status: ${detail.status}). It may still finish. Check \`geni workflow executions\`.`
3445
+ );
3446
+ exit(ExitCode.Timeout);
3447
+ }
3448
+ await sleep2(POLL_INTERVAL_MS);
3449
+ detail = await workflowAuthoringService.getExecution(
3450
+ workflowId,
3451
+ executionId
3452
+ );
3453
+ }
3454
+ return detail;
3455
+ }
3456
+ function printDetail(detail, logs) {
3457
+ if (detail.status === "completed") {
3458
+ printSuccess(`Execution ${detail.id} completed`);
3459
+ } else {
3460
+ printError(`Execution ${detail.id} ${detail.status}`);
3461
+ }
3462
+ if (detail.sandboxRuntimeMs !== null) {
3463
+ printInfo(`runtime: ${detail.sandboxRuntimeMs}ms`);
3464
+ }
3465
+ if (detail.error) printError(`error: ${detail.error}`);
3466
+ if (detail.run !== null && detail.run !== void 0) {
3467
+ process.stdout.write("--- output ---\n");
3468
+ process.stdout.write(JSON.stringify(detail.run, null, 2) + "\n");
3469
+ }
3470
+ if (logs && logs.trim().length > 0) {
3471
+ process.stdout.write("--- logs ---\n");
3472
+ process.stdout.write(logs.endsWith("\n") ? logs : logs + "\n");
3473
+ }
3474
+ }
3475
+ function parsePayload(raw) {
3476
+ if (!raw) return {};
3477
+ let parsed;
3478
+ try {
3479
+ parsed = JSON.parse(raw);
3480
+ } catch {
3481
+ printError(`--payload must be valid JSON. Got: ${raw}`);
3482
+ exit(ExitCode.InvalidArgs);
3483
+ }
3484
+ if (Array.isArray(parsed)) {
3485
+ printError("--payload must be a JSON object, not an array.");
3486
+ exit(ExitCode.InvalidArgs);
3487
+ }
3488
+ const result = z3.record(z3.string(), z3.unknown()).safeParse(parsed);
3489
+ if (!result.success) {
3490
+ printError(`--payload must be a JSON object (e.g. '{"days":7}').`);
3491
+ exit(ExitCode.InvalidArgs);
3492
+ }
3493
+ return result.data;
3494
+ }
3495
+ function parseTimeout(raw) {
3496
+ if (!raw) return 18e4;
3497
+ const seconds = Number.parseInt(raw, 10);
3498
+ if (!Number.isFinite(seconds) || seconds <= 0) {
3499
+ printError("--timeout must be a positive number of seconds.");
3500
+ exit(ExitCode.InvalidArgs);
3501
+ }
3502
+ return seconds * 1e3;
3503
+ }
3504
+ function sleep2(ms) {
3505
+ return new Promise((resolve20) => setTimeout(resolve20, ms));
3506
+ }
3507
+
3508
+ // src/commands/workflow/triggerSample.ts
3509
+ import { resolve as resolve10 } from "path";
3510
+ function registerWorkflowTriggerSample(parent) {
3511
+ parent.command("trigger-sample [id]").description(
3512
+ "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."
3513
+ ).option("--dir <path>", "Resource directory. Defaults to cwd.").option("--json", "Machine-readable output.").action(async (id, opts) => {
3514
+ const dir = resolve10(opts.dir ?? ".");
3515
+ const workflowId = resolveResourceId({ id, dir });
3516
+ try {
3517
+ const result = await workflowAuthoringService.triggerSample(workflowId);
3518
+ if (opts.json) {
3519
+ printJson(result);
3520
+ return;
3521
+ }
3522
+ process.stdout.write(
3523
+ result.result.endsWith("\n") ? result.result : result.result + "\n"
3524
+ );
3525
+ } catch (error) {
3526
+ exitOnApiError(error, {
3527
+ notFoundMessage: `Workflow "${workflowId}" not found in this workspace.`
3528
+ });
3529
+ }
3530
+ });
3531
+ }
3532
+
3533
+ // src/commands/workflow/stageInput.ts
3534
+ import { resolve as resolve11 } from "path";
3535
+ import { existsSync as existsSync2, statSync as statSync2 } from "fs";
3536
+ function registerWorkflowStageInput(parent) {
3537
+ parent.command("stage-input <file>").description(
3538
+ "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`."
3539
+ ).option("--workflow <id>", "Workflow id (else resolved from --dir marker).").option(
3540
+ "--dir <path>",
3541
+ "Resource directory for id resolution. Defaults to cwd."
3542
+ ).option("--json", "Machine-readable output.").action(async (file, opts) => {
3543
+ const localPath = resolve11(file);
3544
+ if (!existsSync2(localPath) || !statSync2(localPath).isFile()) {
3545
+ printError(`File not found: ${localPath}`);
3546
+ exit(ExitCode.InvalidArgs);
3547
+ }
3548
+ const dir = resolve11(opts.dir ?? ".");
3549
+ const workflowId = resolveResourceId({ id: opts.workflow, dir });
3550
+ try {
3551
+ const { storageUri, filename } = await workflowAuthoringService.stageInput({
3552
+ id: workflowId,
3553
+ localPath
3554
+ });
3555
+ if (opts.json) {
3556
+ printJson({ storageUri, filename });
3557
+ return;
3558
+ }
3559
+ printSuccess(`Staged ${filename}`);
3560
+ printInfo(`store URI: ${storageUri}`);
3561
+ printInfo(
3562
+ `Pass it as the file field's value, e.g. \`geni workflow test --payload '{"<field>":"${storageUri}"}'\`.`
3563
+ );
3564
+ } catch (error) {
3565
+ exitOnApiError(error, {
3566
+ notFoundMessage: `Workflow "${workflowId}" not found, or you lack run access.`
3567
+ });
3568
+ }
3569
+ });
3570
+ }
3571
+
3572
+ // src/commands/workflow/executions.ts
3573
+ import { resolve as resolve12 } from "path";
3574
+ function registerWorkflowExecutions(parent) {
3575
+ 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) => {
3576
+ const dir = resolve12(opts.dir ?? ".");
3577
+ const workflowId = resolveResourceId({ id, dir });
3578
+ const limit = opts.limit ? Number.parseInt(opts.limit, 10) : void 0;
3579
+ try {
3580
+ const { executions } = await workflowAuthoringService.listExecutions(
3581
+ workflowId,
3582
+ Number.isFinite(limit) ? limit : void 0
3583
+ );
3584
+ if (opts.json) {
3585
+ printJson({ executions });
3586
+ return;
3587
+ }
3588
+ if (executions.length === 0) {
3589
+ process.stdout.write("No executions yet. Run `geni workflow test`.\n");
3590
+ return;
3591
+ }
3592
+ printTable(
3593
+ ["ID", "STATUS", "TEST", "WHEN", "LABEL"],
3594
+ executions.map((execution) => [
3595
+ execution.id,
3596
+ execution.status,
3597
+ execution.isTest ? "yes" : "no",
3598
+ execution.createdAt,
3599
+ execution.name ?? execution.eventDescription
3600
+ ]),
3601
+ { colorFn: dimColumn(0) }
3602
+ );
3603
+ } catch (error) {
3604
+ exitOnApiError(error, {
3605
+ notFoundMessage: `Workflow "${workflowId}" not found in this workspace.`
3606
+ });
3607
+ }
3608
+ });
3609
+ }
3610
+
3611
+ // src/commands/workflow/execution.ts
3612
+ import { resolve as resolve14 } from "path";
3613
+
3614
+ // src/lib/executionTrace.ts
3615
+ import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync2 } from "fs";
3616
+ import { join as join3, resolve as resolve13 } from "path";
3617
+ function writeExecutionTraceBundle(args) {
3618
+ const { baseDir, trace } = args;
3619
+ const dir = resolve13(baseDir, trace.id);
3620
+ mkdirSync3(dir, { recursive: true });
3621
+ const files = [];
3622
+ const write = (name, content) => {
3623
+ const path = join3(dir, name);
3624
+ writeFileSync2(path, content);
3625
+ files.push(path);
3626
+ };
3627
+ if (trace.run !== null && trace.run !== void 0) {
3628
+ write("run.json", JSON.stringify(trace.run, null, 2));
3629
+ }
3630
+ if (trace.logs !== null) {
3631
+ write("logs.txt", trace.logs);
3632
+ }
3633
+ if (trace.input !== null && trace.input !== void 0) {
3634
+ write("input.json", JSON.stringify(trace.input, null, 2));
3635
+ }
3636
+ if (trace.thread.length > 0) {
3637
+ write("thread.json", JSON.stringify(trace.thread, null, 2));
3638
+ }
3639
+ return { dir, files };
3640
+ }
3641
+
3642
+ // src/commands/workflow/execution.ts
3643
+ function registerWorkflowExecution(parent) {
3644
+ parent.command("execution <executionId>").description(
3645
+ "Inspect one execution. Default shows status and output; add --logs, --input, or --thread for more, or --download to write the full trace to disk."
3646
+ ).option("--workflow <id>", "Workflow id (else resolved from --dir marker).").option(
3647
+ "--dir <path>",
3648
+ "Resource directory for id resolution. Defaults to cwd."
3649
+ ).option("--logs", "Include the execution logs.").option("--input", "Include the trigger input payload.").option(
3650
+ "--thread",
3651
+ "Include the step-by-step execution thread (every step the run took)."
3652
+ ).option(
3653
+ "--download [dir]",
3654
+ 'Write the full trace (run, logs, input, thread) to <dir>/<id>/. Defaults to "executions".'
3655
+ ).option("--json", "Machine-readable output.").action(async (executionId, opts) => {
3656
+ const dir = resolve14(opts.dir ?? ".");
3657
+ const workflowId = resolveResourceId({ id: opts.workflow, dir });
3658
+ const wantsTrace = opts.input || opts.thread || opts.download !== void 0;
3659
+ try {
3660
+ if (wantsTrace) {
3661
+ await runTraceMode({ workflowId, executionId, opts });
3662
+ return;
3663
+ }
3664
+ const detail = await workflowAuthoringService.getExecution(
3665
+ workflowId,
3666
+ executionId
3667
+ );
3668
+ const logs = opts.logs ? (await workflowAuthoringService.getExecutionLogs(
3669
+ workflowId,
3670
+ executionId
3671
+ )).logs : null;
3672
+ if (opts.json) {
3673
+ printJson({ execution: detail, logs });
3674
+ return;
3675
+ }
3676
+ printInfo(`id: ${detail.id}`);
3677
+ printInfo(`status: ${detail.status}`);
3678
+ printInfo(`test: ${detail.isTest ? "yes" : "no"}`);
3679
+ if (detail.sandboxRuntimeMs !== null) {
3680
+ printInfo(`runtime: ${detail.sandboxRuntimeMs}ms`);
3681
+ }
3682
+ if (detail.error) printError(`error: ${detail.error}`);
3683
+ printOutput(detail.run);
3684
+ if (opts.logs) printSection("logs", logs ?? "(no logs)");
3685
+ } catch (error) {
3686
+ exitOnApiError(error, {
3687
+ notFoundMessage: `Execution "${executionId}" not found for that workflow.`
3688
+ });
3689
+ }
3690
+ });
3691
+ }
3692
+ async function runTraceMode(args) {
3693
+ const { workflowId, executionId, opts } = args;
3694
+ const trace = await workflowAuthoringService.getExecutionTrace(
3695
+ workflowId,
3696
+ executionId
3697
+ );
3698
+ const download = opts.download !== void 0 ? writeExecutionTraceBundle({
3699
+ baseDir: typeof opts.download === "string" ? opts.download : "executions",
3700
+ trace
3701
+ }) : null;
3702
+ if (opts.json) {
3703
+ printJson({ execution: trace, download });
3704
+ return;
3705
+ }
3706
+ printInfo(`id: ${trace.id}`);
3707
+ printInfo(`status: ${trace.status}`);
3708
+ if (trace.error) printError(`error: ${trace.error}`);
3709
+ printOutput(trace.run);
3710
+ if (opts.input) {
3711
+ printSection("input", JSON.stringify(trace.input ?? null, null, 2));
3712
+ }
3713
+ if (opts.thread) {
3714
+ printSection(
3715
+ "thread",
3716
+ trace.thread.length > 0 ? JSON.stringify(trace.thread, null, 2) : "(no thread)"
3717
+ );
3718
+ }
3719
+ if (opts.logs) printSection("logs", trace.logs ?? "(no logs)");
3720
+ if (download) {
3721
+ printSuccess(
3722
+ `Wrote ${download.files.length} trace file(s) to ${download.dir}`
3723
+ );
3724
+ for (const file of download.files) printInfo(file);
3725
+ }
3726
+ }
3727
+ function printOutput(run2) {
3728
+ if (run2 === null || run2 === void 0) return;
3729
+ printSection("output", JSON.stringify(run2, null, 2));
3730
+ }
3731
+ function printSection(label, content) {
3732
+ process.stdout.write(`--- ${label} ---
3733
+ `);
3734
+ process.stdout.write(content + "\n");
3735
+ }
3736
+
3737
+ // src/commands/workflow/index.ts
3738
+ function registerWorkflowCommands(program2) {
3739
+ const workflow = program2.command("workflow").alias("workflows").description(
3740
+ "Workflow-only tools: run cloud test executions, sample triggers, switch type, inspect runs. Shared lifecycle (create / validate / publish / config) is under `geni resource`."
3741
+ );
3742
+ registerWorkflowSetType(workflow);
3743
+ registerWorkflowTest(workflow);
3744
+ registerWorkflowTriggerSample(workflow);
3745
+ registerWorkflowStageInput(workflow);
3746
+ registerWorkflowExecutions(workflow);
3747
+ registerWorkflowExecution(workflow);
3748
+ }
3749
+
3750
+ // src/commands/app/buildStatus.ts
3751
+ import { resolve as resolve15 } from "path";
3752
+ function registerAppBuildStatus(parent) {
3753
+ parent.command("build-status [id]").description(
3754
+ "Read the live app Vite build state: status, error, last built."
3755
+ ).option("--dir <path>", "Resource directory. Defaults to cwd.").option("--json", "Machine-readable output.").action(async (id, opts) => {
3756
+ const dir = resolve15(opts.dir ?? ".");
3757
+ const workflowId = resolveResourceId({ id, dir });
3758
+ try {
3759
+ const result = await workflowAuthoringService.appBuildStatus(workflowId);
3760
+ if (opts.json) {
3761
+ printJson(result);
3762
+ return;
3763
+ }
3764
+ printInfo(`status: ${result.buildStatus}`);
3765
+ printInfo(`last built: ${result.lastBuiltAt ?? "(never)"}`);
3766
+ if (result.buildError) printError(`error: ${result.buildError}`);
3767
+ } catch (error) {
3768
+ exitOnApiError(error, {
3769
+ notFoundMessage: `App "${workflowId}" not found in this workspace.`
3770
+ });
3771
+ }
3772
+ });
3773
+ }
3774
+
3775
+ // src/commands/app/runHandler.ts
3776
+ import { resolve as resolve16 } from "path";
3777
+ import { z as z4 } from "zod";
3778
+ function registerAppRunHandler(parent) {
3779
+ parent.command("run-handler <name> [id]").description(
3780
+ "Dispatch a handler against the live app with --input JSON (bypasses cache) and print the result. Publish first. Exits nonzero on handler error."
3781
+ ).option("--input <json>", "Handler input as a JSON object. Defaults to {}.").option(
3782
+ "--timeout <seconds>",
3783
+ "Server-side wait before timing out. Default 60, max 300."
3784
+ ).option("--dir <path>", "Resource directory. Defaults to cwd.").option("--json", "Machine-readable output.").action(
3785
+ async (name, id, opts) => {
3786
+ const dir = resolve16(opts.dir ?? ".");
3787
+ const workflowId = resolveResourceId({ id, dir });
3788
+ const input = parseInput(opts.input);
3789
+ const timeoutSeconds = parseTimeout2(opts.timeout);
3790
+ try {
3791
+ const result = await workflowAuthoringService.runHandler(workflowId, {
3792
+ handlerName: name,
3793
+ input,
3794
+ timeoutSeconds
3795
+ });
3796
+ if (opts.json) {
3797
+ printJson(result);
3798
+ if (!result.ok) exit(ExitCode.GenericError);
3799
+ return;
3800
+ }
3801
+ if (result.ok) {
3802
+ printSuccess(`Handler "${name}" ran`);
3803
+ process.stdout.write("--- result ---\n");
3804
+ process.stdout.write(JSON.stringify(result.result, null, 2) + "\n");
3805
+ return;
3806
+ }
3807
+ printError(
3808
+ `Handler "${name}" failed (${result.status}): ${result.message}`
3809
+ );
3810
+ exit(ExitCode.GenericError);
3811
+ } catch (error) {
3812
+ exitOnApiError(error, {
3813
+ notFoundMessage: `App "${workflowId}" not found, or you lack run access.`
3814
+ });
3815
+ }
3816
+ }
3817
+ );
3818
+ }
3819
+ function parseInput(raw) {
3820
+ if (!raw) return {};
3821
+ let parsed;
3822
+ try {
3823
+ parsed = JSON.parse(raw);
3824
+ } catch {
3825
+ printError(`--input must be valid JSON. Got: ${raw}`);
3826
+ exit(ExitCode.InvalidArgs);
3827
+ }
3828
+ if (Array.isArray(parsed)) {
3829
+ printError("--input must be a JSON object, not an array.");
3830
+ exit(ExitCode.InvalidArgs);
3831
+ }
3832
+ const result = z4.record(z4.string(), z4.unknown()).safeParse(parsed);
3833
+ if (!result.success) {
3834
+ printError(`--input must be a JSON object (e.g. '{"limit":50}').`);
3835
+ exit(ExitCode.InvalidArgs);
3836
+ }
3837
+ return result.data;
3838
+ }
3839
+ function parseTimeout2(raw) {
3840
+ if (!raw) return void 0;
3841
+ const seconds = Number.parseInt(raw, 10);
3842
+ if (!Number.isFinite(seconds) || seconds <= 0) {
3843
+ printError("--timeout must be a positive number of seconds.");
3844
+ exit(ExitCode.InvalidArgs);
3845
+ }
3846
+ return seconds;
3847
+ }
3848
+
3849
+ // src/commands/app/invocations.ts
3850
+ import { resolve as resolve17 } from "path";
3851
+ function registerAppInvocations(parent) {
3852
+ parent.command("invocations [id]").description(
3853
+ "Recent handler invocations: status, timing, cache hits, errors."
3854
+ ).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) => {
3855
+ const dir = resolve17(opts.dir ?? ".");
3856
+ const workflowId = resolveResourceId({ id, dir });
3857
+ const limit = opts.limit ? Number.parseInt(opts.limit, 10) : void 0;
3858
+ try {
3859
+ const { invocations } = await workflowAuthoringService.appInvocations(
3860
+ workflowId,
3861
+ {
3862
+ handler: opts.handler,
3863
+ errors: opts.errors,
3864
+ limit: Number.isFinite(limit) ? limit : void 0
3865
+ }
3866
+ );
3867
+ if (opts.json) {
3868
+ printJson({ invocations });
3869
+ return;
3870
+ }
3871
+ if (invocations.length === 0) {
3872
+ process.stdout.write("No matching invocations.\n");
3873
+ return;
3874
+ }
3875
+ printTable(
3876
+ ["WHEN", "HANDLER", "STATUS", "MS", "CACHED", "ERROR"],
3877
+ invocations.map((invocation) => [
3878
+ invocation.createdAt,
3879
+ invocation.handlerName,
3880
+ invocation.status,
3881
+ String(invocation.durationMs),
3882
+ invocation.cacheHit ? "yes" : "no",
3883
+ invocation.errorMessage ?? ""
3884
+ ]),
3885
+ { colorFn: dimColumn(0) }
3886
+ );
3887
+ } catch (error) {
3888
+ exitOnApiError(error, {
3889
+ notFoundMessage: `App "${workflowId}" not found in this workspace.`
3890
+ });
3891
+ }
3892
+ });
3893
+ }
3894
+
3895
+ // src/commands/app/errors.ts
3896
+ import { resolve as resolve18 } from "path";
3897
+ function registerAppErrors(parent) {
3898
+ parent.command("errors [id]").description(
3899
+ "Recent browser-side errors the iframe reported (render/rejection/load)."
3900
+ ).option("--limit <n>", "Max rows (default 20).").option("--dir <path>", "Resource directory. Defaults to cwd.").option("--json", "Machine-readable output.").action(async (id, opts) => {
3901
+ const dir = resolve18(opts.dir ?? ".");
3902
+ const workflowId = resolveResourceId({ id, dir });
3903
+ const limit = opts.limit ? Number.parseInt(opts.limit, 10) : void 0;
3904
+ try {
3905
+ const { errors } = await workflowAuthoringService.appErrors(
3906
+ workflowId,
3907
+ {
3908
+ limit: Number.isFinite(limit) ? limit : void 0
3909
+ }
3910
+ );
3911
+ if (opts.json) {
3912
+ printJson({ errors });
3913
+ return;
3914
+ }
3915
+ if (errors.length === 0) {
3916
+ process.stdout.write("No errors reported from the iframe.\n");
3917
+ return;
3918
+ }
3919
+ for (const event of errors) {
3920
+ process.stdout.write(
3921
+ `[${event.eventTs}] ${event.kind}: ${event.message}
3922
+ `
3923
+ );
3924
+ if (event.stack) process.stdout.write(event.stack + "\n");
3925
+ }
3926
+ } catch (error) {
3927
+ exitOnApiError(error, {
3928
+ notFoundMessage: `App "${workflowId}" not found in this workspace.`
3929
+ });
3930
+ }
3931
+ });
3932
+ }
3933
+
3934
+ // src/commands/app/clearCache.ts
3935
+ import { resolve as resolve19 } from "path";
3936
+ function registerAppClearCache(parent) {
3937
+ parent.command("clear-cache [id]").description(
3938
+ "Clear the live app handler response cache (republishing also does this)."
3939
+ ).option("--dir <path>", "Resource directory. Defaults to cwd.").action(async (id, opts) => {
3940
+ const dir = resolve19(opts.dir ?? ".");
3941
+ const workflowId = resolveResourceId({ id, dir });
3942
+ try {
3943
+ await workflowAuthoringService.clearHandlerCache(workflowId);
3944
+ printSuccess("Handler cache cleared.");
3945
+ } catch (error) {
3946
+ exitOnApiError(error, {
3947
+ notFoundMessage: `App "${workflowId}" not found, or you lack edit access.`
3948
+ });
3949
+ }
3950
+ });
3951
+ }
3952
+
3953
+ // src/commands/app/index.ts
3954
+ function registerAppCommands(program2) {
3955
+ const app = program2.command("app").alias("apps").description(
3956
+ "App-only tools: build status, run/inspect handlers, browser errors, handler cache. Run against the live (published) app. Shared lifecycle is under `geni resource`."
3957
+ );
3958
+ registerAppBuildStatus(app);
3959
+ registerAppRunHandler(app);
3960
+ registerAppInvocations(app);
3961
+ registerAppErrors(app);
3962
+ registerAppClearCache(app);
3963
+ }
3964
+
3965
+ // src/commands/trigger/list.ts
3966
+ async function executeTriggerList(opts) {
3967
+ try {
3968
+ const { triggers } = await workflowAuthoringService.listTriggers(opts.query);
3969
+ if (opts.json) {
3970
+ printJson({ triggers });
3971
+ return;
3972
+ }
3973
+ if (triggers.length === 0) {
3974
+ process.stdout.write("No trigger providers match.\n");
3975
+ return;
3976
+ }
3977
+ printTable(
3978
+ ["TRIGGER ID", "TYPE", "SERVICE", "NAME"],
3979
+ triggers.map((trigger) => [
3980
+ trigger.type === "poll" ? trigger.triggerId : `(${trigger.type})`,
3981
+ trigger.type,
3982
+ trigger.service,
3983
+ trigger.name
3984
+ ]),
3985
+ { colorFn: dimColumn(0) }
3986
+ );
3987
+ } catch (error) {
3988
+ exitOnApiError(error);
3989
+ }
3990
+ }
3991
+ function registerTriggerList(parent) {
3992
+ parent.command("list").description(
3993
+ "List trigger providers. Poll triggers show a copyable triggerId; webhook/cron are built in and take no triggerId."
3994
+ ).option(
3995
+ "-q, --query <text>",
3996
+ 'Filter by name / service / description (e.g. "email", "reddit", "schedule").'
3997
+ ).option("--json", "Machine-readable output.").action((opts) => executeTriggerList(opts));
3998
+ }
3999
+
4000
+ // src/commands/trigger/index.ts
4001
+ function registerTriggerCommands(program2) {
4002
+ const trigger = program2.command("trigger").alias("triggers").description(
4003
+ "Discover trigger providers (webhook / cron / poll) that can start a workflow. Use a poll trigger id in workflow.json."
4004
+ ).action(() => executeTriggerList({}));
4005
+ registerTriggerList(trigger);
4006
+ }
4007
+
4008
+ // src/commands/config/get.ts
4009
+ var UNSET_PLACEHOLDER = "(unset)";
4010
+ function executeConfigGet(args) {
4011
+ const file = configService.fileValues();
4012
+ if (args.key) {
4013
+ if (!isSettableConfigKey(args.key)) {
4014
+ printError(
4015
+ `Unknown config key "${args.key}". Valid keys: ${SETTABLE_CONFIG_KEYS.join(", ")}.`
4016
+ );
4017
+ exit(ExitCode.InvalidArgs);
4018
+ }
4019
+ const value = file[args.key];
4020
+ if (args.json) {
4021
+ printJson({ [args.key]: value ?? null });
4022
+ return;
4023
+ }
4024
+ process.stdout.write((value ?? UNSET_PLACEHOLDER) + "\n");
4025
+ return;
4026
+ }
4027
+ if (args.json) {
4028
+ const payload = {};
4029
+ for (const k of SETTABLE_CONFIG_KEYS) payload[k] = file[k] ?? null;
4030
+ printJson(payload);
4031
+ return;
2448
4032
  }
2449
4033
  printTable(
2450
4034
  ["KEY", "VALUE"],
2451
4035
  SETTABLE_CONFIG_KEYS.map((k) => [k, file[k] ?? UNSET_PLACEHOLDER])
2452
4036
  );
2453
4037
  }
2454
- function registerConfigGet(parent) {
4038
+ function registerConfigGet2(parent) {
2455
4039
  parent.command("get").argument(
2456
4040
  "[key]",
2457
4041
  `Specific key to read (${SETTABLE_CONFIG_KEYS.join(" | ")}). Omit to list every settable key with its value.`
@@ -2466,7 +4050,7 @@ function registerConfigGet(parent) {
2466
4050
  }
2467
4051
 
2468
4052
  // src/commands/config/set.ts
2469
- function registerConfigSet(parent) {
4053
+ function registerConfigSet2(parent) {
2470
4054
  parent.command("set").argument("<key>", `Config key (${SETTABLE_CONFIG_KEYS.join(" | ")}).`).argument(
2471
4055
  "<value>",
2472
4056
  "New value. URL keys must be valid http:// or https:// URLs (validated on write)."
@@ -2542,8 +4126,8 @@ function registerConfigCommands(program2) {
2542
4126
  const config = program2.command("config").description(
2543
4127
  "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."
2544
4128
  ).action(() => executeConfigGet({}));
2545
- registerConfigGet(config);
2546
- registerConfigSet(config);
4129
+ registerConfigGet2(config);
4130
+ registerConfigSet2(config);
2547
4131
  registerConfigUnset(config);
2548
4132
  registerConfigPath(config);
2549
4133
  config.addHelpText(
@@ -2573,38 +4157,38 @@ logout (or use \`geni login --server <url>\`) to switch in one step.
2573
4157
 
2574
4158
  // src/lib/skills.ts
2575
4159
  import { homedir as homedir2 } from "os";
2576
- import { join as join2 } from "path";
4160
+ import { join as join4 } from "path";
2577
4161
  import {
2578
- mkdirSync,
2579
- writeFileSync,
2580
- existsSync,
2581
- readFileSync as readFileSync2,
4162
+ mkdirSync as mkdirSync4,
4163
+ writeFileSync as writeFileSync3,
4164
+ existsSync as existsSync3,
4165
+ readFileSync as readFileSync4,
2582
4166
  unlinkSync,
2583
4167
  rmSync
2584
4168
  } from "fs";
2585
4169
 
2586
4170
  // src/skills/geni.md
2587
- 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>\"` 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## 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";
4171
+ 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";
2588
4172
 
2589
4173
  // src/lib/skills.ts
2590
4174
  var GENI_SKILL_NAME = "geni";
2591
4175
  var GENI_SKILL_MD = geni_default;
2592
4176
  var home = homedir2();
2593
- var claudeSkillsDir = join2(home, ".claude", "skills");
2594
- var claudeGeniDir = join2(claudeSkillsDir, GENI_SKILL_NAME);
2595
- var agentSkillsDir = join2(home, ".agents", "skills");
2596
- var agentSkillsGeniDir = join2(agentSkillsDir, GENI_SKILL_NAME);
4177
+ var claudeSkillsDir = join4(home, ".claude", "skills");
4178
+ var claudeGeniDir = join4(claudeSkillsDir, GENI_SKILL_NAME);
4179
+ var agentSkillsDir = join4(home, ".agents", "skills");
4180
+ var agentSkillsGeniDir = join4(agentSkillsDir, GENI_SKILL_NAME);
2597
4181
  var TARGETS = [
2598
4182
  {
2599
4183
  name: "Claude Code",
2600
- detect: () => existsSync(join2(home, ".claude")),
4184
+ detect: () => existsSync3(join4(home, ".claude")),
2601
4185
  skillDir: claudeGeniDir,
2602
- skillPath: join2(claudeGeniDir, "SKILL.md"),
4186
+ skillPath: join4(claudeGeniDir, "SKILL.md"),
2603
4187
  // Earlier versions of `geni skills install` wrote a loose .md file
2604
4188
  // at `~/.claude/skills/geni.md`, which Claude Code silently ignores.
2605
4189
  // Clean it up on install/uninstall so upgraders aren't left with a
2606
4190
  // stale sibling that confuses `doctor`.
2607
- legacyPaths: [join2(claudeSkillsDir, `${GENI_SKILL_NAME}.md`)]
4191
+ legacyPaths: [join4(claudeSkillsDir, `${GENI_SKILL_NAME}.md`)]
2608
4192
  },
2609
4193
  {
2610
4194
  name: "Codex CLI / Gemini CLI / VS Code Copilot",
@@ -2614,9 +4198,9 @@ var TARGETS = [
2614
4198
  // because it lives inside VS Code's user data with no clean
2615
4199
  // home-dir signal — Copilot users typically have one of the CLIs
2616
4200
  // installed too, and the install is idempotent if they don't.
2617
- detect: () => existsSync(join2(home, ".codex")) || existsSync(join2(home, ".gemini")) || existsSync(join2(home, ".agents")),
4201
+ detect: () => existsSync3(join4(home, ".codex")) || existsSync3(join4(home, ".gemini")) || existsSync3(join4(home, ".agents")),
2618
4202
  skillDir: agentSkillsGeniDir,
2619
- skillPath: join2(agentSkillsGeniDir, "SKILL.md"),
4203
+ skillPath: join4(agentSkillsGeniDir, "SKILL.md"),
2620
4204
  legacyPaths: []
2621
4205
  }
2622
4206
  ];
@@ -2634,12 +4218,12 @@ function installSkills() {
2634
4218
  if (!target.detect()) continue;
2635
4219
  const path = target.skillPath;
2636
4220
  try {
2637
- mkdirSync(target.skillDir, { recursive: true });
2638
- const previous = existsSync(path) ? readFileSync2(path, "utf-8") : null;
4221
+ mkdirSync4(target.skillDir, { recursive: true });
4222
+ const previous = existsSync3(path) ? readFileSync4(path, "utf-8") : null;
2639
4223
  const changed = previous !== GENI_SKILL_MD;
2640
- writeFileSync(path, GENI_SKILL_MD);
4224
+ writeFileSync3(path, GENI_SKILL_MD);
2641
4225
  for (const legacy of target.legacyPaths) {
2642
- if (existsSync(legacy)) unlinkSync(legacy);
4226
+ if (existsSync3(legacy)) unlinkSync(legacy);
2643
4227
  }
2644
4228
  results.push({
2645
4229
  name: target.name,
@@ -2663,8 +4247,8 @@ function uninstallSkills() {
2663
4247
  if (!target.detect()) continue;
2664
4248
  const dir = target.skillDir;
2665
4249
  const path = target.skillPath;
2666
- const legacies = target.legacyPaths.filter((p2) => existsSync(p2));
2667
- if (!existsSync(dir) && legacies.length === 0) {
4250
+ const legacies = target.legacyPaths.filter((p2) => existsSync3(p2));
4251
+ if (!existsSync3(dir) && legacies.length === 0) {
2668
4252
  results.push({ name: target.name, path, status: "absent" });
2669
4253
  continue;
2670
4254
  }
@@ -2749,10 +4333,10 @@ function registerSkillsCommands(program2) {
2749
4333
  }
2750
4334
 
2751
4335
  // src/commands/doctor.ts
2752
- import chalk8 from "chalk";
4336
+ import chalk9 from "chalk";
2753
4337
 
2754
4338
  // src/lib/preflight.ts
2755
- import { delimiter, join as join3 } from "path";
4339
+ import { delimiter, join as join5 } from "path";
2756
4340
  import { accessSync, constants } from "fs";
2757
4341
  var REQUIRED_RUNTIME_DEPS = ["bash", "curl", "jq"];
2758
4342
  function findOnPath(name) {
@@ -2760,7 +4344,7 @@ function findOnPath(name) {
2760
4344
  if (!path) return null;
2761
4345
  for (const dir of path.split(delimiter)) {
2762
4346
  if (dir.length === 0) continue;
2763
- const candidate = join3(dir, name);
4347
+ const candidate = join5(dir, name);
2764
4348
  try {
2765
4349
  accessSync(candidate, constants.X_OK);
2766
4350
  return candidate;
@@ -2800,7 +4384,7 @@ function installHint(deps) {
2800
4384
  }
2801
4385
 
2802
4386
  // src/commands/doctor.ts
2803
- import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
4387
+ import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
2804
4388
  function registerDoctorCommand(program2) {
2805
4389
  program2.command("doctor").description(
2806
4390
  "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."
@@ -2903,7 +4487,7 @@ function skillsCheck() {
2903
4487
  }
2904
4488
  const out = [];
2905
4489
  for (const target of targets) {
2906
- if (!existsSync2(target.path)) {
4490
+ if (!existsSync4(target.path)) {
2907
4491
  out.push({
2908
4492
  label: `${target.name} skill installed`,
2909
4493
  status: "fail",
@@ -2911,7 +4495,7 @@ function skillsCheck() {
2911
4495
  });
2912
4496
  continue;
2913
4497
  }
2914
- const onDisk = readFileSync3(target.path, "utf-8");
4498
+ const onDisk = readFileSync5(target.path, "utf-8");
2915
4499
  if (onDisk === GENI_SKILL_MD) {
2916
4500
  out.push({
2917
4501
  label: `${target.name} skill installed`,
@@ -2930,17 +4514,17 @@ function skillsCheck() {
2930
4514
  }
2931
4515
  function printReport(checks) {
2932
4516
  for (const check of checks) {
2933
- const mark = check.status === "pass" ? chalk8.green("\u2713") : check.status === "warn" ? chalk8.yellow("!") : chalk8.red("\u2717");
4517
+ const mark = check.status === "pass" ? chalk9.green("\u2713") : check.status === "warn" ? chalk9.yellow("!") : chalk9.red("\u2717");
2934
4518
  process.stdout.write(`${mark} ${check.label}
2935
4519
  `);
2936
- process.stdout.write(` ${chalk8.dim(check.detail)}
4520
+ process.stdout.write(` ${chalk9.dim(check.detail)}
2937
4521
  `);
2938
4522
  }
2939
4523
  const fails = checks.filter((c) => c.status === "fail").length;
2940
4524
  const warns = checks.filter((c) => c.status === "warn").length;
2941
4525
  process.stdout.write("\n");
2942
4526
  if (fails === 0 && warns === 0) {
2943
- process.stdout.write(chalk8.green("All checks passed.\n"));
4527
+ process.stdout.write(chalk9.green("All checks passed.\n"));
2944
4528
  } else {
2945
4529
  process.stdout.write(
2946
4530
  `${fails} failure${fails === 1 ? "" : "s"}, ${warns} warning${warns === 1 ? "" : "s"}.
@@ -2962,6 +4546,10 @@ registerWorkspaceCommands(program);
2962
4546
  registerExecCommands(program);
2963
4547
  registerCredentialCommands(program);
2964
4548
  registerIntegrationCommands(program);
4549
+ registerResourceCommands(program);
4550
+ registerWorkflowCommands(program);
4551
+ registerAppCommands(program);
4552
+ registerTriggerCommands(program);
2965
4553
  registerConfigCommands(program);
2966
4554
  registerSkillsCommands(program);
2967
4555
  registerDoctorCommand(program);