@moxt-ai/cli 0.4.0-beta.0 → 0.4.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 +86 -0
- package/dist/index.js +1372 -2064
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4,204 +4,15 @@
|
|
|
4
4
|
import updateNotifier from "update-notifier";
|
|
5
5
|
|
|
6
6
|
// src/program.ts
|
|
7
|
-
import { Command
|
|
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 = [
|
|
29
|
-
"BASE_API_HOST",
|
|
30
|
-
"SANDBOX_TOOL_API_TOKEN",
|
|
31
|
-
"MOXT_REPO_PAYLOAD",
|
|
32
|
-
"MOXT_WORKSPACE_ID"
|
|
33
|
-
];
|
|
34
|
-
var RuntimeContextError = class extends Error {
|
|
35
|
-
constructor(message) {
|
|
36
|
-
super(message);
|
|
37
|
-
this.name = "RuntimeContextError";
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
|
-
function resolveRuntimeContext(env = process.env) {
|
|
41
|
-
if (hasAnySandboxEnv(env)) {
|
|
42
|
-
return resolveSandboxRuntimeContext(env);
|
|
43
|
-
}
|
|
44
|
-
const apiKey = readNonEmptyEnv(env, "MOXT_API_KEY");
|
|
45
|
-
if (apiKey) {
|
|
46
|
-
return {
|
|
47
|
-
mode: "user",
|
|
48
|
-
host: readNonEmptyEnv(env, "MOXT_HOST") ?? DEFAULT_USER_HOST,
|
|
49
|
-
apiKey
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
throw new RuntimeContextError("Missing auth. Set sandbox environment variables or MOXT_API_KEY.");
|
|
53
|
-
}
|
|
54
|
-
function resolveSandboxRuntimeContext(env = process.env) {
|
|
55
|
-
const missingKeys = SANDBOX_ENV_KEYS.filter((key) => !readNonEmptyEnv(env, key));
|
|
56
|
-
if (missingKeys.length > 0) {
|
|
57
|
-
throw new RuntimeContextError(`Incomplete sandbox environment. Missing: ${missingKeys.join(", ")}.`);
|
|
58
|
-
}
|
|
59
|
-
return {
|
|
60
|
-
mode: "sandbox",
|
|
61
|
-
baseApiHost: normalizeBaseApiHost(readRequiredEnv(env, "BASE_API_HOST")),
|
|
62
|
-
sandboxToolApiToken: readRequiredEnv(env, "SANDBOX_TOOL_API_TOKEN"),
|
|
63
|
-
workspaceId: readRequiredEnv(env, "MOXT_WORKSPACE_ID"),
|
|
64
|
-
repoPayload: parseRepoPayload(readRequiredEnv(env, "MOXT_REPO_PAYLOAD"))
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
function parseRepoPayload(raw) {
|
|
68
|
-
let parsed;
|
|
69
|
-
try {
|
|
70
|
-
parsed = JSON.parse(raw);
|
|
71
|
-
} catch (error) {
|
|
72
|
-
throw new RuntimeContextError(`MOXT_REPO_PAYLOAD must be valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
73
|
-
}
|
|
74
|
-
if (!isRecord(parsed)) {
|
|
75
|
-
throw new RuntimeContextError("MOXT_REPO_PAYLOAD must be a JSON object.");
|
|
76
|
-
}
|
|
77
|
-
const personal = parseRepoEntry(parsed.personal, "personal");
|
|
78
|
-
const teamsValue = parsed.teams;
|
|
79
|
-
if (!Array.isArray(teamsValue)) {
|
|
80
|
-
throw new RuntimeContextError("MOXT_REPO_PAYLOAD.teams must be an array.");
|
|
81
|
-
}
|
|
82
|
-
const teams = teamsValue.map((entry, index) => parseRepoEntry(entry, `teams[${index}]`));
|
|
83
|
-
const workspace = parsed.workspace == null ? null : parseRepoEntry(parsed.workspace, "workspace");
|
|
84
|
-
return { personal, teams, workspace };
|
|
85
|
-
}
|
|
86
|
-
function normalizeBaseApiHost(value) {
|
|
87
|
-
const trimmed = value.trim();
|
|
88
|
-
if (!trimmed) {
|
|
89
|
-
throw new RuntimeContextError("BASE_API_HOST must not be empty.");
|
|
90
|
-
}
|
|
91
|
-
return trimmed.replace(/\/+$/, "");
|
|
92
|
-
}
|
|
93
|
-
function hasAnySandboxEnv(env) {
|
|
94
|
-
return SANDBOX_ENV_KEYS.some((key) => readNonEmptyEnv(env, key) !== void 0);
|
|
95
|
-
}
|
|
96
|
-
function readRequiredEnv(env, key) {
|
|
97
|
-
const value = readNonEmptyEnv(env, key);
|
|
98
|
-
if (!value) {
|
|
99
|
-
throw new RuntimeContextError(`${key} is required.`);
|
|
100
|
-
}
|
|
101
|
-
return value;
|
|
102
|
-
}
|
|
103
|
-
function readNonEmptyEnv(env, key) {
|
|
104
|
-
const value = env[key];
|
|
105
|
-
if (typeof value !== "string") return void 0;
|
|
106
|
-
const trimmed = value.trim();
|
|
107
|
-
return trimmed.length > 0 ? trimmed : void 0;
|
|
108
|
-
}
|
|
109
|
-
function parseRepoEntry(value, path2) {
|
|
110
|
-
if (!isRecord(value)) {
|
|
111
|
-
throw new RuntimeContextError(`MOXT_REPO_PAYLOAD.${path2} must be an object.`);
|
|
112
|
-
}
|
|
113
|
-
const repoId = readStringField(value, "repoId", path2);
|
|
114
|
-
const name = readStringField(value, "name", path2);
|
|
115
|
-
return { repoId, name };
|
|
116
|
-
}
|
|
117
|
-
function readStringField(record2, field, path2) {
|
|
118
|
-
const value = record2[field];
|
|
119
|
-
if (typeof value !== "string" || value.trim().length === 0) {
|
|
120
|
-
throw new RuntimeContextError(`MOXT_REPO_PAYLOAD.${path2}.${field} must be a non-empty string.`);
|
|
121
|
-
}
|
|
122
|
-
return value;
|
|
123
|
-
}
|
|
124
|
-
function isRecord(value) {
|
|
125
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// src/utils/sandbox-path.ts
|
|
129
|
-
var WorkspacePathError = class extends Error {
|
|
130
|
-
constructor(message) {
|
|
131
|
-
super(message);
|
|
132
|
-
this.name = "WorkspacePathError";
|
|
133
|
-
}
|
|
134
|
-
};
|
|
135
|
-
function resolveWorkspacePath(repoPayload, workspacePath) {
|
|
136
|
-
const normalizedPath = normalizeWorkspacePath(workspacePath);
|
|
137
|
-
const [repoName, ...repoPathParts] = normalizedPath.split("/");
|
|
138
|
-
const repoEntries = listRepoEntries(repoPayload);
|
|
139
|
-
const repo = repoEntries.find((entry) => entry.name === repoName);
|
|
140
|
-
if (!repo) {
|
|
141
|
-
throw new WorkspacePathError(`Workspace path must start with one of: ${repoEntries.map((entry) => entry.name).join(", ")}.`);
|
|
142
|
-
}
|
|
143
|
-
return {
|
|
144
|
-
repoId: repo.repoId,
|
|
145
|
-
repoName: repo.name,
|
|
146
|
-
repoPath: repoPathParts.join("/")
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
function listRepoEntries(repoPayload) {
|
|
150
|
-
return [
|
|
151
|
-
repoPayload.personal,
|
|
152
|
-
...repoPayload.teams,
|
|
153
|
-
...repoPayload.workspace ? [repoPayload.workspace] : []
|
|
154
|
-
];
|
|
155
|
-
}
|
|
156
|
-
function normalizeWorkspacePath(workspacePath) {
|
|
157
|
-
const trimmed = workspacePath.trim();
|
|
158
|
-
if (!trimmed) {
|
|
159
|
-
throw new WorkspacePathError("Workspace path must not be empty.");
|
|
160
|
-
}
|
|
161
|
-
if (trimmed.startsWith("/")) {
|
|
162
|
-
throw new WorkspacePathError("Workspace path must be relative.");
|
|
163
|
-
}
|
|
164
|
-
const parts = trimmed.split("/").filter((part) => part.length > 0 && part !== ".");
|
|
165
|
-
if (parts.length === 0) {
|
|
166
|
-
throw new WorkspacePathError("Workspace path must include a repo name.");
|
|
167
|
-
}
|
|
168
|
-
if (parts.some((part) => part === "..")) {
|
|
169
|
-
throw new WorkspacePathError('Workspace path must not contain "..".');
|
|
170
|
-
}
|
|
171
|
-
return parts.join("/");
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// src/commands/auth.ts
|
|
175
|
-
function registerAuthCommand(program2) {
|
|
176
|
-
const auth = program2.command("auth").description("Authentication utilities");
|
|
177
|
-
auth.command("status").description("Show CLI authentication mode").action(() => {
|
|
178
|
-
try {
|
|
179
|
-
const context = resolveRuntimeContext();
|
|
180
|
-
if (context.mode === "sandbox") {
|
|
181
|
-
print(`${colors.bold}Mode${colors.reset}: sandbox`);
|
|
182
|
-
print(`${colors.bold}Workspace${colors.reset}: ${context.workspaceId}`);
|
|
183
|
-
print(`${colors.bold}Base API${colors.reset}: ${context.baseApiHost}`);
|
|
184
|
-
print(`${colors.bold}Repos${colors.reset}:`);
|
|
185
|
-
for (const repo of listRepoEntries(context.repoPayload)) {
|
|
186
|
-
print(` ${repo.name} ${repo.repoId}`);
|
|
187
|
-
}
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
print(`${colors.bold}Mode${colors.reset}: user`);
|
|
191
|
-
print(`${colors.bold}Host${colors.reset}: ${context.host}`);
|
|
192
|
-
} catch (error) {
|
|
193
|
-
if (error instanceof RuntimeContextError) {
|
|
194
|
-
printError(error.message);
|
|
195
|
-
process.exit(1);
|
|
196
|
-
}
|
|
197
|
-
throw error;
|
|
198
|
-
}
|
|
199
|
-
});
|
|
200
|
-
}
|
|
7
|
+
import { Command } from "commander";
|
|
201
8
|
|
|
202
9
|
// src/commands/file.ts
|
|
203
10
|
import * as fs2 from "fs";
|
|
204
11
|
|
|
12
|
+
// src/commands/file-comment.ts
|
|
13
|
+
import * as fs from "fs";
|
|
14
|
+
import { TextDecoder } from "util";
|
|
15
|
+
|
|
205
16
|
// src/utils/file-selector.ts
|
|
206
17
|
var SpaceOptionsError = class extends Error {
|
|
207
18
|
constructor(message) {
|
|
@@ -342,7 +153,7 @@ function getApiKeyOrUndefined() {
|
|
|
342
153
|
|
|
343
154
|
// src/utils/http.ts
|
|
344
155
|
function getUserAgent() {
|
|
345
|
-
return `moxt-cli/${"0.4.0
|
|
156
|
+
return `moxt-cli/${"0.4.0"} (${platform()}/${arch()}) node/${process.versions.node}`;
|
|
346
157
|
}
|
|
347
158
|
async function fetchApi(method, path2, body) {
|
|
348
159
|
const apiKey = getApiKey();
|
|
@@ -382,6 +193,9 @@ async function httpPut(path2, body) {
|
|
|
382
193
|
async function httpPost(path2, body) {
|
|
383
194
|
return request("POST", path2, body);
|
|
384
195
|
}
|
|
196
|
+
async function httpPatch(path2, body) {
|
|
197
|
+
return request("PATCH", path2, body);
|
|
198
|
+
}
|
|
385
199
|
async function httpDelete(path2, body) {
|
|
386
200
|
return request("DELETE", path2, body);
|
|
387
201
|
}
|
|
@@ -395,6 +209,265 @@ async function httpRaw(method, path2, body) {
|
|
|
395
209
|
};
|
|
396
210
|
}
|
|
397
211
|
|
|
212
|
+
// src/utils/output.ts
|
|
213
|
+
var colors = {
|
|
214
|
+
bold: "\x1B[1m",
|
|
215
|
+
blue: "\x1B[1;34m",
|
|
216
|
+
green: "\x1B[32m",
|
|
217
|
+
red: "\x1B[31m",
|
|
218
|
+
cyan: "\x1B[36m",
|
|
219
|
+
dim: "\x1B[2m",
|
|
220
|
+
reset: "\x1B[0m"
|
|
221
|
+
};
|
|
222
|
+
function print(message) {
|
|
223
|
+
console.log(message);
|
|
224
|
+
}
|
|
225
|
+
function printError(message) {
|
|
226
|
+
console.error(`${colors.red}${message}${colors.reset}`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// src/utils/spinner.ts
|
|
230
|
+
var frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
231
|
+
var intervalId = null;
|
|
232
|
+
var frameIndex = 0;
|
|
233
|
+
function startSpinner(message) {
|
|
234
|
+
frameIndex = 0;
|
|
235
|
+
process.stdout.write(`${frames[frameIndex]} ${message}`);
|
|
236
|
+
intervalId = setInterval(() => {
|
|
237
|
+
frameIndex = (frameIndex + 1) % frames.length;
|
|
238
|
+
process.stdout.write(`\r${frames[frameIndex]} ${message}`);
|
|
239
|
+
}, 80);
|
|
240
|
+
}
|
|
241
|
+
function stopSpinner(success, message) {
|
|
242
|
+
if (intervalId) {
|
|
243
|
+
clearInterval(intervalId);
|
|
244
|
+
intervalId = null;
|
|
245
|
+
}
|
|
246
|
+
const symbol = success ? "\x1B[32m\u2713\x1B[0m" : "\x1B[31m\u2717\x1B[0m";
|
|
247
|
+
process.stdout.write(`\x1B[2K\r${symbol} ${message ?? ""}
|
|
248
|
+
`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// src/commands/file-comment.ts
|
|
252
|
+
var FILE_ID_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
253
|
+
var URL_FORMAT_HINT = "Error: invalid Moxt URL format. Expected https://moxt.ai/w/{workspaceId}/{fileId} or https://moxt.ai/w/{workspaceId}/c/{agentId}[/{sessionId}]?file={fileId}";
|
|
254
|
+
var FileCommentOptionsError = class extends Error {
|
|
255
|
+
constructor(message) {
|
|
256
|
+
super(message);
|
|
257
|
+
this.name = "FileCommentOptionsError";
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
function runOrExit(fn) {
|
|
261
|
+
try {
|
|
262
|
+
return fn();
|
|
263
|
+
} catch (err) {
|
|
264
|
+
if (err instanceof FileCommentOptionsError || err instanceof SpaceOptionsError) {
|
|
265
|
+
printError(err.message);
|
|
266
|
+
process.exit(1);
|
|
267
|
+
}
|
|
268
|
+
throw err;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
function parseMoxtFileUrl(url) {
|
|
272
|
+
let parsed;
|
|
273
|
+
try {
|
|
274
|
+
parsed = new URL(url);
|
|
275
|
+
} catch {
|
|
276
|
+
throw new FileCommentOptionsError(URL_FORMAT_HINT);
|
|
277
|
+
}
|
|
278
|
+
const segments = parsed.pathname.split("/").filter(Boolean);
|
|
279
|
+
if (segments.length < 3 || segments[0] !== "w") {
|
|
280
|
+
throw new FileCommentOptionsError(URL_FORMAT_HINT);
|
|
281
|
+
}
|
|
282
|
+
const workspaceId = segments[1];
|
|
283
|
+
let fileId;
|
|
284
|
+
if (segments[2] === "c") {
|
|
285
|
+
fileId = parsed.searchParams.get("file") ?? void 0;
|
|
286
|
+
} else if (segments.length === 3) {
|
|
287
|
+
fileId = segments[2];
|
|
288
|
+
}
|
|
289
|
+
if (!fileId || !FILE_ID_UUID_RE.test(fileId)) {
|
|
290
|
+
throw new FileCommentOptionsError(URL_FORMAT_HINT);
|
|
291
|
+
}
|
|
292
|
+
return { workspaceId, fileId };
|
|
293
|
+
}
|
|
294
|
+
async function resolveFileTarget(options) {
|
|
295
|
+
const mode = runOrExit(() => resolveReadMode(options));
|
|
296
|
+
if (mode.kind === "by-url") {
|
|
297
|
+
return runOrExit(() => parseMoxtFileUrl(mode.url));
|
|
298
|
+
}
|
|
299
|
+
const spaceParams = runOrExit(() => resolveSpaceSelector(options));
|
|
300
|
+
const qs = buildFileQueryString(mode.path, spaceParams);
|
|
301
|
+
startSpinner("Resolving file...");
|
|
302
|
+
const response = await httpGet(`/workspaces/${mode.workspace}/files/url${qs}`);
|
|
303
|
+
if (!response.ok) {
|
|
304
|
+
stopSpinner(false, "Failed");
|
|
305
|
+
if (response.status === 404) {
|
|
306
|
+
print(`${colors.red}\u2620 ${mode.path} does not exist${colors.reset}`);
|
|
307
|
+
} else if (response.status === 400) {
|
|
308
|
+
printError(`Error: ${JSON.stringify(response.data)}`);
|
|
309
|
+
} else {
|
|
310
|
+
printError(`Error [${response.status}]: ${JSON.stringify(response.data)}`);
|
|
311
|
+
}
|
|
312
|
+
process.exit(1);
|
|
313
|
+
}
|
|
314
|
+
stopSpinner(true, response.data.path);
|
|
315
|
+
return runOrExit(() => parseMoxtFileUrl(response.data.url));
|
|
316
|
+
}
|
|
317
|
+
function readCommentContentFile(path2) {
|
|
318
|
+
if (!fs.existsSync(path2)) {
|
|
319
|
+
throw new FileCommentOptionsError(`Error: comment content file not found: ${path2}`);
|
|
320
|
+
}
|
|
321
|
+
const stat = fs.statSync(path2);
|
|
322
|
+
if (!stat.isFile()) {
|
|
323
|
+
throw new FileCommentOptionsError(`Error: comment content path is not a file: ${path2}`);
|
|
324
|
+
}
|
|
325
|
+
const buffer2 = fs.readFileSync(path2);
|
|
326
|
+
if (buffer2.includes(0)) {
|
|
327
|
+
throw new FileCommentOptionsError("Error: comment content file must not contain NUL bytes.");
|
|
328
|
+
}
|
|
329
|
+
let content;
|
|
330
|
+
try {
|
|
331
|
+
content = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(buffer2);
|
|
332
|
+
} catch {
|
|
333
|
+
throw new FileCommentOptionsError("Error: comment content file must contain valid UTF-8 text.");
|
|
334
|
+
}
|
|
335
|
+
if (content.length === 0) {
|
|
336
|
+
throw new FileCommentOptionsError("Error: comment content file must not be empty.");
|
|
337
|
+
}
|
|
338
|
+
return content;
|
|
339
|
+
}
|
|
340
|
+
function parseResolveState(state) {
|
|
341
|
+
if (state === "resolved") return true;
|
|
342
|
+
if (state === "open") return false;
|
|
343
|
+
throw new FileCommentOptionsError('Error: --state must be "resolved" or "open".');
|
|
344
|
+
}
|
|
345
|
+
function indentation(size) {
|
|
346
|
+
return " ".repeat(size);
|
|
347
|
+
}
|
|
348
|
+
function escapeTerminalControlCharacters(value) {
|
|
349
|
+
return value.replace(/\r\n/g, "\n").replace(
|
|
350
|
+
/[\u0000-\u0008\u000B-\u001F\u007F-\u009F]/g,
|
|
351
|
+
(character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
function formatDetailScalar(value) {
|
|
355
|
+
if (value === null) return "null";
|
|
356
|
+
if (typeof value === "string") {
|
|
357
|
+
const escaped = escapeTerminalControlCharacters(value);
|
|
358
|
+
if (escaped === "") return '""';
|
|
359
|
+
if (/\n/.test(escaped)) return JSON.stringify(escaped);
|
|
360
|
+
return escaped;
|
|
361
|
+
}
|
|
362
|
+
return String(value);
|
|
363
|
+
}
|
|
364
|
+
function printDetailField(name, value, indent = 0) {
|
|
365
|
+
print(`${indentation(indent)}${name}: ${formatDetailScalar(value)}`);
|
|
366
|
+
}
|
|
367
|
+
function printDetailTextField(name, value, indent = 0) {
|
|
368
|
+
if (value === null || value === "") {
|
|
369
|
+
printDetailField(name, value, indent);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
const escaped = escapeTerminalControlCharacters(value);
|
|
373
|
+
print(`${indentation(indent)}${name}:`);
|
|
374
|
+
for (const line of escaped.split("\n")) {
|
|
375
|
+
print(`${indentation(indent + 2)}${line}`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
function printAuthorFields(author, indent) {
|
|
379
|
+
printDetailField("humanId", author.humanId, indent);
|
|
380
|
+
printDetailField("assistantId", author.assistantId, indent);
|
|
381
|
+
printDetailField("teammateId", author.teammateId, indent);
|
|
382
|
+
printDetailField("authorName", author.authorName, indent);
|
|
383
|
+
printDetailField("authorAvatarUrl", author.authorAvatarUrl, indent);
|
|
384
|
+
}
|
|
385
|
+
function printCommentItem(item, indent = 0) {
|
|
386
|
+
print(`${indentation(indent)}${colors.bold}Comment${colors.reset}`);
|
|
387
|
+
printDetailField("id", item.id, indent + 2);
|
|
388
|
+
printDetailField("threadId", item.threadId, indent + 2);
|
|
389
|
+
printAuthorFields(item, indent + 2);
|
|
390
|
+
printDetailTextField("content", item.content, indent + 2);
|
|
391
|
+
printDetailField("createdAt", item.createdAt, indent + 2);
|
|
392
|
+
printDetailField("editedAt", item.editedAt, indent + 2);
|
|
393
|
+
}
|
|
394
|
+
function printThread(thread) {
|
|
395
|
+
print(`${colors.bold}Thread${colors.reset}`);
|
|
396
|
+
printDetailField("threadId", thread.threadId, 2);
|
|
397
|
+
printDetailField("fileId", thread.fileId, 2);
|
|
398
|
+
printDetailTextField("quotedText", thread.quotedText, 2);
|
|
399
|
+
printDetailField("resolvedAt", thread.resolvedAt, 2);
|
|
400
|
+
print(`${indentation(2)}createdBy:`);
|
|
401
|
+
printAuthorFields(thread.createdBy, 4);
|
|
402
|
+
printDetailField("createdAt", thread.createdAt, 2);
|
|
403
|
+
print(`${indentation(2)}comments:`);
|
|
404
|
+
thread.items.forEach((item) => printCommentItem(item, 4));
|
|
405
|
+
}
|
|
406
|
+
function addFileSelectorOptions(cmd) {
|
|
407
|
+
return cmd.option("-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)").option("-p, --path <path>", "File path").option("-u, --url <url>", "Moxt file URL (e.g., https://moxt.ai/w/{workspaceId}/{fileId})");
|
|
408
|
+
}
|
|
409
|
+
function registerFileCommentCommands(file) {
|
|
410
|
+
const comment = file.command("comment").description("File comment operations");
|
|
411
|
+
addFileSelectorOptions(
|
|
412
|
+
comment.command("list").description("List comment threads on a file")
|
|
413
|
+
).action(async (options) => {
|
|
414
|
+
const target = await resolveFileTarget(options);
|
|
415
|
+
startSpinner("Fetching comments...");
|
|
416
|
+
const response = await httpGet(
|
|
417
|
+
`/workspaces/${target.workspaceId}/files/${target.fileId}/comment-threads`
|
|
418
|
+
);
|
|
419
|
+
if (!response.ok) {
|
|
420
|
+
stopSpinner(false, "Failed");
|
|
421
|
+
printError(`Error [${response.status}]: ${JSON.stringify(response.data)}`);
|
|
422
|
+
process.exit(1);
|
|
423
|
+
}
|
|
424
|
+
if (response.data.items.length === 0) {
|
|
425
|
+
stopSpinner(true, "No comments found.");
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
stopSpinner(true, `Found ${response.data.items.length} thread(s)`);
|
|
429
|
+
response.data.items.forEach((thread, index) => {
|
|
430
|
+
if (index > 0) print("");
|
|
431
|
+
printThread(thread);
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
addFileSelectorOptions(
|
|
435
|
+
comment.command("update").description("Update a comment you created").argument("<commentId>").requiredOption("--content-path <localPath>", "Local UTF-8 text file for comment content")
|
|
436
|
+
).action(async (commentId, options) => {
|
|
437
|
+
const content = runOrExit(() => readCommentContentFile(options.contentPath));
|
|
438
|
+
const target = await resolveFileTarget(options);
|
|
439
|
+
startSpinner("Updating comment...");
|
|
440
|
+
const response = await httpPatch(
|
|
441
|
+
`/workspaces/${target.workspaceId}/files/${target.fileId}/comments/${commentId}`,
|
|
442
|
+
{ content }
|
|
443
|
+
);
|
|
444
|
+
if (!response.ok) {
|
|
445
|
+
stopSpinner(false, "Failed");
|
|
446
|
+
printError(`Error [${response.status}]: ${JSON.stringify(response.data)}`);
|
|
447
|
+
process.exit(1);
|
|
448
|
+
}
|
|
449
|
+
stopSpinner(true, "Comment updated");
|
|
450
|
+
printCommentItem(response.data);
|
|
451
|
+
});
|
|
452
|
+
addFileSelectorOptions(
|
|
453
|
+
comment.command("resolve").description("Resolve or reopen a comment thread").argument("<threadId>").requiredOption("--state <state>", "Thread state: resolved or open")
|
|
454
|
+
).action(async (threadId, options) => {
|
|
455
|
+
const resolved = runOrExit(() => parseResolveState(options.state));
|
|
456
|
+
const target = await resolveFileTarget(options);
|
|
457
|
+
startSpinner(resolved ? "Resolving thread..." : "Reopening thread...");
|
|
458
|
+
const response = await httpPatch(
|
|
459
|
+
`/workspaces/${target.workspaceId}/files/${target.fileId}/comment-threads/${threadId}/resolve`,
|
|
460
|
+
{ resolved }
|
|
461
|
+
);
|
|
462
|
+
if (!response.ok) {
|
|
463
|
+
stopSpinner(false, "Failed");
|
|
464
|
+
printError(`Error [${response.status}]: ${JSON.stringify(response.data)}`);
|
|
465
|
+
process.exit(1);
|
|
466
|
+
}
|
|
467
|
+
stopSpinner(true, resolved ? `Resolved: ${threadId}` : `Reopened: ${threadId}`);
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
|
|
398
471
|
// src/utils/search-options.ts
|
|
399
472
|
var SearchOptionsError = class extends Error {
|
|
400
473
|
constructor(message) {
|
|
@@ -436,7 +509,7 @@ function parseTeammateId(raw) {
|
|
|
436
509
|
}
|
|
437
510
|
|
|
438
511
|
// src/utils/run-or-exit.ts
|
|
439
|
-
function
|
|
512
|
+
function runOrExit2(fn) {
|
|
440
513
|
try {
|
|
441
514
|
return fn();
|
|
442
515
|
} catch (err) {
|
|
@@ -448,279 +521,38 @@ function runOrExit(fn) {
|
|
|
448
521
|
}
|
|
449
522
|
}
|
|
450
523
|
|
|
451
|
-
// src/
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
Authorization: `Bearer ${this.context.sandboxToolApiToken}`
|
|
475
|
-
},
|
|
476
|
-
body: body === void 0 ? void 0 : JSON.stringify(body),
|
|
477
|
-
signal
|
|
478
|
-
});
|
|
479
|
-
const contentType = response.headers.get("content-type");
|
|
480
|
-
const data = contentType?.includes("application/json") ? await response.json() : await response.text();
|
|
524
|
+
// src/commands/file.ts
|
|
525
|
+
function addSpaceOptions(cmd) {
|
|
526
|
+
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)");
|
|
527
|
+
}
|
|
528
|
+
function registerFileCommand(program2) {
|
|
529
|
+
const file = program2.command("file").description("File operations");
|
|
530
|
+
registerFileCommentCommands(file);
|
|
531
|
+
addSpaceOptions(
|
|
532
|
+
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")
|
|
533
|
+
).action(async (query, options) => {
|
|
534
|
+
const spaceParams = runOrExit2(() => resolveSpaceSelector(options));
|
|
535
|
+
const limit = runOrExit2(() => parseSearchLimit(options.limit, 30, 80));
|
|
536
|
+
const mode = runOrExit2(() => parseSearchMode(options.mode));
|
|
537
|
+
startSpinner("Searching files...");
|
|
538
|
+
const response = await httpPost(
|
|
539
|
+
`/workspaces/${options.workspace}/files/search`,
|
|
540
|
+
{
|
|
541
|
+
query,
|
|
542
|
+
limit,
|
|
543
|
+
mode,
|
|
544
|
+
...spaceParams
|
|
545
|
+
}
|
|
546
|
+
);
|
|
481
547
|
if (!response.ok) {
|
|
482
|
-
|
|
548
|
+
stopSpinner(false, "Failed");
|
|
549
|
+
printError(`Error [${response.status}]: ${JSON.stringify(response.data)}`);
|
|
550
|
+
process.exit(1);
|
|
483
551
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
}
|
|
489
|
-
};
|
|
490
|
-
|
|
491
|
-
// src/utils/sandbox-file.ts
|
|
492
|
-
import { createHash } from "crypto";
|
|
493
|
-
var MFS_FILE_TYPE_REGULAR = 1;
|
|
494
|
-
var MFS_FILE_SIZE_LIMIT_BYTES = 10 * 1024 * 1024;
|
|
495
|
-
var MFS_FILE_SIZE_LIMIT_LABEL = `${MFS_FILE_SIZE_LIMIT_BYTES / (1024 * 1024)}MB`;
|
|
496
|
-
var SandboxFileError = class extends Error {
|
|
497
|
-
constructor(message, code) {
|
|
498
|
-
super(message);
|
|
499
|
-
this.code = code;
|
|
500
|
-
this.name = "SandboxFileError";
|
|
501
|
-
}
|
|
502
|
-
code;
|
|
503
|
-
};
|
|
504
|
-
async function readSandboxFile(context, workspacePath) {
|
|
505
|
-
const resolved = resolveSandboxFilePath(context, workspacePath);
|
|
506
|
-
const tree = await fetchRepoTree(context, resolved);
|
|
507
|
-
const entry = findTreeEntry(tree, resolved);
|
|
508
|
-
if (!entry) {
|
|
509
|
-
throw new SandboxFileError(`${workspacePath} does not exist`, "not-found");
|
|
510
|
-
}
|
|
511
|
-
if (entry.type === "tree") {
|
|
512
|
-
throw new SandboxFileError(`${workspacePath} is a directory`, "directory");
|
|
513
|
-
}
|
|
514
|
-
if (!entry.sha) {
|
|
515
|
-
throw new SandboxFileError(`${workspacePath} does not have downloadable content`, "remote");
|
|
516
|
-
}
|
|
517
|
-
if (entry.size !== null && entry.size > MFS_FILE_SIZE_LIMIT_BYTES) {
|
|
518
|
-
throw new SandboxFileError(`File too large (max ${MFS_FILE_SIZE_LIMIT_LABEL}): ${workspacePath}`, "too-large");
|
|
519
|
-
}
|
|
520
|
-
const client = new SandboxMoxtClient(context);
|
|
521
|
-
const presign = await client.request(
|
|
522
|
-
"POST",
|
|
523
|
-
`/agent/api/pipeline-sandbox/mfs/${encodeURIComponent(resolved.repoId)}/presign-download`,
|
|
524
|
-
{ hashes: [entry.sha] }
|
|
525
|
-
);
|
|
526
|
-
const url = presign.data.urls[entry.sha];
|
|
527
|
-
if (!url) {
|
|
528
|
-
throw new SandboxFileError(`No download URL returned for ${workspacePath}`, "remote");
|
|
529
|
-
}
|
|
530
|
-
const response = await fetch(url);
|
|
531
|
-
if (!response.ok) {
|
|
532
|
-
throw new SandboxFileError(`Download failed [${response.status}]`, "remote");
|
|
533
|
-
}
|
|
534
|
-
const content = await response.text();
|
|
535
|
-
const data = Buffer.from(content, "utf8");
|
|
536
|
-
if (data.length > MFS_FILE_SIZE_LIMIT_BYTES) {
|
|
537
|
-
throw new SandboxFileError(`File too large (max ${MFS_FILE_SIZE_LIMIT_LABEL}): ${workspacePath}`, "too-large");
|
|
538
|
-
}
|
|
539
|
-
const actualSha = createHash("sha256").update(data).digest("hex");
|
|
540
|
-
if (actualSha !== entry.sha) {
|
|
541
|
-
throw new SandboxFileError(`Downloaded content hash mismatch: ${workspacePath}`, "remote");
|
|
542
|
-
}
|
|
543
|
-
return {
|
|
544
|
-
path: workspacePath,
|
|
545
|
-
content,
|
|
546
|
-
sha: entry.sha,
|
|
547
|
-
size: data.length,
|
|
548
|
-
fileId: entry.fileId,
|
|
549
|
-
fileType: entry.fileType
|
|
550
|
-
};
|
|
551
|
-
}
|
|
552
|
-
async function writeSandboxFile(context, workspacePath, content, policy) {
|
|
553
|
-
const data = Buffer.from(content, "utf8");
|
|
554
|
-
if (data.length > MFS_FILE_SIZE_LIMIT_BYTES) {
|
|
555
|
-
throw new SandboxFileError(`File too large (max ${MFS_FILE_SIZE_LIMIT_LABEL}): ${workspacePath}`, "too-large");
|
|
556
|
-
}
|
|
557
|
-
const resolved = resolveSandboxFilePath(context, workspacePath);
|
|
558
|
-
const tree = await fetchRepoTree(context, resolved);
|
|
559
|
-
const existing = findTreeEntry(tree, resolved);
|
|
560
|
-
if (existing?.type === "tree") {
|
|
561
|
-
throw new SandboxFileError(`${workspacePath} is a directory`, "directory");
|
|
562
|
-
}
|
|
563
|
-
if (existing && !existing.sha) {
|
|
564
|
-
throw new SandboxFileError(`${workspacePath} does not have base content hash`, "remote");
|
|
565
|
-
}
|
|
566
|
-
const baseContentHash = resolveBaseContentHash(workspacePath, existing, policy);
|
|
567
|
-
const contentHash = createHash("sha256").update(data).digest("hex");
|
|
568
|
-
const client = new SandboxMoxtClient(context);
|
|
569
|
-
const presign = await client.request(
|
|
570
|
-
"POST",
|
|
571
|
-
`/agent/api/pipeline-sandbox/mfs/${encodeURIComponent(resolved.repoId)}/presign-upload`,
|
|
572
|
-
{ hashes: [contentHash] }
|
|
573
|
-
);
|
|
574
|
-
const uploadUrl = presign.data.urls[contentHash];
|
|
575
|
-
if (!uploadUrl) {
|
|
576
|
-
throw new SandboxFileError(`No upload URL returned for ${workspacePath}`, "remote");
|
|
577
|
-
}
|
|
578
|
-
const upload = await fetch(uploadUrl, {
|
|
579
|
-
method: "PUT",
|
|
580
|
-
headers: { "Content-Type": "application/octet-stream" },
|
|
581
|
-
body: new Uint8Array(data)
|
|
582
|
-
});
|
|
583
|
-
if (!upload.ok) {
|
|
584
|
-
throw new SandboxFileError(`Upload failed [${upload.status}]`, "remote");
|
|
585
|
-
}
|
|
586
|
-
const action = existing ? "update" : "create";
|
|
587
|
-
await client.request(
|
|
588
|
-
"POST",
|
|
589
|
-
"/agent/api/pipeline-sandbox/mfs-workspace/unified-push",
|
|
590
|
-
{
|
|
591
|
-
message: `moxt file write ${workspacePath}`,
|
|
592
|
-
repoPushes: [
|
|
593
|
-
{
|
|
594
|
-
repoId: resolved.repoId,
|
|
595
|
-
changes: [
|
|
596
|
-
{
|
|
597
|
-
action,
|
|
598
|
-
...existing?.fileId ? { fileId: existing.fileId } : {},
|
|
599
|
-
path: resolved.repoPath,
|
|
600
|
-
contentHash,
|
|
601
|
-
...baseContentHash ? { baseContentHash } : {},
|
|
602
|
-
size: data.length,
|
|
603
|
-
fileType: MFS_FILE_TYPE_REGULAR
|
|
604
|
-
}
|
|
605
|
-
]
|
|
606
|
-
}
|
|
607
|
-
],
|
|
608
|
-
crossRepoMoves: []
|
|
609
|
-
}
|
|
610
|
-
);
|
|
611
|
-
return { path: workspacePath, created: !existing };
|
|
612
|
-
}
|
|
613
|
-
function resolveBaseContentHash(workspacePath, existing, policy) {
|
|
614
|
-
if (policy.type === "force") return void 0;
|
|
615
|
-
if (policy.type === "use-current-sha") return existing?.sha ?? void 0;
|
|
616
|
-
if (policy.type === "create-only") {
|
|
617
|
-
if (!existing) return void 0;
|
|
618
|
-
throw new SandboxFileError(
|
|
619
|
-
`Existing file requires --base-sha or --force: ${workspacePath} (actual ${existing.sha})`,
|
|
620
|
-
"conflict"
|
|
621
|
-
);
|
|
622
|
-
}
|
|
623
|
-
if (existing?.sha === policy.sha) return policy.sha;
|
|
624
|
-
throw new SandboxFileError(
|
|
625
|
-
`File version conflict: ${workspacePath} (expected ${policy.sha}, actual ${existing?.sha ?? "missing"})`,
|
|
626
|
-
"conflict"
|
|
627
|
-
);
|
|
628
|
-
}
|
|
629
|
-
function resolveSandboxFilePath(context, workspacePath) {
|
|
630
|
-
let resolved;
|
|
631
|
-
try {
|
|
632
|
-
resolved = resolveWorkspacePath(context.repoPayload, workspacePath);
|
|
633
|
-
} catch (error) {
|
|
634
|
-
throw new SandboxFileError(error instanceof Error ? error.message : String(error), "invalid-path");
|
|
635
|
-
}
|
|
636
|
-
if (!resolved.repoPath) {
|
|
637
|
-
throw new SandboxFileError(`${workspacePath} is a directory`, "directory");
|
|
638
|
-
}
|
|
639
|
-
return resolved;
|
|
640
|
-
}
|
|
641
|
-
async function fetchRepoTree(context, resolved) {
|
|
642
|
-
const client = new SandboxMoxtClient(context);
|
|
643
|
-
const params = new URLSearchParams({ entryFilter: "paths", path: resolved.repoPath });
|
|
644
|
-
const response = await client.request(
|
|
645
|
-
"GET",
|
|
646
|
-
`/agent/api/pipeline-sandbox/mfs/${encodeURIComponent(resolved.repoId)}/tree?${params.toString()}`
|
|
647
|
-
);
|
|
648
|
-
return response.data;
|
|
649
|
-
}
|
|
650
|
-
function findTreeEntry(tree, resolved) {
|
|
651
|
-
return tree.entries.find((entry) => entry.path === resolved.repoPath);
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
// src/utils/spinner.ts
|
|
655
|
-
var frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
656
|
-
var intervalId = null;
|
|
657
|
-
var frameIndex = 0;
|
|
658
|
-
function startSpinner(message) {
|
|
659
|
-
frameIndex = 0;
|
|
660
|
-
process.stdout.write(`${frames[frameIndex]} ${message}`);
|
|
661
|
-
intervalId = setInterval(() => {
|
|
662
|
-
frameIndex = (frameIndex + 1) % frames.length;
|
|
663
|
-
process.stdout.write(`\r${frames[frameIndex]} ${message}`);
|
|
664
|
-
}, 80);
|
|
665
|
-
}
|
|
666
|
-
function stopSpinner(success, message) {
|
|
667
|
-
if (intervalId) {
|
|
668
|
-
clearInterval(intervalId);
|
|
669
|
-
intervalId = null;
|
|
670
|
-
}
|
|
671
|
-
const symbol = success ? "\x1B[32m\u2713\x1B[0m" : "\x1B[31m\u2717\x1B[0m";
|
|
672
|
-
process.stdout.write(`\x1B[2K\r${symbol} ${message ?? ""}
|
|
673
|
-
`);
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
// src/utils/write-content.ts
|
|
677
|
-
import * as fs from "fs";
|
|
678
|
-
function readWriteCommandContentOrExit(options, readContentFile, readStdin = () => fs.readFileSync(0, "utf8")) {
|
|
679
|
-
const inputModes = [options.content !== void 0, options.contentFile !== void 0, options.stdin === true].filter(Boolean);
|
|
680
|
-
if (inputModes.length > 1) {
|
|
681
|
-
printError("Only one of --content, --content-file, or --stdin can be used");
|
|
682
|
-
process.exit(1);
|
|
683
|
-
}
|
|
684
|
-
if (options.content !== void 0) {
|
|
685
|
-
return options.content;
|
|
686
|
-
}
|
|
687
|
-
if (options.contentFile !== void 0) {
|
|
688
|
-
return readContentFile(options.contentFile);
|
|
689
|
-
}
|
|
690
|
-
return readStdin();
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
// src/commands/file.ts
|
|
694
|
-
function addSpaceOptions(cmd) {
|
|
695
|
-
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)");
|
|
696
|
-
}
|
|
697
|
-
function registerFileCommand(program2) {
|
|
698
|
-
const file = program2.command("file").description("File operations");
|
|
699
|
-
addSpaceOptions(
|
|
700
|
-
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")
|
|
701
|
-
).action(async (query, options) => {
|
|
702
|
-
const spaceParams = runOrExit(() => resolveSpaceSelector(options));
|
|
703
|
-
const limit = runOrExit(() => parseSearchLimit(options.limit, 30, 80));
|
|
704
|
-
const mode = runOrExit(() => parseSearchMode(options.mode));
|
|
705
|
-
startSpinner("Searching files...");
|
|
706
|
-
const response = await httpPost(
|
|
707
|
-
`/workspaces/${options.workspace}/files/search`,
|
|
708
|
-
{
|
|
709
|
-
query,
|
|
710
|
-
limit,
|
|
711
|
-
mode,
|
|
712
|
-
...spaceParams
|
|
713
|
-
}
|
|
714
|
-
);
|
|
715
|
-
if (!response.ok) {
|
|
716
|
-
stopSpinner(false, "Failed");
|
|
717
|
-
printError(`Error [${response.status}]: ${JSON.stringify(response.data)}`);
|
|
718
|
-
process.exit(1);
|
|
719
|
-
}
|
|
720
|
-
const { items } = response.data;
|
|
721
|
-
if (items.length === 0) {
|
|
722
|
-
stopSpinner(true, "No files found.");
|
|
723
|
-
return;
|
|
552
|
+
const { items } = response.data;
|
|
553
|
+
if (items.length === 0) {
|
|
554
|
+
stopSpinner(true, "No files found.");
|
|
555
|
+
return;
|
|
724
556
|
}
|
|
725
557
|
stopSpinner(true, `Found ${items.length} file(s)`);
|
|
726
558
|
for (const item of items) {
|
|
@@ -736,7 +568,7 @@ function registerFileCommand(program2) {
|
|
|
736
568
|
addSpaceOptions(
|
|
737
569
|
file.command("get-url").description("Resolve the shareable browser URL for a file").requiredOption("-p, --path <path>", "File path")
|
|
738
570
|
).action(async (options) => {
|
|
739
|
-
const spaceParams =
|
|
571
|
+
const spaceParams = runOrExit2(() => resolveSpaceSelector(options));
|
|
740
572
|
const qs = buildFileQueryString(options.path, spaceParams);
|
|
741
573
|
startSpinner("Resolving URL...");
|
|
742
574
|
const response = await httpGet(
|
|
@@ -759,7 +591,7 @@ function registerFileCommand(program2) {
|
|
|
759
591
|
addSpaceOptions(
|
|
760
592
|
file.command("list").description("List directory contents").option("-p, --path <path>", "Directory path", "/")
|
|
761
593
|
).action(async (options) => {
|
|
762
|
-
const spaceParams =
|
|
594
|
+
const spaceParams = runOrExit2(() => resolveSpaceSelector(options));
|
|
763
595
|
const qs = buildFileQueryString(options.path, spaceParams);
|
|
764
596
|
startSpinner("Listing files...");
|
|
765
597
|
const response = await httpGet(
|
|
@@ -788,40 +620,8 @@ function registerFileCommand(program2) {
|
|
|
788
620
|
}
|
|
789
621
|
}
|
|
790
622
|
});
|
|
791
|
-
file.command("read").description("Read file content").
|
|
792
|
-
const
|
|
793
|
-
if (sandboxRuntime) {
|
|
794
|
-
if (options.url) {
|
|
795
|
-
printError("--url cannot be used in sandbox mode");
|
|
796
|
-
process.exit(1);
|
|
797
|
-
}
|
|
798
|
-
const path2 = workspacePath ?? options.path;
|
|
799
|
-
if (!path2) {
|
|
800
|
-
printError("Workspace path is required in sandbox mode");
|
|
801
|
-
process.exit(1);
|
|
802
|
-
}
|
|
803
|
-
if (!options.json) startSpinner("Reading file...");
|
|
804
|
-
try {
|
|
805
|
-
const result = await readSandboxFile(sandboxRuntime, path2);
|
|
806
|
-
if (options.json) {
|
|
807
|
-
print(JSON.stringify(result, null, 2));
|
|
808
|
-
} else {
|
|
809
|
-
stopSpinner(true, "Done");
|
|
810
|
-
print(result.content);
|
|
811
|
-
}
|
|
812
|
-
} catch (error) {
|
|
813
|
-
handleSandboxFileError(error, path2);
|
|
814
|
-
}
|
|
815
|
-
return;
|
|
816
|
-
}
|
|
817
|
-
if (options.json) {
|
|
818
|
-
printError("--json is only available for sandbox workspace paths");
|
|
819
|
-
process.exit(1);
|
|
820
|
-
}
|
|
821
|
-
if (workspacePath && !options.path) {
|
|
822
|
-
options.path = workspacePath;
|
|
823
|
-
}
|
|
824
|
-
const mode = runOrExit(() => resolveReadMode(options));
|
|
623
|
+
file.command("read").description("Read file content").option("-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)").option("-p, --path <path>", "File path").option("-u, --url <url>", "Moxt file URL (e.g., https://moxt.ai/w/{workspaceId}/{fileId})").action(async (options) => {
|
|
624
|
+
const mode = runOrExit2(() => resolveReadMode(options));
|
|
825
625
|
startSpinner("Reading file...");
|
|
826
626
|
let response;
|
|
827
627
|
let displayPath;
|
|
@@ -832,7 +632,7 @@ function registerFileCommand(program2) {
|
|
|
832
632
|
);
|
|
833
633
|
} else {
|
|
834
634
|
displayPath = mode.path;
|
|
835
|
-
const spaceParams =
|
|
635
|
+
const spaceParams = runOrExit2(() => resolveSpaceSelector(options));
|
|
836
636
|
const qs = buildFileQueryString(mode.path, spaceParams);
|
|
837
637
|
response = await httpGet(
|
|
838
638
|
`/workspaces/${mode.workspace}/files/read${qs}`
|
|
@@ -856,37 +656,29 @@ function registerFileCommand(program2) {
|
|
|
856
656
|
stopSpinner(true, "Done");
|
|
857
657
|
print(content ?? "");
|
|
858
658
|
});
|
|
859
|
-
file.command("put").description("Upload a file").
|
|
860
|
-
async (
|
|
861
|
-
const
|
|
862
|
-
if (
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
process.exit(1);
|
|
866
|
-
}
|
|
867
|
-
const path2 = workspacePath ?? options.path;
|
|
868
|
-
if (!path2) {
|
|
869
|
-
printError("Workspace path is required in sandbox mode");
|
|
870
|
-
process.exit(1);
|
|
871
|
-
}
|
|
872
|
-
const { content: content2 } = readLocalTextFileOrExit(options.localPath);
|
|
873
|
-
startSpinner("Uploading...");
|
|
874
|
-
try {
|
|
875
|
-
const response2 = await writeSandboxFile(sandboxRuntime, path2, content2, {
|
|
876
|
-
type: "use-current-sha"
|
|
877
|
-
});
|
|
878
|
-
const action2 = response2.created ? "Created" : "Updated";
|
|
879
|
-
stopSpinner(true, `${action2}: ${response2.path}`);
|
|
880
|
-
} catch (error) {
|
|
881
|
-
handleSandboxFileError(error, path2);
|
|
882
|
-
}
|
|
883
|
-
return;
|
|
659
|
+
file.command("put").description("Upload a file").option("-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)").option("-p, --path <path>", "Remote file path").option("-u, --url <url>", "Moxt file URL (e.g., https://moxt.ai/w/{workspaceId}/{fileId})").requiredOption("-l, --local-path <localPath>", "Local file path").option("-r, --recursive", "Create parent directories if needed").action(
|
|
660
|
+
async (options) => {
|
|
661
|
+
const mode = runOrExit2(() => resolveWriteMode(options));
|
|
662
|
+
if (!fs2.existsSync(options.localPath)) {
|
|
663
|
+
print(`${colors.red}\u2620 Local file not found: ${options.localPath}${colors.reset}`);
|
|
664
|
+
process.exit(1);
|
|
884
665
|
}
|
|
885
|
-
|
|
886
|
-
|
|
666
|
+
const stats = fs2.statSync(options.localPath);
|
|
667
|
+
if (!stats.isFile()) {
|
|
668
|
+
print(`${colors.red}\u2620 Not a file: ${options.localPath}${colors.reset}`);
|
|
669
|
+
process.exit(1);
|
|
670
|
+
}
|
|
671
|
+
const maxSize = 10 * 1024 * 1024;
|
|
672
|
+
if (stats.size > maxSize) {
|
|
673
|
+
print(`${colors.red}\u2620 File too large (max 10MB): ${options.localPath}${colors.reset}`);
|
|
674
|
+
process.exit(1);
|
|
675
|
+
}
|
|
676
|
+
const buffer2 = fs2.readFileSync(options.localPath);
|
|
677
|
+
if (isBinaryContent(buffer2)) {
|
|
678
|
+
print(`${colors.red}\u2620 Binary files are not allowed. Only text files can be uploaded.${colors.reset}`);
|
|
679
|
+
process.exit(1);
|
|
887
680
|
}
|
|
888
|
-
const
|
|
889
|
-
const { content } = readLocalTextFileOrExit(options.localPath);
|
|
681
|
+
const content = buffer2.toString("utf8");
|
|
890
682
|
startSpinner("Uploading...");
|
|
891
683
|
let response;
|
|
892
684
|
let displayPath;
|
|
@@ -898,7 +690,7 @@ function registerFileCommand(program2) {
|
|
|
898
690
|
});
|
|
899
691
|
} else {
|
|
900
692
|
displayPath = mode.path;
|
|
901
|
-
const spaceParams =
|
|
693
|
+
const spaceParams = runOrExit2(() => resolveSpaceSelector(options));
|
|
902
694
|
response = await httpPut(
|
|
903
695
|
`/workspaces/${mode.workspace}/files/write`,
|
|
904
696
|
{
|
|
@@ -928,28 +720,11 @@ function registerFileCommand(program2) {
|
|
|
928
720
|
stopSpinner(true, `${action}: ${response.data.path}`);
|
|
929
721
|
}
|
|
930
722
|
);
|
|
931
|
-
file.command("write").description("Write file content in sandbox mode").argument("<workspacePath>", "Workspace path, e.g. personal/docs/readme.md").option("--content <content>", "Text content to write").option("--content-file <contentFile>", "Local text file to write").option("--stdin", "Read text content from stdin (default)").option("--base-sha <sha>", "Write only if the remote file still has this SHA").option("--force", "Overwrite an existing remote file without a version condition").action(async (workspacePath, options) => {
|
|
932
|
-
const runtime = resolveSandboxRuntimeContextOrExit();
|
|
933
|
-
if (!runtime) {
|
|
934
|
-
printError("file write is only available in sandbox mode");
|
|
935
|
-
process.exit(1);
|
|
936
|
-
}
|
|
937
|
-
const policy = resolveSandboxFileWritePolicyOrExit(options);
|
|
938
|
-
const content = readWriteCommandContentOrExit(options, (path2) => readLocalTextFileOrExit(path2).content);
|
|
939
|
-
startSpinner("Writing file...");
|
|
940
|
-
try {
|
|
941
|
-
const response = await writeSandboxFile(runtime, workspacePath, content, policy);
|
|
942
|
-
const action = response.created ? "Created" : "Updated";
|
|
943
|
-
stopSpinner(true, `${action}: ${response.path}`);
|
|
944
|
-
} catch (error) {
|
|
945
|
-
handleSandboxFileError(error, workspacePath);
|
|
946
|
-
}
|
|
947
|
-
});
|
|
948
723
|
addSpaceOptions(
|
|
949
724
|
file.command("mkdir").description("Create a directory").requiredOption("-p, --path <path>", "Directory path").option("-r, --recursive", "Create parent directories if needed")
|
|
950
725
|
).action(
|
|
951
726
|
async (options) => {
|
|
952
|
-
const spaceParams =
|
|
727
|
+
const spaceParams = runOrExit2(() => resolveSpaceSelector(options));
|
|
953
728
|
startSpinner("Creating directory...");
|
|
954
729
|
const response = await httpPost(
|
|
955
730
|
`/workspaces/${options.workspace}/files/mkdir`,
|
|
@@ -978,7 +753,7 @@ function registerFileCommand(program2) {
|
|
|
978
753
|
file.command("del").description("Delete a file or empty directory").requiredOption("-p, --path <path>", "File or directory path")
|
|
979
754
|
).action(
|
|
980
755
|
async (options) => {
|
|
981
|
-
const spaceParams =
|
|
756
|
+
const spaceParams = runOrExit2(() => resolveSpaceSelector(options));
|
|
982
757
|
startSpinner("Deleting...");
|
|
983
758
|
const response = await httpDelete(
|
|
984
759
|
`/workspaces/${options.workspace}/files/delete`,
|
|
@@ -1002,90 +777,14 @@ function registerFileCommand(program2) {
|
|
|
1002
777
|
}
|
|
1003
778
|
);
|
|
1004
779
|
}
|
|
1005
|
-
function resolveSandboxFileWritePolicyOrExit(options) {
|
|
1006
|
-
if (options.baseSha && options.force) {
|
|
1007
|
-
printError("Only one of --base-sha or --force can be used");
|
|
1008
|
-
process.exit(1);
|
|
1009
|
-
}
|
|
1010
|
-
if (options.baseSha) {
|
|
1011
|
-
if (!/^[a-f0-9]{64}$/.test(options.baseSha)) {
|
|
1012
|
-
printError("--base-sha must be a lowercase SHA-256 hash");
|
|
1013
|
-
process.exit(1);
|
|
1014
|
-
}
|
|
1015
|
-
return { type: "base-sha", sha: options.baseSha };
|
|
1016
|
-
}
|
|
1017
|
-
return options.force ? { type: "force" } : { type: "create-only" };
|
|
1018
|
-
}
|
|
1019
|
-
function resolveRuntimeContextOrExit() {
|
|
1020
|
-
try {
|
|
1021
|
-
return resolveRuntimeContext();
|
|
1022
|
-
} catch (error) {
|
|
1023
|
-
if (error instanceof RuntimeContextError) {
|
|
1024
|
-
printError(error.message);
|
|
1025
|
-
process.exit(1);
|
|
1026
|
-
}
|
|
1027
|
-
throw error;
|
|
1028
|
-
}
|
|
1029
|
-
}
|
|
1030
|
-
function resolveSandboxRuntimeContextOrExit() {
|
|
1031
|
-
if (!SANDBOX_ENV_KEYS.some((key) => (process.env[key] ?? "").trim().length > 0)) {
|
|
1032
|
-
return void 0;
|
|
1033
|
-
}
|
|
1034
|
-
const context = resolveRuntimeContextOrExit();
|
|
1035
|
-
return context.mode === "sandbox" ? context : void 0;
|
|
1036
|
-
}
|
|
1037
|
-
function readLocalTextFileOrExit(localPath) {
|
|
1038
|
-
if (!fs2.existsSync(localPath)) {
|
|
1039
|
-
print(`${colors.red}\u2620 Local file not found: ${localPath}${colors.reset}`);
|
|
1040
|
-
process.exit(1);
|
|
1041
|
-
}
|
|
1042
|
-
const stats = fs2.statSync(localPath);
|
|
1043
|
-
if (!stats.isFile()) {
|
|
1044
|
-
print(`${colors.red}\u2620 Not a file: ${localPath}${colors.reset}`);
|
|
1045
|
-
process.exit(1);
|
|
1046
|
-
}
|
|
1047
|
-
if (stats.size > MFS_FILE_SIZE_LIMIT_BYTES) {
|
|
1048
|
-
print(`${colors.red}\u2620 File too large (max ${MFS_FILE_SIZE_LIMIT_LABEL}): ${localPath}${colors.reset}`);
|
|
1049
|
-
process.exit(1);
|
|
1050
|
-
}
|
|
1051
|
-
const buffer2 = fs2.readFileSync(localPath);
|
|
1052
|
-
if (isBinaryContent(buffer2)) {
|
|
1053
|
-
print(`${colors.red}\u2620 Binary files are not allowed. Only text files can be uploaded.${colors.reset}`);
|
|
1054
|
-
process.exit(1);
|
|
1055
|
-
}
|
|
1056
|
-
return { content: buffer2.toString("utf8") };
|
|
1057
|
-
}
|
|
1058
|
-
function handleSandboxFileError(error, displayPath) {
|
|
1059
|
-
stopSpinner(false, "Failed");
|
|
1060
|
-
if (error instanceof SandboxFileError) {
|
|
1061
|
-
if (error.code === "not-found") {
|
|
1062
|
-
print(`${colors.red}\u2620 ${displayPath} does not exist${colors.reset}`);
|
|
1063
|
-
} else if (error.code === "directory") {
|
|
1064
|
-
print(`${colors.red}\u2620 ${displayPath} is a directory${colors.reset}`);
|
|
1065
|
-
} else {
|
|
1066
|
-
printError(error.message);
|
|
1067
|
-
}
|
|
1068
|
-
process.exit(1);
|
|
1069
|
-
}
|
|
1070
|
-
if (error instanceof SandboxApiError) {
|
|
1071
|
-
printError(`Sandbox API error [${error.status}]: ${error.body}`);
|
|
1072
|
-
process.exit(1);
|
|
1073
|
-
}
|
|
1074
|
-
if (error instanceof Error) {
|
|
1075
|
-
printError(`Sandbox file operation failed: ${error.message}`);
|
|
1076
|
-
process.exit(1);
|
|
1077
|
-
}
|
|
1078
|
-
printError(`Sandbox file operation failed: ${String(error)}`);
|
|
1079
|
-
process.exit(1);
|
|
1080
|
-
}
|
|
1081
780
|
|
|
1082
781
|
// src/commands/memory.ts
|
|
1083
782
|
function registerMemoryCommand(program2) {
|
|
1084
783
|
const memory = program2.command("memory").description("Memory operations");
|
|
1085
784
|
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(
|
|
1086
785
|
async (query, options) => {
|
|
1087
|
-
const limit =
|
|
1088
|
-
const teammateId =
|
|
786
|
+
const limit = runOrExit2(() => parseSearchLimit(options.limit, 5, 10));
|
|
787
|
+
const teammateId = runOrExit2(() => parseTeammateId(options.teammateId));
|
|
1089
788
|
startSpinner("Searching memory...");
|
|
1090
789
|
const response = await httpPost(
|
|
1091
790
|
`/workspaces/${options.workspace}/memory/search`,
|
|
@@ -1191,7 +890,7 @@ function isAllowedMiniappDbUserEmail(email) {
|
|
|
1191
890
|
}
|
|
1192
891
|
|
|
1193
892
|
// src/commands/miniapp.ts
|
|
1194
|
-
function
|
|
893
|
+
function runOrExit3(fn) {
|
|
1195
894
|
try {
|
|
1196
895
|
return fn();
|
|
1197
896
|
} catch (err) {
|
|
@@ -1251,14 +950,14 @@ function registerMiniappCommand(program2) {
|
|
|
1251
950
|
addMiniappDbTargetOptions(
|
|
1252
951
|
db.command("post").description("Create miniapp data through PostgREST").argument("<path>", "PostgREST path, for example /todos").requiredOption("--body <json>", "JSON object or array request body").option("--pretty", "Pretty-print JSON responses")
|
|
1253
952
|
).action(async (postgrestPath, options) => {
|
|
1254
|
-
const body =
|
|
953
|
+
const body = runOrExit3(() => parseJsonBody(options.body ?? ""));
|
|
1255
954
|
await requestDataApi("POST", postgrestPath, options, body);
|
|
1256
955
|
});
|
|
1257
956
|
addMiniappDbTargetOptions(
|
|
1258
957
|
db.command("patch").description("Update miniapp data through PostgREST").argument("<path>", "PostgREST path with a filter, for example /todos?id=eq.1").requiredOption("--body <json>", "JSON object or array request body").option("--pretty", "Pretty-print JSON responses")
|
|
1259
958
|
).action(async (postgrestPath, options) => {
|
|
1260
|
-
|
|
1261
|
-
const body =
|
|
959
|
+
runOrExit3(() => assertPostgrestFilter(postgrestPath, "PATCH"));
|
|
960
|
+
const body = runOrExit3(() => parseJsonBody(options.body ?? ""));
|
|
1262
961
|
await requestDataApi("PATCH", postgrestPath, options, body);
|
|
1263
962
|
});
|
|
1264
963
|
addMiniappDbTargetOptions(
|
|
@@ -1268,1558 +967,1167 @@ function registerMiniappCommand(program2) {
|
|
|
1268
967
|
printError("Error: delete requires --yes.");
|
|
1269
968
|
process.exit(1);
|
|
1270
969
|
}
|
|
1271
|
-
|
|
970
|
+
runOrExit3(() => assertPostgrestFilter(postgrestPath, "DELETE"));
|
|
1272
971
|
await requestDataApi("DELETE", postgrestPath, options);
|
|
1273
972
|
});
|
|
1274
973
|
}
|
|
1275
974
|
|
|
1276
|
-
// src/
|
|
1277
|
-
import
|
|
1278
|
-
import
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
import { lstat, mkdir as mkdir2, readFile as readFile2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
1283
|
-
import { dirname as dirname2, isAbsolute as isAbsolute2, relative, resolve } from "path";
|
|
1284
|
-
|
|
1285
|
-
// src/utils/checkout-state.ts
|
|
1286
|
-
import { randomUUID } from "crypto";
|
|
1287
|
-
import { homedir } from "os";
|
|
1288
|
-
import { dirname, isAbsolute, join, posix } from "path";
|
|
1289
|
-
import { mkdir, readFile, rename, rm, writeFile } from "fs/promises";
|
|
1290
|
-
var CheckoutStateError = class extends Error {
|
|
1291
|
-
constructor(message) {
|
|
1292
|
-
super(message);
|
|
1293
|
-
this.name = "CheckoutStateError";
|
|
1294
|
-
}
|
|
1295
|
-
};
|
|
1296
|
-
function getCheckoutStatePath() {
|
|
1297
|
-
return join(homedir(), ".moxt", "checkout-state.json");
|
|
975
|
+
// src/telemetry/config.ts
|
|
976
|
+
import * as fs3 from "fs";
|
|
977
|
+
import * as os from "os";
|
|
978
|
+
import * as path from "path";
|
|
979
|
+
function getConfigPath() {
|
|
980
|
+
return path.join(os.homedir(), ".moxt", "config.json");
|
|
1298
981
|
}
|
|
1299
|
-
|
|
1300
|
-
let raw;
|
|
1301
|
-
try {
|
|
1302
|
-
raw = await readFile(statePath, "utf8");
|
|
1303
|
-
} catch (error) {
|
|
1304
|
-
if (isNodeError(error) && error.code === "ENOENT") return void 0;
|
|
1305
|
-
throw new CheckoutStateError(`Failed to read checkout state: ${errorMessage(error)}`);
|
|
1306
|
-
}
|
|
1307
|
-
let value;
|
|
982
|
+
function readTelemetryConfig() {
|
|
1308
983
|
try {
|
|
1309
|
-
|
|
984
|
+
const raw = fs3.readFileSync(getConfigPath(), "utf8");
|
|
985
|
+
const parsed = JSON.parse(raw);
|
|
986
|
+
return parsed.telemetry ?? {};
|
|
1310
987
|
} catch {
|
|
1311
|
-
|
|
988
|
+
return {};
|
|
1312
989
|
}
|
|
1313
|
-
return validateCheckoutState(value);
|
|
1314
990
|
}
|
|
1315
|
-
|
|
1316
|
-
const
|
|
1317
|
-
|
|
1318
|
-
const temporaryPath = `${statePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
1319
|
-
await mkdir(stateDir, { recursive: true });
|
|
991
|
+
function writeTelemetryConfig(update) {
|
|
992
|
+
const configPath = getConfigPath();
|
|
993
|
+
let existing = {};
|
|
1320
994
|
try {
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
} catch (error) {
|
|
1325
|
-
await rm(temporaryPath, { force: true });
|
|
1326
|
-
throw new CheckoutStateError(`Failed to save checkout state: ${errorMessage(error)}`);
|
|
1327
|
-
}
|
|
1328
|
-
}
|
|
1329
|
-
function isCheckoutPathIncluded(state, workspacePath) {
|
|
1330
|
-
if (state.mode === "full") return true;
|
|
1331
|
-
return state.checkedOutScopes.some(
|
|
1332
|
-
(scope) => workspacePath === scope || workspacePath.startsWith(`${scope}/`)
|
|
1333
|
-
);
|
|
1334
|
-
}
|
|
1335
|
-
function doesCheckoutPathIntersect(state, workspacePath) {
|
|
1336
|
-
if (state.mode === "full") return true;
|
|
1337
|
-
return state.checkedOutScopes.some(
|
|
1338
|
-
(scope) => workspacePath === scope || workspacePath.startsWith(`${scope}/`) || scope.startsWith(`${workspacePath}/`)
|
|
1339
|
-
);
|
|
1340
|
-
}
|
|
1341
|
-
function validateCheckoutState(value) {
|
|
1342
|
-
const state = requireRecord(value, "Checkout state");
|
|
1343
|
-
if (state.version !== 1) {
|
|
1344
|
-
throw new CheckoutStateError(`Unsupported checkout state version: ${String(state.version)}`);
|
|
1345
|
-
}
|
|
1346
|
-
const workspaceId = requireNonEmptyString(state.workspaceId, "workspaceId");
|
|
1347
|
-
const checkoutRoot = requireNonEmptyString(state.checkoutRoot, "checkoutRoot");
|
|
1348
|
-
if (!isAbsolute(checkoutRoot)) throw new CheckoutStateError("checkoutRoot must be an absolute path");
|
|
1349
|
-
if (state.mode !== "partial" && state.mode !== "full") {
|
|
1350
|
-
throw new CheckoutStateError("mode must be partial or full");
|
|
1351
|
-
}
|
|
1352
|
-
if (!Array.isArray(state.checkedOutScopes)) {
|
|
1353
|
-
throw new CheckoutStateError("checkedOutScopes must be an array");
|
|
1354
|
-
}
|
|
1355
|
-
const checkedOutScopes = state.checkedOutScopes.map(
|
|
1356
|
-
(scope, index) => requireWorkspacePath(scope, `checkedOutScopes[${index}]`)
|
|
1357
|
-
);
|
|
1358
|
-
if (new Set(checkedOutScopes).size !== checkedOutScopes.length) {
|
|
1359
|
-
throw new CheckoutStateError("checkedOutScopes must not contain duplicates");
|
|
1360
|
-
}
|
|
1361
|
-
const reposValue = requireRecord(state.repos, "repos");
|
|
1362
|
-
const repos = {};
|
|
1363
|
-
for (const [repoId, rawRepo] of Object.entries(reposValue)) {
|
|
1364
|
-
requireNonEmptyString(repoId, "repo id");
|
|
1365
|
-
const repo = requireRecord(rawRepo, `repos.${repoId}`);
|
|
1366
|
-
const name = requireRepoName(repo.name, `repos.${repoId}.name`);
|
|
1367
|
-
repos[repoId] = {
|
|
1368
|
-
name,
|
|
1369
|
-
treeSha: requireNonEmptyString(repo.treeSha, `repos.${repoId}.treeSha`)
|
|
1370
|
-
};
|
|
1371
|
-
}
|
|
1372
|
-
const filesValue = requireRecord(state.files, "files");
|
|
1373
|
-
const files = {};
|
|
1374
|
-
for (const [workspacePath, rawFile] of Object.entries(filesValue)) {
|
|
1375
|
-
requireWorkspacePath(workspacePath, `files.${workspacePath}`);
|
|
1376
|
-
const file = requireRecord(rawFile, `files.${workspacePath}`);
|
|
1377
|
-
const repoId = requireNonEmptyString(file.repoId, `files.${workspacePath}.repoId`);
|
|
1378
|
-
const repo = repos[repoId];
|
|
1379
|
-
if (!repo) throw new CheckoutStateError(`File references unknown repo: ${workspacePath}`);
|
|
1380
|
-
const repoPath = requireWorkspacePath(file.repoPath, `files.${workspacePath}.repoPath`);
|
|
1381
|
-
if (workspacePath !== `${repo.name}/${repoPath}`) {
|
|
1382
|
-
throw new CheckoutStateError(`File path does not match its repo: ${workspacePath}`);
|
|
1383
|
-
}
|
|
1384
|
-
const fileId = file.fileId === null ? null : requireNonEmptyString(file.fileId, `files.${workspacePath}.fileId`);
|
|
1385
|
-
const sha = requireNonEmptyString(file.sha, `files.${workspacePath}.sha`);
|
|
1386
|
-
if (!/^[a-f0-9]{64}$/.test(sha)) throw new CheckoutStateError(`Invalid file SHA: ${workspacePath}`);
|
|
1387
|
-
files[workspacePath] = {
|
|
1388
|
-
repoId,
|
|
1389
|
-
repoPath,
|
|
1390
|
-
fileId,
|
|
1391
|
-
sha,
|
|
1392
|
-
size: requireNonNegativeInteger(file.size, `files.${workspacePath}.size`),
|
|
1393
|
-
fileType: requireNonNegativeInteger(file.fileType, `files.${workspacePath}.fileType`)
|
|
1394
|
-
};
|
|
1395
|
-
}
|
|
1396
|
-
return {
|
|
1397
|
-
version: 1,
|
|
1398
|
-
workspaceId,
|
|
1399
|
-
checkoutRoot,
|
|
1400
|
-
mode: state.mode,
|
|
1401
|
-
checkedOutScopes,
|
|
1402
|
-
repos,
|
|
1403
|
-
files
|
|
1404
|
-
};
|
|
1405
|
-
}
|
|
1406
|
-
function requireRecord(value, field) {
|
|
1407
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
1408
|
-
throw new CheckoutStateError(`${field} must be an object`);
|
|
995
|
+
existing = JSON.parse(fs3.readFileSync(configPath, "utf8"));
|
|
996
|
+
} catch {
|
|
997
|
+
existing = {};
|
|
1409
998
|
}
|
|
1410
|
-
|
|
999
|
+
const currentTelemetry = existing.telemetry ?? {};
|
|
1000
|
+
const next = { ...existing, telemetry: { ...currentTelemetry, ...update } };
|
|
1001
|
+
fs3.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
1002
|
+
fs3.writeFileSync(configPath, JSON.stringify(next, null, 2));
|
|
1411
1003
|
}
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
return value;
|
|
1004
|
+
|
|
1005
|
+
// src/telemetry/opt-out.ts
|
|
1006
|
+
function isCiEnvironment() {
|
|
1007
|
+
return process.env.CI === "true" || process.env.CI === "1";
|
|
1417
1008
|
}
|
|
1418
|
-
function
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
throw new CheckoutStateError(`${field} must be a normalized relative path`);
|
|
1009
|
+
function isTelemetryDisabled() {
|
|
1010
|
+
if (process.env.MOXT_TELEMETRY_DISABLED === "1" || process.env.MOXT_TELEMETRY_DISABLED === "true") {
|
|
1011
|
+
return true;
|
|
1422
1012
|
}
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
function requireRepoName(value, field) {
|
|
1426
|
-
const name = requireWorkspacePath(value, field);
|
|
1427
|
-
if (name.includes("/")) throw new CheckoutStateError(`${field} must be a single path segment`);
|
|
1428
|
-
return name;
|
|
1429
|
-
}
|
|
1430
|
-
function requireNonNegativeInteger(value, field) {
|
|
1431
|
-
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
|
|
1432
|
-
throw new CheckoutStateError(`${field} must be a non-negative integer`);
|
|
1013
|
+
if (process.env.DO_NOT_TRACK === "1" || process.env.DO_NOT_TRACK === "true") {
|
|
1014
|
+
return true;
|
|
1433
1015
|
}
|
|
1434
|
-
return
|
|
1435
|
-
}
|
|
1436
|
-
function isNodeError(error) {
|
|
1437
|
-
return error instanceof Error && "code" in error;
|
|
1438
|
-
}
|
|
1439
|
-
function errorMessage(error) {
|
|
1440
|
-
return error instanceof Error ? error.message : String(error);
|
|
1016
|
+
return readTelemetryConfig().disabled === true;
|
|
1441
1017
|
}
|
|
1442
1018
|
|
|
1443
|
-
// src/
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
const previousState = await loadCheckoutState();
|
|
1456
|
-
const compatibleState = resolveCompatibleState(previousState, context, resolvedTargetDir, repos);
|
|
1457
|
-
const previousFiles = compatibleState?.files ?? {};
|
|
1458
|
-
const selection = resolvePullSelection(context, options.scopes);
|
|
1459
|
-
const repoById = new Map(selection.repos.map((repo) => [repo.repoId, repo]));
|
|
1460
|
-
const tree = await fetchWorkspaceTree(context, selection);
|
|
1461
|
-
const stateRepos = resolveStateRepos(tree, selection.repos);
|
|
1462
|
-
const entries = [...tree.entries].sort((a, b) => a.prefixedPath.localeCompare(b.prefixedPath));
|
|
1463
|
-
const directoryPaths = selection.repos.map((repo) => ({
|
|
1464
|
-
workspacePath: repo.name,
|
|
1465
|
-
localPath: resolveSafeLocalPath(resolvedTargetDir, repo.name, "")
|
|
1466
|
-
}));
|
|
1467
|
-
const blobEntries = [];
|
|
1468
|
-
const remoteBlobPaths = /* @__PURE__ */ new Set();
|
|
1469
|
-
const remotePaths = /* @__PURE__ */ new Set();
|
|
1470
|
-
const matchedScopes = /* @__PURE__ */ new Set();
|
|
1471
|
-
for (const entry of entries) {
|
|
1472
|
-
const repo = repoById.get(entry.repoId);
|
|
1473
|
-
if (!repo) {
|
|
1474
|
-
throw new SandboxPullError(`Remote tree returned unknown repo: ${entry.repoId}`, "remote");
|
|
1475
|
-
}
|
|
1476
|
-
const expectedWorkspacePath = entry.path ? `${repo.name}/${entry.path}` : repo.name;
|
|
1477
|
-
if (entry.prefixedPath !== expectedWorkspacePath) {
|
|
1478
|
-
throw new SandboxPullError(`Remote tree returned inconsistent path: ${entry.prefixedPath}`, "remote");
|
|
1479
|
-
}
|
|
1480
|
-
if (remotePaths.has(expectedWorkspacePath)) {
|
|
1481
|
-
throw new SandboxPullError(`Remote tree returned duplicate path: ${expectedWorkspacePath}`, "remote");
|
|
1482
|
-
}
|
|
1483
|
-
remotePaths.add(expectedWorkspacePath);
|
|
1484
|
-
const localPath = resolveSafeLocalPath(resolvedTargetDir, repo.name, entry.path);
|
|
1485
|
-
for (const scope of selection.scopes) {
|
|
1486
|
-
if (pathIsIncluded(scope, expectedWorkspacePath)) matchedScopes.add(scope);
|
|
1487
|
-
}
|
|
1488
|
-
if (!selection.scopes.some((scope) => pathsIntersect(scope, expectedWorkspacePath))) continue;
|
|
1489
|
-
if (entry.type === "tree") {
|
|
1490
|
-
directoryPaths.push({ workspacePath: entry.prefixedPath, localPath });
|
|
1491
|
-
continue;
|
|
1492
|
-
}
|
|
1493
|
-
if (!entry.sha) {
|
|
1494
|
-
throw new SandboxPullError(`${entry.prefixedPath} does not have downloadable content`, "remote");
|
|
1495
|
-
}
|
|
1496
|
-
if (entry.fileId !== null && (typeof entry.fileId !== "string" || entry.fileId.length === 0)) {
|
|
1497
|
-
throw new SandboxPullError(`${entry.prefixedPath} has invalid file metadata`, "remote");
|
|
1498
|
-
}
|
|
1499
|
-
if (entry.fileType !== null && (!Number.isInteger(entry.fileType) || entry.fileType < 0)) {
|
|
1500
|
-
throw new SandboxPullError(`${entry.prefixedPath} has invalid file metadata`, "remote");
|
|
1501
|
-
}
|
|
1502
|
-
if (entry.size !== null && (!Number.isInteger(entry.size) || entry.size < 0)) {
|
|
1503
|
-
throw new SandboxPullError(`${entry.prefixedPath} has invalid file metadata`, "remote");
|
|
1504
|
-
}
|
|
1505
|
-
if (entry.size !== null && entry.size > MFS_FILE_SIZE_LIMIT_BYTES) {
|
|
1506
|
-
throw new SandboxPullError(`File too large (max ${MFS_FILE_SIZE_LIMIT_LABEL}): ${entry.prefixedPath}`, "too-large");
|
|
1507
|
-
}
|
|
1508
|
-
remoteBlobPaths.add(entry.prefixedPath);
|
|
1509
|
-
blobEntries.push({ entry, localPath });
|
|
1510
|
-
}
|
|
1511
|
-
if (selection.mode === "partial") {
|
|
1512
|
-
for (const scope of selection.scopes) {
|
|
1513
|
-
const repoRoot = selection.repos.some((repo) => repo.name === scope);
|
|
1514
|
-
const previouslyTracked = Object.keys(previousFiles).some((workspacePath) => pathIsIncluded(scope, workspacePath));
|
|
1515
|
-
if (!repoRoot && !matchedScopes.has(scope) && !previouslyTracked) {
|
|
1516
|
-
throw new SandboxPullError(`Workspace path does not exist: ${scope}`, "invalid-path");
|
|
1517
|
-
}
|
|
1019
|
+
// src/commands/telemetry.ts
|
|
1020
|
+
function registerTelemetryCommand(program2) {
|
|
1021
|
+
const telemetry = program2.command("telemetry").description("Manage Moxt CLI telemetry");
|
|
1022
|
+
telemetry.command("status").description("Show telemetry status").action(() => {
|
|
1023
|
+
const cfg = readTelemetryConfig();
|
|
1024
|
+
const envOverride = process.env.MOXT_TELEMETRY_DISABLED === "1" || process.env.MOXT_TELEMETRY_DISABLED === "true" || process.env.DO_NOT_TRACK === "1" || process.env.DO_NOT_TRACK === "true";
|
|
1025
|
+
const state = isTelemetryDisabled() ? "disabled" : "enabled";
|
|
1026
|
+
print(`telemetry: ${state}`);
|
|
1027
|
+
if (envOverride) {
|
|
1028
|
+
print("(disabled via environment variable)");
|
|
1029
|
+
} else if (cfg.disabled) {
|
|
1030
|
+
print("(disabled via config file)");
|
|
1518
1031
|
}
|
|
1519
|
-
}
|
|
1520
|
-
const plannedBlobs = [];
|
|
1521
|
-
for (const { entry, localPath } of blobEntries) {
|
|
1522
|
-
plannedBlobs.push({
|
|
1523
|
-
entry,
|
|
1524
|
-
localPath,
|
|
1525
|
-
shouldWrite: await shouldWritePulledFile(
|
|
1526
|
-
localPath,
|
|
1527
|
-
requireBlobSha(entry),
|
|
1528
|
-
entry.prefixedPath,
|
|
1529
|
-
previousFiles[entry.prefixedPath],
|
|
1530
|
-
Boolean(options.force)
|
|
1531
|
-
)
|
|
1532
|
-
});
|
|
1533
|
-
}
|
|
1534
|
-
const plannedDeletions = await planRemoteDeletions(
|
|
1535
|
-
compatibleState,
|
|
1536
|
-
selection,
|
|
1537
|
-
resolvedTargetDir,
|
|
1538
|
-
remoteBlobPaths,
|
|
1539
|
-
Boolean(options.force)
|
|
1540
|
-
);
|
|
1541
|
-
for (const directory of directoryPaths) {
|
|
1542
|
-
await assertCanCreateDirectory(directory.localPath, directory.workspacePath);
|
|
1543
|
-
}
|
|
1544
|
-
const blobsToDownload = plannedBlobs.filter((planned) => planned.shouldWrite);
|
|
1545
|
-
const downloadUrls = await presignDownloads(context, blobsToDownload.map(({ entry }) => entry));
|
|
1546
|
-
const downloadedEntries = [];
|
|
1547
|
-
for (const planned of blobsToDownload) {
|
|
1548
|
-
const { entry, localPath } = planned;
|
|
1549
|
-
const remoteContent = await downloadBlob(entry, downloadUrls);
|
|
1550
|
-
downloadedEntries.push({ ...planned, content: remoteContent });
|
|
1551
|
-
}
|
|
1552
|
-
for (const deletion of plannedDeletions) {
|
|
1553
|
-
await rm2(deletion.localPath);
|
|
1554
|
-
}
|
|
1555
|
-
for (const directory of directoryPaths) {
|
|
1556
|
-
await mkdir2(directory.localPath, { recursive: true });
|
|
1557
|
-
}
|
|
1558
|
-
for (const downloaded of downloadedEntries) {
|
|
1559
|
-
await writePulledFile(downloaded.localPath, downloaded.content);
|
|
1560
|
-
}
|
|
1561
|
-
const downloadedContentByPath = new Map(
|
|
1562
|
-
downloadedEntries.map(({ entry, content }) => [entry.prefixedPath, content])
|
|
1563
|
-
);
|
|
1564
|
-
const pulledFiles = Object.fromEntries(await Promise.all(plannedBlobs.map(async ({ entry, localPath }) => {
|
|
1565
|
-
const content = downloadedContentByPath.get(entry.prefixedPath);
|
|
1566
|
-
return [
|
|
1567
|
-
entry.prefixedPath,
|
|
1568
|
-
{
|
|
1569
|
-
repoId: entry.repoId,
|
|
1570
|
-
repoPath: entry.path,
|
|
1571
|
-
fileId: entry.fileId,
|
|
1572
|
-
sha: requireBlobSha(entry),
|
|
1573
|
-
size: content?.length ?? entry.size ?? previousFiles[entry.prefixedPath]?.size ?? await localFileSize(localPath, entry.prefixedPath),
|
|
1574
|
-
fileType: entry.fileType ?? 1
|
|
1575
|
-
}
|
|
1576
|
-
];
|
|
1577
|
-
})));
|
|
1578
|
-
await saveCheckoutState(buildNextCheckoutState({
|
|
1579
|
-
context,
|
|
1580
|
-
checkoutRoot: resolvedTargetDir,
|
|
1581
|
-
allRepos: repos,
|
|
1582
|
-
selection,
|
|
1583
|
-
previousState: compatibleState,
|
|
1584
|
-
pulledRepos: stateRepos,
|
|
1585
|
-
pulledFiles
|
|
1586
|
-
}));
|
|
1587
|
-
return {
|
|
1588
|
-
fileCount: blobEntries.length,
|
|
1589
|
-
targetDir: resolvedTargetDir
|
|
1590
|
-
};
|
|
1591
|
-
}
|
|
1592
|
-
function resolveCompatibleState(state, context, checkoutRoot, repos) {
|
|
1593
|
-
if (!state || state.workspaceId !== context.workspaceId || state.checkoutRoot !== checkoutRoot) return void 0;
|
|
1594
|
-
const currentRepos = new Map(repos.map((repo) => [repo.repoId, repo.name]));
|
|
1595
|
-
const stateMatchesRepos = Object.entries(state.repos).every(
|
|
1596
|
-
([repoId, repo]) => currentRepos.get(repoId) === repo.name
|
|
1597
|
-
);
|
|
1598
|
-
return stateMatchesRepos ? state : void 0;
|
|
1599
|
-
}
|
|
1600
|
-
function resolvePullSelection(context, requestedScopes) {
|
|
1601
|
-
const repos = listRepoEntries(context.repoPayload);
|
|
1602
|
-
if (!requestedScopes || requestedScopes.length === 0) {
|
|
1603
|
-
return { mode: "full", scopes: repos.map((repo) => repo.name), repos };
|
|
1604
|
-
}
|
|
1605
|
-
const resolvedScopes = requestedScopes.map((scope) => {
|
|
1606
|
-
const resolved = resolveWorkspacePath(context.repoPayload, scope);
|
|
1607
|
-
return {
|
|
1608
|
-
path: resolved.repoPath ? `${resolved.repoName}/${resolved.repoPath}` : resolved.repoName,
|
|
1609
|
-
repoId: resolved.repoId
|
|
1610
|
-
};
|
|
1611
1032
|
});
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
}
|
|
1620
|
-
function buildNextCheckoutState(input) {
|
|
1621
|
-
const { context, checkoutRoot, allRepos, selection, previousState, pulledRepos, pulledFiles } = input;
|
|
1622
|
-
if (selection.mode === "full") {
|
|
1623
|
-
return {
|
|
1624
|
-
version: 1,
|
|
1625
|
-
workspaceId: context.workspaceId,
|
|
1626
|
-
checkoutRoot,
|
|
1627
|
-
mode: "full",
|
|
1628
|
-
checkedOutScopes: selection.scopes,
|
|
1629
|
-
repos: pulledRepos,
|
|
1630
|
-
files: pulledFiles
|
|
1631
|
-
};
|
|
1632
|
-
}
|
|
1633
|
-
const previousFiles = previousState?.files ?? {};
|
|
1634
|
-
const preservedFiles = Object.fromEntries(
|
|
1635
|
-
Object.entries(previousFiles).filter(
|
|
1636
|
-
([workspacePath]) => !selection.scopes.some((scope) => pathIsIncluded(scope, workspacePath))
|
|
1637
|
-
)
|
|
1638
|
-
);
|
|
1639
|
-
const checkedOutScopes = collapseScopes([...previousState?.checkedOutScopes ?? [], ...selection.scopes]);
|
|
1640
|
-
const mode = allRepos.every((repo) => checkedOutScopes.includes(repo.name)) ? "full" : "partial";
|
|
1641
|
-
return {
|
|
1642
|
-
version: 1,
|
|
1643
|
-
workspaceId: context.workspaceId,
|
|
1644
|
-
checkoutRoot,
|
|
1645
|
-
mode,
|
|
1646
|
-
checkedOutScopes,
|
|
1647
|
-
repos: { ...previousState?.repos ?? {}, ...pulledRepos },
|
|
1648
|
-
files: { ...preservedFiles, ...pulledFiles }
|
|
1649
|
-
};
|
|
1033
|
+
telemetry.command("enable").description("Enable telemetry").action(() => {
|
|
1034
|
+
writeTelemetryConfig({ disabled: false });
|
|
1035
|
+
print("telemetry: enabled");
|
|
1036
|
+
});
|
|
1037
|
+
telemetry.command("disable").description("Disable telemetry").action(() => {
|
|
1038
|
+
writeTelemetryConfig({ disabled: true });
|
|
1039
|
+
print("telemetry: disabled");
|
|
1040
|
+
});
|
|
1650
1041
|
}
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1042
|
+
|
|
1043
|
+
// src/commands/whoami.ts
|
|
1044
|
+
function registerWhoamiCommand(program2) {
|
|
1045
|
+
program2.command("whoami").description("Show current user information").action(async () => {
|
|
1046
|
+
startSpinner("Fetching user info...");
|
|
1047
|
+
const response = await httpGet("/users/whoami");
|
|
1048
|
+
if (response.ok) {
|
|
1049
|
+
stopSpinner(true, "Done");
|
|
1050
|
+
print(`${colors.bold}Nickname${colors.reset}: ${response.data.nickname}`);
|
|
1051
|
+
print(`${colors.bold}Email${colors.reset}: ${response.data.email}`);
|
|
1052
|
+
} else {
|
|
1053
|
+
stopSpinner(false, "Failed");
|
|
1054
|
+
printError(`Error [${response.status}]: ${JSON.stringify(response.data)}`);
|
|
1055
|
+
process.exit(1);
|
|
1657
1056
|
}
|
|
1658
|
-
|
|
1659
|
-
}
|
|
1660
|
-
return result;
|
|
1661
|
-
}
|
|
1662
|
-
function pathsIntersect(left, right) {
|
|
1663
|
-
return pathIsIncluded(left, right) || pathIsIncluded(right, left);
|
|
1664
|
-
}
|
|
1665
|
-
function pathIsIncluded(scope, workspacePath) {
|
|
1666
|
-
return workspacePath === scope || workspacePath.startsWith(`${scope}/`);
|
|
1057
|
+
});
|
|
1667
1058
|
}
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1059
|
+
|
|
1060
|
+
// src/commands/workspace.ts
|
|
1061
|
+
var emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
|
1062
|
+
function isValidEmail(email) {
|
|
1063
|
+
return emailRegex.test(email);
|
|
1064
|
+
}
|
|
1065
|
+
function registerWorkspaceCommand(program2) {
|
|
1066
|
+
const workspace = program2.command("workspace").description("Workspace management");
|
|
1067
|
+
workspace.command("list").description("List all workspaces you belong to").action(async () => {
|
|
1068
|
+
startSpinner("Fetching workspaces...");
|
|
1069
|
+
const response = await httpGet("/workspaces");
|
|
1070
|
+
if (response.ok) {
|
|
1071
|
+
const { workspaces } = response.data;
|
|
1072
|
+
if (workspaces.length === 0) {
|
|
1073
|
+
stopSpinner(true, "No workspaces found.");
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
stopSpinner(true, `Found ${workspaces.length} workspace(s)`);
|
|
1077
|
+
for (const ws of workspaces) {
|
|
1078
|
+
print(`${colors.bold}${ws.workspaceId}${colors.reset} ${ws.name}`);
|
|
1079
|
+
}
|
|
1080
|
+
} else {
|
|
1081
|
+
stopSpinner(false, "Failed");
|
|
1082
|
+
printError(`Error [${response.status}]: ${JSON.stringify(response.data)}`);
|
|
1083
|
+
process.exit(1);
|
|
1084
|
+
}
|
|
1085
|
+
});
|
|
1086
|
+
const members = program2.command("members").description("Manage workspace members");
|
|
1087
|
+
members.command("list").description("List all members in a workspace").requiredOption("-w, --workspace <workspaceId>", "Workspace ID").action(async (options) => {
|
|
1088
|
+
const { workspace: workspaceId } = options;
|
|
1089
|
+
startSpinner("Fetching workspace members...");
|
|
1090
|
+
const response = await httpGet(`/workspaces/${workspaceId}/members`);
|
|
1091
|
+
if (!response.ok) {
|
|
1092
|
+
stopSpinner(false, "Failed to fetch members");
|
|
1093
|
+
printError(`Error [${response.status}]: ${JSON.stringify(response.data)}`);
|
|
1094
|
+
process.exit(1);
|
|
1679
1095
|
}
|
|
1680
|
-
|
|
1681
|
-
|
|
1096
|
+
const { members: members2 } = response.data;
|
|
1097
|
+
if (members2.length === 0) {
|
|
1098
|
+
stopSpinner(true, "No members found.");
|
|
1099
|
+
return;
|
|
1682
1100
|
}
|
|
1683
|
-
|
|
1684
|
-
|
|
1101
|
+
stopSpinner(true, `Found ${members2.length} member(s)`);
|
|
1102
|
+
for (const member of members2) {
|
|
1103
|
+
const name = member.displayName || "-";
|
|
1104
|
+
print(`${colors.bold}${member.email}${colors.reset} ${name} ${member.role}`);
|
|
1685
1105
|
}
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
}
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
const params = new URLSearchParams({ entryFilter: filter.type });
|
|
1694
|
-
if (filter.type === "paths") {
|
|
1695
|
-
for (const path2 of filter.paths) params.append("path", path2);
|
|
1106
|
+
});
|
|
1107
|
+
members.command("remove").description("Remove members from a workspace").requiredOption("-w, --workspace <workspaceId>", "Workspace ID").argument("<emails>", "Emails to remove (comma-separated)").action(async (emailsArg, options) => {
|
|
1108
|
+
const { workspace: workspaceId } = options;
|
|
1109
|
+
const emails = emailsArg.split(",").map((e) => e.trim()).filter(Boolean);
|
|
1110
|
+
if (emails.length === 0) {
|
|
1111
|
+
printError("No valid emails provided");
|
|
1112
|
+
process.exit(1);
|
|
1696
1113
|
}
|
|
1697
|
-
const
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
)
|
|
1707
|
-
|
|
1708
|
-
({
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
)
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
}
|
|
1726
|
-
return { type: "paths", paths: repoPaths };
|
|
1727
|
-
}
|
|
1728
|
-
async function presignDownloads(context, entries) {
|
|
1729
|
-
const client = new SandboxMoxtClient(context);
|
|
1730
|
-
const entriesByRepo = /* @__PURE__ */ new Map();
|
|
1731
|
-
for (const entry of entries) {
|
|
1732
|
-
const existing = entriesByRepo.get(entry.repoId) ?? [];
|
|
1733
|
-
existing.push(entry);
|
|
1734
|
-
entriesByRepo.set(entry.repoId, existing);
|
|
1735
|
-
}
|
|
1736
|
-
const urls = /* @__PURE__ */ new Map();
|
|
1737
|
-
for (const [repoId, repoEntries] of entriesByRepo) {
|
|
1738
|
-
const hashes = [...new Set(repoEntries.map((entry) => entry.sha).filter((sha) => Boolean(sha)))];
|
|
1739
|
-
if (hashes.length === 0) continue;
|
|
1740
|
-
const response = await client.request(
|
|
1741
|
-
"POST",
|
|
1742
|
-
`/agent/api/pipeline-sandbox/mfs/${encodeURIComponent(repoId)}/presign-download`,
|
|
1743
|
-
{ hashes }
|
|
1744
|
-
);
|
|
1745
|
-
for (const hash of hashes) {
|
|
1746
|
-
const url = response.data.urls[hash];
|
|
1747
|
-
if (!url) {
|
|
1748
|
-
throw new SandboxPullError(`No download URL returned for ${hash}`, "remote");
|
|
1114
|
+
const invalidEmails = emails.filter((e) => !isValidEmail(e));
|
|
1115
|
+
if (invalidEmails.length > 0) {
|
|
1116
|
+
for (const email of invalidEmails) {
|
|
1117
|
+
printError(`Invalid email format: ${email}`);
|
|
1118
|
+
}
|
|
1119
|
+
process.exit(1);
|
|
1120
|
+
}
|
|
1121
|
+
startSpinner("Fetching workspace members...");
|
|
1122
|
+
const membersResponse = await httpGet(`/workspaces/${workspaceId}/members`);
|
|
1123
|
+
if (!membersResponse.ok) {
|
|
1124
|
+
stopSpinner(false, "Failed to fetch members");
|
|
1125
|
+
printError(`Error [${membersResponse.status}]: ${JSON.stringify(membersResponse.data)}`);
|
|
1126
|
+
process.exit(1);
|
|
1127
|
+
}
|
|
1128
|
+
stopSpinner(true, "Members fetched");
|
|
1129
|
+
const { members: members2 } = membersResponse.data;
|
|
1130
|
+
const emailToMember = new Map(members2.map((m) => [m.email.toLowerCase(), m]));
|
|
1131
|
+
for (const email of emails) {
|
|
1132
|
+
const member = emailToMember.get(email.toLowerCase());
|
|
1133
|
+
if (!member) {
|
|
1134
|
+
print(`${colors.red}\u2717${colors.reset} ${email} - not found in workspace`);
|
|
1135
|
+
continue;
|
|
1136
|
+
}
|
|
1137
|
+
const deleteResponse = await httpDelete(`/workspaces/${workspaceId}/members/${member.userId}`);
|
|
1138
|
+
if (deleteResponse.ok) {
|
|
1139
|
+
print(`${colors.green}\u2713${colors.reset} ${email} - removed`);
|
|
1140
|
+
} else {
|
|
1141
|
+
print(`${colors.red}\u2717${colors.reset} ${email} - failed: ${JSON.stringify(deleteResponse.data)}`);
|
|
1749
1142
|
}
|
|
1750
|
-
urls.set(hash, url);
|
|
1751
1143
|
}
|
|
1752
|
-
}
|
|
1753
|
-
return urls;
|
|
1754
|
-
}
|
|
1755
|
-
async function downloadBlob(entry, urls) {
|
|
1756
|
-
const hash = entry.sha;
|
|
1757
|
-
if (!hash) {
|
|
1758
|
-
throw new SandboxPullError(`${entry.prefixedPath} does not have downloadable content`, "remote");
|
|
1759
|
-
}
|
|
1760
|
-
const url = urls.get(hash);
|
|
1761
|
-
if (!url) {
|
|
1762
|
-
throw new SandboxPullError(`No download URL returned for ${entry.prefixedPath}`, "remote");
|
|
1763
|
-
}
|
|
1764
|
-
const response = await fetch(url);
|
|
1765
|
-
if (!response.ok) {
|
|
1766
|
-
throw new SandboxPullError(`Download failed [${response.status}]: ${entry.prefixedPath}`, "remote");
|
|
1767
|
-
}
|
|
1768
|
-
const content = Buffer.from(await response.arrayBuffer());
|
|
1769
|
-
if (content.length > MFS_FILE_SIZE_LIMIT_BYTES) {
|
|
1770
|
-
throw new SandboxPullError(`File too large (max ${MFS_FILE_SIZE_LIMIT_LABEL}): ${entry.prefixedPath}`, "too-large");
|
|
1771
|
-
}
|
|
1772
|
-
const contentHash = createHash2("sha256").update(content).digest("hex");
|
|
1773
|
-
if (contentHash !== hash) {
|
|
1774
|
-
throw new SandboxPullError(`Downloaded content hash mismatch: ${entry.prefixedPath}`, "remote");
|
|
1775
|
-
}
|
|
1776
|
-
return content;
|
|
1777
|
-
}
|
|
1778
|
-
async function shouldWritePulledFile(localPath, remoteContentHash, workspacePath, previousFile, force) {
|
|
1779
|
-
const existing = await lstat(localPath).catch((error) => {
|
|
1780
|
-
if (isNodeError2(error) && error.code === "ENOENT") return void 0;
|
|
1781
|
-
throw error;
|
|
1782
1144
|
});
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
if (
|
|
1789
|
-
|
|
1790
|
-
`
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
);
|
|
1802
|
-
}
|
|
1803
|
-
const localChanged = localContentHash !== previousFile.sha;
|
|
1804
|
-
const remoteChanged = remoteContentHash !== previousFile.sha;
|
|
1805
|
-
if (!localChanged && remoteChanged) return true;
|
|
1806
|
-
if (localChanged && !remoteChanged) return false;
|
|
1807
|
-
if (localChanged && remoteChanged) {
|
|
1808
|
-
throw new SandboxPullError(
|
|
1809
|
-
`Local and remote file both changed since checkout: ${workspacePath}`,
|
|
1810
|
-
"file-conflict"
|
|
1145
|
+
const space = program2.command("space").description("Manage spaces");
|
|
1146
|
+
space.command("list").description("List accessible spaces in a workspace").requiredOption("-w, --workspace <workspaceId>", "Workspace ID").action(async (options) => {
|
|
1147
|
+
const { workspace: workspaceId } = options;
|
|
1148
|
+
startSpinner("Fetching spaces...");
|
|
1149
|
+
const response = await httpGet(`/workspaces/${workspaceId}/spaces`);
|
|
1150
|
+
if (!response.ok) {
|
|
1151
|
+
stopSpinner(false, "Failed to fetch spaces");
|
|
1152
|
+
printError(`Error [${response.status}]: ${JSON.stringify(response.data)}`);
|
|
1153
|
+
process.exit(1);
|
|
1154
|
+
}
|
|
1155
|
+
const { spaces } = response.data;
|
|
1156
|
+
if (spaces.length === 0) {
|
|
1157
|
+
stopSpinner(true, "No spaces found.");
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
1160
|
+
stopSpinner(true, `Found ${spaces.length} space(s)`);
|
|
1161
|
+
print(
|
|
1162
|
+
`${colors.bold}TYPE${colors.reset} ${colors.bold}TEAM_SPACE_ID${colors.reset} ${colors.bold}TEAMMATE_ID${colors.reset} ${colors.bold}NAME${colors.reset}`
|
|
1811
1163
|
);
|
|
1812
|
-
|
|
1813
|
-
|
|
1164
|
+
for (const s of spaces) {
|
|
1165
|
+
const teamSpaceId = s.teamSpaceId ?? "";
|
|
1166
|
+
const teammateId = s.agentId != null ? String(s.agentId) : "";
|
|
1167
|
+
print(`${s.type} ${teamSpaceId} ${teammateId} ${s.name}`);
|
|
1168
|
+
}
|
|
1169
|
+
});
|
|
1814
1170
|
}
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
throw new SandboxPullError(
|
|
1836
|
-
`Remote file was deleted but local file changed: ${workspacePath}`,
|
|
1837
|
-
"file-conflict"
|
|
1838
|
-
);
|
|
1171
|
+
|
|
1172
|
+
// src/commands/workflow.ts
|
|
1173
|
+
import * as fs4 from "fs";
|
|
1174
|
+
import { TextDecoder as TextDecoder2 } from "util";
|
|
1175
|
+
var MAX_DESC_LENGTH = 5e3;
|
|
1176
|
+
var MAX_COMMENT_CONTENT_LENGTH = 5e3;
|
|
1177
|
+
var MAX_COMMENT_CONTENT_BYTES = MAX_COMMENT_CONTENT_LENGTH * 4;
|
|
1178
|
+
var WorkflowOptionsError = class extends Error {
|
|
1179
|
+
constructor(message) {
|
|
1180
|
+
super(message);
|
|
1181
|
+
this.name = "WorkflowOptionsError";
|
|
1182
|
+
}
|
|
1183
|
+
};
|
|
1184
|
+
function runOrExit4(fn) {
|
|
1185
|
+
try {
|
|
1186
|
+
return fn();
|
|
1187
|
+
} catch (err) {
|
|
1188
|
+
if (err instanceof WorkflowOptionsError || err instanceof SpaceOptionsError || err instanceof SearchOptionsError) {
|
|
1189
|
+
printError(err.message);
|
|
1190
|
+
process.exit(1);
|
|
1839
1191
|
}
|
|
1840
|
-
|
|
1192
|
+
throw err;
|
|
1841
1193
|
}
|
|
1842
|
-
return deletions;
|
|
1843
1194
|
}
|
|
1844
|
-
|
|
1845
|
-
return
|
|
1195
|
+
function addWorkspaceOption(command) {
|
|
1196
|
+
return command.requiredOption("-w, --workspace <workspaceId>", "Workspace ID");
|
|
1846
1197
|
}
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
if (!stats.isFile()) {
|
|
1850
|
-
throw new SandboxPullError(`Cannot determine local file size: ${workspacePath}`, "directory-conflict");
|
|
1851
|
-
}
|
|
1852
|
-
return stats.size;
|
|
1853
|
-
}
|
|
1854
|
-
function requireBlobSha(entry) {
|
|
1855
|
-
if (!entry.sha) {
|
|
1856
|
-
throw new SandboxPullError(`${entry.prefixedPath} does not have downloadable content`, "remote");
|
|
1857
|
-
}
|
|
1858
|
-
return entry.sha;
|
|
1198
|
+
function addSpaceOptions2(command) {
|
|
1199
|
+
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)");
|
|
1859
1200
|
}
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
if (isNodeError2(error) && error.code === "ENOENT") return void 0;
|
|
1863
|
-
throw error;
|
|
1864
|
-
});
|
|
1865
|
-
if (existing && !existing.isDirectory()) {
|
|
1866
|
-
throw new SandboxPullError(`Refusing to overwrite local path: ${workspacePath}`, "directory-conflict");
|
|
1867
|
-
}
|
|
1201
|
+
function addAssigneeOptions(command) {
|
|
1202
|
+
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");
|
|
1868
1203
|
}
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
await writeFile2(localPath, remoteContent);
|
|
1204
|
+
function addPreferredAssigneeOptions(command) {
|
|
1205
|
+
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");
|
|
1872
1206
|
}
|
|
1873
|
-
function
|
|
1874
|
-
const
|
|
1875
|
-
if (
|
|
1876
|
-
throw new
|
|
1877
|
-
}
|
|
1878
|
-
const resolved = resolve(targetDir, ...pathParts);
|
|
1879
|
-
const relativePath = relative(targetDir, resolved);
|
|
1880
|
-
if (relativePath.startsWith("..") || isAbsolute2(relativePath)) {
|
|
1881
|
-
throw new SandboxPullError(`Remote path escapes target directory: ${repoName}/${repoPath}`, "invalid-path");
|
|
1207
|
+
function parsePositiveInteger(raw, label) {
|
|
1208
|
+
const parsed = Number.parseInt(raw, 10);
|
|
1209
|
+
if (!Number.isInteger(parsed) || parsed <= 0 || String(parsed) !== raw) {
|
|
1210
|
+
throw new WorkflowOptionsError(`Error: ${label} must be a positive integer, got "${raw}".`);
|
|
1882
1211
|
}
|
|
1883
|
-
return
|
|
1212
|
+
return parsed;
|
|
1884
1213
|
}
|
|
1885
|
-
function
|
|
1886
|
-
|
|
1214
|
+
function parseOptionalPositiveInteger(raw, label) {
|
|
1215
|
+
if (raw == null || raw === "") return void 0;
|
|
1216
|
+
return parsePositiveInteger(raw, label);
|
|
1887
1217
|
}
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
try {
|
|
1894
|
-
const invocation = await resolvePullInvocation(context, workspacePath, options);
|
|
1895
|
-
const result = await pullSandboxWorkspace(context, invocation.targetDir, {
|
|
1896
|
-
force: Boolean(options.force),
|
|
1897
|
-
scopes: invocation.scopes
|
|
1898
|
-
});
|
|
1899
|
-
print(`Pulled ${result.fileCount} ${result.fileCount === 1 ? "file" : "files"} to ${result.targetDir}`);
|
|
1900
|
-
} catch (error) {
|
|
1901
|
-
handlePullError(error);
|
|
1902
|
-
}
|
|
1903
|
-
});
|
|
1218
|
+
function parseStatusType(raw) {
|
|
1219
|
+
if (raw == null || raw === "") return void 0;
|
|
1220
|
+
const parsed = Number.parseInt(raw, 10);
|
|
1221
|
+
if ((parsed === 1 || parsed === 2 || parsed === 3) && String(parsed) === raw) return parsed;
|
|
1222
|
+
throw new WorkflowOptionsError("Error: --type must be one of 1 (todo), 2 (in-progress), or 3 (done).");
|
|
1904
1223
|
}
|
|
1905
|
-
|
|
1906
|
-
const
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
throw new Error("Specify either a workspace path or --all, not both");
|
|
1910
|
-
}
|
|
1911
|
-
if (options.scope.length > 0 && (workspacePath || options.all)) {
|
|
1912
|
-
throw new Error("Legacy --scope cannot be combined with a workspace path or --all");
|
|
1224
|
+
function parseRequiredStatusType(raw) {
|
|
1225
|
+
const value = parseStatusType(raw);
|
|
1226
|
+
if (value === void 0) {
|
|
1227
|
+
throw new WorkflowOptionsError("Error: --type is required.");
|
|
1913
1228
|
}
|
|
1914
|
-
|
|
1915
|
-
throw new Error("Specify the local checkout directory only once");
|
|
1916
|
-
}
|
|
1917
|
-
const state = await loadCheckoutState();
|
|
1918
|
-
if (state) validateCheckoutStateContext(state, context);
|
|
1919
|
-
const targetDir = options.dir ?? legacyTargetDir ?? state?.checkoutRoot ?? ".";
|
|
1920
|
-
if (options.all || legacyTargetDir && options.scope.length === 0) {
|
|
1921
|
-
return { targetDir };
|
|
1922
|
-
}
|
|
1923
|
-
if (workspacePath) return { targetDir, scopes: [workspacePath] };
|
|
1924
|
-
if (options.scope.length > 0) return { targetDir, scopes: options.scope };
|
|
1925
|
-
if (!state || state.checkedOutScopes.length === 0) {
|
|
1926
|
-
throw new Error("Checkout is not initialized. Run moxt pull <workspace-path> or moxt pull --all first.");
|
|
1927
|
-
}
|
|
1928
|
-
return {
|
|
1929
|
-
targetDir: state.checkoutRoot,
|
|
1930
|
-
scopes: state.mode === "full" ? void 0 : state.checkedOutScopes
|
|
1931
|
-
};
|
|
1932
|
-
}
|
|
1933
|
-
function isLegacyTargetDirectory(value) {
|
|
1934
|
-
return value === "." || value.startsWith("./") || value.startsWith("../") || isAbsolute3(value);
|
|
1229
|
+
return value;
|
|
1935
1230
|
}
|
|
1936
|
-
function
|
|
1937
|
-
if (
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1231
|
+
function parsePriority(raw) {
|
|
1232
|
+
if (raw == null || raw === "") return void 0;
|
|
1233
|
+
if (raw === "urgent") return 0;
|
|
1234
|
+
if (raw === "high") return 1;
|
|
1235
|
+
if (raw === "medium") return 2;
|
|
1236
|
+
if (raw === "low") return 3;
|
|
1237
|
+
throw new WorkflowOptionsError('Error: --priority must be "urgent", "high", "medium", or "low".');
|
|
1238
|
+
}
|
|
1239
|
+
function parseRequiredPriority(raw) {
|
|
1240
|
+
return parsePriority(raw ?? "medium") ?? 2;
|
|
1241
|
+
}
|
|
1242
|
+
function parseWorkflowScope(raw) {
|
|
1243
|
+
if (raw == null || raw === "") return "all";
|
|
1244
|
+
if (raw === "all" || raw === "owner_is_me") return raw;
|
|
1245
|
+
throw new WorkflowOptionsError('Error: --scope must be "all" or "owner_is_me".');
|
|
1246
|
+
}
|
|
1247
|
+
function parseTaskScope(raw) {
|
|
1248
|
+
if (raw == null || raw === "") return "all";
|
|
1249
|
+
if (raw === "all" || raw === "assigned_to_me" || raw === "subscribed_by_me" || raw === "owner_is_me") {
|
|
1250
|
+
return raw;
|
|
1947
1251
|
}
|
|
1252
|
+
throw new WorkflowOptionsError(
|
|
1253
|
+
'Error: --scope must be "all", "assigned_to_me", "subscribed_by_me", or "owner_is_me".'
|
|
1254
|
+
);
|
|
1948
1255
|
}
|
|
1949
|
-
function
|
|
1950
|
-
return
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
try {
|
|
1955
|
-
context = resolveRuntimeContext();
|
|
1956
|
-
} catch (error) {
|
|
1957
|
-
if (error instanceof RuntimeContextError) {
|
|
1958
|
-
printError(error.message);
|
|
1959
|
-
process.exit(1);
|
|
1960
|
-
}
|
|
1961
|
-
throw error;
|
|
1256
|
+
function parseFileIds(raw, label = "--file-ids") {
|
|
1257
|
+
if (raw == null || raw === "") return void 0;
|
|
1258
|
+
const ids = raw.split(",").map((item) => item.trim()).filter(Boolean);
|
|
1259
|
+
if (ids.length === 0) {
|
|
1260
|
+
throw new WorkflowOptionsError(`Error: ${label} must include at least one file ID.`);
|
|
1962
1261
|
}
|
|
1963
|
-
|
|
1964
|
-
printError("moxt pull is only available in sandbox mode");
|
|
1965
|
-
process.exit(1);
|
|
1966
|
-
}
|
|
1967
|
-
return context;
|
|
1262
|
+
return ids;
|
|
1968
1263
|
}
|
|
1969
|
-
function
|
|
1970
|
-
if (
|
|
1971
|
-
|
|
1972
|
-
|
|
1264
|
+
function readDescFile(path2) {
|
|
1265
|
+
if (path2 == null || path2 === "") return void 0;
|
|
1266
|
+
if (!fs4.existsSync(path2)) {
|
|
1267
|
+
throw new WorkflowOptionsError(`Error: desc file not found: ${path2}`);
|
|
1973
1268
|
}
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
// src/utils/sandbox-push.ts
|
|
1978
|
-
import { createHash as createHash3 } from "crypto";
|
|
1979
|
-
import { lstat as lstat2, readFile as readFile3, readdir } from "fs/promises";
|
|
1980
|
-
import { isAbsolute as isAbsolute4, join as join2, relative as relative2, resolve as resolve2 } from "path";
|
|
1981
|
-
|
|
1982
|
-
// src/utils/concurrency.ts
|
|
1983
|
-
async function runWithConcurrency(items, concurrency, action) {
|
|
1984
|
-
if (!Number.isInteger(concurrency) || concurrency < 1) {
|
|
1985
|
-
throw new RangeError("Concurrency must be a positive integer");
|
|
1269
|
+
const stat = fs4.statSync(path2);
|
|
1270
|
+
if (!stat.isFile()) {
|
|
1271
|
+
throw new WorkflowOptionsError(`Error: desc file is not a file: ${path2}`);
|
|
1986
1272
|
}
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
const item = items[nextIndex];
|
|
1991
|
-
nextIndex += 1;
|
|
1992
|
-
await action(item);
|
|
1993
|
-
}
|
|
1273
|
+
const buffer2 = fs4.readFileSync(path2);
|
|
1274
|
+
if (isBinaryContent(buffer2)) {
|
|
1275
|
+
throw new WorkflowOptionsError("Error: desc file must be UTF-8 text, not binary content.");
|
|
1994
1276
|
}
|
|
1995
|
-
const
|
|
1996
|
-
|
|
1277
|
+
const content = buffer2.toString("utf8");
|
|
1278
|
+
if (content.length > MAX_DESC_LENGTH) {
|
|
1279
|
+
throw new WorkflowOptionsError(`Error: desc file must be at most ${MAX_DESC_LENGTH} characters.`);
|
|
1280
|
+
}
|
|
1281
|
+
return content;
|
|
1997
1282
|
}
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
var MAX_CONCURRENT_UPLOADS = 8;
|
|
2002
|
-
var SandboxPushError = class extends Error {
|
|
2003
|
-
constructor(message, code) {
|
|
2004
|
-
super(message);
|
|
2005
|
-
this.code = code;
|
|
2006
|
-
this.name = "SandboxPushError";
|
|
1283
|
+
function readCommentContentFile2(path2) {
|
|
1284
|
+
if (!fs4.existsSync(path2)) {
|
|
1285
|
+
throw new WorkflowOptionsError(`Error: comment content file not found: ${path2}`);
|
|
2007
1286
|
}
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
const resolvedSourceDir = resolve2(sourceDir);
|
|
2012
|
-
await assertWorkspaceRoot(resolvedSourceDir);
|
|
2013
|
-
const repos = listRepoEntries(context.repoPayload);
|
|
2014
|
-
for (const repo of repos) {
|
|
2015
|
-
assertSafeRepoName(resolvedSourceDir, repo.name);
|
|
2016
|
-
}
|
|
2017
|
-
const state = await resolveCheckoutState(context, resolvedSourceDir, repos);
|
|
2018
|
-
const checkoutRepos = repos.filter(
|
|
2019
|
-
(repo) => state.repos[repo.repoId] && doesCheckoutPathIntersect(state, repo.name)
|
|
2020
|
-
);
|
|
2021
|
-
if (checkoutRepos.length === 0) {
|
|
2022
|
-
throw new SandboxPushError("Checkout state does not contain a pushable scope", "invalid-path");
|
|
2023
|
-
}
|
|
2024
|
-
const tree = await fetchWorkspaceTree2(context, checkoutRepos);
|
|
2025
|
-
const remoteEntries = indexRemoteEntries(tree, checkoutRepos);
|
|
2026
|
-
const localFiles = await collectLocalFiles(resolvedSourceDir, checkoutRepos, remoteEntries, state);
|
|
2027
|
-
const { changes, reconciledFiles, removedWorkspacePaths } = planChanges(
|
|
2028
|
-
localFiles,
|
|
2029
|
-
remoteEntries,
|
|
2030
|
-
state,
|
|
2031
|
-
checkoutRepos
|
|
2032
|
-
);
|
|
2033
|
-
let pushResponse;
|
|
2034
|
-
if (changes.length > 0) {
|
|
2035
|
-
await uploadChangedFiles(context, changes);
|
|
2036
|
-
pushResponse = await pushChanges(context, checkoutRepos, changes, options.message);
|
|
1287
|
+
const stat = fs4.statSync(path2);
|
|
1288
|
+
if (!stat.isFile()) {
|
|
1289
|
+
throw new WorkflowOptionsError(`Error: comment content path is not a file: ${path2}`);
|
|
2037
1290
|
}
|
|
2038
|
-
if (
|
|
2039
|
-
|
|
1291
|
+
if (stat.size > MAX_COMMENT_CONTENT_BYTES) {
|
|
1292
|
+
throw new WorkflowOptionsError(
|
|
1293
|
+
`Error: comment content file must be at most ${MAX_COMMENT_CONTENT_LENGTH} characters.`
|
|
1294
|
+
);
|
|
2040
1295
|
}
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
};
|
|
2045
|
-
}
|
|
2046
|
-
async function resolveCheckoutState(context, sourceDir, repos) {
|
|
2047
|
-
const state = await loadCheckoutState();
|
|
2048
|
-
if (!state) {
|
|
2049
|
-
throw new SandboxPushError("Checkout is not initialized. Run moxt pull first.", "invalid-path");
|
|
1296
|
+
const buffer2 = fs4.readFileSync(path2);
|
|
1297
|
+
if (buffer2.includes(0)) {
|
|
1298
|
+
throw new WorkflowOptionsError("Error: comment content file must not contain NUL bytes.");
|
|
2050
1299
|
}
|
|
2051
|
-
if (
|
|
2052
|
-
throw new
|
|
2053
|
-
`Checkout state belongs to workspace ${state.workspaceId}, not ${context.workspaceId}`,
|
|
2054
|
-
"invalid-path"
|
|
2055
|
-
);
|
|
1300
|
+
if (isBinaryContent(buffer2)) {
|
|
1301
|
+
throw new WorkflowOptionsError("Error: comment content file must be UTF-8 text, not binary content.");
|
|
2056
1302
|
}
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
);
|
|
1303
|
+
let content;
|
|
1304
|
+
try {
|
|
1305
|
+
content = new TextDecoder2("utf-8", { fatal: true, ignoreBOM: true }).decode(buffer2);
|
|
1306
|
+
} catch {
|
|
1307
|
+
throw new WorkflowOptionsError("Error: comment content file must contain valid UTF-8 text.");
|
|
2062
1308
|
}
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
if (currentRepos.get(repoId) !== repo.name) {
|
|
2066
|
-
throw new SandboxPushError(`Checkout state contains unknown repo: ${repoId}`, "invalid-path");
|
|
2067
|
-
}
|
|
1309
|
+
if (content.length === 0) {
|
|
1310
|
+
throw new WorkflowOptionsError("Error: comment content file must not be empty.");
|
|
2068
1311
|
}
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
1312
|
+
if (content.length > MAX_COMMENT_CONTENT_LENGTH) {
|
|
1313
|
+
throw new WorkflowOptionsError(
|
|
1314
|
+
`Error: comment content file must be at most ${MAX_COMMENT_CONTENT_LENGTH} characters.`
|
|
2072
1315
|
);
|
|
2073
|
-
if (!hasRepo) throw new SandboxPushError(`Checkout state contains invalid scope: ${scope}`, "invalid-path");
|
|
2074
1316
|
}
|
|
2075
|
-
return
|
|
1317
|
+
return content;
|
|
2076
1318
|
}
|
|
2077
|
-
|
|
2078
|
-
const
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
1319
|
+
function parseAssigneeBody(options, allowClear) {
|
|
1320
|
+
const hasClear = options.clear === true;
|
|
1321
|
+
const humanId = parseOptionalPositiveInteger(options.humanId, "--human-id");
|
|
1322
|
+
const assistantId = parseOptionalPositiveInteger(options.assistantId, "--assistant-id");
|
|
1323
|
+
const teammateId = parseOptionalPositiveInteger(options.teammateId, "--teammate-id");
|
|
1324
|
+
const count = (humanId !== void 0 ? 1 : 0) + (assistantId !== void 0 ? 1 : 0) + (teammateId !== void 0 ? 1 : 0);
|
|
1325
|
+
if (hasClear && !allowClear) {
|
|
1326
|
+
throw new WorkflowOptionsError("Error: --clear is not supported here.");
|
|
1327
|
+
}
|
|
1328
|
+
if (hasClear && count > 0) {
|
|
1329
|
+
throw new WorkflowOptionsError("Error: --clear cannot be used with assignee ID options.");
|
|
1330
|
+
}
|
|
1331
|
+
if (count > 1) {
|
|
1332
|
+
throw new WorkflowOptionsError("Error: assignee ID options are mutually exclusive.");
|
|
1333
|
+
}
|
|
1334
|
+
if (hasClear) return {};
|
|
1335
|
+
if (humanId !== void 0) return { assigneeHumanId: humanId };
|
|
1336
|
+
if (assistantId !== void 0) return { assigneeAiAssistantId: assistantId };
|
|
1337
|
+
if (teammateId !== void 0) return { assigneeAiTeammateId: teammateId };
|
|
1338
|
+
return {};
|
|
2087
1339
|
}
|
|
2088
|
-
|
|
2089
|
-
const
|
|
2090
|
-
const
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
}
|
|
2106
|
-
const key = remoteEntryKey(entry.repoId, entry.path);
|
|
2107
|
-
if (entries.has(key)) {
|
|
2108
|
-
throw new SandboxPushError(`Remote tree returned duplicate path: ${entry.prefixedPath}`, "remote");
|
|
2109
|
-
}
|
|
2110
|
-
entries.set(key, entry);
|
|
2111
|
-
}
|
|
2112
|
-
return entries;
|
|
2113
|
-
}
|
|
2114
|
-
async function collectLocalFiles(sourceDir, repos, remoteEntries, state) {
|
|
2115
|
-
const files = [];
|
|
2116
|
-
for (const repo of repos) {
|
|
2117
|
-
const repoRoot = resolveSafeRepoRoot(sourceDir, repo.name);
|
|
2118
|
-
const stats = await lstat2(repoRoot).catch((error) => {
|
|
2119
|
-
if (isNodeError3(error) && error.code === "ENOENT") return void 0;
|
|
2120
|
-
throw error;
|
|
2121
|
-
});
|
|
2122
|
-
if (!stats) continue;
|
|
2123
|
-
if (!stats.isDirectory()) {
|
|
2124
|
-
throw new SandboxPushError(`Local repo path is not a directory: ${repo.name}`, "invalid-path");
|
|
2125
|
-
}
|
|
2126
|
-
await collectRepoFiles(repoRoot, repo, "", remoteEntries, state, files);
|
|
2127
|
-
}
|
|
2128
|
-
return files;
|
|
2129
|
-
}
|
|
2130
|
-
async function collectRepoFiles(repoRoot, repo, relativeDir, remoteEntries, state, files) {
|
|
2131
|
-
const localDir = relativeDir ? join2(repoRoot, ...relativeDir.split("/")) : repoRoot;
|
|
2132
|
-
const entries = await readdir(localDir, { withFileTypes: true });
|
|
2133
|
-
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
2134
|
-
for (const entry of entries) {
|
|
2135
|
-
if (entry.name === ".git") continue;
|
|
2136
|
-
const repoPath = relativeDir ? `${relativeDir}/${entry.name}` : entry.name;
|
|
2137
|
-
const workspacePath = `${repo.name}/${repoPath}`;
|
|
2138
|
-
assertSafePathPart(entry.name, workspacePath);
|
|
2139
|
-
if (!doesCheckoutPathIntersect(state, workspacePath)) continue;
|
|
2140
|
-
if (entry.isSymbolicLink()) {
|
|
2141
|
-
throw new SandboxPushError(`Refusing to push symbolic link: ${workspacePath}`, "invalid-file");
|
|
2142
|
-
}
|
|
2143
|
-
if (entry.isDirectory()) {
|
|
2144
|
-
const remote = remoteEntries.get(remoteEntryKey(repo.repoId, repoPath));
|
|
2145
|
-
if (remote?.type === "blob") {
|
|
2146
|
-
throw new SandboxPushError(`Local directory conflicts with remote file: ${workspacePath}`, "invalid-file");
|
|
2147
|
-
}
|
|
2148
|
-
await collectRepoFiles(repoRoot, repo, repoPath, remoteEntries, state, files);
|
|
2149
|
-
continue;
|
|
2150
|
-
}
|
|
2151
|
-
if (!entry.isFile()) {
|
|
2152
|
-
throw new SandboxPushError(`Refusing to push unsupported file: ${workspacePath}`, "invalid-file");
|
|
2153
|
-
}
|
|
2154
|
-
const localPath = join2(repoRoot, ...repoPath.split("/"));
|
|
2155
|
-
const stats = await lstat2(localPath);
|
|
2156
|
-
if (!stats.isFile()) {
|
|
2157
|
-
const kind = stats.isSymbolicLink() ? "symbolic link" : "unsupported file";
|
|
2158
|
-
throw new SandboxPushError(`Refusing to push ${kind}: ${workspacePath}`, "invalid-file");
|
|
2159
|
-
}
|
|
2160
|
-
if (stats.size > MFS_FILE_SIZE_LIMIT_BYTES) {
|
|
2161
|
-
throw new SandboxPushError(
|
|
2162
|
-
`File too large (max ${MFS_FILE_SIZE_LIMIT_LABEL}): ${workspacePath}`,
|
|
2163
|
-
"too-large"
|
|
2164
|
-
);
|
|
2165
|
-
}
|
|
2166
|
-
const content = await readFile3(localPath);
|
|
2167
|
-
if (content.length > MFS_FILE_SIZE_LIMIT_BYTES) {
|
|
2168
|
-
throw new SandboxPushError(
|
|
2169
|
-
`File too large (max ${MFS_FILE_SIZE_LIMIT_LABEL}): ${workspacePath}`,
|
|
2170
|
-
"too-large"
|
|
2171
|
-
);
|
|
2172
|
-
}
|
|
2173
|
-
if (!isCheckoutPathIncluded(state, workspacePath)) continue;
|
|
2174
|
-
files.push({
|
|
2175
|
-
repo,
|
|
2176
|
-
repoPath,
|
|
2177
|
-
workspacePath,
|
|
2178
|
-
localPath,
|
|
2179
|
-
contentHash: createHash3("sha256").update(content).digest("hex"),
|
|
2180
|
-
size: content.length
|
|
2181
|
-
});
|
|
2182
|
-
}
|
|
1340
|
+
function parsePreferredAssigneeBody(options) {
|
|
1341
|
+
const hasClear = options.clearPreferredAssignee === true;
|
|
1342
|
+
const humanId = parseOptionalPositiveInteger(options.preferredHumanId, "--preferred-human-id");
|
|
1343
|
+
const assistantId = parseOptionalPositiveInteger(options.preferredAssistantId, "--preferred-assistant-id");
|
|
1344
|
+
const teammateId = parseOptionalPositiveInteger(options.preferredTeammateId, "--preferred-teammate-id");
|
|
1345
|
+
const count = (humanId !== void 0 ? 1 : 0) + (assistantId !== void 0 ? 1 : 0) + (teammateId !== void 0 ? 1 : 0);
|
|
1346
|
+
if (hasClear && count > 0) {
|
|
1347
|
+
throw new WorkflowOptionsError("Error: --clear-preferred-assignee cannot be used with preferred assignee ID options.");
|
|
1348
|
+
}
|
|
1349
|
+
if (count > 1) {
|
|
1350
|
+
throw new WorkflowOptionsError("Error: preferred assignee ID options are mutually exclusive.");
|
|
1351
|
+
}
|
|
1352
|
+
if (hasClear) return { preferredAssigneeHumanId: null };
|
|
1353
|
+
if (humanId !== void 0) return { preferredAssigneeHumanId: humanId };
|
|
1354
|
+
if (assistantId !== void 0) return { preferredAssigneeAiAssistantId: assistantId };
|
|
1355
|
+
if (teammateId !== void 0) return { preferredAssigneeAiTeammateId: teammateId };
|
|
1356
|
+
return {};
|
|
2183
1357
|
}
|
|
2184
|
-
function
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
const removedWorkspacePaths = [];
|
|
2188
|
-
const localWorkspacePaths = new Set(localFiles.map((file) => file.workspacePath));
|
|
2189
|
-
const reposById = new Map(repos.map((repo) => [repo.repoId, repo]));
|
|
2190
|
-
for (const file of localFiles) {
|
|
2191
|
-
const baseline = state.files[file.workspacePath];
|
|
2192
|
-
if (baseline?.sha === file.contentHash) continue;
|
|
2193
|
-
const existing = remoteEntries.get(remoteEntryKey(file.repo.repoId, file.repoPath));
|
|
2194
|
-
if (existing?.type === "tree") {
|
|
2195
|
-
throw new SandboxPushError(`Local file conflicts with remote directory: ${file.workspacePath}`, "invalid-file");
|
|
2196
|
-
}
|
|
2197
|
-
if (existing?.sha === file.contentHash) {
|
|
2198
|
-
reconciledFiles.push({ ...file, remote: existing });
|
|
2199
|
-
continue;
|
|
2200
|
-
}
|
|
2201
|
-
if (existing && !existing.sha) {
|
|
2202
|
-
throw new SandboxPushError(`${file.workspacePath} does not have a base content hash`, "remote");
|
|
2203
|
-
}
|
|
2204
|
-
if (baseline && existing?.sha !== baseline.sha) {
|
|
2205
|
-
throw new SandboxPushError(`Remote file changed since checkout: ${file.workspacePath}`, "conflict");
|
|
2206
|
-
}
|
|
2207
|
-
if (!baseline && existing) {
|
|
2208
|
-
throw new SandboxPushError(`Remote file changed since checkout: ${file.workspacePath}`, "conflict");
|
|
2209
|
-
}
|
|
2210
|
-
changes.push({
|
|
2211
|
-
...file,
|
|
2212
|
-
action: existing ? "update" : "create",
|
|
2213
|
-
...existing ? { existing } : {}
|
|
2214
|
-
});
|
|
2215
|
-
}
|
|
2216
|
-
for (const [workspacePath, baseline] of Object.entries(state.files)) {
|
|
2217
|
-
if (localWorkspacePaths.has(workspacePath) || !isCheckoutPathIncluded(state, workspacePath)) continue;
|
|
2218
|
-
const repo = reposById.get(baseline.repoId);
|
|
2219
|
-
if (!repo) continue;
|
|
2220
|
-
const existing = remoteEntries.get(remoteEntryKey(baseline.repoId, baseline.repoPath));
|
|
2221
|
-
if (!existing) {
|
|
2222
|
-
removedWorkspacePaths.push(workspacePath);
|
|
2223
|
-
continue;
|
|
2224
|
-
}
|
|
2225
|
-
if (existing.type !== "blob" || existing.sha !== baseline.sha) {
|
|
2226
|
-
throw new SandboxPushError(`Remote file changed since checkout: ${workspacePath}`, "conflict");
|
|
2227
|
-
}
|
|
2228
|
-
changes.push({
|
|
2229
|
-
action: "delete",
|
|
2230
|
-
repo,
|
|
2231
|
-
repoPath: baseline.repoPath,
|
|
2232
|
-
workspacePath,
|
|
2233
|
-
fileId: validFileId(existing.fileId) ?? baseline.fileId
|
|
2234
|
-
});
|
|
1358
|
+
function assertNonEmptyBody(body) {
|
|
1359
|
+
if (Object.keys(body).length === 0) {
|
|
1360
|
+
throw new WorkflowOptionsError("Error: at least one update option must be provided.");
|
|
2235
1361
|
}
|
|
2236
|
-
return { changes, reconciledFiles, removedWorkspacePaths };
|
|
2237
|
-
}
|
|
2238
|
-
async function uploadChangedFiles(context, changes) {
|
|
2239
|
-
const writeChanges = changes.filter(isWriteChange);
|
|
2240
|
-
if (writeChanges.length === 0) return;
|
|
2241
|
-
const client = new SandboxMoxtClient(context);
|
|
2242
|
-
const changesByRepo = groupChangesByRepo(writeChanges);
|
|
2243
|
-
const uploadUrlsByRepo = await Promise.all(
|
|
2244
|
-
[...changesByRepo].map(async ([repoId, repoChanges]) => {
|
|
2245
|
-
const hashes = [...new Set(repoChanges.map((change) => change.contentHash))];
|
|
2246
|
-
const response = await client.request(
|
|
2247
|
-
"POST",
|
|
2248
|
-
`/agent/api/pipeline-sandbox/mfs/${encodeURIComponent(repoId)}/presign-upload`,
|
|
2249
|
-
{ hashes }
|
|
2250
|
-
);
|
|
2251
|
-
return [repoId, response.data.urls];
|
|
2252
|
-
})
|
|
2253
|
-
);
|
|
2254
|
-
const uploadUrls = new Map(uploadUrlsByRepo);
|
|
2255
|
-
await runWithConcurrency(writeChanges, MAX_CONCURRENT_UPLOADS, async (change) => {
|
|
2256
|
-
const uploadUrl = uploadUrls.get(change.repo.repoId)?.[change.contentHash];
|
|
2257
|
-
if (!uploadUrl) {
|
|
2258
|
-
throw new SandboxPushError(`No upload URL returned for ${change.workspacePath}`, "remote");
|
|
2259
|
-
}
|
|
2260
|
-
const content = await readFile3(change.localPath);
|
|
2261
|
-
const currentHash = createHash3("sha256").update(content).digest("hex");
|
|
2262
|
-
if (content.length !== change.size || currentHash !== change.contentHash) {
|
|
2263
|
-
throw new SandboxPushError(`Local file changed while pushing: ${change.workspacePath}`, "invalid-file");
|
|
2264
|
-
}
|
|
2265
|
-
const upload = await fetch(uploadUrl, {
|
|
2266
|
-
method: "PUT",
|
|
2267
|
-
headers: { "Content-Type": "application/octet-stream" },
|
|
2268
|
-
body: new Uint8Array(content)
|
|
2269
|
-
});
|
|
2270
|
-
if (!upload.ok) {
|
|
2271
|
-
throw new SandboxPushError(`Upload failed [${upload.status}]: ${change.workspacePath}`, "remote");
|
|
2272
|
-
}
|
|
2273
|
-
});
|
|
2274
1362
|
}
|
|
2275
|
-
|
|
2276
|
-
const
|
|
2277
|
-
const
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
"/agent/api/pipeline-sandbox/mfs-workspace/unified-push",
|
|
2281
|
-
{
|
|
2282
|
-
message,
|
|
2283
|
-
repoPushes: repos.flatMap((repo) => {
|
|
2284
|
-
const repoChanges = changesByRepo.get(repo.repoId);
|
|
2285
|
-
if (!repoChanges) return [];
|
|
2286
|
-
return [{
|
|
2287
|
-
repoId: repo.repoId,
|
|
2288
|
-
changes: repoChanges.map((change) => {
|
|
2289
|
-
if (change.action === "delete") {
|
|
2290
|
-
return {
|
|
2291
|
-
action: change.action,
|
|
2292
|
-
...change.fileId ? { fileId: change.fileId } : {},
|
|
2293
|
-
path: change.repoPath
|
|
2294
|
-
};
|
|
2295
|
-
}
|
|
2296
|
-
return {
|
|
2297
|
-
action: change.action,
|
|
2298
|
-
...change.existing?.fileId ? { fileId: change.existing.fileId } : {},
|
|
2299
|
-
path: change.repoPath,
|
|
2300
|
-
contentHash: change.contentHash,
|
|
2301
|
-
...change.existing?.sha ? { baseContentHash: change.existing.sha } : {},
|
|
2302
|
-
size: change.size,
|
|
2303
|
-
fileType: MFS_FILE_TYPE_REGULAR2
|
|
2304
|
-
};
|
|
2305
|
-
})
|
|
2306
|
-
}];
|
|
2307
|
-
}),
|
|
2308
|
-
crossRepoMoves: []
|
|
2309
|
-
}
|
|
2310
|
-
);
|
|
2311
|
-
return response.data;
|
|
2312
|
-
}
|
|
2313
|
-
async function updateCheckoutState(state, changes, reconciledFiles, removedWorkspacePaths, response) {
|
|
2314
|
-
const files = { ...state.files };
|
|
2315
|
-
for (const workspacePath of removedWorkspacePaths) delete files[workspacePath];
|
|
2316
|
-
for (const file of reconciledFiles) {
|
|
2317
|
-
files[file.workspacePath] = checkoutStateFile(
|
|
2318
|
-
file,
|
|
2319
|
-
validFileId(file.remote.fileId),
|
|
2320
|
-
validFileType(file.remote.fileType)
|
|
2321
|
-
);
|
|
2322
|
-
}
|
|
2323
|
-
for (const change of changes) {
|
|
2324
|
-
if (change.action === "delete") {
|
|
2325
|
-
delete files[change.workspacePath];
|
|
2326
|
-
continue;
|
|
1363
|
+
function buildQuery(params) {
|
|
1364
|
+
const query = new URLSearchParams();
|
|
1365
|
+
for (const [key, value] of Object.entries(params)) {
|
|
1366
|
+
if (value !== void 0) {
|
|
1367
|
+
query.set(key, String(value));
|
|
2327
1368
|
}
|
|
2328
|
-
const returnedFileId = validFileId(response?.fileMap?.[change.repo.repoId]?.[change.repoPath]);
|
|
2329
|
-
const fileId = returnedFileId ?? change.existing?.fileId ?? state.files[change.workspacePath]?.fileId ?? null;
|
|
2330
|
-
files[change.workspacePath] = checkoutStateFile(change, validFileId(fileId), validFileType(change.existing?.fileType));
|
|
2331
1369
|
}
|
|
2332
|
-
|
|
2333
|
-
}
|
|
2334
|
-
function isWriteChange(change) {
|
|
2335
|
-
return change.action !== "delete";
|
|
2336
|
-
}
|
|
2337
|
-
function checkoutStateFile(file, fileId, fileType) {
|
|
2338
|
-
return {
|
|
2339
|
-
repoId: file.repo.repoId,
|
|
2340
|
-
repoPath: file.repoPath,
|
|
2341
|
-
fileId,
|
|
2342
|
-
sha: file.contentHash,
|
|
2343
|
-
size: file.size,
|
|
2344
|
-
fileType: fileType ?? MFS_FILE_TYPE_REGULAR2
|
|
2345
|
-
};
|
|
1370
|
+
const text = query.toString();
|
|
1371
|
+
return text ? `?${text}` : "";
|
|
2346
1372
|
}
|
|
2347
|
-
function
|
|
2348
|
-
|
|
1373
|
+
function failResponse(response) {
|
|
1374
|
+
printError(`Error [${response.status}]: ${JSON.stringify(response.data)}`);
|
|
1375
|
+
process.exit(1);
|
|
2349
1376
|
}
|
|
2350
|
-
function
|
|
2351
|
-
|
|
1377
|
+
function priorityLabel(value) {
|
|
1378
|
+
switch (value) {
|
|
1379
|
+
case 0:
|
|
1380
|
+
return "urgent";
|
|
1381
|
+
case 1:
|
|
1382
|
+
return "high";
|
|
1383
|
+
case 2:
|
|
1384
|
+
return "medium";
|
|
1385
|
+
case 3:
|
|
1386
|
+
return "low";
|
|
1387
|
+
default: {
|
|
1388
|
+
const unsupportedValue = value;
|
|
1389
|
+
throw new Error(`Unsupported workflow task priority: ${String(unsupportedValue)}`);
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
function statusTypeLabel(value) {
|
|
1394
|
+
switch (value) {
|
|
1395
|
+
case 1:
|
|
1396
|
+
return "todo";
|
|
1397
|
+
case 2:
|
|
1398
|
+
return "in-progress";
|
|
1399
|
+
case 3:
|
|
1400
|
+
return "done";
|
|
1401
|
+
default: {
|
|
1402
|
+
const unsupportedValue = value;
|
|
1403
|
+
throw new Error(`Unsupported workflow status type: ${String(unsupportedValue)}`);
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
function formatAssignee(assignee) {
|
|
1408
|
+
if (!assignee) return "-";
|
|
1409
|
+
if (assignee.type === "human") return assignee.displayName ?? `human:${assignee.humanId}`;
|
|
1410
|
+
if (assignee.type === "assistant") return assignee.displayName ?? `assistant:${assignee.aiAssistantId}`;
|
|
1411
|
+
if (assignee.type === "teammate") return assignee.displayName ?? `teammate:${assignee.aiTeammateId}`;
|
|
1412
|
+
return "unknown";
|
|
1413
|
+
}
|
|
1414
|
+
function printWorkflow(item) {
|
|
1415
|
+
print(`${colors.bold}${item.fileId}${colors.reset} owner=${item.ownerHumanId ?? "-"} desc=${item.desc ?? "-"}`);
|
|
1416
|
+
}
|
|
1417
|
+
function indentation2(size) {
|
|
1418
|
+
return " ".repeat(size);
|
|
1419
|
+
}
|
|
1420
|
+
function escapeTerminalControlCharacters2(value) {
|
|
1421
|
+
return value.replace(/\r\n/g, "\n").replace(
|
|
1422
|
+
/[\u0000-\u0008\u000B-\u001F\u007F-\u009F]/g,
|
|
1423
|
+
(character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`
|
|
1424
|
+
);
|
|
2352
1425
|
}
|
|
2353
|
-
function
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
const
|
|
2357
|
-
|
|
2358
|
-
|
|
1426
|
+
function formatDetailScalar2(value) {
|
|
1427
|
+
if (value === null) return "null";
|
|
1428
|
+
if (typeof value === "string") {
|
|
1429
|
+
const escaped = escapeTerminalControlCharacters2(value);
|
|
1430
|
+
if (escaped === "") return '""';
|
|
1431
|
+
if (/\n/.test(escaped)) return JSON.stringify(escaped);
|
|
1432
|
+
return escaped;
|
|
2359
1433
|
}
|
|
2360
|
-
return
|
|
1434
|
+
return String(value);
|
|
2361
1435
|
}
|
|
2362
|
-
function
|
|
2363
|
-
|
|
1436
|
+
function printDetailField2(name, value, indent = 0) {
|
|
1437
|
+
print(`${indentation2(indent)}${name}: ${formatDetailScalar2(value)}`);
|
|
2364
1438
|
}
|
|
2365
|
-
function
|
|
2366
|
-
if (
|
|
2367
|
-
|
|
1439
|
+
function printDetailTextField2(name, value, indent = 0) {
|
|
1440
|
+
if (value === null || value === "") {
|
|
1441
|
+
printDetailField2(name, value, indent);
|
|
1442
|
+
return;
|
|
2368
1443
|
}
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
}
|
|
2373
|
-
function resolveSafeRepoRoot(sourceDir, repoName) {
|
|
2374
|
-
assertSafePathPart(repoName, repoName);
|
|
2375
|
-
const repoRoot = resolve2(sourceDir, repoName);
|
|
2376
|
-
const relativePath = relative2(sourceDir, repoRoot);
|
|
2377
|
-
if (relativePath.startsWith("..") || isAbsolute4(relativePath)) {
|
|
2378
|
-
throw new SandboxPushError(`Repo path escapes local workspace: ${repoName}`, "invalid-path");
|
|
1444
|
+
const escaped = escapeTerminalControlCharacters2(value);
|
|
1445
|
+
print(`${indentation2(indent)}${name}:`);
|
|
1446
|
+
for (const line of escaped.split("\n")) {
|
|
1447
|
+
print(`${indentation2(indent + 2)}${line}`);
|
|
2379
1448
|
}
|
|
2380
|
-
return repoRoot;
|
|
2381
1449
|
}
|
|
2382
|
-
function
|
|
2383
|
-
|
|
1450
|
+
function printAssigneeDetail(name, assignee, indent = 0) {
|
|
1451
|
+
if (assignee === null) {
|
|
1452
|
+
printDetailField2(name, null, indent);
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
print(`${indentation2(indent)}${name}:`);
|
|
1456
|
+
printDetailField2("type", assignee.type, indent + 2);
|
|
1457
|
+
switch (assignee.type) {
|
|
1458
|
+
case "human":
|
|
1459
|
+
printDetailField2("humanId", assignee.humanId, indent + 2);
|
|
1460
|
+
printDetailField2("displayName", assignee.displayName, indent + 2);
|
|
1461
|
+
printDetailField2("avatarUrl", assignee.avatarUrl, indent + 2);
|
|
1462
|
+
return;
|
|
1463
|
+
case "assistant":
|
|
1464
|
+
printDetailField2("aiAssistantId", assignee.aiAssistantId, indent + 2);
|
|
1465
|
+
printDetailField2("displayName", assignee.displayName, indent + 2);
|
|
1466
|
+
printDetailField2("avatarUrl", assignee.avatarUrl, indent + 2);
|
|
1467
|
+
return;
|
|
1468
|
+
case "teammate":
|
|
1469
|
+
printDetailField2("aiTeammateId", assignee.aiTeammateId, indent + 2);
|
|
1470
|
+
printDetailField2("displayName", assignee.displayName, indent + 2);
|
|
1471
|
+
printDetailField2("avatarUrl", assignee.avatarUrl, indent + 2);
|
|
1472
|
+
return;
|
|
1473
|
+
case "unknown":
|
|
1474
|
+
printDetailField2("humanId", assignee.humanId, indent + 2);
|
|
1475
|
+
printDetailField2("aiAssistantId", assignee.aiAssistantId, indent + 2);
|
|
1476
|
+
printDetailField2("aiTeammateId", assignee.aiTeammateId, indent + 2);
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
function printStatusRemainingFields(item, indent) {
|
|
1480
|
+
printDetailField2("name", item.name, indent);
|
|
1481
|
+
printDetailTextField2("desc", item.desc, indent);
|
|
1482
|
+
printDetailField2("type", `${item.type} (${statusTypeLabel(item.type)})`, indent);
|
|
1483
|
+
printAssigneeDetail("preferredAssignee", item.preferredAssignee, indent);
|
|
1484
|
+
printDetailField2("order", item.order, indent);
|
|
1485
|
+
}
|
|
1486
|
+
function printStatusDetail(item) {
|
|
1487
|
+
print(`${colors.bold}Status${colors.reset}`);
|
|
1488
|
+
printDetailField2("scopedId", item.scopedId);
|
|
1489
|
+
printStatusRemainingFields(item, 0);
|
|
1490
|
+
}
|
|
1491
|
+
function printWorkflowDetail(item) {
|
|
1492
|
+
print(`${colors.bold}Workflow${colors.reset}`);
|
|
1493
|
+
printDetailField2("fileId", item.fileId);
|
|
1494
|
+
printDetailField2("ownerHumanId", item.ownerHumanId);
|
|
1495
|
+
printDetailTextField2("desc", item.desc);
|
|
1496
|
+
if (item.statusList.length === 0) {
|
|
1497
|
+
print("statusList: []");
|
|
1498
|
+
} else {
|
|
1499
|
+
print("statusList:");
|
|
1500
|
+
for (const status of item.statusList) {
|
|
1501
|
+
print(` - scopedId: ${status.scopedId}`);
|
|
1502
|
+
printStatusRemainingFields(status, 4);
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
if (item.transitions.length === 0) {
|
|
1506
|
+
print("transitions: []");
|
|
1507
|
+
} else {
|
|
1508
|
+
print("transitions:");
|
|
1509
|
+
for (const transition of item.transitions) {
|
|
1510
|
+
print(` - statusFromScopedId: ${transition.statusFromScopedId}`);
|
|
1511
|
+
printDetailField2("statusToScopedId", transition.statusToScopedId, 4);
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
function printTaskSummary(item) {
|
|
1516
|
+
const runIds = item.agentRuns.map((run) => run.pipelineTriggerId).join(",") || "-";
|
|
1517
|
+
print(
|
|
1518
|
+
`${colors.bold}#${item.scopedId}${colors.reset} ${item.title} status=${item.statusScopedId} priority=${priorityLabel(item.priority)} assignee=${formatAssignee(item.assignee)} runs=${runIds}`
|
|
1519
|
+
);
|
|
2384
1520
|
}
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
1521
|
+
function printTaskDetail(item) {
|
|
1522
|
+
print(`${colors.bold}Task${colors.reset}`);
|
|
1523
|
+
printDetailField2("scopedId", item.scopedId);
|
|
1524
|
+
printDetailField2("workflowFileId", item.workflowFileId);
|
|
1525
|
+
printDetailField2("ownerHumanId", item.ownerHumanId);
|
|
1526
|
+
printDetailField2("title", item.title);
|
|
1527
|
+
printDetailTextField2("desc", item.desc);
|
|
1528
|
+
printDetailField2("statusScopedId", item.statusScopedId);
|
|
1529
|
+
printDetailField2("priority", `${item.priority} (${priorityLabel(item.priority)})`);
|
|
1530
|
+
printAssigneeDetail("assignee", item.assignee);
|
|
1531
|
+
if (item.linkedFileIds.length === 0) {
|
|
1532
|
+
print("linkedFileIds: []");
|
|
1533
|
+
} else {
|
|
1534
|
+
print("linkedFileIds:");
|
|
1535
|
+
for (const fileId of item.linkedFileIds) print(` - ${fileId}`);
|
|
1536
|
+
}
|
|
1537
|
+
printDetailField2("subscribed", item.subscribed);
|
|
1538
|
+
if (item.agentRuns.length === 0) {
|
|
1539
|
+
print("agentRuns: []");
|
|
1540
|
+
} else {
|
|
1541
|
+
print("agentRuns:");
|
|
1542
|
+
for (const run of item.agentRuns) {
|
|
1543
|
+
print(` - pipelineTriggerId: ${run.pipelineTriggerId}`);
|
|
1544
|
+
printAssigneeDetail("executor", run.executor, 4);
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
printDetailField2("createdAt", item.createdAt);
|
|
1548
|
+
printDetailField2("latestActAt", item.latestActAt);
|
|
1549
|
+
}
|
|
1550
|
+
function printCommentDetail(item) {
|
|
1551
|
+
print(`${colors.bold}Comment${colors.reset}`);
|
|
1552
|
+
printDetailField2("id", item.id);
|
|
1553
|
+
printDetailField2("workflowFileId", item.workflowFileId);
|
|
1554
|
+
printDetailField2("workflowTaskScopedId", item.workflowTaskScopedId);
|
|
1555
|
+
printDetailTextField2("content", item.content);
|
|
1556
|
+
printAssigneeDetail("createdBy", item.createdBy);
|
|
1557
|
+
printDetailField2("createdAt", item.createdAt);
|
|
1558
|
+
printDetailField2("updatedAt", item.updatedAt);
|
|
1559
|
+
}
|
|
1560
|
+
function registerWorkflowCommand(program2) {
|
|
1561
|
+
const workflow = program2.command("workflow").description("Workflow operations");
|
|
1562
|
+
addWorkspaceOption(
|
|
1563
|
+
workflow.command("list").description("List accessible workflows").option("--scope <scope>", "Workflow scope: all or owner_is_me", "all")
|
|
1564
|
+
).action(async (options) => {
|
|
1565
|
+
const scope = runOrExit4(() => parseWorkflowScope(options.scope));
|
|
1566
|
+
startSpinner("Fetching workflows...");
|
|
1567
|
+
const response = await httpGet(
|
|
1568
|
+
`/workspaces/${options.workspace}/workflows${buildQuery({ scope })}`
|
|
1569
|
+
);
|
|
1570
|
+
if (!response.ok) {
|
|
1571
|
+
stopSpinner(false, "Failed");
|
|
1572
|
+
failResponse(response);
|
|
2403
1573
|
}
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
let context;
|
|
2408
|
-
try {
|
|
2409
|
-
context = resolveRuntimeContext();
|
|
2410
|
-
} catch (error) {
|
|
2411
|
-
if (error instanceof RuntimeContextError) {
|
|
2412
|
-
printError(error.message);
|
|
2413
|
-
process.exit(1);
|
|
1574
|
+
if (response.data.workflows.length === 0) {
|
|
1575
|
+
stopSpinner(true, "No workflows found.");
|
|
1576
|
+
return;
|
|
2414
1577
|
}
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
constructor(message) {
|
|
2438
|
-
super(message);
|
|
2439
|
-
this.name = "CheckoutStatusError";
|
|
2440
|
-
}
|
|
2441
|
-
};
|
|
2442
|
-
async function inspectCheckoutStatus(state) {
|
|
2443
|
-
await requireCheckoutDirectory(state.checkoutRoot);
|
|
2444
|
-
const localFiles = /* @__PURE__ */ new Map();
|
|
2445
|
-
for (const repo of Object.values(state.repos)) {
|
|
2446
|
-
await scanDirectory(state, repo.name, join3(state.checkoutRoot, repo.name), localFiles);
|
|
2447
|
-
}
|
|
2448
|
-
const changes = [];
|
|
2449
|
-
for (const [path2, sha] of localFiles) {
|
|
2450
|
-
const baseline = state.files[path2];
|
|
2451
|
-
if (!baseline) changes.push({ action: "added", path: path2 });
|
|
2452
|
-
else if (baseline.sha !== sha) changes.push({ action: "modified", path: path2 });
|
|
2453
|
-
}
|
|
2454
|
-
for (const path2 of Object.keys(state.files)) {
|
|
2455
|
-
if (isCheckoutPathIncluded(state, path2) && !localFiles.has(path2)) changes.push({ action: "deleted", path: path2 });
|
|
2456
|
-
}
|
|
2457
|
-
return changes.sort((left, right) => left.path.localeCompare(right.path));
|
|
2458
|
-
}
|
|
2459
|
-
async function requireCheckoutDirectory(checkoutRoot) {
|
|
2460
|
-
let stats;
|
|
2461
|
-
try {
|
|
2462
|
-
stats = await lstat3(checkoutRoot);
|
|
2463
|
-
} catch (error) {
|
|
2464
|
-
throw new CheckoutStatusError(`Checkout root is not available: ${errorMessage2(error)}`);
|
|
2465
|
-
}
|
|
2466
|
-
if (stats.isSymbolicLink()) throw new CheckoutStatusError(`Refusing to inspect symbolic link: ${checkoutRoot}`);
|
|
2467
|
-
if (!stats.isDirectory()) throw new CheckoutStatusError(`Checkout root is not a directory: ${checkoutRoot}`);
|
|
2468
|
-
}
|
|
2469
|
-
async function scanDirectory(state, workspacePath, localPath, files) {
|
|
2470
|
-
let entries;
|
|
2471
|
-
try {
|
|
2472
|
-
entries = await readdir2(localPath, { withFileTypes: true });
|
|
2473
|
-
} catch (error) {
|
|
2474
|
-
if (isNodeError4(error) && error.code === "ENOENT") return;
|
|
2475
|
-
throw new CheckoutStatusError(`Failed to inspect ${workspacePath}: ${errorMessage2(error)}`);
|
|
2476
|
-
}
|
|
2477
|
-
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
2478
|
-
if (entry.name === ".git") continue;
|
|
2479
|
-
const childWorkspacePath = `${workspacePath}/${entry.name}`;
|
|
2480
|
-
if (!doesCheckoutPathIntersect(state, childWorkspacePath)) continue;
|
|
2481
|
-
const childLocalPath = join3(localPath, entry.name);
|
|
2482
|
-
const stats = await lstat3(childLocalPath);
|
|
2483
|
-
if (stats.isSymbolicLink()) {
|
|
2484
|
-
throw new CheckoutStatusError(`Refusing to inspect symbolic link: ${childWorkspacePath}`);
|
|
1578
|
+
stopSpinner(true, `Found ${response.data.workflows.length} workflow(s)`);
|
|
1579
|
+
for (const item of response.data.workflows) printWorkflow(item);
|
|
1580
|
+
});
|
|
1581
|
+
addSpaceOptions2(
|
|
1582
|
+
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")
|
|
1583
|
+
).action(async (options) => {
|
|
1584
|
+
const body = runOrExit4(() => {
|
|
1585
|
+
const next = {
|
|
1586
|
+
path: options.path,
|
|
1587
|
+
...resolveSpaceSelector(options)
|
|
1588
|
+
};
|
|
1589
|
+
const desc = readDescFile(options.descFile);
|
|
1590
|
+
if (desc !== void 0) next.desc = desc;
|
|
1591
|
+
const ownerHumanId = parseOptionalPositiveInteger(options.ownerHumanId, "--owner-human-id");
|
|
1592
|
+
if (ownerHumanId !== void 0) next.ownerHumanId = ownerHumanId;
|
|
1593
|
+
return next;
|
|
1594
|
+
});
|
|
1595
|
+
startSpinner("Creating workflow...");
|
|
1596
|
+
const response = await httpPost(`/workspaces/${options.workspace}/workflows`, body);
|
|
1597
|
+
if (!response.ok) {
|
|
1598
|
+
stopSpinner(false, "Failed");
|
|
1599
|
+
failResponse(response);
|
|
2485
1600
|
}
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
1601
|
+
stopSpinner(true, "Workflow created");
|
|
1602
|
+
printWorkflow(response.data);
|
|
1603
|
+
});
|
|
1604
|
+
addWorkspaceOption(workflow.command("get").description("Get a workflow").argument("<workflowFileId>")).action(
|
|
1605
|
+
async (workflowFileId, options) => {
|
|
1606
|
+
startSpinner("Fetching workflow...");
|
|
1607
|
+
const response = await httpGet(
|
|
1608
|
+
`/workspaces/${options.workspace}/workflows/${workflowFileId}`
|
|
1609
|
+
);
|
|
1610
|
+
if (!response.ok) {
|
|
1611
|
+
stopSpinner(false, "Failed");
|
|
1612
|
+
failResponse(response);
|
|
1613
|
+
}
|
|
1614
|
+
stopSpinner(true, "Workflow fetched");
|
|
1615
|
+
printWorkflowDetail(response.data);
|
|
2490
1616
|
}
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
const context = resolveRuntimeContextOrExit2();
|
|
2510
|
-
if (context.mode !== "sandbox") {
|
|
2511
|
-
printError("moxt status is only available in sandbox mode");
|
|
2512
|
-
process.exit(1);
|
|
1617
|
+
);
|
|
1618
|
+
addWorkspaceOption(
|
|
1619
|
+
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")
|
|
1620
|
+
).action(async (workflowFileId, options) => {
|
|
1621
|
+
const body = runOrExit4(() => {
|
|
1622
|
+
const next = {};
|
|
1623
|
+
const desc = readDescFile(options.descFile);
|
|
1624
|
+
if (desc !== void 0) next.desc = desc;
|
|
1625
|
+
const ownerHumanId = parseOptionalPositiveInteger(options.ownerHumanId, "--owner-human-id");
|
|
1626
|
+
if (ownerHumanId !== void 0) next.ownerHumanId = ownerHumanId;
|
|
1627
|
+
assertNonEmptyBody(next);
|
|
1628
|
+
return next;
|
|
1629
|
+
});
|
|
1630
|
+
startSpinner("Updating workflow...");
|
|
1631
|
+
const response = await httpPatch(`/workspaces/${options.workspace}/workflows/${workflowFileId}`, body);
|
|
1632
|
+
if (!response.ok) {
|
|
1633
|
+
stopSpinner(false, "Failed");
|
|
1634
|
+
failResponse(response);
|
|
2513
1635
|
}
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
1636
|
+
stopSpinner(true, "Workflow updated");
|
|
1637
|
+
printWorkflow(response.data);
|
|
1638
|
+
});
|
|
1639
|
+
addWorkspaceOption(
|
|
1640
|
+
workflow.command("delete").description("Delete a workflow").argument("<workflowFileId>").option("--message <message>", "Commit message")
|
|
1641
|
+
).action(async (workflowFileId, options) => {
|
|
1642
|
+
startSpinner("Deleting workflow...");
|
|
1643
|
+
const response = await httpDelete(
|
|
1644
|
+
`/workspaces/${options.workspace}/workflows/${workflowFileId}`,
|
|
1645
|
+
options.message ? { message: options.message } : {}
|
|
1646
|
+
);
|
|
1647
|
+
if (!response.ok) {
|
|
1648
|
+
stopSpinner(false, "Failed");
|
|
1649
|
+
failResponse(response);
|
|
2518
1650
|
}
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
print(`${colors.bold}Local changes${colors.reset}: not tracked`);
|
|
2535
|
-
return;
|
|
1651
|
+
stopSpinner(true, `Deleted: ${response.data.fileId}`);
|
|
1652
|
+
});
|
|
1653
|
+
addWorkspaceOption(workflow.command("duplicate").description("Duplicate a workflow").argument("<workflowFileId>")).action(
|
|
1654
|
+
async (workflowFileId, options) => {
|
|
1655
|
+
startSpinner("Duplicating workflow...");
|
|
1656
|
+
const response = await httpPost(
|
|
1657
|
+
`/workspaces/${options.workspace}/workflows/${workflowFileId}/duplicate`,
|
|
1658
|
+
{}
|
|
1659
|
+
);
|
|
1660
|
+
if (!response.ok) {
|
|
1661
|
+
stopSpinner(false, "Failed");
|
|
1662
|
+
failResponse(response);
|
|
1663
|
+
}
|
|
1664
|
+
stopSpinner(true, "Workflow duplicated");
|
|
1665
|
+
printWorkflow(response.data);
|
|
2536
1666
|
}
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
1667
|
+
);
|
|
1668
|
+
registerStatusCommands(workflow);
|
|
1669
|
+
registerTransitionCommands(workflow);
|
|
1670
|
+
registerTaskCommands(workflow);
|
|
1671
|
+
registerCommentCommands(workflow);
|
|
1672
|
+
registerRunCommands(workflow);
|
|
1673
|
+
}
|
|
1674
|
+
function registerStatusCommands(workflow) {
|
|
1675
|
+
const status = workflow.command("status").description("Workflow status operations");
|
|
1676
|
+
addPreferredAssigneeOptions(
|
|
1677
|
+
addWorkspaceOption(
|
|
1678
|
+
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")
|
|
1679
|
+
)
|
|
1680
|
+
).action(
|
|
1681
|
+
async (workflowFileId, options) => {
|
|
1682
|
+
const body = runOrExit4(() => {
|
|
1683
|
+
const next = {
|
|
1684
|
+
name: options.name,
|
|
1685
|
+
type: parseRequiredStatusType(options.type)
|
|
1686
|
+
};
|
|
1687
|
+
const desc = readDescFile(options.descFile);
|
|
1688
|
+
if (desc !== void 0) next.desc = desc;
|
|
1689
|
+
Object.assign(next, parsePreferredAssigneeBody(options));
|
|
1690
|
+
return next;
|
|
1691
|
+
});
|
|
1692
|
+
startSpinner("Creating status...");
|
|
1693
|
+
const response = await httpPost(
|
|
1694
|
+
`/workspaces/${options.workspace}/workflows/${workflowFileId}/statuses`,
|
|
1695
|
+
body
|
|
1696
|
+
);
|
|
1697
|
+
if (!response.ok) {
|
|
1698
|
+
stopSpinner(false, "Failed");
|
|
1699
|
+
failResponse(response);
|
|
1700
|
+
}
|
|
1701
|
+
stopSpinner(true, "Status created");
|
|
1702
|
+
printStatusDetail(response.data);
|
|
2542
1703
|
}
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
checkedOutScopes: [],
|
|
2578
|
-
localChangesTracked: false
|
|
2579
|
-
},
|
|
2580
|
-
repos
|
|
2581
|
-
};
|
|
2582
|
-
}
|
|
2583
|
-
function validateStateContext(state, context) {
|
|
2584
|
-
if (state.workspaceId !== context.workspaceId) {
|
|
2585
|
-
throw new Error(`Checkout state belongs to workspace ${state.workspaceId}, not ${context.workspaceId}`);
|
|
2586
|
-
}
|
|
2587
|
-
const currentRepos = new Map(listRepoEntries(context.repoPayload).map((repo) => [repo.repoId, repo.name]));
|
|
2588
|
-
for (const [repoId, repo] of Object.entries(state.repos)) {
|
|
2589
|
-
const currentName = currentRepos.get(repoId);
|
|
2590
|
-
if (!currentName) throw new Error(`Checkout state contains unknown repo: ${repoId}`);
|
|
2591
|
-
if (currentName !== repo.name) {
|
|
2592
|
-
throw new Error(`Checkout state repo name does not match current workspace: ${repo.name}`);
|
|
1704
|
+
);
|
|
1705
|
+
addPreferredAssigneeOptions(
|
|
1706
|
+
addWorkspaceOption(
|
|
1707
|
+
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")
|
|
1708
|
+
)
|
|
1709
|
+
).action(
|
|
1710
|
+
async (workflowFileId, statusScopedId, options) => {
|
|
1711
|
+
const body = runOrExit4(() => {
|
|
1712
|
+
if (options.descFile && options.clearDesc) {
|
|
1713
|
+
throw new WorkflowOptionsError("Error: --desc-file cannot be used with --clear-desc.");
|
|
1714
|
+
}
|
|
1715
|
+
const next = {};
|
|
1716
|
+
if (options.name) next.name = options.name;
|
|
1717
|
+
const type = parseStatusType(options.type);
|
|
1718
|
+
if (type !== void 0) next.type = type;
|
|
1719
|
+
const desc = readDescFile(options.descFile);
|
|
1720
|
+
if (desc !== void 0) next.desc = desc;
|
|
1721
|
+
if (options.clearDesc) next.desc = null;
|
|
1722
|
+
Object.assign(next, parsePreferredAssigneeBody(options));
|
|
1723
|
+
assertNonEmptyBody(next);
|
|
1724
|
+
return next;
|
|
1725
|
+
});
|
|
1726
|
+
const id = runOrExit4(() => parsePositiveInteger(statusScopedId, "statusScopedId"));
|
|
1727
|
+
startSpinner("Updating status...");
|
|
1728
|
+
const response = await httpPatch(
|
|
1729
|
+
`/workspaces/${options.workspace}/workflows/${workflowFileId}/statuses/${id}`,
|
|
1730
|
+
body
|
|
1731
|
+
);
|
|
1732
|
+
if (!response.ok) {
|
|
1733
|
+
stopSpinner(false, "Failed");
|
|
1734
|
+
failResponse(response);
|
|
1735
|
+
}
|
|
1736
|
+
stopSpinner(true, "Status updated");
|
|
1737
|
+
printStatusDetail(response.data);
|
|
2593
1738
|
}
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
1739
|
+
);
|
|
1740
|
+
addWorkspaceOption(
|
|
1741
|
+
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")
|
|
1742
|
+
).action(
|
|
1743
|
+
async (workflowFileId, statusScopedId, options) => {
|
|
1744
|
+
const { id, anchoredStatusScopedId } = runOrExit4(() => {
|
|
1745
|
+
if (options.after && options.first) {
|
|
1746
|
+
throw new WorkflowOptionsError("Error: --after cannot be used with --first.");
|
|
1747
|
+
}
|
|
1748
|
+
if (!options.after && !options.first) {
|
|
1749
|
+
throw new WorkflowOptionsError("Error: either --after or --first is required.");
|
|
1750
|
+
}
|
|
1751
|
+
return {
|
|
1752
|
+
id: parsePositiveInteger(statusScopedId, "statusScopedId"),
|
|
1753
|
+
anchoredStatusScopedId: options.first ? null : parsePositiveInteger(options.after, "--after")
|
|
1754
|
+
};
|
|
1755
|
+
});
|
|
1756
|
+
startSpinner("Moving status...");
|
|
1757
|
+
const response = await httpPost(
|
|
1758
|
+
`/workspaces/${options.workspace}/workflows/${workflowFileId}/statuses/${id}/move-after`,
|
|
1759
|
+
{ anchoredStatusScopedId }
|
|
1760
|
+
);
|
|
1761
|
+
if (!response.ok) {
|
|
1762
|
+
stopSpinner(false, "Failed");
|
|
1763
|
+
failResponse(response);
|
|
1764
|
+
}
|
|
1765
|
+
stopSpinner(true, "Status moved");
|
|
2603
1766
|
}
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
}
|
|
2618
|
-
|
|
2619
|
-
try {
|
|
2620
|
-
const raw = fs3.readFileSync(getConfigPath(), "utf8");
|
|
2621
|
-
const parsed = JSON.parse(raw);
|
|
2622
|
-
return parsed.telemetry ?? {};
|
|
2623
|
-
} catch {
|
|
2624
|
-
return {};
|
|
2625
|
-
}
|
|
2626
|
-
}
|
|
2627
|
-
function writeTelemetryConfig(update) {
|
|
2628
|
-
const configPath = getConfigPath();
|
|
2629
|
-
let existing = {};
|
|
2630
|
-
try {
|
|
2631
|
-
existing = JSON.parse(fs3.readFileSync(configPath, "utf8"));
|
|
2632
|
-
} catch {
|
|
2633
|
-
existing = {};
|
|
2634
|
-
}
|
|
2635
|
-
const currentTelemetry = existing.telemetry ?? {};
|
|
2636
|
-
const next = { ...existing, telemetry: { ...currentTelemetry, ...update } };
|
|
2637
|
-
fs3.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
2638
|
-
fs3.writeFileSync(configPath, JSON.stringify(next, null, 2));
|
|
2639
|
-
}
|
|
2640
|
-
|
|
2641
|
-
// src/telemetry/opt-out.ts
|
|
2642
|
-
function isCiEnvironment() {
|
|
2643
|
-
return process.env.CI === "true" || process.env.CI === "1";
|
|
1767
|
+
);
|
|
1768
|
+
addWorkspaceOption(
|
|
1769
|
+
status.command("delete").description("Delete a workflow status").argument("<workflowFileId>").argument("<statusScopedId>")
|
|
1770
|
+
).action(async (workflowFileId, statusScopedId, options) => {
|
|
1771
|
+
const id = runOrExit4(() => parsePositiveInteger(statusScopedId, "statusScopedId"));
|
|
1772
|
+
startSpinner("Deleting status...");
|
|
1773
|
+
const response = await httpDelete(
|
|
1774
|
+
`/workspaces/${options.workspace}/workflows/${workflowFileId}/statuses/${id}`
|
|
1775
|
+
);
|
|
1776
|
+
if (!response.ok) {
|
|
1777
|
+
stopSpinner(false, "Failed");
|
|
1778
|
+
failResponse(response);
|
|
1779
|
+
}
|
|
1780
|
+
stopSpinner(true, `Deleted: ${response.data.scopedId}`);
|
|
1781
|
+
});
|
|
2644
1782
|
}
|
|
2645
|
-
function
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
1783
|
+
function registerTransitionCommands(workflow) {
|
|
1784
|
+
const transition = workflow.command("transition").description("Workflow transition operations");
|
|
1785
|
+
for (const action of ["create", "delete"]) {
|
|
1786
|
+
addWorkspaceOption(
|
|
1787
|
+
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")
|
|
1788
|
+
).action(async (workflowFileId, options) => {
|
|
1789
|
+
const body = runOrExit4(() => ({
|
|
1790
|
+
statusFromScopedId: parsePositiveInteger(options.from, "--from"),
|
|
1791
|
+
statusToScopedId: parsePositiveInteger(options.to, "--to")
|
|
1792
|
+
}));
|
|
1793
|
+
startSpinner(`${action === "create" ? "Creating" : "Deleting"} transition...`);
|
|
1794
|
+
const response = action === "create" ? await httpPost(
|
|
1795
|
+
`/workspaces/${options.workspace}/workflows/${workflowFileId}/transitions`,
|
|
1796
|
+
body
|
|
1797
|
+
) : await httpDelete(
|
|
1798
|
+
`/workspaces/${options.workspace}/workflows/${workflowFileId}/transitions`,
|
|
1799
|
+
body
|
|
1800
|
+
);
|
|
1801
|
+
if (!response.ok) {
|
|
1802
|
+
stopSpinner(false, "Failed");
|
|
1803
|
+
failResponse(response);
|
|
1804
|
+
}
|
|
1805
|
+
stopSpinner(true, action === "create" ? "Transition created" : "Transition deleted");
|
|
1806
|
+
print(`${response.data.statusFromScopedId} -> ${response.data.statusToScopedId}`);
|
|
1807
|
+
});
|
|
2651
1808
|
}
|
|
2652
|
-
return readTelemetryConfig().disabled === true;
|
|
2653
1809
|
}
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
1810
|
+
function registerTaskCommands(workflow) {
|
|
1811
|
+
const task = workflow.command("task").description("Workflow task operations");
|
|
1812
|
+
addWorkspaceOption(
|
|
1813
|
+
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")
|
|
1814
|
+
).action(
|
|
1815
|
+
async (options) => {
|
|
1816
|
+
const query = runOrExit4(() => buildQuery({
|
|
1817
|
+
scope: parseTaskScope(options.scope),
|
|
1818
|
+
limit: parseSearchLimit(options.limit, 20, 100),
|
|
1819
|
+
cursor: options.cursor,
|
|
1820
|
+
workflowFileId: options.workflowFileId
|
|
1821
|
+
}));
|
|
1822
|
+
startSpinner("Fetching workflow tasks...");
|
|
1823
|
+
const response = await httpGet(`/workspaces/${options.workspace}/workflow-tasks${query}`);
|
|
1824
|
+
if (!response.ok) {
|
|
1825
|
+
stopSpinner(false, "Failed");
|
|
1826
|
+
failResponse(response);
|
|
1827
|
+
}
|
|
1828
|
+
if (response.data.tasks.length === 0) {
|
|
1829
|
+
stopSpinner(true, "No workflow tasks found.");
|
|
1830
|
+
return;
|
|
1831
|
+
}
|
|
1832
|
+
stopSpinner(true, `Found ${response.data.tasks.length} task(s)`);
|
|
1833
|
+
for (const item of response.data.tasks) printTaskSummary(item);
|
|
1834
|
+
if (response.data.nextCursor) print(`${colors.dim}nextCursor=${response.data.nextCursor}${colors.reset}`);
|
|
2667
1835
|
}
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
// src/commands/whoami.ts
|
|
2680
|
-
function registerWhoamiCommand(program2) {
|
|
2681
|
-
program2.command("whoami").description("Show current user information").action(async () => {
|
|
2682
|
-
startSpinner("Fetching user info...");
|
|
2683
|
-
const response = await httpGet("/users/whoami");
|
|
2684
|
-
if (response.ok) {
|
|
2685
|
-
stopSpinner(true, "Done");
|
|
2686
|
-
print(`${colors.bold}Nickname${colors.reset}: ${response.data.nickname}`);
|
|
2687
|
-
print(`${colors.bold}Email${colors.reset}: ${response.data.email}`);
|
|
2688
|
-
} else {
|
|
1836
|
+
);
|
|
1837
|
+
addWorkspaceOption(
|
|
1838
|
+
task.command("get").description("Get a workflow task").argument("<workflowFileId>").argument("<taskScopedId>")
|
|
1839
|
+
).action(async (workflowFileId, taskScopedId, options) => {
|
|
1840
|
+
const id = runOrExit4(() => parsePositiveInteger(taskScopedId, "taskScopedId"));
|
|
1841
|
+
startSpinner("Fetching task...");
|
|
1842
|
+
const response = await httpGet(
|
|
1843
|
+
`/workspaces/${options.workspace}/workflows/${workflowFileId}/tasks/${id}`
|
|
1844
|
+
);
|
|
1845
|
+
if (!response.ok) {
|
|
2689
1846
|
stopSpinner(false, "Failed");
|
|
2690
|
-
|
|
2691
|
-
process.exit(1);
|
|
1847
|
+
failResponse(response);
|
|
2692
1848
|
}
|
|
1849
|
+
stopSpinner(true, "Task fetched");
|
|
1850
|
+
printTaskDetail(response.data);
|
|
2693
1851
|
});
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
1852
|
+
addAssigneeOptions(
|
|
1853
|
+
addWorkspaceOption(
|
|
1854
|
+
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")
|
|
1855
|
+
)
|
|
1856
|
+
).action(
|
|
1857
|
+
async (workflowFileId, options) => {
|
|
1858
|
+
const body = runOrExit4(() => {
|
|
1859
|
+
const next = {
|
|
1860
|
+
title: options.title,
|
|
1861
|
+
statusScopedId: parsePositiveInteger(options.status, "--status"),
|
|
1862
|
+
priority: parseRequiredPriority(options.priority)
|
|
1863
|
+
};
|
|
1864
|
+
const desc = readDescFile(options.descFile);
|
|
1865
|
+
if (desc !== void 0) next.desc = desc;
|
|
1866
|
+
const ownerHumanId = parseOptionalPositiveInteger(options.ownerHumanId, "--owner-human-id");
|
|
1867
|
+
if (ownerHumanId !== void 0) next.ownerHumanId = ownerHumanId;
|
|
1868
|
+
Object.assign(next, parseAssigneeBody(options, false));
|
|
1869
|
+
return next;
|
|
1870
|
+
});
|
|
1871
|
+
startSpinner("Creating task...");
|
|
1872
|
+
const response = await httpPost(
|
|
1873
|
+
`/workspaces/${options.workspace}/workflows/${workflowFileId}/tasks`,
|
|
1874
|
+
body
|
|
1875
|
+
);
|
|
1876
|
+
if (!response.ok) {
|
|
1877
|
+
stopSpinner(false, "Failed");
|
|
1878
|
+
failResponse(response);
|
|
2711
1879
|
}
|
|
2712
|
-
stopSpinner(true,
|
|
2713
|
-
|
|
2714
|
-
|
|
1880
|
+
stopSpinner(true, "Task created");
|
|
1881
|
+
printTaskDetail(response.data);
|
|
1882
|
+
}
|
|
1883
|
+
);
|
|
1884
|
+
addWorkspaceOption(
|
|
1885
|
+
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")
|
|
1886
|
+
).action(
|
|
1887
|
+
async (workflowFileId, taskScopedId, options) => {
|
|
1888
|
+
const body = runOrExit4(() => {
|
|
1889
|
+
if (options.descFile && options.clearDesc) {
|
|
1890
|
+
throw new WorkflowOptionsError("Error: --desc-file cannot be used with --clear-desc.");
|
|
1891
|
+
}
|
|
1892
|
+
const next = {};
|
|
1893
|
+
if (options.title) next.title = options.title;
|
|
1894
|
+
const priority = parsePriority(options.priority);
|
|
1895
|
+
if (priority !== void 0) next.priority = priority;
|
|
1896
|
+
const desc = readDescFile(options.descFile);
|
|
1897
|
+
if (desc !== void 0) next.desc = desc;
|
|
1898
|
+
if (options.clearDesc) next.desc = null;
|
|
1899
|
+
const ownerHumanId = parseOptionalPositiveInteger(options.ownerHumanId, "--owner-human-id");
|
|
1900
|
+
if (ownerHumanId !== void 0) next.ownerHumanId = ownerHumanId;
|
|
1901
|
+
assertNonEmptyBody(next);
|
|
1902
|
+
return next;
|
|
1903
|
+
});
|
|
1904
|
+
const id = runOrExit4(() => parsePositiveInteger(taskScopedId, "taskScopedId"));
|
|
1905
|
+
startSpinner("Updating task...");
|
|
1906
|
+
const response = await httpPatch(
|
|
1907
|
+
`/workspaces/${options.workspace}/workflows/${workflowFileId}/tasks/${id}/metadata`,
|
|
1908
|
+
body
|
|
1909
|
+
);
|
|
1910
|
+
if (!response.ok) {
|
|
1911
|
+
stopSpinner(false, "Failed");
|
|
1912
|
+
failResponse(response);
|
|
2715
1913
|
}
|
|
2716
|
-
|
|
1914
|
+
stopSpinner(true, "Task updated");
|
|
1915
|
+
printTaskDetail(response.data);
|
|
1916
|
+
}
|
|
1917
|
+
);
|
|
1918
|
+
for (const action of ["add-linked-files", "remove-linked-files"]) {
|
|
1919
|
+
addWorkspaceOption(
|
|
1920
|
+
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")
|
|
1921
|
+
).action(
|
|
1922
|
+
async (workflowFileId, taskScopedId, options) => {
|
|
1923
|
+
const { id, fileIds } = runOrExit4(() => ({
|
|
1924
|
+
id: parsePositiveInteger(taskScopedId, "taskScopedId"),
|
|
1925
|
+
fileIds: parseFileIds(options.fileIds, "--file-ids")
|
|
1926
|
+
}));
|
|
1927
|
+
startSpinner(action === "add-linked-files" ? "Adding linked files..." : "Removing linked files...");
|
|
1928
|
+
const path2 = `/workspaces/${options.workspace}/workflows/${workflowFileId}/tasks/${id}/linked-files`;
|
|
1929
|
+
const response = action === "add-linked-files" ? await httpPost(path2, { fileIds }) : await httpDelete(path2, { fileIds });
|
|
1930
|
+
if (!response.ok) {
|
|
1931
|
+
stopSpinner(false, "Failed");
|
|
1932
|
+
failResponse(response);
|
|
1933
|
+
}
|
|
1934
|
+
stopSpinner(true, action === "add-linked-files" ? "Linked files added" : "Linked files removed");
|
|
1935
|
+
printTaskDetail(response.data);
|
|
1936
|
+
}
|
|
1937
|
+
);
|
|
1938
|
+
}
|
|
1939
|
+
addWorkspaceOption(
|
|
1940
|
+
task.command("move").description("Move a workflow task to another status").argument("<workflowFileId>").argument("<taskScopedId>").requiredOption("--status <statusScopedId>", "Target status scoped ID")
|
|
1941
|
+
).action(
|
|
1942
|
+
async (workflowFileId, taskScopedId, options) => {
|
|
1943
|
+
const { id, statusScopedId } = runOrExit4(() => ({
|
|
1944
|
+
id: parsePositiveInteger(taskScopedId, "taskScopedId"),
|
|
1945
|
+
statusScopedId: parsePositiveInteger(options.status, "--status")
|
|
1946
|
+
}));
|
|
1947
|
+
startSpinner("Moving task...");
|
|
1948
|
+
const response = await httpPatch(
|
|
1949
|
+
`/workspaces/${options.workspace}/workflows/${workflowFileId}/tasks/${id}/status`,
|
|
1950
|
+
{ statusScopedId }
|
|
1951
|
+
);
|
|
1952
|
+
if (!response.ok) {
|
|
1953
|
+
stopSpinner(false, "Failed");
|
|
1954
|
+
failResponse(response);
|
|
1955
|
+
}
|
|
1956
|
+
stopSpinner(true, "Task moved");
|
|
1957
|
+
printTaskDetail(response.data);
|
|
1958
|
+
}
|
|
1959
|
+
);
|
|
1960
|
+
addAssigneeOptions(
|
|
1961
|
+
addWorkspaceOption(
|
|
1962
|
+
task.command("assign").description("Update workflow task assignee").argument("<workflowFileId>").argument("<taskScopedId>").option("--clear", "Clear assignee")
|
|
1963
|
+
)
|
|
1964
|
+
).action(
|
|
1965
|
+
async (workflowFileId, taskScopedId, options) => {
|
|
1966
|
+
const body = runOrExit4(() => {
|
|
1967
|
+
const next = parseAssigneeBody(options, true);
|
|
1968
|
+
if (Object.keys(next).length === 0 && options.clear !== true) assertNonEmptyBody(next);
|
|
1969
|
+
return next;
|
|
1970
|
+
});
|
|
1971
|
+
const id = runOrExit4(() => parsePositiveInteger(taskScopedId, "taskScopedId"));
|
|
1972
|
+
startSpinner("Updating task assignee...");
|
|
1973
|
+
const response = await httpPatch(
|
|
1974
|
+
`/workspaces/${options.workspace}/workflows/${workflowFileId}/tasks/${id}/assignee`,
|
|
1975
|
+
body
|
|
1976
|
+
);
|
|
1977
|
+
if (!response.ok) {
|
|
1978
|
+
stopSpinner(false, "Failed");
|
|
1979
|
+
failResponse(response);
|
|
1980
|
+
}
|
|
1981
|
+
stopSpinner(true, "Task assignee updated");
|
|
1982
|
+
printTaskDetail(response.data);
|
|
1983
|
+
}
|
|
1984
|
+
);
|
|
1985
|
+
addWorkspaceOption(
|
|
1986
|
+
task.command("delete").description("Delete a workflow task").argument("<workflowFileId>").argument("<taskScopedId>")
|
|
1987
|
+
).action(async (workflowFileId, taskScopedId, options) => {
|
|
1988
|
+
const id = runOrExit4(() => parsePositiveInteger(taskScopedId, "taskScopedId"));
|
|
1989
|
+
startSpinner("Deleting task...");
|
|
1990
|
+
const response = await httpDelete(
|
|
1991
|
+
`/workspaces/${options.workspace}/workflows/${workflowFileId}/tasks/${id}`
|
|
1992
|
+
);
|
|
1993
|
+
if (!response.ok) {
|
|
2717
1994
|
stopSpinner(false, "Failed");
|
|
2718
|
-
|
|
2719
|
-
process.exit(1);
|
|
1995
|
+
failResponse(response);
|
|
2720
1996
|
}
|
|
1997
|
+
stopSpinner(true, `Deleted: ${response.data.scopedId}`);
|
|
2721
1998
|
});
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
1999
|
+
}
|
|
2000
|
+
function registerCommentCommands(workflow) {
|
|
2001
|
+
const comment = workflow.command("comment").description("Workflow task comment operations");
|
|
2002
|
+
addWorkspaceOption(
|
|
2003
|
+
comment.command("create").description("Create a workflow task comment").argument("<workflowFileId>").argument("<taskScopedId>").requiredOption("--content-path <localPath>", "Local UTF-8 text file for comment content")
|
|
2004
|
+
).action(
|
|
2005
|
+
async (workflowFileId, taskScopedId, options) => {
|
|
2006
|
+
const { id, content } = runOrExit4(() => ({
|
|
2007
|
+
id: parsePositiveInteger(taskScopedId, "taskScopedId"),
|
|
2008
|
+
content: readCommentContentFile2(options.contentPath)
|
|
2009
|
+
}));
|
|
2010
|
+
startSpinner("Creating comment...");
|
|
2011
|
+
const response = await httpPost(
|
|
2012
|
+
`/workspaces/${options.workspace}/workflows/${workflowFileId}/tasks/${id}/comments`,
|
|
2013
|
+
{ content }
|
|
2014
|
+
);
|
|
2015
|
+
if (!response.ok) {
|
|
2016
|
+
stopSpinner(false, "Failed");
|
|
2017
|
+
failResponse(response);
|
|
2018
|
+
}
|
|
2019
|
+
stopSpinner(true, "Comment created");
|
|
2020
|
+
printCommentDetail(response.data);
|
|
2021
|
+
}
|
|
2022
|
+
);
|
|
2023
|
+
addWorkspaceOption(
|
|
2024
|
+
comment.command("list").description("List workflow task comments").argument("<workflowFileId>").argument("<taskScopedId>")
|
|
2025
|
+
).action(async (workflowFileId, taskScopedId, options) => {
|
|
2026
|
+
const id = runOrExit4(() => parsePositiveInteger(taskScopedId, "taskScopedId"));
|
|
2027
|
+
startSpinner("Fetching comments...");
|
|
2028
|
+
const response = await httpGet(
|
|
2029
|
+
`/workspaces/${options.workspace}/workflows/${workflowFileId}/tasks/${id}/comments`
|
|
2030
|
+
);
|
|
2727
2031
|
if (!response.ok) {
|
|
2728
|
-
stopSpinner(false, "Failed
|
|
2729
|
-
|
|
2730
|
-
process.exit(1);
|
|
2032
|
+
stopSpinner(false, "Failed");
|
|
2033
|
+
failResponse(response);
|
|
2731
2034
|
}
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
stopSpinner(true, "No members found.");
|
|
2035
|
+
if (response.data.items.length === 0) {
|
|
2036
|
+
stopSpinner(true, "No comments found.");
|
|
2735
2037
|
return;
|
|
2736
2038
|
}
|
|
2737
|
-
stopSpinner(true, `Found ${
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
}
|
|
2039
|
+
stopSpinner(true, `Found ${response.data.items.length} comment(s)`);
|
|
2040
|
+
response.data.items.forEach((item, index) => {
|
|
2041
|
+
if (index > 0) print("");
|
|
2042
|
+
printCommentDetail(item);
|
|
2043
|
+
});
|
|
2742
2044
|
});
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2045
|
+
addWorkspaceOption(
|
|
2046
|
+
comment.command("delete").description("Delete a workflow task comment").argument("<workflowFileId>").argument("<taskScopedId>").argument("<commentId>")
|
|
2047
|
+
).action(
|
|
2048
|
+
async (workflowFileId, taskScopedId, commentId, options) => {
|
|
2049
|
+
const ids = runOrExit4(() => ({
|
|
2050
|
+
task: parsePositiveInteger(taskScopedId, "taskScopedId"),
|
|
2051
|
+
comment: parsePositiveInteger(commentId, "commentId")
|
|
2052
|
+
}));
|
|
2053
|
+
startSpinner("Deleting comment...");
|
|
2054
|
+
const response = await httpDelete(
|
|
2055
|
+
`/workspaces/${options.workspace}/workflows/${workflowFileId}/tasks/${ids.task}/comments/${ids.comment}`
|
|
2056
|
+
);
|
|
2057
|
+
if (!response.ok) {
|
|
2058
|
+
stopSpinner(false, "Failed");
|
|
2059
|
+
failResponse(response);
|
|
2754
2060
|
}
|
|
2755
|
-
|
|
2061
|
+
stopSpinner(true, `Deleted: ${response.data.id}`);
|
|
2756
2062
|
}
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2063
|
+
);
|
|
2064
|
+
}
|
|
2065
|
+
function registerRunCommands(workflow) {
|
|
2066
|
+
const run = workflow.command("run").description("Workflow agent run operations");
|
|
2067
|
+
addWorkspaceOption(
|
|
2068
|
+
run.command("status").description("Get workflow agent run statuses").argument("<workflowFileId>").argument("<triggerIds...>")
|
|
2069
|
+
).action(async (workflowFileId, triggerIds, options) => {
|
|
2070
|
+
const workflowPipelineTriggerIds = runOrExit4(
|
|
2071
|
+
() => triggerIds.map((triggerId) => parsePositiveInteger(triggerId, "workflowPipelineTriggerId"))
|
|
2072
|
+
);
|
|
2073
|
+
startSpinner("Fetching run statuses...");
|
|
2074
|
+
const response = await httpPost(
|
|
2075
|
+
`/workspaces/${options.workspace}/workflows/${workflowFileId}/pipeline/triggers/statuses`,
|
|
2076
|
+
{ workflowPipelineTriggerIds }
|
|
2077
|
+
);
|
|
2078
|
+
if (!response.ok) {
|
|
2079
|
+
stopSpinner(false, "Failed");
|
|
2080
|
+
failResponse(response);
|
|
2763
2081
|
}
|
|
2764
|
-
stopSpinner(true,
|
|
2765
|
-
const
|
|
2766
|
-
|
|
2767
|
-
for (const email of emails) {
|
|
2768
|
-
const member = emailToMember.get(email.toLowerCase());
|
|
2769
|
-
if (!member) {
|
|
2770
|
-
print(`${colors.red}\u2717${colors.reset} ${email} - not found in workspace`);
|
|
2771
|
-
continue;
|
|
2772
|
-
}
|
|
2773
|
-
const deleteResponse = await httpDelete(`/workspaces/${workspaceId}/members/${member.userId}`);
|
|
2774
|
-
if (deleteResponse.ok) {
|
|
2775
|
-
print(`${colors.green}\u2713${colors.reset} ${email} - removed`);
|
|
2776
|
-
} else {
|
|
2777
|
-
print(`${colors.red}\u2717${colors.reset} ${email} - failed: ${JSON.stringify(deleteResponse.data)}`);
|
|
2778
|
-
}
|
|
2082
|
+
stopSpinner(true, `Found ${response.data.length} run status item(s)`);
|
|
2083
|
+
for (const item of response.data) {
|
|
2084
|
+
print(`${colors.bold}${item.workflowPipelineTriggerId}${colors.reset} ${item.pipelineStatus}`);
|
|
2779
2085
|
}
|
|
2780
2086
|
});
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2087
|
+
addWorkspaceOption(
|
|
2088
|
+
run.command("output").description("Get workflow agent run output events").argument("<workflowFileId>").argument("<triggerId>")
|
|
2089
|
+
).action(async (workflowFileId, triggerId, options) => {
|
|
2090
|
+
const id = runOrExit4(() => parsePositiveInteger(triggerId, "workflowPipelineTriggerId"));
|
|
2091
|
+
startSpinner("Fetching run output...");
|
|
2092
|
+
const response = await httpGet(
|
|
2093
|
+
`/workspaces/${options.workspace}/workflows/${workflowFileId}/pipeline/trigger/${id}/output`
|
|
2094
|
+
);
|
|
2786
2095
|
if (!response.ok) {
|
|
2787
|
-
stopSpinner(false, "Failed
|
|
2788
|
-
|
|
2789
|
-
process.exit(1);
|
|
2790
|
-
}
|
|
2791
|
-
const { spaces } = response.data;
|
|
2792
|
-
if (spaces.length === 0) {
|
|
2793
|
-
stopSpinner(true, "No spaces found.");
|
|
2794
|
-
return;
|
|
2096
|
+
stopSpinner(false, "Failed");
|
|
2097
|
+
failResponse(response);
|
|
2795
2098
|
}
|
|
2796
|
-
stopSpinner(true, `
|
|
2797
|
-
print(
|
|
2798
|
-
|
|
2099
|
+
stopSpinner(true, `Run ${response.data.workflowPipelineTriggerId}`);
|
|
2100
|
+
for (const event of response.data.pipelineEvents) print(event);
|
|
2101
|
+
});
|
|
2102
|
+
addWorkspaceOption(
|
|
2103
|
+
run.command("abort").description("Abort a workflow agent run").argument("<workflowFileId>").argument("<triggerId>")
|
|
2104
|
+
).action(async (workflowFileId, triggerId, options) => {
|
|
2105
|
+
const id = runOrExit4(() => parsePositiveInteger(triggerId, "workflowPipelineTriggerId"));
|
|
2106
|
+
startSpinner("Aborting run...");
|
|
2107
|
+
const response = await httpPost(
|
|
2108
|
+
`/workspaces/${options.workspace}/workflows/${workflowFileId}/pipeline/trigger/${id}/abort`,
|
|
2109
|
+
{}
|
|
2799
2110
|
);
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
print(`${s.type} ${teamSpaceId} ${teammateId} ${s.name}`);
|
|
2111
|
+
if (!response.ok) {
|
|
2112
|
+
stopSpinner(false, "Failed");
|
|
2113
|
+
failResponse(response);
|
|
2804
2114
|
}
|
|
2115
|
+
stopSpinner(true, `Abort accepted: ${id}`);
|
|
2805
2116
|
});
|
|
2806
2117
|
}
|
|
2807
2118
|
|
|
2808
2119
|
// src/program.ts
|
|
2809
2120
|
function createProgram(version) {
|
|
2810
|
-
const program2 = new
|
|
2121
|
+
const program2 = new Command();
|
|
2811
2122
|
program2.name("moxt").description("Moxt CLI - AI Workspace").version(version).action(() => {
|
|
2812
2123
|
program2.help();
|
|
2813
2124
|
});
|
|
2814
|
-
registerAuthCommand(program2);
|
|
2815
2125
|
registerWhoamiCommand(program2);
|
|
2816
2126
|
registerWorkspaceCommand(program2);
|
|
2817
2127
|
registerFileCommand(program2);
|
|
2818
2128
|
registerMemoryCommand(program2);
|
|
2129
|
+
registerWorkflowCommand(program2);
|
|
2819
2130
|
registerMiniappCommand(program2);
|
|
2820
|
-
registerPullCommand(program2);
|
|
2821
|
-
registerPushCommand(program2);
|
|
2822
|
-
registerStatusCommand(program2);
|
|
2823
2131
|
registerTelemetryCommand(program2);
|
|
2824
2132
|
return program2;
|
|
2825
2133
|
}
|
|
@@ -2831,7 +2139,7 @@ var FLUSH_TIMEOUT_MS = 3e3;
|
|
|
2831
2139
|
var MAX_BATCH_SIZE = 100;
|
|
2832
2140
|
function commonLabels() {
|
|
2833
2141
|
return {
|
|
2834
|
-
cli_version: "0.4.0
|
|
2142
|
+
cli_version: "0.4.0",
|
|
2835
2143
|
node_version: process.versions.node,
|
|
2836
2144
|
os: platform2(),
|
|
2837
2145
|
arch: arch2(),
|
|
@@ -2877,7 +2185,7 @@ async function flush() {
|
|
|
2877
2185
|
}
|
|
2878
2186
|
const batch = buffer.splice(0, MAX_BATCH_SIZE);
|
|
2879
2187
|
const payload = {
|
|
2880
|
-
release_id: "0.4.0
|
|
2188
|
+
release_id: "0.4.0",
|
|
2881
2189
|
client_type: "cli",
|
|
2882
2190
|
metric_data_list: batch.map((m) => ({
|
|
2883
2191
|
profile: "cli",
|
|
@@ -3018,9 +2326,9 @@ function installExitHandler() {
|
|
|
3018
2326
|
|
|
3019
2327
|
// src/index.ts
|
|
3020
2328
|
updateNotifier({
|
|
3021
|
-
pkg: { name: "@moxt-ai/cli", version: "0.4.0
|
|
2329
|
+
pkg: { name: "@moxt-ai/cli", version: "0.4.0" }
|
|
3022
2330
|
}).notify();
|
|
3023
|
-
var program = createProgram("0.4.0
|
|
2331
|
+
var program = createProgram("0.4.0");
|
|
3024
2332
|
instrumentProgram(program);
|
|
3025
2333
|
installExitHandler();
|
|
3026
2334
|
program.parseAsync(process.argv).then(async () => {
|