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