@moxt-ai/cli 0.3.1 → 0.3.3-beta.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/README.md +78 -0
- package/dist/index.js +846 -25
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { Command } from "commander";
|
|
5
4
|
import updateNotifier from "update-notifier";
|
|
6
5
|
|
|
6
|
+
// src/program.ts
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
|
|
7
9
|
// src/commands/file.ts
|
|
8
10
|
import * as fs from "fs";
|
|
9
11
|
|
|
@@ -147,7 +149,7 @@ function getApiKeyOrUndefined() {
|
|
|
147
149
|
|
|
148
150
|
// src/utils/http.ts
|
|
149
151
|
function getUserAgent() {
|
|
150
|
-
return `moxt-cli/${"0.3.
|
|
152
|
+
return `moxt-cli/${"0.3.3-beta.0"} (${platform()}/${arch()}) node/${process.versions.node}`;
|
|
151
153
|
}
|
|
152
154
|
async function fetchApi(method, path2, body) {
|
|
153
155
|
const apiKey = getApiKey();
|
|
@@ -187,6 +189,9 @@ async function httpPut(path2, body) {
|
|
|
187
189
|
async function httpPost(path2, body) {
|
|
188
190
|
return request("POST", path2, body);
|
|
189
191
|
}
|
|
192
|
+
async function httpPatch(path2, body) {
|
|
193
|
+
return request("PATCH", path2, body);
|
|
194
|
+
}
|
|
190
195
|
async function httpDelete(path2, body) {
|
|
191
196
|
return request("DELETE", path2, body);
|
|
192
197
|
}
|
|
@@ -217,6 +222,59 @@ function printError(message) {
|
|
|
217
222
|
console.error(`${colors.red}${message}${colors.reset}`);
|
|
218
223
|
}
|
|
219
224
|
|
|
225
|
+
// src/utils/search-options.ts
|
|
226
|
+
var SearchOptionsError = class extends Error {
|
|
227
|
+
constructor(message) {
|
|
228
|
+
super(message);
|
|
229
|
+
this.name = "SearchOptionsError";
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
function parseSearchLimit(raw, defaultValue, max) {
|
|
233
|
+
if (raw == null || raw === "") {
|
|
234
|
+
return defaultValue;
|
|
235
|
+
}
|
|
236
|
+
const parsed = Number.parseInt(raw, 10);
|
|
237
|
+
if (!Number.isInteger(parsed) || parsed <= 0 || String(parsed) !== raw) {
|
|
238
|
+
throw new SearchOptionsError(`Error: --limit must be a positive integer, got "${raw}".`);
|
|
239
|
+
}
|
|
240
|
+
if (parsed > max) {
|
|
241
|
+
throw new SearchOptionsError(`Error: --limit must be less than or equal to ${max}.`);
|
|
242
|
+
}
|
|
243
|
+
return parsed;
|
|
244
|
+
}
|
|
245
|
+
function parseSearchMode(raw) {
|
|
246
|
+
if (raw == null || raw === "") {
|
|
247
|
+
return "any";
|
|
248
|
+
}
|
|
249
|
+
if (raw === "all" || raw === "any") {
|
|
250
|
+
return raw;
|
|
251
|
+
}
|
|
252
|
+
throw new SearchOptionsError(`Error: --mode must be "any" or "all", got "${raw}".`);
|
|
253
|
+
}
|
|
254
|
+
function parseTeammateId(raw) {
|
|
255
|
+
if (raw == null || raw === "") {
|
|
256
|
+
return void 0;
|
|
257
|
+
}
|
|
258
|
+
const parsed = Number.parseInt(raw, 10);
|
|
259
|
+
if (!Number.isInteger(parsed) || parsed <= 0 || String(parsed) !== raw) {
|
|
260
|
+
throw new SearchOptionsError(`Error: --teammate-id must be a positive integer, got "${raw}".`);
|
|
261
|
+
}
|
|
262
|
+
return parsed;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// src/utils/run-or-exit.ts
|
|
266
|
+
function runOrExit(fn) {
|
|
267
|
+
try {
|
|
268
|
+
return fn();
|
|
269
|
+
} catch (err) {
|
|
270
|
+
if (err instanceof SpaceOptionsError || err instanceof SearchOptionsError) {
|
|
271
|
+
printError(err.message);
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
274
|
+
throw err;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
220
278
|
// src/utils/spinner.ts
|
|
221
279
|
var frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
222
280
|
var intervalId = null;
|
|
@@ -240,22 +298,48 @@ function stopSpinner(success, message) {
|
|
|
240
298
|
}
|
|
241
299
|
|
|
242
300
|
// src/commands/file.ts
|
|
243
|
-
function runOrExit(fn) {
|
|
244
|
-
try {
|
|
245
|
-
return fn();
|
|
246
|
-
} catch (err) {
|
|
247
|
-
if (err instanceof SpaceOptionsError) {
|
|
248
|
-
printError(err.message);
|
|
249
|
-
process.exit(1);
|
|
250
|
-
}
|
|
251
|
-
throw err;
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
301
|
function addSpaceOptions(cmd) {
|
|
255
302
|
return cmd.requiredOption("-w, --workspace <workspaceId>", "Workspace ID").option("-s, --space <name>", "Space name (personal or teamspace name)").option("--personal", "Use personal space (default)").option("--team-space-id <teamSpaceId>", "Team space ID").option("--teammate-id <teammateId>", "Teammate ID (teammate space)");
|
|
256
303
|
}
|
|
257
304
|
function registerFileCommand(program2) {
|
|
258
305
|
const file = program2.command("file").description("File operations");
|
|
306
|
+
addSpaceOptions(
|
|
307
|
+
file.command("search").description("Search files in accessible spaces").argument("<query>", "Search query").option("-l, --limit <limit>", "Maximum number of files to return", "30").option("--mode <mode>", "Keyword matching mode: any or all", "any")
|
|
308
|
+
).action(async (query, options) => {
|
|
309
|
+
const spaceParams = runOrExit(() => resolveSpaceSelector(options));
|
|
310
|
+
const limit = runOrExit(() => parseSearchLimit(options.limit, 30, 80));
|
|
311
|
+
const mode = runOrExit(() => parseSearchMode(options.mode));
|
|
312
|
+
startSpinner("Searching files...");
|
|
313
|
+
const response = await httpPost(
|
|
314
|
+
`/workspaces/${options.workspace}/files/search`,
|
|
315
|
+
{
|
|
316
|
+
query,
|
|
317
|
+
limit,
|
|
318
|
+
mode,
|
|
319
|
+
...spaceParams
|
|
320
|
+
}
|
|
321
|
+
);
|
|
322
|
+
if (!response.ok) {
|
|
323
|
+
stopSpinner(false, "Failed");
|
|
324
|
+
printError(`Error [${response.status}]: ${JSON.stringify(response.data)}`);
|
|
325
|
+
process.exit(1);
|
|
326
|
+
}
|
|
327
|
+
const { items } = response.data;
|
|
328
|
+
if (items.length === 0) {
|
|
329
|
+
stopSpinner(true, "No files found.");
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
stopSpinner(true, `Found ${items.length} file(s)`);
|
|
333
|
+
for (const item of items) {
|
|
334
|
+
print(`${colors.bold}${item.rank}. ${item.filePath}${colors.reset}`);
|
|
335
|
+
print(`${colors.dim}repo=${item.repoId} file=${item.fileId}${colors.reset}`);
|
|
336
|
+
const snippet = item.contentHighlight?.text ?? item.semanticSnippets[0]?.content;
|
|
337
|
+
if (snippet) {
|
|
338
|
+
print(snippet);
|
|
339
|
+
}
|
|
340
|
+
print("");
|
|
341
|
+
}
|
|
342
|
+
});
|
|
259
343
|
addSpaceOptions(
|
|
260
344
|
file.command("get-url").description("Resolve the shareable browser URL for a file").requiredOption("-p, --path <path>", "File path")
|
|
261
345
|
).action(async (options) => {
|
|
@@ -469,6 +553,43 @@ function registerFileCommand(program2) {
|
|
|
469
553
|
);
|
|
470
554
|
}
|
|
471
555
|
|
|
556
|
+
// src/commands/memory.ts
|
|
557
|
+
function registerMemoryCommand(program2) {
|
|
558
|
+
const memory = program2.command("memory").description("Memory operations");
|
|
559
|
+
memory.command("search").description("Search archived agent memory").requiredOption("-w, --workspace <workspaceId>", "Workspace ID").option("--teammate-id <teammateId>", "Search a managed AI teammate memory").option("-l, --limit <limit>", "Maximum number of memory chunks to return", "5").argument("<query>", "Search query").action(
|
|
560
|
+
async (query, options) => {
|
|
561
|
+
const limit = runOrExit(() => parseSearchLimit(options.limit, 5, 10));
|
|
562
|
+
const teammateId = runOrExit(() => parseTeammateId(options.teammateId));
|
|
563
|
+
startSpinner("Searching memory...");
|
|
564
|
+
const response = await httpPost(
|
|
565
|
+
`/workspaces/${options.workspace}/memory/search`,
|
|
566
|
+
{
|
|
567
|
+
query,
|
|
568
|
+
limit,
|
|
569
|
+
...teammateId !== void 0 ? { agentId: teammateId } : {}
|
|
570
|
+
}
|
|
571
|
+
);
|
|
572
|
+
if (!response.ok) {
|
|
573
|
+
stopSpinner(false, "Failed");
|
|
574
|
+
printError(`Error [${response.status}]: ${JSON.stringify(response.data)}`);
|
|
575
|
+
process.exit(1);
|
|
576
|
+
}
|
|
577
|
+
const { items } = response.data;
|
|
578
|
+
if (items.length === 0) {
|
|
579
|
+
stopSpinner(true, "No memory found.");
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
stopSpinner(true, `Found ${items.length} memory chunk(s)`);
|
|
583
|
+
for (const item of items) {
|
|
584
|
+
print(`${colors.bold}${item.rank}. pipeline=${item.pipelineId} chunk=${item.chunkIndex}${colors.reset}`);
|
|
585
|
+
print(`${colors.dim}chunkId=${item.chunkId}${item.contentTruncated ? " truncated=true" : ""}${colors.reset}`);
|
|
586
|
+
print(item.content);
|
|
587
|
+
print("");
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
|
|
472
593
|
// src/utils/miniapp-db.ts
|
|
473
594
|
var MiniappDbOptionsError = class extends Error {
|
|
474
595
|
constructor(message) {
|
|
@@ -823,6 +944,714 @@ function registerWorkspaceCommand(program2) {
|
|
|
823
944
|
});
|
|
824
945
|
}
|
|
825
946
|
|
|
947
|
+
// src/commands/workflow.ts
|
|
948
|
+
import * as fs3 from "fs";
|
|
949
|
+
var MAX_DESC_LENGTH = 5e3;
|
|
950
|
+
var WorkflowOptionsError = class extends Error {
|
|
951
|
+
constructor(message) {
|
|
952
|
+
super(message);
|
|
953
|
+
this.name = "WorkflowOptionsError";
|
|
954
|
+
}
|
|
955
|
+
};
|
|
956
|
+
function runOrExit3(fn) {
|
|
957
|
+
try {
|
|
958
|
+
return fn();
|
|
959
|
+
} catch (err) {
|
|
960
|
+
if (err instanceof WorkflowOptionsError || err instanceof SpaceOptionsError || err instanceof SearchOptionsError) {
|
|
961
|
+
printError(err.message);
|
|
962
|
+
process.exit(1);
|
|
963
|
+
}
|
|
964
|
+
throw err;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
function addWorkspaceOption(command) {
|
|
968
|
+
return command.requiredOption("-w, --workspace <workspaceId>", "Workspace ID");
|
|
969
|
+
}
|
|
970
|
+
function addSpaceOptions2(command) {
|
|
971
|
+
return addWorkspaceOption(command).option("-s, --space <name>", "Space name (personal or teamspace name)").option("--personal", "Use personal space (default)").option("--team-space-id <teamSpaceId>", "Team space ID").option("--teammate-id <teammateId>", "Teammate ID (teammate space)");
|
|
972
|
+
}
|
|
973
|
+
function addAssigneeOptions(command) {
|
|
974
|
+
return command.option("--human-id <humanId>", "Human assignee ID").option("--assistant-id <assistantId>", "AI assistant assignee ID").option("--teammate-id <teammateId>", "AI teammate assignee ID");
|
|
975
|
+
}
|
|
976
|
+
function addPreferredAssigneeOptions(command) {
|
|
977
|
+
return command.option("--preferred-human-id <humanId>", "Preferred human assignee ID").option("--preferred-assistant-id <assistantId>", "Preferred AI assistant assignee ID").option("--preferred-teammate-id <teammateId>", "Preferred AI teammate assignee ID");
|
|
978
|
+
}
|
|
979
|
+
function parsePositiveInteger(raw, label) {
|
|
980
|
+
const parsed = Number.parseInt(raw, 10);
|
|
981
|
+
if (!Number.isInteger(parsed) || parsed <= 0 || String(parsed) !== raw) {
|
|
982
|
+
throw new WorkflowOptionsError(`Error: ${label} must be a positive integer, got "${raw}".`);
|
|
983
|
+
}
|
|
984
|
+
return parsed;
|
|
985
|
+
}
|
|
986
|
+
function parseOptionalPositiveInteger(raw, label) {
|
|
987
|
+
if (raw == null || raw === "") return void 0;
|
|
988
|
+
return parsePositiveInteger(raw, label);
|
|
989
|
+
}
|
|
990
|
+
function parseStatusType(raw) {
|
|
991
|
+
if (raw == null || raw === "") return void 0;
|
|
992
|
+
const parsed = Number.parseInt(raw, 10);
|
|
993
|
+
if ((parsed === 1 || parsed === 2 || parsed === 3) && String(parsed) === raw) return parsed;
|
|
994
|
+
throw new WorkflowOptionsError("Error: --type must be one of 1 (todo), 2 (in-progress), or 3 (done).");
|
|
995
|
+
}
|
|
996
|
+
function parseRequiredStatusType(raw) {
|
|
997
|
+
const value = parseStatusType(raw);
|
|
998
|
+
if (value === void 0) {
|
|
999
|
+
throw new WorkflowOptionsError("Error: --type is required.");
|
|
1000
|
+
}
|
|
1001
|
+
return value;
|
|
1002
|
+
}
|
|
1003
|
+
function parsePriority(raw) {
|
|
1004
|
+
if (raw == null || raw === "") return void 0;
|
|
1005
|
+
if (raw === "urgent") return 0;
|
|
1006
|
+
if (raw === "high") return 1;
|
|
1007
|
+
if (raw === "medium") return 2;
|
|
1008
|
+
if (raw === "low") return 3;
|
|
1009
|
+
throw new WorkflowOptionsError('Error: --priority must be "urgent", "high", "medium", or "low".');
|
|
1010
|
+
}
|
|
1011
|
+
function parseRequiredPriority(raw) {
|
|
1012
|
+
return parsePriority(raw ?? "medium") ?? 2;
|
|
1013
|
+
}
|
|
1014
|
+
function parseWorkflowScope(raw) {
|
|
1015
|
+
if (raw == null || raw === "") return "all";
|
|
1016
|
+
if (raw === "all" || raw === "owner_is_me") return raw;
|
|
1017
|
+
throw new WorkflowOptionsError('Error: --scope must be "all" or "owner_is_me".');
|
|
1018
|
+
}
|
|
1019
|
+
function parseTaskScope(raw) {
|
|
1020
|
+
if (raw == null || raw === "") return "all";
|
|
1021
|
+
if (raw === "all" || raw === "assigned_to_me" || raw === "subscribed_by_me" || raw === "owner_is_me") {
|
|
1022
|
+
return raw;
|
|
1023
|
+
}
|
|
1024
|
+
throw new WorkflowOptionsError(
|
|
1025
|
+
'Error: --scope must be "all", "assigned_to_me", "subscribed_by_me", or "owner_is_me".'
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
function parseFileIds(raw, label = "--file-ids") {
|
|
1029
|
+
if (raw == null || raw === "") return void 0;
|
|
1030
|
+
const ids = raw.split(",").map((item) => item.trim()).filter(Boolean);
|
|
1031
|
+
if (ids.length === 0) {
|
|
1032
|
+
throw new WorkflowOptionsError(`Error: ${label} must include at least one file ID.`);
|
|
1033
|
+
}
|
|
1034
|
+
return ids;
|
|
1035
|
+
}
|
|
1036
|
+
function readDescFile(path2) {
|
|
1037
|
+
if (path2 == null || path2 === "") return void 0;
|
|
1038
|
+
if (!fs3.existsSync(path2)) {
|
|
1039
|
+
throw new WorkflowOptionsError(`Error: desc file not found: ${path2}`);
|
|
1040
|
+
}
|
|
1041
|
+
const stat = fs3.statSync(path2);
|
|
1042
|
+
if (!stat.isFile()) {
|
|
1043
|
+
throw new WorkflowOptionsError(`Error: desc file is not a file: ${path2}`);
|
|
1044
|
+
}
|
|
1045
|
+
const buffer2 = fs3.readFileSync(path2);
|
|
1046
|
+
if (isBinaryContent(buffer2)) {
|
|
1047
|
+
throw new WorkflowOptionsError("Error: desc file must be UTF-8 text, not binary content.");
|
|
1048
|
+
}
|
|
1049
|
+
const content = buffer2.toString("utf8");
|
|
1050
|
+
if (content.length > MAX_DESC_LENGTH) {
|
|
1051
|
+
throw new WorkflowOptionsError(`Error: desc file must be at most ${MAX_DESC_LENGTH} characters.`);
|
|
1052
|
+
}
|
|
1053
|
+
return content;
|
|
1054
|
+
}
|
|
1055
|
+
function parseAssigneeBody(options, allowClear) {
|
|
1056
|
+
const hasClear = options.clear === true;
|
|
1057
|
+
const humanId = parseOptionalPositiveInteger(options.humanId, "--human-id");
|
|
1058
|
+
const assistantId = parseOptionalPositiveInteger(options.assistantId, "--assistant-id");
|
|
1059
|
+
const teammateId = parseOptionalPositiveInteger(options.teammateId, "--teammate-id");
|
|
1060
|
+
const count = (humanId !== void 0 ? 1 : 0) + (assistantId !== void 0 ? 1 : 0) + (teammateId !== void 0 ? 1 : 0);
|
|
1061
|
+
if (hasClear && !allowClear) {
|
|
1062
|
+
throw new WorkflowOptionsError("Error: --clear is not supported here.");
|
|
1063
|
+
}
|
|
1064
|
+
if (hasClear && count > 0) {
|
|
1065
|
+
throw new WorkflowOptionsError("Error: --clear cannot be used with assignee ID options.");
|
|
1066
|
+
}
|
|
1067
|
+
if (count > 1) {
|
|
1068
|
+
throw new WorkflowOptionsError("Error: assignee ID options are mutually exclusive.");
|
|
1069
|
+
}
|
|
1070
|
+
if (hasClear) return {};
|
|
1071
|
+
if (humanId !== void 0) return { assigneeHumanId: humanId };
|
|
1072
|
+
if (assistantId !== void 0) return { assigneeAiAssistantId: assistantId };
|
|
1073
|
+
if (teammateId !== void 0) return { assigneeAiTeammateId: teammateId };
|
|
1074
|
+
return {};
|
|
1075
|
+
}
|
|
1076
|
+
function parsePreferredAssigneeBody(options) {
|
|
1077
|
+
const hasClear = options.clearPreferredAssignee === true;
|
|
1078
|
+
const humanId = parseOptionalPositiveInteger(options.preferredHumanId, "--preferred-human-id");
|
|
1079
|
+
const assistantId = parseOptionalPositiveInteger(options.preferredAssistantId, "--preferred-assistant-id");
|
|
1080
|
+
const teammateId = parseOptionalPositiveInteger(options.preferredTeammateId, "--preferred-teammate-id");
|
|
1081
|
+
const count = (humanId !== void 0 ? 1 : 0) + (assistantId !== void 0 ? 1 : 0) + (teammateId !== void 0 ? 1 : 0);
|
|
1082
|
+
if (hasClear && count > 0) {
|
|
1083
|
+
throw new WorkflowOptionsError("Error: --clear-preferred-assignee cannot be used with preferred assignee ID options.");
|
|
1084
|
+
}
|
|
1085
|
+
if (count > 1) {
|
|
1086
|
+
throw new WorkflowOptionsError("Error: preferred assignee ID options are mutually exclusive.");
|
|
1087
|
+
}
|
|
1088
|
+
if (hasClear) return { preferredAssigneeHumanId: null };
|
|
1089
|
+
if (humanId !== void 0) return { preferredAssigneeHumanId: humanId };
|
|
1090
|
+
if (assistantId !== void 0) return { preferredAssigneeAiAssistantId: assistantId };
|
|
1091
|
+
if (teammateId !== void 0) return { preferredAssigneeAiTeammateId: teammateId };
|
|
1092
|
+
return {};
|
|
1093
|
+
}
|
|
1094
|
+
function assertNonEmptyBody(body) {
|
|
1095
|
+
if (Object.keys(body).length === 0) {
|
|
1096
|
+
throw new WorkflowOptionsError("Error: at least one update option must be provided.");
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
function buildQuery(params) {
|
|
1100
|
+
const query = new URLSearchParams();
|
|
1101
|
+
for (const [key, value] of Object.entries(params)) {
|
|
1102
|
+
if (value !== void 0) {
|
|
1103
|
+
query.set(key, String(value));
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
const text = query.toString();
|
|
1107
|
+
return text ? `?${text}` : "";
|
|
1108
|
+
}
|
|
1109
|
+
function failResponse(response) {
|
|
1110
|
+
printError(`Error [${response.status}]: ${JSON.stringify(response.data)}`);
|
|
1111
|
+
process.exit(1);
|
|
1112
|
+
}
|
|
1113
|
+
function priorityLabel(value) {
|
|
1114
|
+
if (value === 0) return "urgent";
|
|
1115
|
+
if (value === 1) return "high";
|
|
1116
|
+
if (value === 2) return "medium";
|
|
1117
|
+
return "low";
|
|
1118
|
+
}
|
|
1119
|
+
function formatAssignee(assignee) {
|
|
1120
|
+
if (!assignee) return "-";
|
|
1121
|
+
if (assignee.type === "human") return assignee.displayName ?? `human:${assignee.humanId}`;
|
|
1122
|
+
if (assignee.type === "assistant") return assignee.displayName ?? `assistant:${assignee.aiAssistantId}`;
|
|
1123
|
+
if (assignee.type === "teammate") return assignee.displayName ?? `teammate:${assignee.aiTeammateId}`;
|
|
1124
|
+
return "unknown";
|
|
1125
|
+
}
|
|
1126
|
+
function printWorkflow(item) {
|
|
1127
|
+
print(`${colors.bold}${item.fileId}${colors.reset} owner=${item.ownerHumanId ?? "-"} desc=${item.desc ?? "-"}`);
|
|
1128
|
+
}
|
|
1129
|
+
function printStatus(item) {
|
|
1130
|
+
print(
|
|
1131
|
+
`${colors.bold}${item.scopedId}${colors.reset} ${item.name} type=${item.type} preferred=${formatAssignee(item.preferredAssignee)}`
|
|
1132
|
+
);
|
|
1133
|
+
}
|
|
1134
|
+
function printTask(item) {
|
|
1135
|
+
const runIds = item.agentRuns.map((run) => run.pipelineTriggerId).join(",") || "-";
|
|
1136
|
+
print(
|
|
1137
|
+
`${colors.bold}#${item.scopedId}${colors.reset} ${item.title} status=${item.statusScopedId} priority=${priorityLabel(item.priority)} assignee=${formatAssignee(item.assignee)} runs=${runIds}`
|
|
1138
|
+
);
|
|
1139
|
+
}
|
|
1140
|
+
function registerWorkflowCommand(program2) {
|
|
1141
|
+
const workflow = program2.command("workflow").description("Workflow operations");
|
|
1142
|
+
addWorkspaceOption(
|
|
1143
|
+
workflow.command("list").description("List accessible workflows").option("--scope <scope>", "Workflow scope: all or owner_is_me", "all")
|
|
1144
|
+
).action(async (options) => {
|
|
1145
|
+
const scope = runOrExit3(() => parseWorkflowScope(options.scope));
|
|
1146
|
+
startSpinner("Fetching workflows...");
|
|
1147
|
+
const response = await httpGet(
|
|
1148
|
+
`/workspaces/${options.workspace}/workflows${buildQuery({ scope })}`
|
|
1149
|
+
);
|
|
1150
|
+
if (!response.ok) {
|
|
1151
|
+
stopSpinner(false, "Failed");
|
|
1152
|
+
failResponse(response);
|
|
1153
|
+
}
|
|
1154
|
+
if (response.data.workflows.length === 0) {
|
|
1155
|
+
stopSpinner(true, "No workflows found.");
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
stopSpinner(true, `Found ${response.data.workflows.length} workflow(s)`);
|
|
1159
|
+
for (const item of response.data.workflows) printWorkflow(item);
|
|
1160
|
+
});
|
|
1161
|
+
addSpaceOptions2(
|
|
1162
|
+
workflow.command("create").description("Create a blank workflow").requiredOption("-p, --path <path>", "Target .workflow path").option("--desc-file <localPath>", "Local UTF-8 text file for workflow description").option("--owner-human-id <humanId>", "Workflow owner human ID")
|
|
1163
|
+
).action(async (options) => {
|
|
1164
|
+
const body = runOrExit3(() => {
|
|
1165
|
+
const next = {
|
|
1166
|
+
path: options.path,
|
|
1167
|
+
...resolveSpaceSelector(options)
|
|
1168
|
+
};
|
|
1169
|
+
const desc = readDescFile(options.descFile);
|
|
1170
|
+
if (desc !== void 0) next.desc = desc;
|
|
1171
|
+
const ownerHumanId = parseOptionalPositiveInteger(options.ownerHumanId, "--owner-human-id");
|
|
1172
|
+
if (ownerHumanId !== void 0) next.ownerHumanId = ownerHumanId;
|
|
1173
|
+
return next;
|
|
1174
|
+
});
|
|
1175
|
+
startSpinner("Creating workflow...");
|
|
1176
|
+
const response = await httpPost(`/workspaces/${options.workspace}/workflows`, body);
|
|
1177
|
+
if (!response.ok) {
|
|
1178
|
+
stopSpinner(false, "Failed");
|
|
1179
|
+
failResponse(response);
|
|
1180
|
+
}
|
|
1181
|
+
stopSpinner(true, "Workflow created");
|
|
1182
|
+
printWorkflow(response.data);
|
|
1183
|
+
});
|
|
1184
|
+
addWorkspaceOption(workflow.command("get").description("Get a workflow").argument("<workflowFileId>")).action(
|
|
1185
|
+
async (workflowFileId, options) => {
|
|
1186
|
+
startSpinner("Fetching workflow...");
|
|
1187
|
+
const response = await httpGet(
|
|
1188
|
+
`/workspaces/${options.workspace}/workflows/${workflowFileId}`
|
|
1189
|
+
);
|
|
1190
|
+
if (!response.ok) {
|
|
1191
|
+
stopSpinner(false, "Failed");
|
|
1192
|
+
failResponse(response);
|
|
1193
|
+
}
|
|
1194
|
+
stopSpinner(true, "Workflow fetched");
|
|
1195
|
+
printWorkflow(response.data);
|
|
1196
|
+
print(`${colors.bold}Statuses${colors.reset}`);
|
|
1197
|
+
for (const status of response.data.statusList) printStatus(status);
|
|
1198
|
+
print(`${colors.bold}Transitions${colors.reset}`);
|
|
1199
|
+
for (const transition of response.data.transitions) {
|
|
1200
|
+
print(`${transition.statusFromScopedId} -> ${transition.statusToScopedId}`);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
);
|
|
1204
|
+
addWorkspaceOption(
|
|
1205
|
+
workflow.command("update").description("Update workflow metadata").argument("<workflowFileId>").option("--desc-file <localPath>", "Local UTF-8 text file for workflow description").option("--owner-human-id <humanId>", "Workflow owner human ID")
|
|
1206
|
+
).action(async (workflowFileId, options) => {
|
|
1207
|
+
const body = runOrExit3(() => {
|
|
1208
|
+
const next = {};
|
|
1209
|
+
const desc = readDescFile(options.descFile);
|
|
1210
|
+
if (desc !== void 0) next.desc = desc;
|
|
1211
|
+
const ownerHumanId = parseOptionalPositiveInteger(options.ownerHumanId, "--owner-human-id");
|
|
1212
|
+
if (ownerHumanId !== void 0) next.ownerHumanId = ownerHumanId;
|
|
1213
|
+
assertNonEmptyBody(next);
|
|
1214
|
+
return next;
|
|
1215
|
+
});
|
|
1216
|
+
startSpinner("Updating workflow...");
|
|
1217
|
+
const response = await httpPatch(`/workspaces/${options.workspace}/workflows/${workflowFileId}`, body);
|
|
1218
|
+
if (!response.ok) {
|
|
1219
|
+
stopSpinner(false, "Failed");
|
|
1220
|
+
failResponse(response);
|
|
1221
|
+
}
|
|
1222
|
+
stopSpinner(true, "Workflow updated");
|
|
1223
|
+
printWorkflow(response.data);
|
|
1224
|
+
});
|
|
1225
|
+
addWorkspaceOption(
|
|
1226
|
+
workflow.command("delete").description("Delete a workflow").argument("<workflowFileId>").option("--message <message>", "Commit message")
|
|
1227
|
+
).action(async (workflowFileId, options) => {
|
|
1228
|
+
startSpinner("Deleting workflow...");
|
|
1229
|
+
const response = await httpDelete(
|
|
1230
|
+
`/workspaces/${options.workspace}/workflows/${workflowFileId}`,
|
|
1231
|
+
options.message ? { message: options.message } : {}
|
|
1232
|
+
);
|
|
1233
|
+
if (!response.ok) {
|
|
1234
|
+
stopSpinner(false, "Failed");
|
|
1235
|
+
failResponse(response);
|
|
1236
|
+
}
|
|
1237
|
+
stopSpinner(true, `Deleted: ${response.data.fileId}`);
|
|
1238
|
+
});
|
|
1239
|
+
addWorkspaceOption(workflow.command("duplicate").description("Duplicate a workflow").argument("<workflowFileId>")).action(
|
|
1240
|
+
async (workflowFileId, options) => {
|
|
1241
|
+
startSpinner("Duplicating workflow...");
|
|
1242
|
+
const response = await httpPost(
|
|
1243
|
+
`/workspaces/${options.workspace}/workflows/${workflowFileId}/duplicate`,
|
|
1244
|
+
{}
|
|
1245
|
+
);
|
|
1246
|
+
if (!response.ok) {
|
|
1247
|
+
stopSpinner(false, "Failed");
|
|
1248
|
+
failResponse(response);
|
|
1249
|
+
}
|
|
1250
|
+
stopSpinner(true, "Workflow duplicated");
|
|
1251
|
+
printWorkflow(response.data);
|
|
1252
|
+
}
|
|
1253
|
+
);
|
|
1254
|
+
registerStatusCommands(workflow);
|
|
1255
|
+
registerTransitionCommands(workflow);
|
|
1256
|
+
registerTaskCommands(workflow);
|
|
1257
|
+
registerRunCommands(workflow);
|
|
1258
|
+
}
|
|
1259
|
+
function registerStatusCommands(workflow) {
|
|
1260
|
+
const status = workflow.command("status").description("Workflow status operations");
|
|
1261
|
+
addPreferredAssigneeOptions(
|
|
1262
|
+
addWorkspaceOption(
|
|
1263
|
+
status.command("create").description("Create a workflow status").argument("<workflowFileId>").requiredOption("--name <name>", "Status name").requiredOption("--type <type>", "Status type value: 1=todo, 2=in-progress, 3=done").option("--desc-file <localPath>", "Local UTF-8 text file for status description")
|
|
1264
|
+
)
|
|
1265
|
+
).action(
|
|
1266
|
+
async (workflowFileId, options) => {
|
|
1267
|
+
const body = runOrExit3(() => {
|
|
1268
|
+
const next = {
|
|
1269
|
+
name: options.name,
|
|
1270
|
+
type: parseRequiredStatusType(options.type)
|
|
1271
|
+
};
|
|
1272
|
+
const desc = readDescFile(options.descFile);
|
|
1273
|
+
if (desc !== void 0) next.desc = desc;
|
|
1274
|
+
Object.assign(next, parsePreferredAssigneeBody(options));
|
|
1275
|
+
return next;
|
|
1276
|
+
});
|
|
1277
|
+
startSpinner("Creating status...");
|
|
1278
|
+
const response = await httpPost(
|
|
1279
|
+
`/workspaces/${options.workspace}/workflows/${workflowFileId}/statuses`,
|
|
1280
|
+
body
|
|
1281
|
+
);
|
|
1282
|
+
if (!response.ok) {
|
|
1283
|
+
stopSpinner(false, "Failed");
|
|
1284
|
+
failResponse(response);
|
|
1285
|
+
}
|
|
1286
|
+
stopSpinner(true, "Status created");
|
|
1287
|
+
printStatus(response.data);
|
|
1288
|
+
}
|
|
1289
|
+
);
|
|
1290
|
+
addPreferredAssigneeOptions(
|
|
1291
|
+
addWorkspaceOption(
|
|
1292
|
+
status.command("update").description("Update a workflow status").argument("<workflowFileId>").argument("<statusScopedId>").option("--name <name>", "Status name").option("--type <type>", "Status type value: 1=todo, 2=in-progress, 3=done").option("--desc-file <localPath>", "Local UTF-8 text file for status description").option("--clear-desc", "Clear status description").option("--clear-preferred-assignee", "Clear preferred assignee")
|
|
1293
|
+
)
|
|
1294
|
+
).action(
|
|
1295
|
+
async (workflowFileId, statusScopedId, options) => {
|
|
1296
|
+
const body = runOrExit3(() => {
|
|
1297
|
+
if (options.descFile && options.clearDesc) {
|
|
1298
|
+
throw new WorkflowOptionsError("Error: --desc-file cannot be used with --clear-desc.");
|
|
1299
|
+
}
|
|
1300
|
+
const next = {};
|
|
1301
|
+
if (options.name) next.name = options.name;
|
|
1302
|
+
const type = parseStatusType(options.type);
|
|
1303
|
+
if (type !== void 0) next.type = type;
|
|
1304
|
+
const desc = readDescFile(options.descFile);
|
|
1305
|
+
if (desc !== void 0) next.desc = desc;
|
|
1306
|
+
if (options.clearDesc) next.desc = null;
|
|
1307
|
+
Object.assign(next, parsePreferredAssigneeBody(options));
|
|
1308
|
+
assertNonEmptyBody(next);
|
|
1309
|
+
return next;
|
|
1310
|
+
});
|
|
1311
|
+
const id = runOrExit3(() => parsePositiveInteger(statusScopedId, "statusScopedId"));
|
|
1312
|
+
startSpinner("Updating status...");
|
|
1313
|
+
const response = await httpPatch(
|
|
1314
|
+
`/workspaces/${options.workspace}/workflows/${workflowFileId}/statuses/${id}`,
|
|
1315
|
+
body
|
|
1316
|
+
);
|
|
1317
|
+
if (!response.ok) {
|
|
1318
|
+
stopSpinner(false, "Failed");
|
|
1319
|
+
failResponse(response);
|
|
1320
|
+
}
|
|
1321
|
+
stopSpinner(true, "Status updated");
|
|
1322
|
+
printStatus(response.data);
|
|
1323
|
+
}
|
|
1324
|
+
);
|
|
1325
|
+
addWorkspaceOption(
|
|
1326
|
+
status.command("move-after").description("Move a workflow status after another status").argument("<workflowFileId>").argument("<statusScopedId>").option("--after <statusScopedId>", "Anchor status scoped ID").option("--first", "Move to first position")
|
|
1327
|
+
).action(
|
|
1328
|
+
async (workflowFileId, statusScopedId, options) => {
|
|
1329
|
+
const { id, anchoredStatusScopedId } = runOrExit3(() => {
|
|
1330
|
+
if (options.after && options.first) {
|
|
1331
|
+
throw new WorkflowOptionsError("Error: --after cannot be used with --first.");
|
|
1332
|
+
}
|
|
1333
|
+
if (!options.after && !options.first) {
|
|
1334
|
+
throw new WorkflowOptionsError("Error: either --after or --first is required.");
|
|
1335
|
+
}
|
|
1336
|
+
return {
|
|
1337
|
+
id: parsePositiveInteger(statusScopedId, "statusScopedId"),
|
|
1338
|
+
anchoredStatusScopedId: options.first ? null : parsePositiveInteger(options.after, "--after")
|
|
1339
|
+
};
|
|
1340
|
+
});
|
|
1341
|
+
startSpinner("Moving status...");
|
|
1342
|
+
const response = await httpPost(
|
|
1343
|
+
`/workspaces/${options.workspace}/workflows/${workflowFileId}/statuses/${id}/move-after`,
|
|
1344
|
+
{ anchoredStatusScopedId }
|
|
1345
|
+
);
|
|
1346
|
+
if (!response.ok) {
|
|
1347
|
+
stopSpinner(false, "Failed");
|
|
1348
|
+
failResponse(response);
|
|
1349
|
+
}
|
|
1350
|
+
stopSpinner(true, "Status moved");
|
|
1351
|
+
}
|
|
1352
|
+
);
|
|
1353
|
+
addWorkspaceOption(
|
|
1354
|
+
status.command("delete").description("Delete a workflow status").argument("<workflowFileId>").argument("<statusScopedId>")
|
|
1355
|
+
).action(async (workflowFileId, statusScopedId, options) => {
|
|
1356
|
+
const id = runOrExit3(() => parsePositiveInteger(statusScopedId, "statusScopedId"));
|
|
1357
|
+
startSpinner("Deleting status...");
|
|
1358
|
+
const response = await httpDelete(
|
|
1359
|
+
`/workspaces/${options.workspace}/workflows/${workflowFileId}/statuses/${id}`
|
|
1360
|
+
);
|
|
1361
|
+
if (!response.ok) {
|
|
1362
|
+
stopSpinner(false, "Failed");
|
|
1363
|
+
failResponse(response);
|
|
1364
|
+
}
|
|
1365
|
+
stopSpinner(true, `Deleted: ${response.data.scopedId}`);
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
function registerTransitionCommands(workflow) {
|
|
1369
|
+
const transition = workflow.command("transition").description("Workflow transition operations");
|
|
1370
|
+
for (const action of ["create", "delete"]) {
|
|
1371
|
+
addWorkspaceOption(
|
|
1372
|
+
transition.command(action).description(`${action === "create" ? "Create" : "Delete"} a workflow transition`).argument("<workflowFileId>").requiredOption("--from <statusScopedId>", "Source status scoped ID").requiredOption("--to <statusScopedId>", "Target status scoped ID")
|
|
1373
|
+
).action(async (workflowFileId, options) => {
|
|
1374
|
+
const body = runOrExit3(() => ({
|
|
1375
|
+
statusFromScopedId: parsePositiveInteger(options.from, "--from"),
|
|
1376
|
+
statusToScopedId: parsePositiveInteger(options.to, "--to")
|
|
1377
|
+
}));
|
|
1378
|
+
startSpinner(`${action === "create" ? "Creating" : "Deleting"} transition...`);
|
|
1379
|
+
const response = action === "create" ? await httpPost(
|
|
1380
|
+
`/workspaces/${options.workspace}/workflows/${workflowFileId}/transitions`,
|
|
1381
|
+
body
|
|
1382
|
+
) : await httpDelete(
|
|
1383
|
+
`/workspaces/${options.workspace}/workflows/${workflowFileId}/transitions`,
|
|
1384
|
+
body
|
|
1385
|
+
);
|
|
1386
|
+
if (!response.ok) {
|
|
1387
|
+
stopSpinner(false, "Failed");
|
|
1388
|
+
failResponse(response);
|
|
1389
|
+
}
|
|
1390
|
+
stopSpinner(true, action === "create" ? "Transition created" : "Transition deleted");
|
|
1391
|
+
print(`${response.data.statusFromScopedId} -> ${response.data.statusToScopedId}`);
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
function registerTaskCommands(workflow) {
|
|
1396
|
+
const task = workflow.command("task").description("Workflow task operations");
|
|
1397
|
+
addWorkspaceOption(
|
|
1398
|
+
task.command("list").description("List workflow tasks").option("--workflow-file-id <workflowFileId>", "Filter by workflow file ID").option("--scope <scope>", "Task scope", "all").option("-l, --limit <limit>", "Maximum number of tasks to return", "20").option("--cursor <cursor>", "Pagination cursor")
|
|
1399
|
+
).action(
|
|
1400
|
+
async (options) => {
|
|
1401
|
+
const query = runOrExit3(() => buildQuery({
|
|
1402
|
+
scope: parseTaskScope(options.scope),
|
|
1403
|
+
limit: parseSearchLimit(options.limit, 20, 100),
|
|
1404
|
+
cursor: options.cursor,
|
|
1405
|
+
workflowFileId: options.workflowFileId
|
|
1406
|
+
}));
|
|
1407
|
+
startSpinner("Fetching workflow tasks...");
|
|
1408
|
+
const response = await httpGet(`/workspaces/${options.workspace}/workflow-tasks${query}`);
|
|
1409
|
+
if (!response.ok) {
|
|
1410
|
+
stopSpinner(false, "Failed");
|
|
1411
|
+
failResponse(response);
|
|
1412
|
+
}
|
|
1413
|
+
if (response.data.tasks.length === 0) {
|
|
1414
|
+
stopSpinner(true, "No workflow tasks found.");
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
stopSpinner(true, `Found ${response.data.tasks.length} task(s)`);
|
|
1418
|
+
for (const item of response.data.tasks) printTask(item);
|
|
1419
|
+
if (response.data.nextCursor) print(`${colors.dim}nextCursor=${response.data.nextCursor}${colors.reset}`);
|
|
1420
|
+
}
|
|
1421
|
+
);
|
|
1422
|
+
addWorkspaceOption(
|
|
1423
|
+
task.command("get").description("Get a workflow task").argument("<workflowFileId>").argument("<taskScopedId>")
|
|
1424
|
+
).action(async (workflowFileId, taskScopedId, options) => {
|
|
1425
|
+
const id = runOrExit3(() => parsePositiveInteger(taskScopedId, "taskScopedId"));
|
|
1426
|
+
startSpinner("Fetching task...");
|
|
1427
|
+
const response = await httpGet(
|
|
1428
|
+
`/workspaces/${options.workspace}/workflows/${workflowFileId}/tasks/${id}`
|
|
1429
|
+
);
|
|
1430
|
+
if (!response.ok) {
|
|
1431
|
+
stopSpinner(false, "Failed");
|
|
1432
|
+
failResponse(response);
|
|
1433
|
+
}
|
|
1434
|
+
stopSpinner(true, "Task fetched");
|
|
1435
|
+
printTask(response.data);
|
|
1436
|
+
});
|
|
1437
|
+
addAssigneeOptions(
|
|
1438
|
+
addWorkspaceOption(
|
|
1439
|
+
task.command("create").description("Create a workflow task").argument("<workflowFileId>").requiredOption("--title <title>", "Task title").requiredOption("--status <statusScopedId>", "Status scoped ID").option("--priority <priority>", "Priority: urgent, high, medium, or low", "medium").option("--desc-file <localPath>", "Local UTF-8 text file for task description").option("--owner-human-id <humanId>", "Task owner human ID")
|
|
1440
|
+
)
|
|
1441
|
+
).action(
|
|
1442
|
+
async (workflowFileId, options) => {
|
|
1443
|
+
const body = runOrExit3(() => {
|
|
1444
|
+
const next = {
|
|
1445
|
+
title: options.title,
|
|
1446
|
+
statusScopedId: parsePositiveInteger(options.status, "--status"),
|
|
1447
|
+
priority: parseRequiredPriority(options.priority)
|
|
1448
|
+
};
|
|
1449
|
+
const desc = readDescFile(options.descFile);
|
|
1450
|
+
if (desc !== void 0) next.desc = desc;
|
|
1451
|
+
const ownerHumanId = parseOptionalPositiveInteger(options.ownerHumanId, "--owner-human-id");
|
|
1452
|
+
if (ownerHumanId !== void 0) next.ownerHumanId = ownerHumanId;
|
|
1453
|
+
Object.assign(next, parseAssigneeBody(options, false));
|
|
1454
|
+
return next;
|
|
1455
|
+
});
|
|
1456
|
+
startSpinner("Creating task...");
|
|
1457
|
+
const response = await httpPost(
|
|
1458
|
+
`/workspaces/${options.workspace}/workflows/${workflowFileId}/tasks`,
|
|
1459
|
+
body
|
|
1460
|
+
);
|
|
1461
|
+
if (!response.ok) {
|
|
1462
|
+
stopSpinner(false, "Failed");
|
|
1463
|
+
failResponse(response);
|
|
1464
|
+
}
|
|
1465
|
+
stopSpinner(true, "Task created");
|
|
1466
|
+
printTask(response.data);
|
|
1467
|
+
}
|
|
1468
|
+
);
|
|
1469
|
+
addWorkspaceOption(
|
|
1470
|
+
task.command("update").description("Update workflow task metadata").argument("<workflowFileId>").argument("<taskScopedId>").option("--title <title>", "Task title").option("--priority <priority>", "Priority: urgent, high, medium, or low").option("--desc-file <localPath>", "Local UTF-8 text file for task description").option("--clear-desc", "Clear task description").option("--owner-human-id <humanId>", "Task owner human ID")
|
|
1471
|
+
).action(
|
|
1472
|
+
async (workflowFileId, taskScopedId, options) => {
|
|
1473
|
+
const body = runOrExit3(() => {
|
|
1474
|
+
if (options.descFile && options.clearDesc) {
|
|
1475
|
+
throw new WorkflowOptionsError("Error: --desc-file cannot be used with --clear-desc.");
|
|
1476
|
+
}
|
|
1477
|
+
const next = {};
|
|
1478
|
+
if (options.title) next.title = options.title;
|
|
1479
|
+
const priority = parsePriority(options.priority);
|
|
1480
|
+
if (priority !== void 0) next.priority = priority;
|
|
1481
|
+
const desc = readDescFile(options.descFile);
|
|
1482
|
+
if (desc !== void 0) next.desc = desc;
|
|
1483
|
+
if (options.clearDesc) next.desc = null;
|
|
1484
|
+
const ownerHumanId = parseOptionalPositiveInteger(options.ownerHumanId, "--owner-human-id");
|
|
1485
|
+
if (ownerHumanId !== void 0) next.ownerHumanId = ownerHumanId;
|
|
1486
|
+
assertNonEmptyBody(next);
|
|
1487
|
+
return next;
|
|
1488
|
+
});
|
|
1489
|
+
const id = runOrExit3(() => parsePositiveInteger(taskScopedId, "taskScopedId"));
|
|
1490
|
+
startSpinner("Updating task...");
|
|
1491
|
+
const response = await httpPatch(
|
|
1492
|
+
`/workspaces/${options.workspace}/workflows/${workflowFileId}/tasks/${id}/metadata`,
|
|
1493
|
+
body
|
|
1494
|
+
);
|
|
1495
|
+
if (!response.ok) {
|
|
1496
|
+
stopSpinner(false, "Failed");
|
|
1497
|
+
failResponse(response);
|
|
1498
|
+
}
|
|
1499
|
+
stopSpinner(true, "Task updated");
|
|
1500
|
+
printTask(response.data);
|
|
1501
|
+
}
|
|
1502
|
+
);
|
|
1503
|
+
for (const action of ["add-linked-files", "remove-linked-files"]) {
|
|
1504
|
+
addWorkspaceOption(
|
|
1505
|
+
task.command(action).description(`${action === "add-linked-files" ? "Add" : "Remove"} linked files on a workflow task`).argument("<workflowFileId>").argument("<taskScopedId>").requiredOption("--file-ids <ids>", "Comma-separated file IDs")
|
|
1506
|
+
).action(
|
|
1507
|
+
async (workflowFileId, taskScopedId, options) => {
|
|
1508
|
+
const { id, fileIds } = runOrExit3(() => ({
|
|
1509
|
+
id: parsePositiveInteger(taskScopedId, "taskScopedId"),
|
|
1510
|
+
fileIds: parseFileIds(options.fileIds, "--file-ids")
|
|
1511
|
+
}));
|
|
1512
|
+
startSpinner(action === "add-linked-files" ? "Adding linked files..." : "Removing linked files...");
|
|
1513
|
+
const path2 = `/workspaces/${options.workspace}/workflows/${workflowFileId}/tasks/${id}/linked-files`;
|
|
1514
|
+
const response = action === "add-linked-files" ? await httpPost(path2, { fileIds }) : await httpDelete(path2, { fileIds });
|
|
1515
|
+
if (!response.ok) {
|
|
1516
|
+
stopSpinner(false, "Failed");
|
|
1517
|
+
failResponse(response);
|
|
1518
|
+
}
|
|
1519
|
+
stopSpinner(true, action === "add-linked-files" ? "Linked files added" : "Linked files removed");
|
|
1520
|
+
printTask(response.data);
|
|
1521
|
+
}
|
|
1522
|
+
);
|
|
1523
|
+
}
|
|
1524
|
+
addWorkspaceOption(
|
|
1525
|
+
task.command("move").description("Move a workflow task to another status").argument("<workflowFileId>").argument("<taskScopedId>").requiredOption("--status <statusScopedId>", "Target status scoped ID")
|
|
1526
|
+
).action(
|
|
1527
|
+
async (workflowFileId, taskScopedId, options) => {
|
|
1528
|
+
const { id, statusScopedId } = runOrExit3(() => ({
|
|
1529
|
+
id: parsePositiveInteger(taskScopedId, "taskScopedId"),
|
|
1530
|
+
statusScopedId: parsePositiveInteger(options.status, "--status")
|
|
1531
|
+
}));
|
|
1532
|
+
startSpinner("Moving task...");
|
|
1533
|
+
const response = await httpPatch(
|
|
1534
|
+
`/workspaces/${options.workspace}/workflows/${workflowFileId}/tasks/${id}/status`,
|
|
1535
|
+
{ statusScopedId }
|
|
1536
|
+
);
|
|
1537
|
+
if (!response.ok) {
|
|
1538
|
+
stopSpinner(false, "Failed");
|
|
1539
|
+
failResponse(response);
|
|
1540
|
+
}
|
|
1541
|
+
stopSpinner(true, "Task moved");
|
|
1542
|
+
printTask(response.data);
|
|
1543
|
+
}
|
|
1544
|
+
);
|
|
1545
|
+
addAssigneeOptions(
|
|
1546
|
+
addWorkspaceOption(
|
|
1547
|
+
task.command("assign").description("Update workflow task assignee").argument("<workflowFileId>").argument("<taskScopedId>").option("--clear", "Clear assignee")
|
|
1548
|
+
)
|
|
1549
|
+
).action(
|
|
1550
|
+
async (workflowFileId, taskScopedId, options) => {
|
|
1551
|
+
const body = runOrExit3(() => {
|
|
1552
|
+
const next = parseAssigneeBody(options, true);
|
|
1553
|
+
if (Object.keys(next).length === 0 && options.clear !== true) assertNonEmptyBody(next);
|
|
1554
|
+
return next;
|
|
1555
|
+
});
|
|
1556
|
+
const id = runOrExit3(() => parsePositiveInteger(taskScopedId, "taskScopedId"));
|
|
1557
|
+
startSpinner("Updating task assignee...");
|
|
1558
|
+
const response = await httpPatch(
|
|
1559
|
+
`/workspaces/${options.workspace}/workflows/${workflowFileId}/tasks/${id}/assignee`,
|
|
1560
|
+
body
|
|
1561
|
+
);
|
|
1562
|
+
if (!response.ok) {
|
|
1563
|
+
stopSpinner(false, "Failed");
|
|
1564
|
+
failResponse(response);
|
|
1565
|
+
}
|
|
1566
|
+
stopSpinner(true, "Task assignee updated");
|
|
1567
|
+
printTask(response.data);
|
|
1568
|
+
}
|
|
1569
|
+
);
|
|
1570
|
+
addWorkspaceOption(
|
|
1571
|
+
task.command("delete").description("Delete a workflow task").argument("<workflowFileId>").argument("<taskScopedId>")
|
|
1572
|
+
).action(async (workflowFileId, taskScopedId, options) => {
|
|
1573
|
+
const id = runOrExit3(() => parsePositiveInteger(taskScopedId, "taskScopedId"));
|
|
1574
|
+
startSpinner("Deleting task...");
|
|
1575
|
+
const response = await httpDelete(
|
|
1576
|
+
`/workspaces/${options.workspace}/workflows/${workflowFileId}/tasks/${id}`
|
|
1577
|
+
);
|
|
1578
|
+
if (!response.ok) {
|
|
1579
|
+
stopSpinner(false, "Failed");
|
|
1580
|
+
failResponse(response);
|
|
1581
|
+
}
|
|
1582
|
+
stopSpinner(true, `Deleted: ${response.data.scopedId}`);
|
|
1583
|
+
});
|
|
1584
|
+
}
|
|
1585
|
+
function registerRunCommands(workflow) {
|
|
1586
|
+
const run = workflow.command("run").description("Workflow agent run operations");
|
|
1587
|
+
addWorkspaceOption(
|
|
1588
|
+
run.command("status").description("Get workflow agent run statuses").argument("<workflowFileId>").argument("<triggerIds...>")
|
|
1589
|
+
).action(async (workflowFileId, triggerIds, options) => {
|
|
1590
|
+
const workflowPipelineTriggerIds = runOrExit3(
|
|
1591
|
+
() => triggerIds.map((triggerId) => parsePositiveInteger(triggerId, "workflowPipelineTriggerId"))
|
|
1592
|
+
);
|
|
1593
|
+
startSpinner("Fetching run statuses...");
|
|
1594
|
+
const response = await httpPost(
|
|
1595
|
+
`/workspaces/${options.workspace}/workflows/${workflowFileId}/pipeline/triggers/statuses`,
|
|
1596
|
+
{ workflowPipelineTriggerIds }
|
|
1597
|
+
);
|
|
1598
|
+
if (!response.ok) {
|
|
1599
|
+
stopSpinner(false, "Failed");
|
|
1600
|
+
failResponse(response);
|
|
1601
|
+
}
|
|
1602
|
+
stopSpinner(true, `Found ${response.data.length} run status item(s)`);
|
|
1603
|
+
for (const item of response.data) {
|
|
1604
|
+
print(`${colors.bold}${item.workflowPipelineTriggerId}${colors.reset} ${item.pipelineStatus}`);
|
|
1605
|
+
}
|
|
1606
|
+
});
|
|
1607
|
+
addWorkspaceOption(
|
|
1608
|
+
run.command("output").description("Get workflow agent run output events").argument("<workflowFileId>").argument("<triggerId>")
|
|
1609
|
+
).action(async (workflowFileId, triggerId, options) => {
|
|
1610
|
+
const id = runOrExit3(() => parsePositiveInteger(triggerId, "workflowPipelineTriggerId"));
|
|
1611
|
+
startSpinner("Fetching run output...");
|
|
1612
|
+
const response = await httpGet(
|
|
1613
|
+
`/workspaces/${options.workspace}/workflows/${workflowFileId}/pipeline/trigger/${id}/output`
|
|
1614
|
+
);
|
|
1615
|
+
if (!response.ok) {
|
|
1616
|
+
stopSpinner(false, "Failed");
|
|
1617
|
+
failResponse(response);
|
|
1618
|
+
}
|
|
1619
|
+
stopSpinner(true, `Run ${response.data.workflowPipelineTriggerId}`);
|
|
1620
|
+
for (const event of response.data.pipelineEvents) print(event);
|
|
1621
|
+
});
|
|
1622
|
+
addWorkspaceOption(
|
|
1623
|
+
run.command("abort").description("Abort a workflow agent run").argument("<workflowFileId>").argument("<triggerId>")
|
|
1624
|
+
).action(async (workflowFileId, triggerId, options) => {
|
|
1625
|
+
const id = runOrExit3(() => parsePositiveInteger(triggerId, "workflowPipelineTriggerId"));
|
|
1626
|
+
startSpinner("Aborting run...");
|
|
1627
|
+
const response = await httpPost(
|
|
1628
|
+
`/workspaces/${options.workspace}/workflows/${workflowFileId}/pipeline/trigger/${id}/abort`,
|
|
1629
|
+
{}
|
|
1630
|
+
);
|
|
1631
|
+
if (!response.ok) {
|
|
1632
|
+
stopSpinner(false, "Failed");
|
|
1633
|
+
failResponse(response);
|
|
1634
|
+
}
|
|
1635
|
+
stopSpinner(true, `Abort accepted: ${id}`);
|
|
1636
|
+
});
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
// src/program.ts
|
|
1640
|
+
function createProgram(version) {
|
|
1641
|
+
const program2 = new Command();
|
|
1642
|
+
program2.name("moxt").description("Moxt CLI - AI Workspace").version(version).action(() => {
|
|
1643
|
+
program2.help();
|
|
1644
|
+
});
|
|
1645
|
+
registerWhoamiCommand(program2);
|
|
1646
|
+
registerWorkspaceCommand(program2);
|
|
1647
|
+
registerFileCommand(program2);
|
|
1648
|
+
registerMemoryCommand(program2);
|
|
1649
|
+
registerWorkflowCommand(program2);
|
|
1650
|
+
registerMiniappCommand(program2);
|
|
1651
|
+
registerTelemetryCommand(program2);
|
|
1652
|
+
return program2;
|
|
1653
|
+
}
|
|
1654
|
+
|
|
826
1655
|
// src/telemetry/client.ts
|
|
827
1656
|
import { arch as arch2, platform as platform2 } from "os";
|
|
828
1657
|
var buffer = [];
|
|
@@ -830,7 +1659,7 @@ var FLUSH_TIMEOUT_MS = 3e3;
|
|
|
830
1659
|
var MAX_BATCH_SIZE = 100;
|
|
831
1660
|
function commonLabels() {
|
|
832
1661
|
return {
|
|
833
|
-
cli_version: "0.3.
|
|
1662
|
+
cli_version: "0.3.3-beta.0",
|
|
834
1663
|
node_version: process.versions.node,
|
|
835
1664
|
os: platform2(),
|
|
836
1665
|
arch: arch2(),
|
|
@@ -876,7 +1705,7 @@ async function flush() {
|
|
|
876
1705
|
}
|
|
877
1706
|
const batch = buffer.splice(0, MAX_BATCH_SIZE);
|
|
878
1707
|
const payload = {
|
|
879
|
-
release_id: "0.3.
|
|
1708
|
+
release_id: "0.3.3-beta.0",
|
|
880
1709
|
client_type: "cli",
|
|
881
1710
|
metric_data_list: batch.map((m) => ({
|
|
882
1711
|
profile: "cli",
|
|
@@ -1017,19 +1846,11 @@ function installExitHandler() {
|
|
|
1017
1846
|
|
|
1018
1847
|
// src/index.ts
|
|
1019
1848
|
updateNotifier({
|
|
1020
|
-
pkg: { name: "@moxt-ai/cli", version: "0.3.
|
|
1849
|
+
pkg: { name: "@moxt-ai/cli", version: "0.3.3-beta.0" }
|
|
1021
1850
|
}).notify();
|
|
1022
|
-
var program =
|
|
1023
|
-
program.name("moxt").description("Moxt CLI - AI Workspace").version("0.3.1").action(() => {
|
|
1024
|
-
program.help();
|
|
1025
|
-
});
|
|
1851
|
+
var program = createProgram("0.3.3-beta.0");
|
|
1026
1852
|
instrumentProgram(program);
|
|
1027
1853
|
installExitHandler();
|
|
1028
|
-
registerWhoamiCommand(program);
|
|
1029
|
-
registerWorkspaceCommand(program);
|
|
1030
|
-
registerFileCommand(program);
|
|
1031
|
-
registerMiniappCommand(program);
|
|
1032
|
-
registerTelemetryCommand(program);
|
|
1033
1854
|
program.parseAsync(process.argv).then(async () => {
|
|
1034
1855
|
await flush();
|
|
1035
1856
|
}).catch(async (err) => {
|