@moxt-ai/cli 0.3.2 → 0.3.3-beta.1
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 +51 -0
- package/dist/index.js +1000 -32
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,9 +1,162 @@
|
|
|
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
|
+
|
|
9
|
+
// src/utils/output.ts
|
|
10
|
+
var colors = {
|
|
11
|
+
bold: "\x1B[1m",
|
|
12
|
+
blue: "\x1B[1;34m",
|
|
13
|
+
green: "\x1B[32m",
|
|
14
|
+
red: "\x1B[31m",
|
|
15
|
+
cyan: "\x1B[36m",
|
|
16
|
+
dim: "\x1B[2m",
|
|
17
|
+
reset: "\x1B[0m"
|
|
18
|
+
};
|
|
19
|
+
function print(message) {
|
|
20
|
+
console.log(message);
|
|
21
|
+
}
|
|
22
|
+
function printError(message) {
|
|
23
|
+
console.error(`${colors.red}${message}${colors.reset}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// src/utils/runtime-context.ts
|
|
27
|
+
var DEFAULT_USER_HOST = "api.moxt.ai";
|
|
28
|
+
var SANDBOX_ENV_KEYS = ["BASE_API_HOST", "SANDBOX_TOOL_API_TOKEN", "MOXT_REPO_PAYLOAD", "MOXT_WORKSPACE_ID"];
|
|
29
|
+
var RuntimeContextError = class extends Error {
|
|
30
|
+
constructor(message) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.name = "RuntimeContextError";
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
function resolveRuntimeContext(env = process.env) {
|
|
36
|
+
if (hasAnySandboxEnv(env)) {
|
|
37
|
+
return resolveSandboxRuntimeContext(env);
|
|
38
|
+
}
|
|
39
|
+
const apiKey = readNonEmptyEnv(env, "MOXT_API_KEY");
|
|
40
|
+
if (apiKey) {
|
|
41
|
+
return {
|
|
42
|
+
mode: "user",
|
|
43
|
+
host: readNonEmptyEnv(env, "MOXT_HOST") ?? DEFAULT_USER_HOST,
|
|
44
|
+
apiKey
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
throw new RuntimeContextError("Missing auth. Set sandbox environment variables or MOXT_API_KEY.");
|
|
48
|
+
}
|
|
49
|
+
function resolveSandboxRuntimeContext(env = process.env) {
|
|
50
|
+
const missingKeys = SANDBOX_ENV_KEYS.filter((key) => !readNonEmptyEnv(env, key));
|
|
51
|
+
if (missingKeys.length > 0) {
|
|
52
|
+
throw new RuntimeContextError(`Incomplete sandbox environment. Missing: ${missingKeys.join(", ")}.`);
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
mode: "sandbox",
|
|
56
|
+
baseApiHost: normalizeBaseApiHost(readRequiredEnv(env, "BASE_API_HOST")),
|
|
57
|
+
sandboxToolApiToken: readRequiredEnv(env, "SANDBOX_TOOL_API_TOKEN"),
|
|
58
|
+
workspaceId: readRequiredEnv(env, "MOXT_WORKSPACE_ID"),
|
|
59
|
+
repoPayload: parseRepoPayload(readRequiredEnv(env, "MOXT_REPO_PAYLOAD"))
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function parseRepoPayload(raw) {
|
|
63
|
+
let parsed;
|
|
64
|
+
try {
|
|
65
|
+
parsed = JSON.parse(raw);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
throw new RuntimeContextError(`MOXT_REPO_PAYLOAD must be valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
68
|
+
}
|
|
69
|
+
if (!isRecord(parsed)) {
|
|
70
|
+
throw new RuntimeContextError("MOXT_REPO_PAYLOAD must be a JSON object.");
|
|
71
|
+
}
|
|
72
|
+
const personal = parseRepoEntry(parsed.personal, "personal");
|
|
73
|
+
const teamsValue = parsed.teams;
|
|
74
|
+
if (!Array.isArray(teamsValue)) {
|
|
75
|
+
throw new RuntimeContextError("MOXT_REPO_PAYLOAD.teams must be an array.");
|
|
76
|
+
}
|
|
77
|
+
const teams = teamsValue.map((entry, index) => parseRepoEntry(entry, `teams[${index}]`));
|
|
78
|
+
const workspace = parsed.workspace == null ? null : parseRepoEntry(parsed.workspace, "workspace");
|
|
79
|
+
return { personal, teams, workspace };
|
|
80
|
+
}
|
|
81
|
+
function normalizeBaseApiHost(value) {
|
|
82
|
+
const trimmed = value.trim();
|
|
83
|
+
if (!trimmed) {
|
|
84
|
+
throw new RuntimeContextError("BASE_API_HOST must not be empty.");
|
|
85
|
+
}
|
|
86
|
+
return trimmed.replace(/\/+$/, "");
|
|
87
|
+
}
|
|
88
|
+
function hasAnySandboxEnv(env) {
|
|
89
|
+
return SANDBOX_ENV_KEYS.some((key) => readNonEmptyEnv(env, key) !== void 0);
|
|
90
|
+
}
|
|
91
|
+
function readRequiredEnv(env, key) {
|
|
92
|
+
const value = readNonEmptyEnv(env, key);
|
|
93
|
+
if (!value) {
|
|
94
|
+
throw new RuntimeContextError(`${key} is required.`);
|
|
95
|
+
}
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
function readNonEmptyEnv(env, key) {
|
|
99
|
+
const value = env[key];
|
|
100
|
+
if (typeof value !== "string") return void 0;
|
|
101
|
+
const trimmed = value.trim();
|
|
102
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
103
|
+
}
|
|
104
|
+
function parseRepoEntry(value, path2) {
|
|
105
|
+
if (!isRecord(value)) {
|
|
106
|
+
throw new RuntimeContextError(`MOXT_REPO_PAYLOAD.${path2} must be an object.`);
|
|
107
|
+
}
|
|
108
|
+
const repoId = readStringField(value, "repoId", path2);
|
|
109
|
+
const name = readStringField(value, "name", path2);
|
|
110
|
+
return { repoId, name };
|
|
111
|
+
}
|
|
112
|
+
function readStringField(record2, field, path2) {
|
|
113
|
+
const value = record2[field];
|
|
114
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
115
|
+
throw new RuntimeContextError(`MOXT_REPO_PAYLOAD.${path2}.${field} must be a non-empty string.`);
|
|
116
|
+
}
|
|
117
|
+
return value;
|
|
118
|
+
}
|
|
119
|
+
function isRecord(value) {
|
|
120
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// src/utils/sandbox-path.ts
|
|
124
|
+
function listRepoEntries(repoPayload) {
|
|
125
|
+
return [
|
|
126
|
+
repoPayload.personal,
|
|
127
|
+
...repoPayload.teams,
|
|
128
|
+
...repoPayload.workspace ? [repoPayload.workspace] : []
|
|
129
|
+
];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// src/commands/auth.ts
|
|
133
|
+
function registerAuthCommand(program2) {
|
|
134
|
+
const auth = program2.command("auth").description("Authentication utilities");
|
|
135
|
+
auth.command("status").description("Show CLI authentication mode").action(() => {
|
|
136
|
+
try {
|
|
137
|
+
const context = resolveRuntimeContext();
|
|
138
|
+
if (context.mode === "sandbox") {
|
|
139
|
+
print(`${colors.bold}Mode${colors.reset}: sandbox`);
|
|
140
|
+
print(`${colors.bold}Workspace${colors.reset}: ${context.workspaceId}`);
|
|
141
|
+
print(`${colors.bold}Base API${colors.reset}: ${context.baseApiHost}`);
|
|
142
|
+
print(`${colors.bold}Repos${colors.reset}:`);
|
|
143
|
+
for (const repo of listRepoEntries(context.repoPayload)) {
|
|
144
|
+
print(` ${repo.name} ${repo.repoId}`);
|
|
145
|
+
}
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
print(`${colors.bold}Mode${colors.reset}: user`);
|
|
149
|
+
print(`${colors.bold}Host${colors.reset}: ${context.host}`);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
if (error instanceof RuntimeContextError) {
|
|
152
|
+
printError(error.message);
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
7
160
|
// src/commands/file.ts
|
|
8
161
|
import * as fs from "fs";
|
|
9
162
|
|
|
@@ -147,7 +300,7 @@ function getApiKeyOrUndefined() {
|
|
|
147
300
|
|
|
148
301
|
// src/utils/http.ts
|
|
149
302
|
function getUserAgent() {
|
|
150
|
-
return `moxt-cli/${"0.3.
|
|
303
|
+
return `moxt-cli/${"0.3.3-beta.1"} (${platform()}/${arch()}) node/${process.versions.node}`;
|
|
151
304
|
}
|
|
152
305
|
async function fetchApi(method, path2, body) {
|
|
153
306
|
const apiKey = getApiKey();
|
|
@@ -187,6 +340,9 @@ async function httpPut(path2, body) {
|
|
|
187
340
|
async function httpPost(path2, body) {
|
|
188
341
|
return request("POST", path2, body);
|
|
189
342
|
}
|
|
343
|
+
async function httpPatch(path2, body) {
|
|
344
|
+
return request("PATCH", path2, body);
|
|
345
|
+
}
|
|
190
346
|
async function httpDelete(path2, body) {
|
|
191
347
|
return request("DELETE", path2, body);
|
|
192
348
|
}
|
|
@@ -200,23 +356,6 @@ async function httpRaw(method, path2, body) {
|
|
|
200
356
|
};
|
|
201
357
|
}
|
|
202
358
|
|
|
203
|
-
// src/utils/output.ts
|
|
204
|
-
var colors = {
|
|
205
|
-
bold: "\x1B[1m",
|
|
206
|
-
blue: "\x1B[1;34m",
|
|
207
|
-
green: "\x1B[32m",
|
|
208
|
-
red: "\x1B[31m",
|
|
209
|
-
cyan: "\x1B[36m",
|
|
210
|
-
dim: "\x1B[2m",
|
|
211
|
-
reset: "\x1B[0m"
|
|
212
|
-
};
|
|
213
|
-
function print(message) {
|
|
214
|
-
console.log(message);
|
|
215
|
-
}
|
|
216
|
-
function printError(message) {
|
|
217
|
-
console.error(`${colors.red}${message}${colors.reset}`);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
359
|
// src/utils/search-options.ts
|
|
221
360
|
var SearchOptionsError = class extends Error {
|
|
222
361
|
constructor(message) {
|
|
@@ -939,6 +1078,844 @@ function registerWorkspaceCommand(program2) {
|
|
|
939
1078
|
});
|
|
940
1079
|
}
|
|
941
1080
|
|
|
1081
|
+
// src/commands/workflow.ts
|
|
1082
|
+
import * as fs3 from "fs";
|
|
1083
|
+
var MAX_DESC_LENGTH = 5e3;
|
|
1084
|
+
var WorkflowOptionsError = class extends Error {
|
|
1085
|
+
constructor(message) {
|
|
1086
|
+
super(message);
|
|
1087
|
+
this.name = "WorkflowOptionsError";
|
|
1088
|
+
}
|
|
1089
|
+
};
|
|
1090
|
+
function runOrExit3(fn) {
|
|
1091
|
+
try {
|
|
1092
|
+
return fn();
|
|
1093
|
+
} catch (err) {
|
|
1094
|
+
if (err instanceof WorkflowOptionsError || err instanceof SpaceOptionsError || err instanceof SearchOptionsError) {
|
|
1095
|
+
printError(err.message);
|
|
1096
|
+
process.exit(1);
|
|
1097
|
+
}
|
|
1098
|
+
throw err;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
function addWorkspaceOption(command) {
|
|
1102
|
+
return command.requiredOption("-w, --workspace <workspaceId>", "Workspace ID");
|
|
1103
|
+
}
|
|
1104
|
+
function addSpaceOptions2(command) {
|
|
1105
|
+
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)");
|
|
1106
|
+
}
|
|
1107
|
+
function addAssigneeOptions(command) {
|
|
1108
|
+
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");
|
|
1109
|
+
}
|
|
1110
|
+
function addPreferredAssigneeOptions(command) {
|
|
1111
|
+
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");
|
|
1112
|
+
}
|
|
1113
|
+
function parsePositiveInteger(raw, label) {
|
|
1114
|
+
const parsed = Number.parseInt(raw, 10);
|
|
1115
|
+
if (!Number.isInteger(parsed) || parsed <= 0 || String(parsed) !== raw) {
|
|
1116
|
+
throw new WorkflowOptionsError(`Error: ${label} must be a positive integer, got "${raw}".`);
|
|
1117
|
+
}
|
|
1118
|
+
return parsed;
|
|
1119
|
+
}
|
|
1120
|
+
function parseOptionalPositiveInteger(raw, label) {
|
|
1121
|
+
if (raw == null || raw === "") return void 0;
|
|
1122
|
+
return parsePositiveInteger(raw, label);
|
|
1123
|
+
}
|
|
1124
|
+
function parseStatusType(raw) {
|
|
1125
|
+
if (raw == null || raw === "") return void 0;
|
|
1126
|
+
const parsed = Number.parseInt(raw, 10);
|
|
1127
|
+
if ((parsed === 1 || parsed === 2 || parsed === 3) && String(parsed) === raw) return parsed;
|
|
1128
|
+
throw new WorkflowOptionsError("Error: --type must be one of 1 (todo), 2 (in-progress), or 3 (done).");
|
|
1129
|
+
}
|
|
1130
|
+
function parseRequiredStatusType(raw) {
|
|
1131
|
+
const value = parseStatusType(raw);
|
|
1132
|
+
if (value === void 0) {
|
|
1133
|
+
throw new WorkflowOptionsError("Error: --type is required.");
|
|
1134
|
+
}
|
|
1135
|
+
return value;
|
|
1136
|
+
}
|
|
1137
|
+
function parsePriority(raw) {
|
|
1138
|
+
if (raw == null || raw === "") return void 0;
|
|
1139
|
+
if (raw === "urgent") return 0;
|
|
1140
|
+
if (raw === "high") return 1;
|
|
1141
|
+
if (raw === "medium") return 2;
|
|
1142
|
+
if (raw === "low") return 3;
|
|
1143
|
+
throw new WorkflowOptionsError('Error: --priority must be "urgent", "high", "medium", or "low".');
|
|
1144
|
+
}
|
|
1145
|
+
function parseRequiredPriority(raw) {
|
|
1146
|
+
return parsePriority(raw ?? "medium") ?? 2;
|
|
1147
|
+
}
|
|
1148
|
+
function parseWorkflowScope(raw) {
|
|
1149
|
+
if (raw == null || raw === "") return "all";
|
|
1150
|
+
if (raw === "all" || raw === "owner_is_me") return raw;
|
|
1151
|
+
throw new WorkflowOptionsError('Error: --scope must be "all" or "owner_is_me".');
|
|
1152
|
+
}
|
|
1153
|
+
function parseTaskScope(raw) {
|
|
1154
|
+
if (raw == null || raw === "") return "all";
|
|
1155
|
+
if (raw === "all" || raw === "assigned_to_me" || raw === "subscribed_by_me" || raw === "owner_is_me") {
|
|
1156
|
+
return raw;
|
|
1157
|
+
}
|
|
1158
|
+
throw new WorkflowOptionsError(
|
|
1159
|
+
'Error: --scope must be "all", "assigned_to_me", "subscribed_by_me", or "owner_is_me".'
|
|
1160
|
+
);
|
|
1161
|
+
}
|
|
1162
|
+
function parseFileIds(raw, label = "--file-ids") {
|
|
1163
|
+
if (raw == null || raw === "") return void 0;
|
|
1164
|
+
const ids = raw.split(",").map((item) => item.trim()).filter(Boolean);
|
|
1165
|
+
if (ids.length === 0) {
|
|
1166
|
+
throw new WorkflowOptionsError(`Error: ${label} must include at least one file ID.`);
|
|
1167
|
+
}
|
|
1168
|
+
return ids;
|
|
1169
|
+
}
|
|
1170
|
+
function readDescFile(path2) {
|
|
1171
|
+
if (path2 == null || path2 === "") return void 0;
|
|
1172
|
+
if (!fs3.existsSync(path2)) {
|
|
1173
|
+
throw new WorkflowOptionsError(`Error: desc file not found: ${path2}`);
|
|
1174
|
+
}
|
|
1175
|
+
const stat = fs3.statSync(path2);
|
|
1176
|
+
if (!stat.isFile()) {
|
|
1177
|
+
throw new WorkflowOptionsError(`Error: desc file is not a file: ${path2}`);
|
|
1178
|
+
}
|
|
1179
|
+
const buffer2 = fs3.readFileSync(path2);
|
|
1180
|
+
if (isBinaryContent(buffer2)) {
|
|
1181
|
+
throw new WorkflowOptionsError("Error: desc file must be UTF-8 text, not binary content.");
|
|
1182
|
+
}
|
|
1183
|
+
const content = buffer2.toString("utf8");
|
|
1184
|
+
if (content.length > MAX_DESC_LENGTH) {
|
|
1185
|
+
throw new WorkflowOptionsError(`Error: desc file must be at most ${MAX_DESC_LENGTH} characters.`);
|
|
1186
|
+
}
|
|
1187
|
+
return content;
|
|
1188
|
+
}
|
|
1189
|
+
function parseAssigneeBody(options, allowClear) {
|
|
1190
|
+
const hasClear = options.clear === true;
|
|
1191
|
+
const humanId = parseOptionalPositiveInteger(options.humanId, "--human-id");
|
|
1192
|
+
const assistantId = parseOptionalPositiveInteger(options.assistantId, "--assistant-id");
|
|
1193
|
+
const teammateId = parseOptionalPositiveInteger(options.teammateId, "--teammate-id");
|
|
1194
|
+
const count = (humanId !== void 0 ? 1 : 0) + (assistantId !== void 0 ? 1 : 0) + (teammateId !== void 0 ? 1 : 0);
|
|
1195
|
+
if (hasClear && !allowClear) {
|
|
1196
|
+
throw new WorkflowOptionsError("Error: --clear is not supported here.");
|
|
1197
|
+
}
|
|
1198
|
+
if (hasClear && count > 0) {
|
|
1199
|
+
throw new WorkflowOptionsError("Error: --clear cannot be used with assignee ID options.");
|
|
1200
|
+
}
|
|
1201
|
+
if (count > 1) {
|
|
1202
|
+
throw new WorkflowOptionsError("Error: assignee ID options are mutually exclusive.");
|
|
1203
|
+
}
|
|
1204
|
+
if (hasClear) return {};
|
|
1205
|
+
if (humanId !== void 0) return { assigneeHumanId: humanId };
|
|
1206
|
+
if (assistantId !== void 0) return { assigneeAiAssistantId: assistantId };
|
|
1207
|
+
if (teammateId !== void 0) return { assigneeAiTeammateId: teammateId };
|
|
1208
|
+
return {};
|
|
1209
|
+
}
|
|
1210
|
+
function parsePreferredAssigneeBody(options) {
|
|
1211
|
+
const hasClear = options.clearPreferredAssignee === true;
|
|
1212
|
+
const humanId = parseOptionalPositiveInteger(options.preferredHumanId, "--preferred-human-id");
|
|
1213
|
+
const assistantId = parseOptionalPositiveInteger(options.preferredAssistantId, "--preferred-assistant-id");
|
|
1214
|
+
const teammateId = parseOptionalPositiveInteger(options.preferredTeammateId, "--preferred-teammate-id");
|
|
1215
|
+
const count = (humanId !== void 0 ? 1 : 0) + (assistantId !== void 0 ? 1 : 0) + (teammateId !== void 0 ? 1 : 0);
|
|
1216
|
+
if (hasClear && count > 0) {
|
|
1217
|
+
throw new WorkflowOptionsError("Error: --clear-preferred-assignee cannot be used with preferred assignee ID options.");
|
|
1218
|
+
}
|
|
1219
|
+
if (count > 1) {
|
|
1220
|
+
throw new WorkflowOptionsError("Error: preferred assignee ID options are mutually exclusive.");
|
|
1221
|
+
}
|
|
1222
|
+
if (hasClear) return { preferredAssigneeHumanId: null };
|
|
1223
|
+
if (humanId !== void 0) return { preferredAssigneeHumanId: humanId };
|
|
1224
|
+
if (assistantId !== void 0) return { preferredAssigneeAiAssistantId: assistantId };
|
|
1225
|
+
if (teammateId !== void 0) return { preferredAssigneeAiTeammateId: teammateId };
|
|
1226
|
+
return {};
|
|
1227
|
+
}
|
|
1228
|
+
function assertNonEmptyBody(body) {
|
|
1229
|
+
if (Object.keys(body).length === 0) {
|
|
1230
|
+
throw new WorkflowOptionsError("Error: at least one update option must be provided.");
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
function buildQuery(params) {
|
|
1234
|
+
const query = new URLSearchParams();
|
|
1235
|
+
for (const [key, value] of Object.entries(params)) {
|
|
1236
|
+
if (value !== void 0) {
|
|
1237
|
+
query.set(key, String(value));
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
const text = query.toString();
|
|
1241
|
+
return text ? `?${text}` : "";
|
|
1242
|
+
}
|
|
1243
|
+
function failResponse(response) {
|
|
1244
|
+
printError(`Error [${response.status}]: ${JSON.stringify(response.data)}`);
|
|
1245
|
+
process.exit(1);
|
|
1246
|
+
}
|
|
1247
|
+
function priorityLabel(value) {
|
|
1248
|
+
switch (value) {
|
|
1249
|
+
case 0:
|
|
1250
|
+
return "urgent";
|
|
1251
|
+
case 1:
|
|
1252
|
+
return "high";
|
|
1253
|
+
case 2:
|
|
1254
|
+
return "medium";
|
|
1255
|
+
case 3:
|
|
1256
|
+
return "low";
|
|
1257
|
+
default: {
|
|
1258
|
+
const unsupportedValue = value;
|
|
1259
|
+
throw new Error(`Unsupported workflow task priority: ${String(unsupportedValue)}`);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
function statusTypeLabel(value) {
|
|
1264
|
+
switch (value) {
|
|
1265
|
+
case 1:
|
|
1266
|
+
return "todo";
|
|
1267
|
+
case 2:
|
|
1268
|
+
return "in-progress";
|
|
1269
|
+
case 3:
|
|
1270
|
+
return "done";
|
|
1271
|
+
default: {
|
|
1272
|
+
const unsupportedValue = value;
|
|
1273
|
+
throw new Error(`Unsupported workflow status type: ${String(unsupportedValue)}`);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
function formatAssignee(assignee) {
|
|
1278
|
+
if (!assignee) return "-";
|
|
1279
|
+
if (assignee.type === "human") return assignee.displayName ?? `human:${assignee.humanId}`;
|
|
1280
|
+
if (assignee.type === "assistant") return assignee.displayName ?? `assistant:${assignee.aiAssistantId}`;
|
|
1281
|
+
if (assignee.type === "teammate") return assignee.displayName ?? `teammate:${assignee.aiTeammateId}`;
|
|
1282
|
+
return "unknown";
|
|
1283
|
+
}
|
|
1284
|
+
function printWorkflow(item) {
|
|
1285
|
+
print(`${colors.bold}${item.fileId}${colors.reset} owner=${item.ownerHumanId ?? "-"} desc=${item.desc ?? "-"}`);
|
|
1286
|
+
}
|
|
1287
|
+
function indentation(size) {
|
|
1288
|
+
return " ".repeat(size);
|
|
1289
|
+
}
|
|
1290
|
+
function formatDetailScalar(value) {
|
|
1291
|
+
if (value === null) return "null";
|
|
1292
|
+
if (value === "") return '""';
|
|
1293
|
+
if (typeof value === "string" && /[\r\n]/.test(value)) return JSON.stringify(value);
|
|
1294
|
+
return String(value);
|
|
1295
|
+
}
|
|
1296
|
+
function printDetailField(name, value, indent = 0) {
|
|
1297
|
+
print(`${indentation(indent)}${name}: ${formatDetailScalar(value)}`);
|
|
1298
|
+
}
|
|
1299
|
+
function printDetailTextField(name, value, indent = 0) {
|
|
1300
|
+
if (value === null || value === "") {
|
|
1301
|
+
printDetailField(name, value, indent);
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
print(`${indentation(indent)}${name}:`);
|
|
1305
|
+
for (const line of value.split(/\r?\n/)) {
|
|
1306
|
+
print(`${indentation(indent + 2)}${line}`);
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
function printAssigneeDetail(name, assignee, indent = 0) {
|
|
1310
|
+
if (assignee === null) {
|
|
1311
|
+
printDetailField(name, null, indent);
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
print(`${indentation(indent)}${name}:`);
|
|
1315
|
+
printDetailField("type", assignee.type, indent + 2);
|
|
1316
|
+
switch (assignee.type) {
|
|
1317
|
+
case "human":
|
|
1318
|
+
printDetailField("humanId", assignee.humanId, indent + 2);
|
|
1319
|
+
printDetailField("displayName", assignee.displayName, indent + 2);
|
|
1320
|
+
printDetailField("avatarUrl", assignee.avatarUrl, indent + 2);
|
|
1321
|
+
return;
|
|
1322
|
+
case "assistant":
|
|
1323
|
+
printDetailField("aiAssistantId", assignee.aiAssistantId, indent + 2);
|
|
1324
|
+
printDetailField("displayName", assignee.displayName, indent + 2);
|
|
1325
|
+
printDetailField("avatarUrl", assignee.avatarUrl, indent + 2);
|
|
1326
|
+
return;
|
|
1327
|
+
case "teammate":
|
|
1328
|
+
printDetailField("aiTeammateId", assignee.aiTeammateId, indent + 2);
|
|
1329
|
+
printDetailField("displayName", assignee.displayName, indent + 2);
|
|
1330
|
+
printDetailField("avatarUrl", assignee.avatarUrl, indent + 2);
|
|
1331
|
+
return;
|
|
1332
|
+
case "unknown":
|
|
1333
|
+
printDetailField("humanId", assignee.humanId, indent + 2);
|
|
1334
|
+
printDetailField("aiAssistantId", assignee.aiAssistantId, indent + 2);
|
|
1335
|
+
printDetailField("aiTeammateId", assignee.aiTeammateId, indent + 2);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
function printStatusRemainingFields(item, indent) {
|
|
1339
|
+
printDetailField("name", item.name, indent);
|
|
1340
|
+
printDetailTextField("desc", item.desc, indent);
|
|
1341
|
+
printDetailField("type", `${item.type} (${statusTypeLabel(item.type)})`, indent);
|
|
1342
|
+
printAssigneeDetail("preferredAssignee", item.preferredAssignee, indent);
|
|
1343
|
+
printDetailField("order", item.order, indent);
|
|
1344
|
+
}
|
|
1345
|
+
function printStatusDetail(item) {
|
|
1346
|
+
print(`${colors.bold}Status${colors.reset}`);
|
|
1347
|
+
printDetailField("scopedId", item.scopedId);
|
|
1348
|
+
printStatusRemainingFields(item, 0);
|
|
1349
|
+
}
|
|
1350
|
+
function printWorkflowDetail(item) {
|
|
1351
|
+
print(`${colors.bold}Workflow${colors.reset}`);
|
|
1352
|
+
printDetailField("fileId", item.fileId);
|
|
1353
|
+
printDetailField("ownerHumanId", item.ownerHumanId);
|
|
1354
|
+
printDetailTextField("desc", item.desc);
|
|
1355
|
+
if (item.statusList.length === 0) {
|
|
1356
|
+
print("statusList: []");
|
|
1357
|
+
} else {
|
|
1358
|
+
print("statusList:");
|
|
1359
|
+
for (const status of item.statusList) {
|
|
1360
|
+
print(` - scopedId: ${status.scopedId}`);
|
|
1361
|
+
printStatusRemainingFields(status, 4);
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
if (item.transitions.length === 0) {
|
|
1365
|
+
print("transitions: []");
|
|
1366
|
+
} else {
|
|
1367
|
+
print("transitions:");
|
|
1368
|
+
for (const transition of item.transitions) {
|
|
1369
|
+
print(` - statusFromScopedId: ${transition.statusFromScopedId}`);
|
|
1370
|
+
printDetailField("statusToScopedId", transition.statusToScopedId, 4);
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
function printTaskSummary(item) {
|
|
1375
|
+
const runIds = item.agentRuns.map((run) => run.pipelineTriggerId).join(",") || "-";
|
|
1376
|
+
print(
|
|
1377
|
+
`${colors.bold}#${item.scopedId}${colors.reset} ${item.title} status=${item.statusScopedId} priority=${priorityLabel(item.priority)} assignee=${formatAssignee(item.assignee)} runs=${runIds}`
|
|
1378
|
+
);
|
|
1379
|
+
}
|
|
1380
|
+
function printTaskDetail(item) {
|
|
1381
|
+
print(`${colors.bold}Task${colors.reset}`);
|
|
1382
|
+
printDetailField("scopedId", item.scopedId);
|
|
1383
|
+
printDetailField("workflowFileId", item.workflowFileId);
|
|
1384
|
+
printDetailField("ownerHumanId", item.ownerHumanId);
|
|
1385
|
+
printDetailField("title", item.title);
|
|
1386
|
+
printDetailTextField("desc", item.desc);
|
|
1387
|
+
printDetailField("statusScopedId", item.statusScopedId);
|
|
1388
|
+
printDetailField("priority", `${item.priority} (${priorityLabel(item.priority)})`);
|
|
1389
|
+
printAssigneeDetail("assignee", item.assignee);
|
|
1390
|
+
if (item.linkedFileIds.length === 0) {
|
|
1391
|
+
print("linkedFileIds: []");
|
|
1392
|
+
} else {
|
|
1393
|
+
print("linkedFileIds:");
|
|
1394
|
+
for (const fileId of item.linkedFileIds) print(` - ${fileId}`);
|
|
1395
|
+
}
|
|
1396
|
+
printDetailField("subscribed", item.subscribed);
|
|
1397
|
+
if (item.agentRuns.length === 0) {
|
|
1398
|
+
print("agentRuns: []");
|
|
1399
|
+
} else {
|
|
1400
|
+
print("agentRuns:");
|
|
1401
|
+
for (const run of item.agentRuns) {
|
|
1402
|
+
print(` - pipelineTriggerId: ${run.pipelineTriggerId}`);
|
|
1403
|
+
printAssigneeDetail("executor", run.executor, 4);
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
printDetailField("createdAt", item.createdAt);
|
|
1407
|
+
printDetailField("latestActAt", item.latestActAt);
|
|
1408
|
+
}
|
|
1409
|
+
function registerWorkflowCommand(program2) {
|
|
1410
|
+
const workflow = program2.command("workflow").description("Workflow operations");
|
|
1411
|
+
addWorkspaceOption(
|
|
1412
|
+
workflow.command("list").description("List accessible workflows").option("--scope <scope>", "Workflow scope: all or owner_is_me", "all")
|
|
1413
|
+
).action(async (options) => {
|
|
1414
|
+
const scope = runOrExit3(() => parseWorkflowScope(options.scope));
|
|
1415
|
+
startSpinner("Fetching workflows...");
|
|
1416
|
+
const response = await httpGet(
|
|
1417
|
+
`/workspaces/${options.workspace}/workflows${buildQuery({ scope })}`
|
|
1418
|
+
);
|
|
1419
|
+
if (!response.ok) {
|
|
1420
|
+
stopSpinner(false, "Failed");
|
|
1421
|
+
failResponse(response);
|
|
1422
|
+
}
|
|
1423
|
+
if (response.data.workflows.length === 0) {
|
|
1424
|
+
stopSpinner(true, "No workflows found.");
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1427
|
+
stopSpinner(true, `Found ${response.data.workflows.length} workflow(s)`);
|
|
1428
|
+
for (const item of response.data.workflows) printWorkflow(item);
|
|
1429
|
+
});
|
|
1430
|
+
addSpaceOptions2(
|
|
1431
|
+
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")
|
|
1432
|
+
).action(async (options) => {
|
|
1433
|
+
const body = runOrExit3(() => {
|
|
1434
|
+
const next = {
|
|
1435
|
+
path: options.path,
|
|
1436
|
+
...resolveSpaceSelector(options)
|
|
1437
|
+
};
|
|
1438
|
+
const desc = readDescFile(options.descFile);
|
|
1439
|
+
if (desc !== void 0) next.desc = desc;
|
|
1440
|
+
const ownerHumanId = parseOptionalPositiveInteger(options.ownerHumanId, "--owner-human-id");
|
|
1441
|
+
if (ownerHumanId !== void 0) next.ownerHumanId = ownerHumanId;
|
|
1442
|
+
return next;
|
|
1443
|
+
});
|
|
1444
|
+
startSpinner("Creating workflow...");
|
|
1445
|
+
const response = await httpPost(`/workspaces/${options.workspace}/workflows`, body);
|
|
1446
|
+
if (!response.ok) {
|
|
1447
|
+
stopSpinner(false, "Failed");
|
|
1448
|
+
failResponse(response);
|
|
1449
|
+
}
|
|
1450
|
+
stopSpinner(true, "Workflow created");
|
|
1451
|
+
printWorkflow(response.data);
|
|
1452
|
+
});
|
|
1453
|
+
addWorkspaceOption(workflow.command("get").description("Get a workflow").argument("<workflowFileId>")).action(
|
|
1454
|
+
async (workflowFileId, options) => {
|
|
1455
|
+
startSpinner("Fetching workflow...");
|
|
1456
|
+
const response = await httpGet(
|
|
1457
|
+
`/workspaces/${options.workspace}/workflows/${workflowFileId}`
|
|
1458
|
+
);
|
|
1459
|
+
if (!response.ok) {
|
|
1460
|
+
stopSpinner(false, "Failed");
|
|
1461
|
+
failResponse(response);
|
|
1462
|
+
}
|
|
1463
|
+
stopSpinner(true, "Workflow fetched");
|
|
1464
|
+
printWorkflowDetail(response.data);
|
|
1465
|
+
}
|
|
1466
|
+
);
|
|
1467
|
+
addWorkspaceOption(
|
|
1468
|
+
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")
|
|
1469
|
+
).action(async (workflowFileId, options) => {
|
|
1470
|
+
const body = runOrExit3(() => {
|
|
1471
|
+
const next = {};
|
|
1472
|
+
const desc = readDescFile(options.descFile);
|
|
1473
|
+
if (desc !== void 0) next.desc = desc;
|
|
1474
|
+
const ownerHumanId = parseOptionalPositiveInteger(options.ownerHumanId, "--owner-human-id");
|
|
1475
|
+
if (ownerHumanId !== void 0) next.ownerHumanId = ownerHumanId;
|
|
1476
|
+
assertNonEmptyBody(next);
|
|
1477
|
+
return next;
|
|
1478
|
+
});
|
|
1479
|
+
startSpinner("Updating workflow...");
|
|
1480
|
+
const response = await httpPatch(`/workspaces/${options.workspace}/workflows/${workflowFileId}`, body);
|
|
1481
|
+
if (!response.ok) {
|
|
1482
|
+
stopSpinner(false, "Failed");
|
|
1483
|
+
failResponse(response);
|
|
1484
|
+
}
|
|
1485
|
+
stopSpinner(true, "Workflow updated");
|
|
1486
|
+
printWorkflow(response.data);
|
|
1487
|
+
});
|
|
1488
|
+
addWorkspaceOption(
|
|
1489
|
+
workflow.command("delete").description("Delete a workflow").argument("<workflowFileId>").option("--message <message>", "Commit message")
|
|
1490
|
+
).action(async (workflowFileId, options) => {
|
|
1491
|
+
startSpinner("Deleting workflow...");
|
|
1492
|
+
const response = await httpDelete(
|
|
1493
|
+
`/workspaces/${options.workspace}/workflows/${workflowFileId}`,
|
|
1494
|
+
options.message ? { message: options.message } : {}
|
|
1495
|
+
);
|
|
1496
|
+
if (!response.ok) {
|
|
1497
|
+
stopSpinner(false, "Failed");
|
|
1498
|
+
failResponse(response);
|
|
1499
|
+
}
|
|
1500
|
+
stopSpinner(true, `Deleted: ${response.data.fileId}`);
|
|
1501
|
+
});
|
|
1502
|
+
addWorkspaceOption(workflow.command("duplicate").description("Duplicate a workflow").argument("<workflowFileId>")).action(
|
|
1503
|
+
async (workflowFileId, options) => {
|
|
1504
|
+
startSpinner("Duplicating workflow...");
|
|
1505
|
+
const response = await httpPost(
|
|
1506
|
+
`/workspaces/${options.workspace}/workflows/${workflowFileId}/duplicate`,
|
|
1507
|
+
{}
|
|
1508
|
+
);
|
|
1509
|
+
if (!response.ok) {
|
|
1510
|
+
stopSpinner(false, "Failed");
|
|
1511
|
+
failResponse(response);
|
|
1512
|
+
}
|
|
1513
|
+
stopSpinner(true, "Workflow duplicated");
|
|
1514
|
+
printWorkflow(response.data);
|
|
1515
|
+
}
|
|
1516
|
+
);
|
|
1517
|
+
registerStatusCommands(workflow);
|
|
1518
|
+
registerTransitionCommands(workflow);
|
|
1519
|
+
registerTaskCommands(workflow);
|
|
1520
|
+
registerRunCommands(workflow);
|
|
1521
|
+
}
|
|
1522
|
+
function registerStatusCommands(workflow) {
|
|
1523
|
+
const status = workflow.command("status").description("Workflow status operations");
|
|
1524
|
+
addPreferredAssigneeOptions(
|
|
1525
|
+
addWorkspaceOption(
|
|
1526
|
+
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")
|
|
1527
|
+
)
|
|
1528
|
+
).action(
|
|
1529
|
+
async (workflowFileId, options) => {
|
|
1530
|
+
const body = runOrExit3(() => {
|
|
1531
|
+
const next = {
|
|
1532
|
+
name: options.name,
|
|
1533
|
+
type: parseRequiredStatusType(options.type)
|
|
1534
|
+
};
|
|
1535
|
+
const desc = readDescFile(options.descFile);
|
|
1536
|
+
if (desc !== void 0) next.desc = desc;
|
|
1537
|
+
Object.assign(next, parsePreferredAssigneeBody(options));
|
|
1538
|
+
return next;
|
|
1539
|
+
});
|
|
1540
|
+
startSpinner("Creating status...");
|
|
1541
|
+
const response = await httpPost(
|
|
1542
|
+
`/workspaces/${options.workspace}/workflows/${workflowFileId}/statuses`,
|
|
1543
|
+
body
|
|
1544
|
+
);
|
|
1545
|
+
if (!response.ok) {
|
|
1546
|
+
stopSpinner(false, "Failed");
|
|
1547
|
+
failResponse(response);
|
|
1548
|
+
}
|
|
1549
|
+
stopSpinner(true, "Status created");
|
|
1550
|
+
printStatusDetail(response.data);
|
|
1551
|
+
}
|
|
1552
|
+
);
|
|
1553
|
+
addPreferredAssigneeOptions(
|
|
1554
|
+
addWorkspaceOption(
|
|
1555
|
+
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")
|
|
1556
|
+
)
|
|
1557
|
+
).action(
|
|
1558
|
+
async (workflowFileId, statusScopedId, options) => {
|
|
1559
|
+
const body = runOrExit3(() => {
|
|
1560
|
+
if (options.descFile && options.clearDesc) {
|
|
1561
|
+
throw new WorkflowOptionsError("Error: --desc-file cannot be used with --clear-desc.");
|
|
1562
|
+
}
|
|
1563
|
+
const next = {};
|
|
1564
|
+
if (options.name) next.name = options.name;
|
|
1565
|
+
const type = parseStatusType(options.type);
|
|
1566
|
+
if (type !== void 0) next.type = type;
|
|
1567
|
+
const desc = readDescFile(options.descFile);
|
|
1568
|
+
if (desc !== void 0) next.desc = desc;
|
|
1569
|
+
if (options.clearDesc) next.desc = null;
|
|
1570
|
+
Object.assign(next, parsePreferredAssigneeBody(options));
|
|
1571
|
+
assertNonEmptyBody(next);
|
|
1572
|
+
return next;
|
|
1573
|
+
});
|
|
1574
|
+
const id = runOrExit3(() => parsePositiveInteger(statusScopedId, "statusScopedId"));
|
|
1575
|
+
startSpinner("Updating status...");
|
|
1576
|
+
const response = await httpPatch(
|
|
1577
|
+
`/workspaces/${options.workspace}/workflows/${workflowFileId}/statuses/${id}`,
|
|
1578
|
+
body
|
|
1579
|
+
);
|
|
1580
|
+
if (!response.ok) {
|
|
1581
|
+
stopSpinner(false, "Failed");
|
|
1582
|
+
failResponse(response);
|
|
1583
|
+
}
|
|
1584
|
+
stopSpinner(true, "Status updated");
|
|
1585
|
+
printStatusDetail(response.data);
|
|
1586
|
+
}
|
|
1587
|
+
);
|
|
1588
|
+
addWorkspaceOption(
|
|
1589
|
+
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")
|
|
1590
|
+
).action(
|
|
1591
|
+
async (workflowFileId, statusScopedId, options) => {
|
|
1592
|
+
const { id, anchoredStatusScopedId } = runOrExit3(() => {
|
|
1593
|
+
if (options.after && options.first) {
|
|
1594
|
+
throw new WorkflowOptionsError("Error: --after cannot be used with --first.");
|
|
1595
|
+
}
|
|
1596
|
+
if (!options.after && !options.first) {
|
|
1597
|
+
throw new WorkflowOptionsError("Error: either --after or --first is required.");
|
|
1598
|
+
}
|
|
1599
|
+
return {
|
|
1600
|
+
id: parsePositiveInteger(statusScopedId, "statusScopedId"),
|
|
1601
|
+
anchoredStatusScopedId: options.first ? null : parsePositiveInteger(options.after, "--after")
|
|
1602
|
+
};
|
|
1603
|
+
});
|
|
1604
|
+
startSpinner("Moving status...");
|
|
1605
|
+
const response = await httpPost(
|
|
1606
|
+
`/workspaces/${options.workspace}/workflows/${workflowFileId}/statuses/${id}/move-after`,
|
|
1607
|
+
{ anchoredStatusScopedId }
|
|
1608
|
+
);
|
|
1609
|
+
if (!response.ok) {
|
|
1610
|
+
stopSpinner(false, "Failed");
|
|
1611
|
+
failResponse(response);
|
|
1612
|
+
}
|
|
1613
|
+
stopSpinner(true, "Status moved");
|
|
1614
|
+
}
|
|
1615
|
+
);
|
|
1616
|
+
addWorkspaceOption(
|
|
1617
|
+
status.command("delete").description("Delete a workflow status").argument("<workflowFileId>").argument("<statusScopedId>")
|
|
1618
|
+
).action(async (workflowFileId, statusScopedId, options) => {
|
|
1619
|
+
const id = runOrExit3(() => parsePositiveInteger(statusScopedId, "statusScopedId"));
|
|
1620
|
+
startSpinner("Deleting status...");
|
|
1621
|
+
const response = await httpDelete(
|
|
1622
|
+
`/workspaces/${options.workspace}/workflows/${workflowFileId}/statuses/${id}`
|
|
1623
|
+
);
|
|
1624
|
+
if (!response.ok) {
|
|
1625
|
+
stopSpinner(false, "Failed");
|
|
1626
|
+
failResponse(response);
|
|
1627
|
+
}
|
|
1628
|
+
stopSpinner(true, `Deleted: ${response.data.scopedId}`);
|
|
1629
|
+
});
|
|
1630
|
+
}
|
|
1631
|
+
function registerTransitionCommands(workflow) {
|
|
1632
|
+
const transition = workflow.command("transition").description("Workflow transition operations");
|
|
1633
|
+
for (const action of ["create", "delete"]) {
|
|
1634
|
+
addWorkspaceOption(
|
|
1635
|
+
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")
|
|
1636
|
+
).action(async (workflowFileId, options) => {
|
|
1637
|
+
const body = runOrExit3(() => ({
|
|
1638
|
+
statusFromScopedId: parsePositiveInteger(options.from, "--from"),
|
|
1639
|
+
statusToScopedId: parsePositiveInteger(options.to, "--to")
|
|
1640
|
+
}));
|
|
1641
|
+
startSpinner(`${action === "create" ? "Creating" : "Deleting"} transition...`);
|
|
1642
|
+
const response = action === "create" ? await httpPost(
|
|
1643
|
+
`/workspaces/${options.workspace}/workflows/${workflowFileId}/transitions`,
|
|
1644
|
+
body
|
|
1645
|
+
) : await httpDelete(
|
|
1646
|
+
`/workspaces/${options.workspace}/workflows/${workflowFileId}/transitions`,
|
|
1647
|
+
body
|
|
1648
|
+
);
|
|
1649
|
+
if (!response.ok) {
|
|
1650
|
+
stopSpinner(false, "Failed");
|
|
1651
|
+
failResponse(response);
|
|
1652
|
+
}
|
|
1653
|
+
stopSpinner(true, action === "create" ? "Transition created" : "Transition deleted");
|
|
1654
|
+
print(`${response.data.statusFromScopedId} -> ${response.data.statusToScopedId}`);
|
|
1655
|
+
});
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
function registerTaskCommands(workflow) {
|
|
1659
|
+
const task = workflow.command("task").description("Workflow task operations");
|
|
1660
|
+
addWorkspaceOption(
|
|
1661
|
+
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")
|
|
1662
|
+
).action(
|
|
1663
|
+
async (options) => {
|
|
1664
|
+
const query = runOrExit3(() => buildQuery({
|
|
1665
|
+
scope: parseTaskScope(options.scope),
|
|
1666
|
+
limit: parseSearchLimit(options.limit, 20, 100),
|
|
1667
|
+
cursor: options.cursor,
|
|
1668
|
+
workflowFileId: options.workflowFileId
|
|
1669
|
+
}));
|
|
1670
|
+
startSpinner("Fetching workflow tasks...");
|
|
1671
|
+
const response = await httpGet(`/workspaces/${options.workspace}/workflow-tasks${query}`);
|
|
1672
|
+
if (!response.ok) {
|
|
1673
|
+
stopSpinner(false, "Failed");
|
|
1674
|
+
failResponse(response);
|
|
1675
|
+
}
|
|
1676
|
+
if (response.data.tasks.length === 0) {
|
|
1677
|
+
stopSpinner(true, "No workflow tasks found.");
|
|
1678
|
+
return;
|
|
1679
|
+
}
|
|
1680
|
+
stopSpinner(true, `Found ${response.data.tasks.length} task(s)`);
|
|
1681
|
+
for (const item of response.data.tasks) printTaskSummary(item);
|
|
1682
|
+
if (response.data.nextCursor) print(`${colors.dim}nextCursor=${response.data.nextCursor}${colors.reset}`);
|
|
1683
|
+
}
|
|
1684
|
+
);
|
|
1685
|
+
addWorkspaceOption(
|
|
1686
|
+
task.command("get").description("Get a workflow task").argument("<workflowFileId>").argument("<taskScopedId>")
|
|
1687
|
+
).action(async (workflowFileId, taskScopedId, options) => {
|
|
1688
|
+
const id = runOrExit3(() => parsePositiveInteger(taskScopedId, "taskScopedId"));
|
|
1689
|
+
startSpinner("Fetching task...");
|
|
1690
|
+
const response = await httpGet(
|
|
1691
|
+
`/workspaces/${options.workspace}/workflows/${workflowFileId}/tasks/${id}`
|
|
1692
|
+
);
|
|
1693
|
+
if (!response.ok) {
|
|
1694
|
+
stopSpinner(false, "Failed");
|
|
1695
|
+
failResponse(response);
|
|
1696
|
+
}
|
|
1697
|
+
stopSpinner(true, "Task fetched");
|
|
1698
|
+
printTaskDetail(response.data);
|
|
1699
|
+
});
|
|
1700
|
+
addAssigneeOptions(
|
|
1701
|
+
addWorkspaceOption(
|
|
1702
|
+
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")
|
|
1703
|
+
)
|
|
1704
|
+
).action(
|
|
1705
|
+
async (workflowFileId, options) => {
|
|
1706
|
+
const body = runOrExit3(() => {
|
|
1707
|
+
const next = {
|
|
1708
|
+
title: options.title,
|
|
1709
|
+
statusScopedId: parsePositiveInteger(options.status, "--status"),
|
|
1710
|
+
priority: parseRequiredPriority(options.priority)
|
|
1711
|
+
};
|
|
1712
|
+
const desc = readDescFile(options.descFile);
|
|
1713
|
+
if (desc !== void 0) next.desc = desc;
|
|
1714
|
+
const ownerHumanId = parseOptionalPositiveInteger(options.ownerHumanId, "--owner-human-id");
|
|
1715
|
+
if (ownerHumanId !== void 0) next.ownerHumanId = ownerHumanId;
|
|
1716
|
+
Object.assign(next, parseAssigneeBody(options, false));
|
|
1717
|
+
return next;
|
|
1718
|
+
});
|
|
1719
|
+
startSpinner("Creating task...");
|
|
1720
|
+
const response = await httpPost(
|
|
1721
|
+
`/workspaces/${options.workspace}/workflows/${workflowFileId}/tasks`,
|
|
1722
|
+
body
|
|
1723
|
+
);
|
|
1724
|
+
if (!response.ok) {
|
|
1725
|
+
stopSpinner(false, "Failed");
|
|
1726
|
+
failResponse(response);
|
|
1727
|
+
}
|
|
1728
|
+
stopSpinner(true, "Task created");
|
|
1729
|
+
printTaskDetail(response.data);
|
|
1730
|
+
}
|
|
1731
|
+
);
|
|
1732
|
+
addWorkspaceOption(
|
|
1733
|
+
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")
|
|
1734
|
+
).action(
|
|
1735
|
+
async (workflowFileId, taskScopedId, options) => {
|
|
1736
|
+
const body = runOrExit3(() => {
|
|
1737
|
+
if (options.descFile && options.clearDesc) {
|
|
1738
|
+
throw new WorkflowOptionsError("Error: --desc-file cannot be used with --clear-desc.");
|
|
1739
|
+
}
|
|
1740
|
+
const next = {};
|
|
1741
|
+
if (options.title) next.title = options.title;
|
|
1742
|
+
const priority = parsePriority(options.priority);
|
|
1743
|
+
if (priority !== void 0) next.priority = priority;
|
|
1744
|
+
const desc = readDescFile(options.descFile);
|
|
1745
|
+
if (desc !== void 0) next.desc = desc;
|
|
1746
|
+
if (options.clearDesc) next.desc = null;
|
|
1747
|
+
const ownerHumanId = parseOptionalPositiveInteger(options.ownerHumanId, "--owner-human-id");
|
|
1748
|
+
if (ownerHumanId !== void 0) next.ownerHumanId = ownerHumanId;
|
|
1749
|
+
assertNonEmptyBody(next);
|
|
1750
|
+
return next;
|
|
1751
|
+
});
|
|
1752
|
+
const id = runOrExit3(() => parsePositiveInteger(taskScopedId, "taskScopedId"));
|
|
1753
|
+
startSpinner("Updating task...");
|
|
1754
|
+
const response = await httpPatch(
|
|
1755
|
+
`/workspaces/${options.workspace}/workflows/${workflowFileId}/tasks/${id}/metadata`,
|
|
1756
|
+
body
|
|
1757
|
+
);
|
|
1758
|
+
if (!response.ok) {
|
|
1759
|
+
stopSpinner(false, "Failed");
|
|
1760
|
+
failResponse(response);
|
|
1761
|
+
}
|
|
1762
|
+
stopSpinner(true, "Task updated");
|
|
1763
|
+
printTaskDetail(response.data);
|
|
1764
|
+
}
|
|
1765
|
+
);
|
|
1766
|
+
for (const action of ["add-linked-files", "remove-linked-files"]) {
|
|
1767
|
+
addWorkspaceOption(
|
|
1768
|
+
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")
|
|
1769
|
+
).action(
|
|
1770
|
+
async (workflowFileId, taskScopedId, options) => {
|
|
1771
|
+
const { id, fileIds } = runOrExit3(() => ({
|
|
1772
|
+
id: parsePositiveInteger(taskScopedId, "taskScopedId"),
|
|
1773
|
+
fileIds: parseFileIds(options.fileIds, "--file-ids")
|
|
1774
|
+
}));
|
|
1775
|
+
startSpinner(action === "add-linked-files" ? "Adding linked files..." : "Removing linked files...");
|
|
1776
|
+
const path2 = `/workspaces/${options.workspace}/workflows/${workflowFileId}/tasks/${id}/linked-files`;
|
|
1777
|
+
const response = action === "add-linked-files" ? await httpPost(path2, { fileIds }) : await httpDelete(path2, { fileIds });
|
|
1778
|
+
if (!response.ok) {
|
|
1779
|
+
stopSpinner(false, "Failed");
|
|
1780
|
+
failResponse(response);
|
|
1781
|
+
}
|
|
1782
|
+
stopSpinner(true, action === "add-linked-files" ? "Linked files added" : "Linked files removed");
|
|
1783
|
+
printTaskDetail(response.data);
|
|
1784
|
+
}
|
|
1785
|
+
);
|
|
1786
|
+
}
|
|
1787
|
+
addWorkspaceOption(
|
|
1788
|
+
task.command("move").description("Move a workflow task to another status").argument("<workflowFileId>").argument("<taskScopedId>").requiredOption("--status <statusScopedId>", "Target status scoped ID")
|
|
1789
|
+
).action(
|
|
1790
|
+
async (workflowFileId, taskScopedId, options) => {
|
|
1791
|
+
const { id, statusScopedId } = runOrExit3(() => ({
|
|
1792
|
+
id: parsePositiveInteger(taskScopedId, "taskScopedId"),
|
|
1793
|
+
statusScopedId: parsePositiveInteger(options.status, "--status")
|
|
1794
|
+
}));
|
|
1795
|
+
startSpinner("Moving task...");
|
|
1796
|
+
const response = await httpPatch(
|
|
1797
|
+
`/workspaces/${options.workspace}/workflows/${workflowFileId}/tasks/${id}/status`,
|
|
1798
|
+
{ statusScopedId }
|
|
1799
|
+
);
|
|
1800
|
+
if (!response.ok) {
|
|
1801
|
+
stopSpinner(false, "Failed");
|
|
1802
|
+
failResponse(response);
|
|
1803
|
+
}
|
|
1804
|
+
stopSpinner(true, "Task moved");
|
|
1805
|
+
printTaskDetail(response.data);
|
|
1806
|
+
}
|
|
1807
|
+
);
|
|
1808
|
+
addAssigneeOptions(
|
|
1809
|
+
addWorkspaceOption(
|
|
1810
|
+
task.command("assign").description("Update workflow task assignee").argument("<workflowFileId>").argument("<taskScopedId>").option("--clear", "Clear assignee")
|
|
1811
|
+
)
|
|
1812
|
+
).action(
|
|
1813
|
+
async (workflowFileId, taskScopedId, options) => {
|
|
1814
|
+
const body = runOrExit3(() => {
|
|
1815
|
+
const next = parseAssigneeBody(options, true);
|
|
1816
|
+
if (Object.keys(next).length === 0 && options.clear !== true) assertNonEmptyBody(next);
|
|
1817
|
+
return next;
|
|
1818
|
+
});
|
|
1819
|
+
const id = runOrExit3(() => parsePositiveInteger(taskScopedId, "taskScopedId"));
|
|
1820
|
+
startSpinner("Updating task assignee...");
|
|
1821
|
+
const response = await httpPatch(
|
|
1822
|
+
`/workspaces/${options.workspace}/workflows/${workflowFileId}/tasks/${id}/assignee`,
|
|
1823
|
+
body
|
|
1824
|
+
);
|
|
1825
|
+
if (!response.ok) {
|
|
1826
|
+
stopSpinner(false, "Failed");
|
|
1827
|
+
failResponse(response);
|
|
1828
|
+
}
|
|
1829
|
+
stopSpinner(true, "Task assignee updated");
|
|
1830
|
+
printTaskDetail(response.data);
|
|
1831
|
+
}
|
|
1832
|
+
);
|
|
1833
|
+
addWorkspaceOption(
|
|
1834
|
+
task.command("delete").description("Delete a workflow task").argument("<workflowFileId>").argument("<taskScopedId>")
|
|
1835
|
+
).action(async (workflowFileId, taskScopedId, options) => {
|
|
1836
|
+
const id = runOrExit3(() => parsePositiveInteger(taskScopedId, "taskScopedId"));
|
|
1837
|
+
startSpinner("Deleting task...");
|
|
1838
|
+
const response = await httpDelete(
|
|
1839
|
+
`/workspaces/${options.workspace}/workflows/${workflowFileId}/tasks/${id}`
|
|
1840
|
+
);
|
|
1841
|
+
if (!response.ok) {
|
|
1842
|
+
stopSpinner(false, "Failed");
|
|
1843
|
+
failResponse(response);
|
|
1844
|
+
}
|
|
1845
|
+
stopSpinner(true, `Deleted: ${response.data.scopedId}`);
|
|
1846
|
+
});
|
|
1847
|
+
}
|
|
1848
|
+
function registerRunCommands(workflow) {
|
|
1849
|
+
const run = workflow.command("run").description("Workflow agent run operations");
|
|
1850
|
+
addWorkspaceOption(
|
|
1851
|
+
run.command("status").description("Get workflow agent run statuses").argument("<workflowFileId>").argument("<triggerIds...>")
|
|
1852
|
+
).action(async (workflowFileId, triggerIds, options) => {
|
|
1853
|
+
const workflowPipelineTriggerIds = runOrExit3(
|
|
1854
|
+
() => triggerIds.map((triggerId) => parsePositiveInteger(triggerId, "workflowPipelineTriggerId"))
|
|
1855
|
+
);
|
|
1856
|
+
startSpinner("Fetching run statuses...");
|
|
1857
|
+
const response = await httpPost(
|
|
1858
|
+
`/workspaces/${options.workspace}/workflows/${workflowFileId}/pipeline/triggers/statuses`,
|
|
1859
|
+
{ workflowPipelineTriggerIds }
|
|
1860
|
+
);
|
|
1861
|
+
if (!response.ok) {
|
|
1862
|
+
stopSpinner(false, "Failed");
|
|
1863
|
+
failResponse(response);
|
|
1864
|
+
}
|
|
1865
|
+
stopSpinner(true, `Found ${response.data.length} run status item(s)`);
|
|
1866
|
+
for (const item of response.data) {
|
|
1867
|
+
print(`${colors.bold}${item.workflowPipelineTriggerId}${colors.reset} ${item.pipelineStatus}`);
|
|
1868
|
+
}
|
|
1869
|
+
});
|
|
1870
|
+
addWorkspaceOption(
|
|
1871
|
+
run.command("output").description("Get workflow agent run output events").argument("<workflowFileId>").argument("<triggerId>")
|
|
1872
|
+
).action(async (workflowFileId, triggerId, options) => {
|
|
1873
|
+
const id = runOrExit3(() => parsePositiveInteger(triggerId, "workflowPipelineTriggerId"));
|
|
1874
|
+
startSpinner("Fetching run output...");
|
|
1875
|
+
const response = await httpGet(
|
|
1876
|
+
`/workspaces/${options.workspace}/workflows/${workflowFileId}/pipeline/trigger/${id}/output`
|
|
1877
|
+
);
|
|
1878
|
+
if (!response.ok) {
|
|
1879
|
+
stopSpinner(false, "Failed");
|
|
1880
|
+
failResponse(response);
|
|
1881
|
+
}
|
|
1882
|
+
stopSpinner(true, `Run ${response.data.workflowPipelineTriggerId}`);
|
|
1883
|
+
for (const event of response.data.pipelineEvents) print(event);
|
|
1884
|
+
});
|
|
1885
|
+
addWorkspaceOption(
|
|
1886
|
+
run.command("abort").description("Abort a workflow agent run").argument("<workflowFileId>").argument("<triggerId>")
|
|
1887
|
+
).action(async (workflowFileId, triggerId, options) => {
|
|
1888
|
+
const id = runOrExit3(() => parsePositiveInteger(triggerId, "workflowPipelineTriggerId"));
|
|
1889
|
+
startSpinner("Aborting run...");
|
|
1890
|
+
const response = await httpPost(
|
|
1891
|
+
`/workspaces/${options.workspace}/workflows/${workflowFileId}/pipeline/trigger/${id}/abort`,
|
|
1892
|
+
{}
|
|
1893
|
+
);
|
|
1894
|
+
if (!response.ok) {
|
|
1895
|
+
stopSpinner(false, "Failed");
|
|
1896
|
+
failResponse(response);
|
|
1897
|
+
}
|
|
1898
|
+
stopSpinner(true, `Abort accepted: ${id}`);
|
|
1899
|
+
});
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
// src/program.ts
|
|
1903
|
+
function createProgram(version) {
|
|
1904
|
+
const program2 = new Command();
|
|
1905
|
+
program2.name("moxt").description("Moxt CLI - AI Workspace").version(version).action(() => {
|
|
1906
|
+
program2.help();
|
|
1907
|
+
});
|
|
1908
|
+
registerAuthCommand(program2);
|
|
1909
|
+
registerWhoamiCommand(program2);
|
|
1910
|
+
registerWorkspaceCommand(program2);
|
|
1911
|
+
registerFileCommand(program2);
|
|
1912
|
+
registerMemoryCommand(program2);
|
|
1913
|
+
registerWorkflowCommand(program2);
|
|
1914
|
+
registerMiniappCommand(program2);
|
|
1915
|
+
registerTelemetryCommand(program2);
|
|
1916
|
+
return program2;
|
|
1917
|
+
}
|
|
1918
|
+
|
|
942
1919
|
// src/telemetry/client.ts
|
|
943
1920
|
import { arch as arch2, platform as platform2 } from "os";
|
|
944
1921
|
var buffer = [];
|
|
@@ -946,7 +1923,7 @@ var FLUSH_TIMEOUT_MS = 3e3;
|
|
|
946
1923
|
var MAX_BATCH_SIZE = 100;
|
|
947
1924
|
function commonLabels() {
|
|
948
1925
|
return {
|
|
949
|
-
cli_version: "0.3.
|
|
1926
|
+
cli_version: "0.3.3-beta.1",
|
|
950
1927
|
node_version: process.versions.node,
|
|
951
1928
|
os: platform2(),
|
|
952
1929
|
arch: arch2(),
|
|
@@ -992,7 +1969,7 @@ async function flush() {
|
|
|
992
1969
|
}
|
|
993
1970
|
const batch = buffer.splice(0, MAX_BATCH_SIZE);
|
|
994
1971
|
const payload = {
|
|
995
|
-
release_id: "0.3.
|
|
1972
|
+
release_id: "0.3.3-beta.1",
|
|
996
1973
|
client_type: "cli",
|
|
997
1974
|
metric_data_list: batch.map((m) => ({
|
|
998
1975
|
profile: "cli",
|
|
@@ -1133,20 +2110,11 @@ function installExitHandler() {
|
|
|
1133
2110
|
|
|
1134
2111
|
// src/index.ts
|
|
1135
2112
|
updateNotifier({
|
|
1136
|
-
pkg: { name: "@moxt-ai/cli", version: "0.3.
|
|
2113
|
+
pkg: { name: "@moxt-ai/cli", version: "0.3.3-beta.1" }
|
|
1137
2114
|
}).notify();
|
|
1138
|
-
var program =
|
|
1139
|
-
program.name("moxt").description("Moxt CLI - AI Workspace").version("0.3.2").action(() => {
|
|
1140
|
-
program.help();
|
|
1141
|
-
});
|
|
2115
|
+
var program = createProgram("0.3.3-beta.1");
|
|
1142
2116
|
instrumentProgram(program);
|
|
1143
2117
|
installExitHandler();
|
|
1144
|
-
registerWhoamiCommand(program);
|
|
1145
|
-
registerWorkspaceCommand(program);
|
|
1146
|
-
registerFileCommand(program);
|
|
1147
|
-
registerMemoryCommand(program);
|
|
1148
|
-
registerMiniappCommand(program);
|
|
1149
|
-
registerTelemetryCommand(program);
|
|
1150
2118
|
program.parseAsync(process.argv).then(async () => {
|
|
1151
2119
|
await flush();
|
|
1152
2120
|
}).catch(async (err) => {
|