@moxt-ai/cli 0.4.0-beta.1 → 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 +1369 -2093
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4,236 +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
|
-
import { readFileSync } from "fs";
|
|
28
|
-
var DEFAULT_USER_HOST = "api.moxt.ai";
|
|
29
|
-
var SANDBOX_ENV_FILE_KEY = "SANDBOX_ENV_FILE";
|
|
30
|
-
var SANDBOX_ENV_KEYS = [
|
|
31
|
-
"BASE_API_HOST",
|
|
32
|
-
"SANDBOX_TOOL_API_TOKEN",
|
|
33
|
-
"MOXT_REPO_PAYLOAD",
|
|
34
|
-
"MOXT_WORKSPACE_ID"
|
|
35
|
-
];
|
|
36
|
-
var RuntimeContextError = class extends Error {
|
|
37
|
-
constructor(message) {
|
|
38
|
-
super(message);
|
|
39
|
-
this.name = "RuntimeContextError";
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
|
-
function resolveRuntimeContext(env = process.env) {
|
|
43
|
-
if (hasAnySandboxEnv(env)) {
|
|
44
|
-
return resolveSandboxRuntimeContext(env);
|
|
45
|
-
}
|
|
46
|
-
const apiKey = readNonEmptyEnv(env, "MOXT_API_KEY");
|
|
47
|
-
if (apiKey) {
|
|
48
|
-
return {
|
|
49
|
-
mode: "user",
|
|
50
|
-
host: readNonEmptyEnv(env, "MOXT_HOST") ?? DEFAULT_USER_HOST,
|
|
51
|
-
apiKey
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
throw new RuntimeContextError("Missing auth. Set sandbox environment variables or MOXT_API_KEY.");
|
|
55
|
-
}
|
|
56
|
-
function resolveSandboxRuntimeContext(env = process.env) {
|
|
57
|
-
const directRepoPayload = readNonEmptyEnv(env, "MOXT_REPO_PAYLOAD");
|
|
58
|
-
const sandboxEnvFile = readNonEmptyEnv(env, SANDBOX_ENV_FILE_KEY);
|
|
59
|
-
const missingKeys = SANDBOX_ENV_KEYS.filter((key) => {
|
|
60
|
-
if (key === "MOXT_REPO_PAYLOAD") return !directRepoPayload && !sandboxEnvFile;
|
|
61
|
-
return !readNonEmptyEnv(env, key);
|
|
62
|
-
});
|
|
63
|
-
if (missingKeys.length > 0) {
|
|
64
|
-
throw new RuntimeContextError(`Incomplete sandbox environment. Missing: ${missingKeys.join(", ")}.`);
|
|
65
|
-
}
|
|
66
|
-
let repoPayload = directRepoPayload;
|
|
67
|
-
if (!repoPayload) {
|
|
68
|
-
if (!sandboxEnvFile) {
|
|
69
|
-
throw new RuntimeContextError("MOXT_REPO_PAYLOAD is required.");
|
|
70
|
-
}
|
|
71
|
-
repoPayload = readRepoPayloadFromSandboxEnvFile(sandboxEnvFile);
|
|
72
|
-
}
|
|
73
|
-
return {
|
|
74
|
-
mode: "sandbox",
|
|
75
|
-
baseApiHost: normalizeBaseApiHost(readRequiredEnv(env, "BASE_API_HOST")),
|
|
76
|
-
sandboxToolApiToken: readRequiredEnv(env, "SANDBOX_TOOL_API_TOKEN"),
|
|
77
|
-
workspaceId: readRequiredEnv(env, "MOXT_WORKSPACE_ID"),
|
|
78
|
-
repoPayload: parseRepoPayload(repoPayload)
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
function readRepoPayloadFromSandboxEnvFile(path2) {
|
|
82
|
-
let parsed;
|
|
83
|
-
try {
|
|
84
|
-
parsed = JSON.parse(readFileSync(path2, "utf8"));
|
|
85
|
-
} catch (error) {
|
|
86
|
-
throw new RuntimeContextError(
|
|
87
|
-
`Failed to read SANDBOX_ENV_FILE: ${error instanceof Error ? error.message : String(error)}`
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
if (!isRecord(parsed)) {
|
|
91
|
-
throw new RuntimeContextError("SANDBOX_ENV_FILE must contain a JSON object.");
|
|
92
|
-
}
|
|
93
|
-
const repoPayload = parsed.MOXT_REPO_PAYLOAD;
|
|
94
|
-
if (typeof repoPayload !== "string" || repoPayload.trim().length === 0) {
|
|
95
|
-
throw new RuntimeContextError("MOXT_REPO_PAYLOAD is missing from SANDBOX_ENV_FILE.");
|
|
96
|
-
}
|
|
97
|
-
return repoPayload.trim();
|
|
98
|
-
}
|
|
99
|
-
function parseRepoPayload(raw) {
|
|
100
|
-
let parsed;
|
|
101
|
-
try {
|
|
102
|
-
parsed = JSON.parse(raw);
|
|
103
|
-
} catch (error) {
|
|
104
|
-
throw new RuntimeContextError(`MOXT_REPO_PAYLOAD must be valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
105
|
-
}
|
|
106
|
-
if (!isRecord(parsed)) {
|
|
107
|
-
throw new RuntimeContextError("MOXT_REPO_PAYLOAD must be a JSON object.");
|
|
108
|
-
}
|
|
109
|
-
const personal = parseRepoEntry(parsed.personal, "personal");
|
|
110
|
-
const teamsValue = parsed.teams;
|
|
111
|
-
if (!Array.isArray(teamsValue)) {
|
|
112
|
-
throw new RuntimeContextError("MOXT_REPO_PAYLOAD.teams must be an array.");
|
|
113
|
-
}
|
|
114
|
-
const teams = teamsValue.map((entry, index) => parseRepoEntry(entry, `teams[${index}]`));
|
|
115
|
-
const workspace = parsed.workspace == null ? null : parseRepoEntry(parsed.workspace, "workspace");
|
|
116
|
-
return { personal, teams, workspace };
|
|
117
|
-
}
|
|
118
|
-
function normalizeBaseApiHost(value) {
|
|
119
|
-
const trimmed = value.trim();
|
|
120
|
-
if (!trimmed) {
|
|
121
|
-
throw new RuntimeContextError("BASE_API_HOST must not be empty.");
|
|
122
|
-
}
|
|
123
|
-
return trimmed.replace(/\/+$/, "");
|
|
124
|
-
}
|
|
125
|
-
function hasAnySandboxEnv(env) {
|
|
126
|
-
return SANDBOX_ENV_KEYS.some((key) => readNonEmptyEnv(env, key) !== void 0);
|
|
127
|
-
}
|
|
128
|
-
function readRequiredEnv(env, key) {
|
|
129
|
-
const value = readNonEmptyEnv(env, key);
|
|
130
|
-
if (!value) {
|
|
131
|
-
throw new RuntimeContextError(`${key} is required.`);
|
|
132
|
-
}
|
|
133
|
-
return value;
|
|
134
|
-
}
|
|
135
|
-
function readNonEmptyEnv(env, key) {
|
|
136
|
-
const value = env[key];
|
|
137
|
-
if (typeof value !== "string") return void 0;
|
|
138
|
-
const trimmed = value.trim();
|
|
139
|
-
return trimmed.length > 0 ? trimmed : void 0;
|
|
140
|
-
}
|
|
141
|
-
function parseRepoEntry(value, path2) {
|
|
142
|
-
if (!isRecord(value)) {
|
|
143
|
-
throw new RuntimeContextError(`MOXT_REPO_PAYLOAD.${path2} must be an object.`);
|
|
144
|
-
}
|
|
145
|
-
const repoId = readStringField(value, "repoId", path2);
|
|
146
|
-
const name = readStringField(value, "name", path2);
|
|
147
|
-
return { repoId, name };
|
|
148
|
-
}
|
|
149
|
-
function readStringField(record2, field, path2) {
|
|
150
|
-
const value = record2[field];
|
|
151
|
-
if (typeof value !== "string" || value.trim().length === 0) {
|
|
152
|
-
throw new RuntimeContextError(`MOXT_REPO_PAYLOAD.${path2}.${field} must be a non-empty string.`);
|
|
153
|
-
}
|
|
154
|
-
return value;
|
|
155
|
-
}
|
|
156
|
-
function isRecord(value) {
|
|
157
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// src/utils/sandbox-path.ts
|
|
161
|
-
var WorkspacePathError = class extends Error {
|
|
162
|
-
constructor(message) {
|
|
163
|
-
super(message);
|
|
164
|
-
this.name = "WorkspacePathError";
|
|
165
|
-
}
|
|
166
|
-
};
|
|
167
|
-
function resolveWorkspacePath(repoPayload, workspacePath) {
|
|
168
|
-
const normalizedPath = normalizeWorkspacePath(workspacePath);
|
|
169
|
-
const [repoName, ...repoPathParts] = normalizedPath.split("/");
|
|
170
|
-
const repoEntries = listRepoEntries(repoPayload);
|
|
171
|
-
const repo = repoEntries.find((entry) => entry.name === repoName);
|
|
172
|
-
if (!repo) {
|
|
173
|
-
throw new WorkspacePathError(`Workspace path must start with one of: ${repoEntries.map((entry) => entry.name).join(", ")}.`);
|
|
174
|
-
}
|
|
175
|
-
return {
|
|
176
|
-
repoId: repo.repoId,
|
|
177
|
-
repoName: repo.name,
|
|
178
|
-
repoPath: repoPathParts.join("/")
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
function listRepoEntries(repoPayload) {
|
|
182
|
-
return [
|
|
183
|
-
repoPayload.personal,
|
|
184
|
-
...repoPayload.teams,
|
|
185
|
-
...repoPayload.workspace ? [repoPayload.workspace] : []
|
|
186
|
-
];
|
|
187
|
-
}
|
|
188
|
-
function normalizeWorkspacePath(workspacePath) {
|
|
189
|
-
const trimmed = workspacePath.trim();
|
|
190
|
-
if (!trimmed) {
|
|
191
|
-
throw new WorkspacePathError("Workspace path must not be empty.");
|
|
192
|
-
}
|
|
193
|
-
if (trimmed.startsWith("/")) {
|
|
194
|
-
throw new WorkspacePathError("Workspace path must be relative.");
|
|
195
|
-
}
|
|
196
|
-
const parts = trimmed.split("/").filter((part) => part.length > 0 && part !== ".");
|
|
197
|
-
if (parts.length === 0) {
|
|
198
|
-
throw new WorkspacePathError("Workspace path must include a repo name.");
|
|
199
|
-
}
|
|
200
|
-
if (parts.some((part) => part === "..")) {
|
|
201
|
-
throw new WorkspacePathError('Workspace path must not contain "..".');
|
|
202
|
-
}
|
|
203
|
-
return parts.join("/");
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// src/commands/auth.ts
|
|
207
|
-
function registerAuthCommand(program2) {
|
|
208
|
-
const auth = program2.command("auth").description("Authentication utilities");
|
|
209
|
-
auth.command("status").description("Show CLI authentication mode").action(() => {
|
|
210
|
-
try {
|
|
211
|
-
const context = resolveRuntimeContext();
|
|
212
|
-
if (context.mode === "sandbox") {
|
|
213
|
-
print(`${colors.bold}Mode${colors.reset}: sandbox`);
|
|
214
|
-
print(`${colors.bold}Workspace${colors.reset}: ${context.workspaceId}`);
|
|
215
|
-
print(`${colors.bold}Base API${colors.reset}: ${context.baseApiHost}`);
|
|
216
|
-
print(`${colors.bold}Repos${colors.reset}:`);
|
|
217
|
-
for (const repo of listRepoEntries(context.repoPayload)) {
|
|
218
|
-
print(` ${repo.name} ${repo.repoId}`);
|
|
219
|
-
}
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
print(`${colors.bold}Mode${colors.reset}: user`);
|
|
223
|
-
print(`${colors.bold}Host${colors.reset}: ${context.host}`);
|
|
224
|
-
} catch (error) {
|
|
225
|
-
if (error instanceof RuntimeContextError) {
|
|
226
|
-
printError(error.message);
|
|
227
|
-
process.exit(1);
|
|
228
|
-
}
|
|
229
|
-
throw error;
|
|
230
|
-
}
|
|
231
|
-
});
|
|
232
|
-
}
|
|
7
|
+
import { Command } from "commander";
|
|
233
8
|
|
|
234
9
|
// src/commands/file.ts
|
|
235
10
|
import * as fs2 from "fs";
|
|
236
11
|
|
|
12
|
+
// src/commands/file-comment.ts
|
|
13
|
+
import * as fs from "fs";
|
|
14
|
+
import { TextDecoder } from "util";
|
|
15
|
+
|
|
237
16
|
// src/utils/file-selector.ts
|
|
238
17
|
var SpaceOptionsError = class extends Error {
|
|
239
18
|
constructor(message) {
|
|
@@ -374,7 +153,7 @@ function getApiKeyOrUndefined() {
|
|
|
374
153
|
|
|
375
154
|
// src/utils/http.ts
|
|
376
155
|
function getUserAgent() {
|
|
377
|
-
return `moxt-cli/${"0.4.0
|
|
156
|
+
return `moxt-cli/${"0.4.0"} (${platform()}/${arch()}) node/${process.versions.node}`;
|
|
378
157
|
}
|
|
379
158
|
async function fetchApi(method, path2, body) {
|
|
380
159
|
const apiKey = getApiKey();
|
|
@@ -414,6 +193,9 @@ async function httpPut(path2, body) {
|
|
|
414
193
|
async function httpPost(path2, body) {
|
|
415
194
|
return request("POST", path2, body);
|
|
416
195
|
}
|
|
196
|
+
async function httpPatch(path2, body) {
|
|
197
|
+
return request("PATCH", path2, body);
|
|
198
|
+
}
|
|
417
199
|
async function httpDelete(path2, body) {
|
|
418
200
|
return request("DELETE", path2, body);
|
|
419
201
|
}
|
|
@@ -427,6 +209,265 @@ async function httpRaw(method, path2, body) {
|
|
|
427
209
|
};
|
|
428
210
|
}
|
|
429
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
|
+
|
|
430
471
|
// src/utils/search-options.ts
|
|
431
472
|
var SearchOptionsError = class extends Error {
|
|
432
473
|
constructor(message) {
|
|
@@ -468,7 +509,7 @@ function parseTeammateId(raw) {
|
|
|
468
509
|
}
|
|
469
510
|
|
|
470
511
|
// src/utils/run-or-exit.ts
|
|
471
|
-
function
|
|
512
|
+
function runOrExit2(fn) {
|
|
472
513
|
try {
|
|
473
514
|
return fn();
|
|
474
515
|
} catch (err) {
|
|
@@ -480,279 +521,38 @@ function runOrExit(fn) {
|
|
|
480
521
|
}
|
|
481
522
|
}
|
|
482
523
|
|
|
483
|
-
// src/
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
Authorization: `Bearer ${this.context.sandboxToolApiToken}`
|
|
507
|
-
},
|
|
508
|
-
body: body === void 0 ? void 0 : JSON.stringify(body),
|
|
509
|
-
signal
|
|
510
|
-
});
|
|
511
|
-
const contentType = response.headers.get("content-type");
|
|
512
|
-
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
|
+
);
|
|
513
547
|
if (!response.ok) {
|
|
514
|
-
|
|
548
|
+
stopSpinner(false, "Failed");
|
|
549
|
+
printError(`Error [${response.status}]: ${JSON.stringify(response.data)}`);
|
|
550
|
+
process.exit(1);
|
|
515
551
|
}
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
}
|
|
521
|
-
};
|
|
522
|
-
|
|
523
|
-
// src/utils/sandbox-file.ts
|
|
524
|
-
import { createHash } from "crypto";
|
|
525
|
-
var MFS_FILE_TYPE_REGULAR = 1;
|
|
526
|
-
var MFS_FILE_SIZE_LIMIT_BYTES = 10 * 1024 * 1024;
|
|
527
|
-
var MFS_FILE_SIZE_LIMIT_LABEL = `${MFS_FILE_SIZE_LIMIT_BYTES / (1024 * 1024)}MB`;
|
|
528
|
-
var SandboxFileError = class extends Error {
|
|
529
|
-
constructor(message, code) {
|
|
530
|
-
super(message);
|
|
531
|
-
this.code = code;
|
|
532
|
-
this.name = "SandboxFileError";
|
|
533
|
-
}
|
|
534
|
-
code;
|
|
535
|
-
};
|
|
536
|
-
async function readSandboxFile(context, workspacePath) {
|
|
537
|
-
const resolved = resolveSandboxFilePath(context, workspacePath);
|
|
538
|
-
const tree = await fetchRepoTree(context, resolved);
|
|
539
|
-
const entry = findTreeEntry(tree, resolved);
|
|
540
|
-
if (!entry) {
|
|
541
|
-
throw new SandboxFileError(`${workspacePath} does not exist`, "not-found");
|
|
542
|
-
}
|
|
543
|
-
if (entry.type === "tree") {
|
|
544
|
-
throw new SandboxFileError(`${workspacePath} is a directory`, "directory");
|
|
545
|
-
}
|
|
546
|
-
if (!entry.sha) {
|
|
547
|
-
throw new SandboxFileError(`${workspacePath} does not have downloadable content`, "remote");
|
|
548
|
-
}
|
|
549
|
-
if (entry.size !== null && entry.size > MFS_FILE_SIZE_LIMIT_BYTES) {
|
|
550
|
-
throw new SandboxFileError(`File too large (max ${MFS_FILE_SIZE_LIMIT_LABEL}): ${workspacePath}`, "too-large");
|
|
551
|
-
}
|
|
552
|
-
const client = new SandboxMoxtClient(context);
|
|
553
|
-
const presign = await client.request(
|
|
554
|
-
"POST",
|
|
555
|
-
`/agent/api/pipeline-sandbox/mfs/${encodeURIComponent(resolved.repoId)}/presign-download`,
|
|
556
|
-
{ hashes: [entry.sha] }
|
|
557
|
-
);
|
|
558
|
-
const url = presign.data.urls[entry.sha];
|
|
559
|
-
if (!url) {
|
|
560
|
-
throw new SandboxFileError(`No download URL returned for ${workspacePath}`, "remote");
|
|
561
|
-
}
|
|
562
|
-
const response = await fetch(url);
|
|
563
|
-
if (!response.ok) {
|
|
564
|
-
throw new SandboxFileError(`Download failed [${response.status}]`, "remote");
|
|
565
|
-
}
|
|
566
|
-
const content = await response.text();
|
|
567
|
-
const data = Buffer.from(content, "utf8");
|
|
568
|
-
if (data.length > MFS_FILE_SIZE_LIMIT_BYTES) {
|
|
569
|
-
throw new SandboxFileError(`File too large (max ${MFS_FILE_SIZE_LIMIT_LABEL}): ${workspacePath}`, "too-large");
|
|
570
|
-
}
|
|
571
|
-
const actualSha = createHash("sha256").update(data).digest("hex");
|
|
572
|
-
if (actualSha !== entry.sha) {
|
|
573
|
-
throw new SandboxFileError(`Downloaded content hash mismatch: ${workspacePath}`, "remote");
|
|
574
|
-
}
|
|
575
|
-
return {
|
|
576
|
-
path: workspacePath,
|
|
577
|
-
content,
|
|
578
|
-
sha: entry.sha,
|
|
579
|
-
size: data.length,
|
|
580
|
-
fileId: entry.fileId,
|
|
581
|
-
fileType: entry.fileType
|
|
582
|
-
};
|
|
583
|
-
}
|
|
584
|
-
async function writeSandboxFile(context, workspacePath, content, policy) {
|
|
585
|
-
const data = Buffer.from(content, "utf8");
|
|
586
|
-
if (data.length > MFS_FILE_SIZE_LIMIT_BYTES) {
|
|
587
|
-
throw new SandboxFileError(`File too large (max ${MFS_FILE_SIZE_LIMIT_LABEL}): ${workspacePath}`, "too-large");
|
|
588
|
-
}
|
|
589
|
-
const resolved = resolveSandboxFilePath(context, workspacePath);
|
|
590
|
-
const tree = await fetchRepoTree(context, resolved);
|
|
591
|
-
const existing = findTreeEntry(tree, resolved);
|
|
592
|
-
if (existing?.type === "tree") {
|
|
593
|
-
throw new SandboxFileError(`${workspacePath} is a directory`, "directory");
|
|
594
|
-
}
|
|
595
|
-
if (existing && !existing.sha) {
|
|
596
|
-
throw new SandboxFileError(`${workspacePath} does not have base content hash`, "remote");
|
|
597
|
-
}
|
|
598
|
-
const baseContentHash = resolveBaseContentHash(workspacePath, existing, policy);
|
|
599
|
-
const contentHash = createHash("sha256").update(data).digest("hex");
|
|
600
|
-
const client = new SandboxMoxtClient(context);
|
|
601
|
-
const presign = await client.request(
|
|
602
|
-
"POST",
|
|
603
|
-
`/agent/api/pipeline-sandbox/mfs/${encodeURIComponent(resolved.repoId)}/presign-upload`,
|
|
604
|
-
{ hashes: [contentHash] }
|
|
605
|
-
);
|
|
606
|
-
const uploadUrl = presign.data.urls[contentHash];
|
|
607
|
-
if (!uploadUrl) {
|
|
608
|
-
throw new SandboxFileError(`No upload URL returned for ${workspacePath}`, "remote");
|
|
609
|
-
}
|
|
610
|
-
const upload = await fetch(uploadUrl, {
|
|
611
|
-
method: "PUT",
|
|
612
|
-
headers: { "Content-Type": "application/octet-stream" },
|
|
613
|
-
body: new Uint8Array(data)
|
|
614
|
-
});
|
|
615
|
-
if (!upload.ok) {
|
|
616
|
-
throw new SandboxFileError(`Upload failed [${upload.status}]`, "remote");
|
|
617
|
-
}
|
|
618
|
-
const action = existing ? "update" : "create";
|
|
619
|
-
await client.request(
|
|
620
|
-
"POST",
|
|
621
|
-
"/agent/api/pipeline-sandbox/mfs-workspace/unified-push",
|
|
622
|
-
{
|
|
623
|
-
message: `moxt file write ${workspacePath}`,
|
|
624
|
-
repoPushes: [
|
|
625
|
-
{
|
|
626
|
-
repoId: resolved.repoId,
|
|
627
|
-
changes: [
|
|
628
|
-
{
|
|
629
|
-
action,
|
|
630
|
-
...existing?.fileId ? { fileId: existing.fileId } : {},
|
|
631
|
-
path: resolved.repoPath,
|
|
632
|
-
contentHash,
|
|
633
|
-
...baseContentHash ? { baseContentHash } : {},
|
|
634
|
-
size: data.length,
|
|
635
|
-
fileType: MFS_FILE_TYPE_REGULAR
|
|
636
|
-
}
|
|
637
|
-
]
|
|
638
|
-
}
|
|
639
|
-
],
|
|
640
|
-
crossRepoMoves: []
|
|
641
|
-
}
|
|
642
|
-
);
|
|
643
|
-
return { path: workspacePath, created: !existing };
|
|
644
|
-
}
|
|
645
|
-
function resolveBaseContentHash(workspacePath, existing, policy) {
|
|
646
|
-
if (policy.type === "force") return void 0;
|
|
647
|
-
if (policy.type === "use-current-sha") return existing?.sha ?? void 0;
|
|
648
|
-
if (policy.type === "create-only") {
|
|
649
|
-
if (!existing) return void 0;
|
|
650
|
-
throw new SandboxFileError(
|
|
651
|
-
`Existing file requires --base-sha or --force: ${workspacePath} (actual ${existing.sha})`,
|
|
652
|
-
"conflict"
|
|
653
|
-
);
|
|
654
|
-
}
|
|
655
|
-
if (existing?.sha === policy.sha) return policy.sha;
|
|
656
|
-
throw new SandboxFileError(
|
|
657
|
-
`File version conflict: ${workspacePath} (expected ${policy.sha}, actual ${existing?.sha ?? "missing"})`,
|
|
658
|
-
"conflict"
|
|
659
|
-
);
|
|
660
|
-
}
|
|
661
|
-
function resolveSandboxFilePath(context, workspacePath) {
|
|
662
|
-
let resolved;
|
|
663
|
-
try {
|
|
664
|
-
resolved = resolveWorkspacePath(context.repoPayload, workspacePath);
|
|
665
|
-
} catch (error) {
|
|
666
|
-
throw new SandboxFileError(error instanceof Error ? error.message : String(error), "invalid-path");
|
|
667
|
-
}
|
|
668
|
-
if (!resolved.repoPath) {
|
|
669
|
-
throw new SandboxFileError(`${workspacePath} is a directory`, "directory");
|
|
670
|
-
}
|
|
671
|
-
return resolved;
|
|
672
|
-
}
|
|
673
|
-
async function fetchRepoTree(context, resolved) {
|
|
674
|
-
const client = new SandboxMoxtClient(context);
|
|
675
|
-
const params = new URLSearchParams({ entryFilter: "paths", path: resolved.repoPath });
|
|
676
|
-
const response = await client.request(
|
|
677
|
-
"GET",
|
|
678
|
-
`/agent/api/pipeline-sandbox/mfs/${encodeURIComponent(resolved.repoId)}/tree?${params.toString()}`
|
|
679
|
-
);
|
|
680
|
-
return response.data;
|
|
681
|
-
}
|
|
682
|
-
function findTreeEntry(tree, resolved) {
|
|
683
|
-
return tree.entries.find((entry) => entry.path === resolved.repoPath);
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
// src/utils/spinner.ts
|
|
687
|
-
var frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
688
|
-
var intervalId = null;
|
|
689
|
-
var frameIndex = 0;
|
|
690
|
-
function startSpinner(message) {
|
|
691
|
-
frameIndex = 0;
|
|
692
|
-
process.stdout.write(`${frames[frameIndex]} ${message}`);
|
|
693
|
-
intervalId = setInterval(() => {
|
|
694
|
-
frameIndex = (frameIndex + 1) % frames.length;
|
|
695
|
-
process.stdout.write(`\r${frames[frameIndex]} ${message}`);
|
|
696
|
-
}, 80);
|
|
697
|
-
}
|
|
698
|
-
function stopSpinner(success, message) {
|
|
699
|
-
if (intervalId) {
|
|
700
|
-
clearInterval(intervalId);
|
|
701
|
-
intervalId = null;
|
|
702
|
-
}
|
|
703
|
-
const symbol = success ? "\x1B[32m\u2713\x1B[0m" : "\x1B[31m\u2717\x1B[0m";
|
|
704
|
-
process.stdout.write(`\x1B[2K\r${symbol} ${message ?? ""}
|
|
705
|
-
`);
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
// src/utils/write-content.ts
|
|
709
|
-
import * as fs from "fs";
|
|
710
|
-
function readWriteCommandContentOrExit(options, readContentFile, readStdin = () => fs.readFileSync(0, "utf8")) {
|
|
711
|
-
const inputModes = [options.content !== void 0, options.contentFile !== void 0, options.stdin === true].filter(Boolean);
|
|
712
|
-
if (inputModes.length > 1) {
|
|
713
|
-
printError("Only one of --content, --content-file, or --stdin can be used");
|
|
714
|
-
process.exit(1);
|
|
715
|
-
}
|
|
716
|
-
if (options.content !== void 0) {
|
|
717
|
-
return options.content;
|
|
718
|
-
}
|
|
719
|
-
if (options.contentFile !== void 0) {
|
|
720
|
-
return readContentFile(options.contentFile);
|
|
721
|
-
}
|
|
722
|
-
return readStdin();
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
// src/commands/file.ts
|
|
726
|
-
function addSpaceOptions(cmd) {
|
|
727
|
-
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)");
|
|
728
|
-
}
|
|
729
|
-
function registerFileCommand(program2) {
|
|
730
|
-
const file = program2.command("file").description("File operations");
|
|
731
|
-
addSpaceOptions(
|
|
732
|
-
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")
|
|
733
|
-
).action(async (query, options) => {
|
|
734
|
-
const spaceParams = runOrExit(() => resolveSpaceSelector(options));
|
|
735
|
-
const limit = runOrExit(() => parseSearchLimit(options.limit, 30, 80));
|
|
736
|
-
const mode = runOrExit(() => parseSearchMode(options.mode));
|
|
737
|
-
startSpinner("Searching files...");
|
|
738
|
-
const response = await httpPost(
|
|
739
|
-
`/workspaces/${options.workspace}/files/search`,
|
|
740
|
-
{
|
|
741
|
-
query,
|
|
742
|
-
limit,
|
|
743
|
-
mode,
|
|
744
|
-
...spaceParams
|
|
745
|
-
}
|
|
746
|
-
);
|
|
747
|
-
if (!response.ok) {
|
|
748
|
-
stopSpinner(false, "Failed");
|
|
749
|
-
printError(`Error [${response.status}]: ${JSON.stringify(response.data)}`);
|
|
750
|
-
process.exit(1);
|
|
751
|
-
}
|
|
752
|
-
const { items } = response.data;
|
|
753
|
-
if (items.length === 0) {
|
|
754
|
-
stopSpinner(true, "No files found.");
|
|
755
|
-
return;
|
|
552
|
+
const { items } = response.data;
|
|
553
|
+
if (items.length === 0) {
|
|
554
|
+
stopSpinner(true, "No files found.");
|
|
555
|
+
return;
|
|
756
556
|
}
|
|
757
557
|
stopSpinner(true, `Found ${items.length} file(s)`);
|
|
758
558
|
for (const item of items) {
|
|
@@ -768,7 +568,7 @@ function registerFileCommand(program2) {
|
|
|
768
568
|
addSpaceOptions(
|
|
769
569
|
file.command("get-url").description("Resolve the shareable browser URL for a file").requiredOption("-p, --path <path>", "File path")
|
|
770
570
|
).action(async (options) => {
|
|
771
|
-
const spaceParams =
|
|
571
|
+
const spaceParams = runOrExit2(() => resolveSpaceSelector(options));
|
|
772
572
|
const qs = buildFileQueryString(options.path, spaceParams);
|
|
773
573
|
startSpinner("Resolving URL...");
|
|
774
574
|
const response = await httpGet(
|
|
@@ -791,7 +591,7 @@ function registerFileCommand(program2) {
|
|
|
791
591
|
addSpaceOptions(
|
|
792
592
|
file.command("list").description("List directory contents").option("-p, --path <path>", "Directory path", "/")
|
|
793
593
|
).action(async (options) => {
|
|
794
|
-
const spaceParams =
|
|
594
|
+
const spaceParams = runOrExit2(() => resolveSpaceSelector(options));
|
|
795
595
|
const qs = buildFileQueryString(options.path, spaceParams);
|
|
796
596
|
startSpinner("Listing files...");
|
|
797
597
|
const response = await httpGet(
|
|
@@ -820,40 +620,8 @@ function registerFileCommand(program2) {
|
|
|
820
620
|
}
|
|
821
621
|
}
|
|
822
622
|
});
|
|
823
|
-
file.command("read").description("Read file content").
|
|
824
|
-
const
|
|
825
|
-
if (sandboxRuntime) {
|
|
826
|
-
if (options.url) {
|
|
827
|
-
printError("--url cannot be used in sandbox mode");
|
|
828
|
-
process.exit(1);
|
|
829
|
-
}
|
|
830
|
-
const path2 = workspacePath ?? options.path;
|
|
831
|
-
if (!path2) {
|
|
832
|
-
printError("Workspace path is required in sandbox mode");
|
|
833
|
-
process.exit(1);
|
|
834
|
-
}
|
|
835
|
-
if (!options.json) startSpinner("Reading file...");
|
|
836
|
-
try {
|
|
837
|
-
const result = await readSandboxFile(sandboxRuntime, path2);
|
|
838
|
-
if (options.json) {
|
|
839
|
-
print(JSON.stringify(result, null, 2));
|
|
840
|
-
} else {
|
|
841
|
-
stopSpinner(true, "Done");
|
|
842
|
-
print(result.content);
|
|
843
|
-
}
|
|
844
|
-
} catch (error) {
|
|
845
|
-
handleSandboxFileError(error, path2);
|
|
846
|
-
}
|
|
847
|
-
return;
|
|
848
|
-
}
|
|
849
|
-
if (options.json) {
|
|
850
|
-
printError("--json is only available for sandbox workspace paths");
|
|
851
|
-
process.exit(1);
|
|
852
|
-
}
|
|
853
|
-
if (workspacePath && !options.path) {
|
|
854
|
-
options.path = workspacePath;
|
|
855
|
-
}
|
|
856
|
-
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));
|
|
857
625
|
startSpinner("Reading file...");
|
|
858
626
|
let response;
|
|
859
627
|
let displayPath;
|
|
@@ -864,7 +632,7 @@ function registerFileCommand(program2) {
|
|
|
864
632
|
);
|
|
865
633
|
} else {
|
|
866
634
|
displayPath = mode.path;
|
|
867
|
-
const spaceParams =
|
|
635
|
+
const spaceParams = runOrExit2(() => resolveSpaceSelector(options));
|
|
868
636
|
const qs = buildFileQueryString(mode.path, spaceParams);
|
|
869
637
|
response = await httpGet(
|
|
870
638
|
`/workspaces/${mode.workspace}/files/read${qs}`
|
|
@@ -888,37 +656,29 @@ function registerFileCommand(program2) {
|
|
|
888
656
|
stopSpinner(true, "Done");
|
|
889
657
|
print(content ?? "");
|
|
890
658
|
});
|
|
891
|
-
file.command("put").description("Upload a file").
|
|
892
|
-
async (
|
|
893
|
-
const
|
|
894
|
-
if (
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
process.exit(1);
|
|
898
|
-
}
|
|
899
|
-
const path2 = workspacePath ?? options.path;
|
|
900
|
-
if (!path2) {
|
|
901
|
-
printError("Workspace path is required in sandbox mode");
|
|
902
|
-
process.exit(1);
|
|
903
|
-
}
|
|
904
|
-
const { content: content2 } = readLocalTextFileOrExit(options.localPath);
|
|
905
|
-
startSpinner("Uploading...");
|
|
906
|
-
try {
|
|
907
|
-
const response2 = await writeSandboxFile(sandboxRuntime, path2, content2, {
|
|
908
|
-
type: "use-current-sha"
|
|
909
|
-
});
|
|
910
|
-
const action2 = response2.created ? "Created" : "Updated";
|
|
911
|
-
stopSpinner(true, `${action2}: ${response2.path}`);
|
|
912
|
-
} catch (error) {
|
|
913
|
-
handleSandboxFileError(error, path2);
|
|
914
|
-
}
|
|
915
|
-
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);
|
|
916
665
|
}
|
|
917
|
-
|
|
918
|
-
|
|
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);
|
|
919
680
|
}
|
|
920
|
-
const
|
|
921
|
-
const { content } = readLocalTextFileOrExit(options.localPath);
|
|
681
|
+
const content = buffer2.toString("utf8");
|
|
922
682
|
startSpinner("Uploading...");
|
|
923
683
|
let response;
|
|
924
684
|
let displayPath;
|
|
@@ -930,7 +690,7 @@ function registerFileCommand(program2) {
|
|
|
930
690
|
});
|
|
931
691
|
} else {
|
|
932
692
|
displayPath = mode.path;
|
|
933
|
-
const spaceParams =
|
|
693
|
+
const spaceParams = runOrExit2(() => resolveSpaceSelector(options));
|
|
934
694
|
response = await httpPut(
|
|
935
695
|
`/workspaces/${mode.workspace}/files/write`,
|
|
936
696
|
{
|
|
@@ -960,28 +720,11 @@ function registerFileCommand(program2) {
|
|
|
960
720
|
stopSpinner(true, `${action}: ${response.data.path}`);
|
|
961
721
|
}
|
|
962
722
|
);
|
|
963
|
-
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) => {
|
|
964
|
-
const runtime = resolveSandboxRuntimeContextOrExit();
|
|
965
|
-
if (!runtime) {
|
|
966
|
-
printError("file write is only available in sandbox mode");
|
|
967
|
-
process.exit(1);
|
|
968
|
-
}
|
|
969
|
-
const policy = resolveSandboxFileWritePolicyOrExit(options);
|
|
970
|
-
const content = readWriteCommandContentOrExit(options, (path2) => readLocalTextFileOrExit(path2).content);
|
|
971
|
-
startSpinner("Writing file...");
|
|
972
|
-
try {
|
|
973
|
-
const response = await writeSandboxFile(runtime, workspacePath, content, policy);
|
|
974
|
-
const action = response.created ? "Created" : "Updated";
|
|
975
|
-
stopSpinner(true, `${action}: ${response.path}`);
|
|
976
|
-
} catch (error) {
|
|
977
|
-
handleSandboxFileError(error, workspacePath);
|
|
978
|
-
}
|
|
979
|
-
});
|
|
980
723
|
addSpaceOptions(
|
|
981
724
|
file.command("mkdir").description("Create a directory").requiredOption("-p, --path <path>", "Directory path").option("-r, --recursive", "Create parent directories if needed")
|
|
982
725
|
).action(
|
|
983
726
|
async (options) => {
|
|
984
|
-
const spaceParams =
|
|
727
|
+
const spaceParams = runOrExit2(() => resolveSpaceSelector(options));
|
|
985
728
|
startSpinner("Creating directory...");
|
|
986
729
|
const response = await httpPost(
|
|
987
730
|
`/workspaces/${options.workspace}/files/mkdir`,
|
|
@@ -1010,7 +753,7 @@ function registerFileCommand(program2) {
|
|
|
1010
753
|
file.command("del").description("Delete a file or empty directory").requiredOption("-p, --path <path>", "File or directory path")
|
|
1011
754
|
).action(
|
|
1012
755
|
async (options) => {
|
|
1013
|
-
const spaceParams =
|
|
756
|
+
const spaceParams = runOrExit2(() => resolveSpaceSelector(options));
|
|
1014
757
|
startSpinner("Deleting...");
|
|
1015
758
|
const response = await httpDelete(
|
|
1016
759
|
`/workspaces/${options.workspace}/files/delete`,
|
|
@@ -1034,90 +777,14 @@ function registerFileCommand(program2) {
|
|
|
1034
777
|
}
|
|
1035
778
|
);
|
|
1036
779
|
}
|
|
1037
|
-
function resolveSandboxFileWritePolicyOrExit(options) {
|
|
1038
|
-
if (options.baseSha && options.force) {
|
|
1039
|
-
printError("Only one of --base-sha or --force can be used");
|
|
1040
|
-
process.exit(1);
|
|
1041
|
-
}
|
|
1042
|
-
if (options.baseSha) {
|
|
1043
|
-
if (!/^[a-f0-9]{64}$/.test(options.baseSha)) {
|
|
1044
|
-
printError("--base-sha must be a lowercase SHA-256 hash");
|
|
1045
|
-
process.exit(1);
|
|
1046
|
-
}
|
|
1047
|
-
return { type: "base-sha", sha: options.baseSha };
|
|
1048
|
-
}
|
|
1049
|
-
return options.force ? { type: "force" } : { type: "create-only" };
|
|
1050
|
-
}
|
|
1051
|
-
function resolveRuntimeContextOrExit() {
|
|
1052
|
-
try {
|
|
1053
|
-
return resolveRuntimeContext();
|
|
1054
|
-
} catch (error) {
|
|
1055
|
-
if (error instanceof RuntimeContextError) {
|
|
1056
|
-
printError(error.message);
|
|
1057
|
-
process.exit(1);
|
|
1058
|
-
}
|
|
1059
|
-
throw error;
|
|
1060
|
-
}
|
|
1061
|
-
}
|
|
1062
|
-
function resolveSandboxRuntimeContextOrExit() {
|
|
1063
|
-
if (!SANDBOX_ENV_KEYS.some((key) => (process.env[key] ?? "").trim().length > 0)) {
|
|
1064
|
-
return void 0;
|
|
1065
|
-
}
|
|
1066
|
-
const context = resolveRuntimeContextOrExit();
|
|
1067
|
-
return context.mode === "sandbox" ? context : void 0;
|
|
1068
|
-
}
|
|
1069
|
-
function readLocalTextFileOrExit(localPath) {
|
|
1070
|
-
if (!fs2.existsSync(localPath)) {
|
|
1071
|
-
print(`${colors.red}\u2620 Local file not found: ${localPath}${colors.reset}`);
|
|
1072
|
-
process.exit(1);
|
|
1073
|
-
}
|
|
1074
|
-
const stats = fs2.statSync(localPath);
|
|
1075
|
-
if (!stats.isFile()) {
|
|
1076
|
-
print(`${colors.red}\u2620 Not a file: ${localPath}${colors.reset}`);
|
|
1077
|
-
process.exit(1);
|
|
1078
|
-
}
|
|
1079
|
-
if (stats.size > MFS_FILE_SIZE_LIMIT_BYTES) {
|
|
1080
|
-
print(`${colors.red}\u2620 File too large (max ${MFS_FILE_SIZE_LIMIT_LABEL}): ${localPath}${colors.reset}`);
|
|
1081
|
-
process.exit(1);
|
|
1082
|
-
}
|
|
1083
|
-
const buffer2 = fs2.readFileSync(localPath);
|
|
1084
|
-
if (isBinaryContent(buffer2)) {
|
|
1085
|
-
print(`${colors.red}\u2620 Binary files are not allowed. Only text files can be uploaded.${colors.reset}`);
|
|
1086
|
-
process.exit(1);
|
|
1087
|
-
}
|
|
1088
|
-
return { content: buffer2.toString("utf8") };
|
|
1089
|
-
}
|
|
1090
|
-
function handleSandboxFileError(error, displayPath) {
|
|
1091
|
-
stopSpinner(false, "Failed");
|
|
1092
|
-
if (error instanceof SandboxFileError) {
|
|
1093
|
-
if (error.code === "not-found") {
|
|
1094
|
-
print(`${colors.red}\u2620 ${displayPath} does not exist${colors.reset}`);
|
|
1095
|
-
} else if (error.code === "directory") {
|
|
1096
|
-
print(`${colors.red}\u2620 ${displayPath} is a directory${colors.reset}`);
|
|
1097
|
-
} else {
|
|
1098
|
-
printError(error.message);
|
|
1099
|
-
}
|
|
1100
|
-
process.exit(1);
|
|
1101
|
-
}
|
|
1102
|
-
if (error instanceof SandboxApiError) {
|
|
1103
|
-
printError(`Sandbox API error [${error.status}]: ${error.body}`);
|
|
1104
|
-
process.exit(1);
|
|
1105
|
-
}
|
|
1106
|
-
if (error instanceof Error) {
|
|
1107
|
-
printError(`Sandbox file operation failed: ${error.message}`);
|
|
1108
|
-
process.exit(1);
|
|
1109
|
-
}
|
|
1110
|
-
printError(`Sandbox file operation failed: ${String(error)}`);
|
|
1111
|
-
process.exit(1);
|
|
1112
|
-
}
|
|
1113
780
|
|
|
1114
781
|
// src/commands/memory.ts
|
|
1115
782
|
function registerMemoryCommand(program2) {
|
|
1116
783
|
const memory = program2.command("memory").description("Memory operations");
|
|
1117
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(
|
|
1118
785
|
async (query, options) => {
|
|
1119
|
-
const limit =
|
|
1120
|
-
const teammateId =
|
|
786
|
+
const limit = runOrExit2(() => parseSearchLimit(options.limit, 5, 10));
|
|
787
|
+
const teammateId = runOrExit2(() => parseTeammateId(options.teammateId));
|
|
1121
788
|
startSpinner("Searching memory...");
|
|
1122
789
|
const response = await httpPost(
|
|
1123
790
|
`/workspaces/${options.workspace}/memory/search`,
|
|
@@ -1223,7 +890,7 @@ function isAllowedMiniappDbUserEmail(email) {
|
|
|
1223
890
|
}
|
|
1224
891
|
|
|
1225
892
|
// src/commands/miniapp.ts
|
|
1226
|
-
function
|
|
893
|
+
function runOrExit3(fn) {
|
|
1227
894
|
try {
|
|
1228
895
|
return fn();
|
|
1229
896
|
} catch (err) {
|
|
@@ -1283,14 +950,14 @@ function registerMiniappCommand(program2) {
|
|
|
1283
950
|
addMiniappDbTargetOptions(
|
|
1284
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")
|
|
1285
952
|
).action(async (postgrestPath, options) => {
|
|
1286
|
-
const body =
|
|
953
|
+
const body = runOrExit3(() => parseJsonBody(options.body ?? ""));
|
|
1287
954
|
await requestDataApi("POST", postgrestPath, options, body);
|
|
1288
955
|
});
|
|
1289
956
|
addMiniappDbTargetOptions(
|
|
1290
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")
|
|
1291
958
|
).action(async (postgrestPath, options) => {
|
|
1292
|
-
|
|
1293
|
-
const body =
|
|
959
|
+
runOrExit3(() => assertPostgrestFilter(postgrestPath, "PATCH"));
|
|
960
|
+
const body = runOrExit3(() => parseJsonBody(options.body ?? ""));
|
|
1294
961
|
await requestDataApi("PATCH", postgrestPath, options, body);
|
|
1295
962
|
});
|
|
1296
963
|
addMiniappDbTargetOptions(
|
|
@@ -1300,1558 +967,1167 @@ function registerMiniappCommand(program2) {
|
|
|
1300
967
|
printError("Error: delete requires --yes.");
|
|
1301
968
|
process.exit(1);
|
|
1302
969
|
}
|
|
1303
|
-
|
|
970
|
+
runOrExit3(() => assertPostgrestFilter(postgrestPath, "DELETE"));
|
|
1304
971
|
await requestDataApi("DELETE", postgrestPath, options);
|
|
1305
972
|
});
|
|
1306
973
|
}
|
|
1307
974
|
|
|
1308
|
-
// src/
|
|
1309
|
-
import
|
|
1310
|
-
import
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
import { lstat, mkdir as mkdir2, readFile as readFile2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
1315
|
-
import { dirname as dirname2, isAbsolute as isAbsolute2, relative, resolve } from "path";
|
|
1316
|
-
|
|
1317
|
-
// src/utils/checkout-state.ts
|
|
1318
|
-
import { randomUUID } from "crypto";
|
|
1319
|
-
import { homedir } from "os";
|
|
1320
|
-
import { dirname, isAbsolute, join, posix } from "path";
|
|
1321
|
-
import { mkdir, readFile, rename, rm, writeFile } from "fs/promises";
|
|
1322
|
-
var CheckoutStateError = class extends Error {
|
|
1323
|
-
constructor(message) {
|
|
1324
|
-
super(message);
|
|
1325
|
-
this.name = "CheckoutStateError";
|
|
1326
|
-
}
|
|
1327
|
-
};
|
|
1328
|
-
function getCheckoutStatePath() {
|
|
1329
|
-
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");
|
|
1330
981
|
}
|
|
1331
|
-
|
|
1332
|
-
let raw;
|
|
1333
|
-
try {
|
|
1334
|
-
raw = await readFile(statePath, "utf8");
|
|
1335
|
-
} catch (error) {
|
|
1336
|
-
if (isNodeError(error) && error.code === "ENOENT") return void 0;
|
|
1337
|
-
throw new CheckoutStateError(`Failed to read checkout state: ${errorMessage(error)}`);
|
|
1338
|
-
}
|
|
1339
|
-
let value;
|
|
982
|
+
function readTelemetryConfig() {
|
|
1340
983
|
try {
|
|
1341
|
-
|
|
984
|
+
const raw = fs3.readFileSync(getConfigPath(), "utf8");
|
|
985
|
+
const parsed = JSON.parse(raw);
|
|
986
|
+
return parsed.telemetry ?? {};
|
|
1342
987
|
} catch {
|
|
1343
|
-
|
|
988
|
+
return {};
|
|
1344
989
|
}
|
|
1345
|
-
return validateCheckoutState(value);
|
|
1346
990
|
}
|
|
1347
|
-
|
|
1348
|
-
const
|
|
1349
|
-
|
|
1350
|
-
const temporaryPath = `${statePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
1351
|
-
await mkdir(stateDir, { recursive: true });
|
|
991
|
+
function writeTelemetryConfig(update) {
|
|
992
|
+
const configPath = getConfigPath();
|
|
993
|
+
let existing = {};
|
|
1352
994
|
try {
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
} catch (error) {
|
|
1357
|
-
await rm(temporaryPath, { force: true });
|
|
1358
|
-
throw new CheckoutStateError(`Failed to save checkout state: ${errorMessage(error)}`);
|
|
1359
|
-
}
|
|
1360
|
-
}
|
|
1361
|
-
function isCheckoutPathIncluded(state, workspacePath) {
|
|
1362
|
-
if (state.mode === "full") return true;
|
|
1363
|
-
return state.checkedOutScopes.some(
|
|
1364
|
-
(scope) => workspacePath === scope || workspacePath.startsWith(`${scope}/`)
|
|
1365
|
-
);
|
|
1366
|
-
}
|
|
1367
|
-
function doesCheckoutPathIntersect(state, workspacePath) {
|
|
1368
|
-
if (state.mode === "full") return true;
|
|
1369
|
-
return state.checkedOutScopes.some(
|
|
1370
|
-
(scope) => workspacePath === scope || workspacePath.startsWith(`${scope}/`) || scope.startsWith(`${workspacePath}/`)
|
|
1371
|
-
);
|
|
1372
|
-
}
|
|
1373
|
-
function validateCheckoutState(value) {
|
|
1374
|
-
const state = requireRecord(value, "Checkout state");
|
|
1375
|
-
if (state.version !== 1) {
|
|
1376
|
-
throw new CheckoutStateError(`Unsupported checkout state version: ${String(state.version)}`);
|
|
1377
|
-
}
|
|
1378
|
-
const workspaceId = requireNonEmptyString(state.workspaceId, "workspaceId");
|
|
1379
|
-
const checkoutRoot = requireNonEmptyString(state.checkoutRoot, "checkoutRoot");
|
|
1380
|
-
if (!isAbsolute(checkoutRoot)) throw new CheckoutStateError("checkoutRoot must be an absolute path");
|
|
1381
|
-
if (state.mode !== "partial" && state.mode !== "full") {
|
|
1382
|
-
throw new CheckoutStateError("mode must be partial or full");
|
|
1383
|
-
}
|
|
1384
|
-
if (!Array.isArray(state.checkedOutScopes)) {
|
|
1385
|
-
throw new CheckoutStateError("checkedOutScopes must be an array");
|
|
1386
|
-
}
|
|
1387
|
-
const checkedOutScopes = state.checkedOutScopes.map(
|
|
1388
|
-
(scope, index) => requireWorkspacePath(scope, `checkedOutScopes[${index}]`)
|
|
1389
|
-
);
|
|
1390
|
-
if (new Set(checkedOutScopes).size !== checkedOutScopes.length) {
|
|
1391
|
-
throw new CheckoutStateError("checkedOutScopes must not contain duplicates");
|
|
1392
|
-
}
|
|
1393
|
-
const reposValue = requireRecord(state.repos, "repos");
|
|
1394
|
-
const repos = {};
|
|
1395
|
-
for (const [repoId, rawRepo] of Object.entries(reposValue)) {
|
|
1396
|
-
requireNonEmptyString(repoId, "repo id");
|
|
1397
|
-
const repo = requireRecord(rawRepo, `repos.${repoId}`);
|
|
1398
|
-
const name = requireRepoName(repo.name, `repos.${repoId}.name`);
|
|
1399
|
-
repos[repoId] = {
|
|
1400
|
-
name,
|
|
1401
|
-
treeSha: requireNonEmptyString(repo.treeSha, `repos.${repoId}.treeSha`)
|
|
1402
|
-
};
|
|
1403
|
-
}
|
|
1404
|
-
const filesValue = requireRecord(state.files, "files");
|
|
1405
|
-
const files = {};
|
|
1406
|
-
for (const [workspacePath, rawFile] of Object.entries(filesValue)) {
|
|
1407
|
-
requireWorkspacePath(workspacePath, `files.${workspacePath}`);
|
|
1408
|
-
const file = requireRecord(rawFile, `files.${workspacePath}`);
|
|
1409
|
-
const repoId = requireNonEmptyString(file.repoId, `files.${workspacePath}.repoId`);
|
|
1410
|
-
const repo = repos[repoId];
|
|
1411
|
-
if (!repo) throw new CheckoutStateError(`File references unknown repo: ${workspacePath}`);
|
|
1412
|
-
const repoPath = requireWorkspacePath(file.repoPath, `files.${workspacePath}.repoPath`);
|
|
1413
|
-
if (workspacePath !== `${repo.name}/${repoPath}`) {
|
|
1414
|
-
throw new CheckoutStateError(`File path does not match its repo: ${workspacePath}`);
|
|
1415
|
-
}
|
|
1416
|
-
const fileId = file.fileId === null ? null : requireNonEmptyString(file.fileId, `files.${workspacePath}.fileId`);
|
|
1417
|
-
const sha = requireNonEmptyString(file.sha, `files.${workspacePath}.sha`);
|
|
1418
|
-
if (!/^[a-f0-9]{64}$/.test(sha)) throw new CheckoutStateError(`Invalid file SHA: ${workspacePath}`);
|
|
1419
|
-
files[workspacePath] = {
|
|
1420
|
-
repoId,
|
|
1421
|
-
repoPath,
|
|
1422
|
-
fileId,
|
|
1423
|
-
sha,
|
|
1424
|
-
size: requireNonNegativeInteger(file.size, `files.${workspacePath}.size`),
|
|
1425
|
-
fileType: requireNonNegativeInteger(file.fileType, `files.${workspacePath}.fileType`)
|
|
1426
|
-
};
|
|
1427
|
-
}
|
|
1428
|
-
return {
|
|
1429
|
-
version: 1,
|
|
1430
|
-
workspaceId,
|
|
1431
|
-
checkoutRoot,
|
|
1432
|
-
mode: state.mode,
|
|
1433
|
-
checkedOutScopes,
|
|
1434
|
-
repos,
|
|
1435
|
-
files
|
|
1436
|
-
};
|
|
1437
|
-
}
|
|
1438
|
-
function requireRecord(value, field) {
|
|
1439
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
1440
|
-
throw new CheckoutStateError(`${field} must be an object`);
|
|
995
|
+
existing = JSON.parse(fs3.readFileSync(configPath, "utf8"));
|
|
996
|
+
} catch {
|
|
997
|
+
existing = {};
|
|
1441
998
|
}
|
|
1442
|
-
|
|
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));
|
|
1443
1003
|
}
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
return value;
|
|
1004
|
+
|
|
1005
|
+
// src/telemetry/opt-out.ts
|
|
1006
|
+
function isCiEnvironment() {
|
|
1007
|
+
return process.env.CI === "true" || process.env.CI === "1";
|
|
1449
1008
|
}
|
|
1450
|
-
function
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
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;
|
|
1454
1012
|
}
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
function requireRepoName(value, field) {
|
|
1458
|
-
const name = requireWorkspacePath(value, field);
|
|
1459
|
-
if (name.includes("/")) throw new CheckoutStateError(`${field} must be a single path segment`);
|
|
1460
|
-
return name;
|
|
1461
|
-
}
|
|
1462
|
-
function requireNonNegativeInteger(value, field) {
|
|
1463
|
-
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
|
|
1464
|
-
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;
|
|
1465
1015
|
}
|
|
1466
|
-
return
|
|
1467
|
-
}
|
|
1468
|
-
function isNodeError(error) {
|
|
1469
|
-
return error instanceof Error && "code" in error;
|
|
1470
|
-
}
|
|
1471
|
-
function errorMessage(error) {
|
|
1472
|
-
return error instanceof Error ? error.message : String(error);
|
|
1016
|
+
return readTelemetryConfig().disabled === true;
|
|
1473
1017
|
}
|
|
1474
1018
|
|
|
1475
|
-
// src/
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
const previousState = await loadCheckoutState();
|
|
1488
|
-
const compatibleState = resolveCompatibleState(previousState, context, resolvedTargetDir, repos);
|
|
1489
|
-
const previousFiles = compatibleState?.files ?? {};
|
|
1490
|
-
const selection = resolvePullSelection(context, options.scopes);
|
|
1491
|
-
const repoById = new Map(selection.repos.map((repo) => [repo.repoId, repo]));
|
|
1492
|
-
const tree = await fetchWorkspaceTree(context, selection);
|
|
1493
|
-
const stateRepos = resolveStateRepos(tree, selection.repos);
|
|
1494
|
-
const entries = [...tree.entries].sort((a, b) => a.prefixedPath.localeCompare(b.prefixedPath));
|
|
1495
|
-
const directoryPaths = selection.repos.map((repo) => ({
|
|
1496
|
-
workspacePath: repo.name,
|
|
1497
|
-
localPath: resolveSafeLocalPath(resolvedTargetDir, repo.name, "")
|
|
1498
|
-
}));
|
|
1499
|
-
const blobEntries = [];
|
|
1500
|
-
const remoteBlobPaths = /* @__PURE__ */ new Set();
|
|
1501
|
-
const remotePaths = /* @__PURE__ */ new Set();
|
|
1502
|
-
const matchedScopes = /* @__PURE__ */ new Set();
|
|
1503
|
-
for (const entry of entries) {
|
|
1504
|
-
const repo = repoById.get(entry.repoId);
|
|
1505
|
-
if (!repo) {
|
|
1506
|
-
throw new SandboxPullError(`Remote tree returned unknown repo: ${entry.repoId}`, "remote");
|
|
1507
|
-
}
|
|
1508
|
-
const expectedWorkspacePath = entry.path ? `${repo.name}/${entry.path}` : repo.name;
|
|
1509
|
-
if (entry.prefixedPath !== expectedWorkspacePath) {
|
|
1510
|
-
throw new SandboxPullError(`Remote tree returned inconsistent path: ${entry.prefixedPath}`, "remote");
|
|
1511
|
-
}
|
|
1512
|
-
if (remotePaths.has(expectedWorkspacePath)) {
|
|
1513
|
-
throw new SandboxPullError(`Remote tree returned duplicate path: ${expectedWorkspacePath}`, "remote");
|
|
1514
|
-
}
|
|
1515
|
-
remotePaths.add(expectedWorkspacePath);
|
|
1516
|
-
const localPath = resolveSafeLocalPath(resolvedTargetDir, repo.name, entry.path);
|
|
1517
|
-
for (const scope of selection.scopes) {
|
|
1518
|
-
if (pathIsIncluded(scope, expectedWorkspacePath)) matchedScopes.add(scope);
|
|
1519
|
-
}
|
|
1520
|
-
if (!selection.scopes.some((scope) => pathsIntersect(scope, expectedWorkspacePath))) continue;
|
|
1521
|
-
if (entry.type === "tree") {
|
|
1522
|
-
directoryPaths.push({ workspacePath: entry.prefixedPath, localPath });
|
|
1523
|
-
continue;
|
|
1524
|
-
}
|
|
1525
|
-
if (!entry.sha) {
|
|
1526
|
-
throw new SandboxPullError(`${entry.prefixedPath} does not have downloadable content`, "remote");
|
|
1527
|
-
}
|
|
1528
|
-
if (entry.fileId !== null && (typeof entry.fileId !== "string" || entry.fileId.length === 0)) {
|
|
1529
|
-
throw new SandboxPullError(`${entry.prefixedPath} has invalid file metadata`, "remote");
|
|
1530
|
-
}
|
|
1531
|
-
if (entry.fileType !== null && (!Number.isInteger(entry.fileType) || entry.fileType < 0)) {
|
|
1532
|
-
throw new SandboxPullError(`${entry.prefixedPath} has invalid file metadata`, "remote");
|
|
1533
|
-
}
|
|
1534
|
-
if (entry.size !== null && (!Number.isInteger(entry.size) || entry.size < 0)) {
|
|
1535
|
-
throw new SandboxPullError(`${entry.prefixedPath} has invalid file metadata`, "remote");
|
|
1536
|
-
}
|
|
1537
|
-
if (entry.size !== null && entry.size > MFS_FILE_SIZE_LIMIT_BYTES) {
|
|
1538
|
-
throw new SandboxPullError(`File too large (max ${MFS_FILE_SIZE_LIMIT_LABEL}): ${entry.prefixedPath}`, "too-large");
|
|
1539
|
-
}
|
|
1540
|
-
remoteBlobPaths.add(entry.prefixedPath);
|
|
1541
|
-
blobEntries.push({ entry, localPath });
|
|
1542
|
-
}
|
|
1543
|
-
if (selection.mode === "partial") {
|
|
1544
|
-
for (const scope of selection.scopes) {
|
|
1545
|
-
const repoRoot = selection.repos.some((repo) => repo.name === scope);
|
|
1546
|
-
const previouslyTracked = Object.keys(previousFiles).some((workspacePath) => pathIsIncluded(scope, workspacePath));
|
|
1547
|
-
if (!repoRoot && !matchedScopes.has(scope) && !previouslyTracked) {
|
|
1548
|
-
throw new SandboxPullError(`Workspace path does not exist: ${scope}`, "invalid-path");
|
|
1549
|
-
}
|
|
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)");
|
|
1550
1031
|
}
|
|
1551
|
-
}
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
entry.prefixedPath,
|
|
1561
|
-
previousFiles[entry.prefixedPath],
|
|
1562
|
-
Boolean(options.force)
|
|
1563
|
-
)
|
|
1564
|
-
});
|
|
1565
|
-
}
|
|
1566
|
-
const plannedDeletions = await planRemoteDeletions(
|
|
1567
|
-
compatibleState,
|
|
1568
|
-
selection,
|
|
1569
|
-
resolvedTargetDir,
|
|
1570
|
-
remoteBlobPaths,
|
|
1571
|
-
Boolean(options.force)
|
|
1572
|
-
);
|
|
1573
|
-
for (const directory of directoryPaths) {
|
|
1574
|
-
await assertCanCreateDirectory(directory.localPath, directory.workspacePath);
|
|
1575
|
-
}
|
|
1576
|
-
const blobsToDownload = plannedBlobs.filter((planned) => planned.shouldWrite);
|
|
1577
|
-
const downloadUrls = await presignDownloads(context, blobsToDownload.map(({ entry }) => entry));
|
|
1578
|
-
const downloadedEntries = [];
|
|
1579
|
-
for (const planned of blobsToDownload) {
|
|
1580
|
-
const { entry, localPath } = planned;
|
|
1581
|
-
const remoteContent = await downloadBlob(entry, downloadUrls);
|
|
1582
|
-
downloadedEntries.push({ ...planned, content: remoteContent });
|
|
1583
|
-
}
|
|
1584
|
-
for (const deletion of plannedDeletions) {
|
|
1585
|
-
await rm2(deletion.localPath);
|
|
1586
|
-
}
|
|
1587
|
-
for (const directory of directoryPaths) {
|
|
1588
|
-
await mkdir2(directory.localPath, { recursive: true });
|
|
1589
|
-
}
|
|
1590
|
-
for (const downloaded of downloadedEntries) {
|
|
1591
|
-
await writePulledFile(downloaded.localPath, downloaded.content);
|
|
1592
|
-
}
|
|
1593
|
-
const downloadedContentByPath = new Map(
|
|
1594
|
-
downloadedEntries.map(({ entry, content }) => [entry.prefixedPath, content])
|
|
1595
|
-
);
|
|
1596
|
-
const pulledFiles = Object.fromEntries(await Promise.all(plannedBlobs.map(async ({ entry, localPath }) => {
|
|
1597
|
-
const content = downloadedContentByPath.get(entry.prefixedPath);
|
|
1598
|
-
return [
|
|
1599
|
-
entry.prefixedPath,
|
|
1600
|
-
{
|
|
1601
|
-
repoId: entry.repoId,
|
|
1602
|
-
repoPath: entry.path,
|
|
1603
|
-
fileId: entry.fileId,
|
|
1604
|
-
sha: requireBlobSha(entry),
|
|
1605
|
-
size: content?.length ?? entry.size ?? previousFiles[entry.prefixedPath]?.size ?? await localFileSize(localPath, entry.prefixedPath),
|
|
1606
|
-
fileType: entry.fileType ?? 1
|
|
1607
|
-
}
|
|
1608
|
-
];
|
|
1609
|
-
})));
|
|
1610
|
-
await saveCheckoutState(buildNextCheckoutState({
|
|
1611
|
-
context,
|
|
1612
|
-
checkoutRoot: resolvedTargetDir,
|
|
1613
|
-
allRepos: repos,
|
|
1614
|
-
selection,
|
|
1615
|
-
previousState: compatibleState,
|
|
1616
|
-
pulledRepos: stateRepos,
|
|
1617
|
-
pulledFiles
|
|
1618
|
-
}));
|
|
1619
|
-
return {
|
|
1620
|
-
fileCount: blobEntries.length,
|
|
1621
|
-
targetDir: resolvedTargetDir
|
|
1622
|
-
};
|
|
1032
|
+
});
|
|
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
|
+
});
|
|
1623
1041
|
}
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
(
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
return {
|
|
1640
|
-
path: resolved.repoPath ? `${resolved.repoName}/${resolved.repoPath}` : resolved.repoName,
|
|
1641
|
-
repoId: resolved.repoId
|
|
1642
|
-
};
|
|
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);
|
|
1056
|
+
}
|
|
1643
1057
|
});
|
|
1644
|
-
const scopes = collapseScopes(resolvedScopes.map((scope) => scope.path));
|
|
1645
|
-
const repoIds = new Set(resolvedScopes.map((scope) => scope.repoId));
|
|
1646
|
-
return {
|
|
1647
|
-
mode: "partial",
|
|
1648
|
-
scopes,
|
|
1649
|
-
repos: repos.filter((repo) => repoIds.has(repo.repoId))
|
|
1650
|
-
};
|
|
1651
1058
|
}
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
workspaceId: context.workspaceId,
|
|
1658
|
-
checkoutRoot,
|
|
1659
|
-
mode: "full",
|
|
1660
|
-
checkedOutScopes: selection.scopes,
|
|
1661
|
-
repos: pulledRepos,
|
|
1662
|
-
files: pulledFiles
|
|
1663
|
-
};
|
|
1664
|
-
}
|
|
1665
|
-
const previousFiles = previousState?.files ?? {};
|
|
1666
|
-
const preservedFiles = Object.fromEntries(
|
|
1667
|
-
Object.entries(previousFiles).filter(
|
|
1668
|
-
([workspacePath]) => !selection.scopes.some((scope) => pathIsIncluded(scope, workspacePath))
|
|
1669
|
-
)
|
|
1670
|
-
);
|
|
1671
|
-
const checkedOutScopes = collapseScopes([...previousState?.checkedOutScopes ?? [], ...selection.scopes]);
|
|
1672
|
-
const mode = allRepos.every((repo) => checkedOutScopes.includes(repo.name)) ? "full" : "partial";
|
|
1673
|
-
return {
|
|
1674
|
-
version: 1,
|
|
1675
|
-
workspaceId: context.workspaceId,
|
|
1676
|
-
checkoutRoot,
|
|
1677
|
-
mode,
|
|
1678
|
-
checkedOutScopes,
|
|
1679
|
-
repos: { ...previousState?.repos ?? {}, ...pulledRepos },
|
|
1680
|
-
files: { ...preservedFiles, ...pulledFiles }
|
|
1681
|
-
};
|
|
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);
|
|
1682
1064
|
}
|
|
1683
|
-
function
|
|
1684
|
-
const
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
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);
|
|
1689
1084
|
}
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
}
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
function resolveStateRepos(tree, repos) {
|
|
1701
|
-
const knownRepoIds = new Set(repos.map((repo) => repo.repoId));
|
|
1702
|
-
for (const repoId of Object.keys(tree.repos)) {
|
|
1703
|
-
if (!knownRepoIds.has(repoId)) {
|
|
1704
|
-
throw new SandboxPullError(`Remote tree returned unknown repo: ${repoId}`, "remote");
|
|
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);
|
|
1705
1095
|
}
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
throw new SandboxPullError(`Remote tree did not return repo: ${repo.repoId}`, "remote");
|
|
1096
|
+
const { members: members2 } = response.data;
|
|
1097
|
+
if (members2.length === 0) {
|
|
1098
|
+
stopSpinner(true, "No members found.");
|
|
1099
|
+
return;
|
|
1711
1100
|
}
|
|
1712
|
-
|
|
1713
|
-
|
|
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}`);
|
|
1714
1105
|
}
|
|
1715
|
-
|
|
1716
|
-
|
|
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);
|
|
1717
1113
|
}
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
const filter = resolveRepoTreeFilter(repo, selection);
|
|
1725
|
-
const params = new URLSearchParams({ entryFilter: filter.type });
|
|
1726
|
-
if (filter.type === "paths") {
|
|
1727
|
-
for (const path2 of filter.paths) params.append("path", path2);
|
|
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);
|
|
1728
1120
|
}
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
),
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
}
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
}
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
const repoPaths = [];
|
|
1751
|
-
for (const scope of selection.scopes) {
|
|
1752
|
-
if (scope === repo.name) return { type: "all" };
|
|
1753
|
-
if (scope.startsWith(`${repo.name}/`)) repoPaths.push(scope.slice(repo.name.length + 1));
|
|
1754
|
-
}
|
|
1755
|
-
if (repoPaths.length === 0) {
|
|
1756
|
-
throw new SandboxPullError(`No pull scope resolved for repo: ${repo.name}`, "invalid-path");
|
|
1757
|
-
}
|
|
1758
|
-
return { type: "paths", paths: repoPaths };
|
|
1759
|
-
}
|
|
1760
|
-
async function presignDownloads(context, entries) {
|
|
1761
|
-
const client = new SandboxMoxtClient(context);
|
|
1762
|
-
const entriesByRepo = /* @__PURE__ */ new Map();
|
|
1763
|
-
for (const entry of entries) {
|
|
1764
|
-
const existing = entriesByRepo.get(entry.repoId) ?? [];
|
|
1765
|
-
existing.push(entry);
|
|
1766
|
-
entriesByRepo.set(entry.repoId, existing);
|
|
1767
|
-
}
|
|
1768
|
-
const urls = /* @__PURE__ */ new Map();
|
|
1769
|
-
for (const [repoId, repoEntries] of entriesByRepo) {
|
|
1770
|
-
const hashes = [...new Set(repoEntries.map((entry) => entry.sha).filter((sha) => Boolean(sha)))];
|
|
1771
|
-
if (hashes.length === 0) continue;
|
|
1772
|
-
const response = await client.request(
|
|
1773
|
-
"POST",
|
|
1774
|
-
`/agent/api/pipeline-sandbox/mfs/${encodeURIComponent(repoId)}/presign-download`,
|
|
1775
|
-
{ hashes }
|
|
1776
|
-
);
|
|
1777
|
-
for (const hash of hashes) {
|
|
1778
|
-
const url = response.data.urls[hash];
|
|
1779
|
-
if (!url) {
|
|
1780
|
-
throw new SandboxPullError(`No download URL returned for ${hash}`, "remote");
|
|
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)}`);
|
|
1781
1142
|
}
|
|
1782
|
-
urls.set(hash, url);
|
|
1783
1143
|
}
|
|
1784
|
-
}
|
|
1785
|
-
return urls;
|
|
1786
|
-
}
|
|
1787
|
-
async function downloadBlob(entry, urls) {
|
|
1788
|
-
const hash = entry.sha;
|
|
1789
|
-
if (!hash) {
|
|
1790
|
-
throw new SandboxPullError(`${entry.prefixedPath} does not have downloadable content`, "remote");
|
|
1791
|
-
}
|
|
1792
|
-
const url = urls.get(hash);
|
|
1793
|
-
if (!url) {
|
|
1794
|
-
throw new SandboxPullError(`No download URL returned for ${entry.prefixedPath}`, "remote");
|
|
1795
|
-
}
|
|
1796
|
-
const response = await fetch(url);
|
|
1797
|
-
if (!response.ok) {
|
|
1798
|
-
throw new SandboxPullError(`Download failed [${response.status}]: ${entry.prefixedPath}`, "remote");
|
|
1799
|
-
}
|
|
1800
|
-
const content = Buffer.from(await response.arrayBuffer());
|
|
1801
|
-
if (content.length > MFS_FILE_SIZE_LIMIT_BYTES) {
|
|
1802
|
-
throw new SandboxPullError(`File too large (max ${MFS_FILE_SIZE_LIMIT_LABEL}): ${entry.prefixedPath}`, "too-large");
|
|
1803
|
-
}
|
|
1804
|
-
const contentHash = createHash2("sha256").update(content).digest("hex");
|
|
1805
|
-
if (contentHash !== hash) {
|
|
1806
|
-
throw new SandboxPullError(`Downloaded content hash mismatch: ${entry.prefixedPath}`, "remote");
|
|
1807
|
-
}
|
|
1808
|
-
return content;
|
|
1809
|
-
}
|
|
1810
|
-
async function shouldWritePulledFile(localPath, remoteContentHash, workspacePath, previousFile, force) {
|
|
1811
|
-
const existing = await lstat(localPath).catch((error) => {
|
|
1812
|
-
if (isNodeError2(error) && error.code === "ENOENT") return void 0;
|
|
1813
|
-
throw error;
|
|
1814
1144
|
});
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
if (
|
|
1821
|
-
|
|
1822
|
-
`
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
);
|
|
1834
|
-
}
|
|
1835
|
-
const localChanged = localContentHash !== previousFile.sha;
|
|
1836
|
-
const remoteChanged = remoteContentHash !== previousFile.sha;
|
|
1837
|
-
if (!localChanged && remoteChanged) return true;
|
|
1838
|
-
if (localChanged && !remoteChanged) return false;
|
|
1839
|
-
if (localChanged && remoteChanged) {
|
|
1840
|
-
throw new SandboxPullError(
|
|
1841
|
-
`Local and remote file both changed since checkout: ${workspacePath}`,
|
|
1842
|
-
"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}`
|
|
1843
1163
|
);
|
|
1844
|
-
|
|
1845
|
-
|
|
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
|
+
});
|
|
1846
1170
|
}
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
throw new SandboxPullError(
|
|
1868
|
-
`Remote file was deleted but local file changed: ${workspacePath}`,
|
|
1869
|
-
"file-conflict"
|
|
1870
|
-
);
|
|
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);
|
|
1871
1191
|
}
|
|
1872
|
-
|
|
1192
|
+
throw err;
|
|
1873
1193
|
}
|
|
1874
|
-
return deletions;
|
|
1875
1194
|
}
|
|
1876
|
-
|
|
1877
|
-
return
|
|
1195
|
+
function addWorkspaceOption(command) {
|
|
1196
|
+
return command.requiredOption("-w, --workspace <workspaceId>", "Workspace ID");
|
|
1878
1197
|
}
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
if (!stats.isFile()) {
|
|
1882
|
-
throw new SandboxPullError(`Cannot determine local file size: ${workspacePath}`, "directory-conflict");
|
|
1883
|
-
}
|
|
1884
|
-
return stats.size;
|
|
1885
|
-
}
|
|
1886
|
-
function requireBlobSha(entry) {
|
|
1887
|
-
if (!entry.sha) {
|
|
1888
|
-
throw new SandboxPullError(`${entry.prefixedPath} does not have downloadable content`, "remote");
|
|
1889
|
-
}
|
|
1890
|
-
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)");
|
|
1891
1200
|
}
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
if (isNodeError2(error) && error.code === "ENOENT") return void 0;
|
|
1895
|
-
throw error;
|
|
1896
|
-
});
|
|
1897
|
-
if (existing && !existing.isDirectory()) {
|
|
1898
|
-
throw new SandboxPullError(`Refusing to overwrite local path: ${workspacePath}`, "directory-conflict");
|
|
1899
|
-
}
|
|
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");
|
|
1900
1203
|
}
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
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");
|
|
1904
1206
|
}
|
|
1905
|
-
function
|
|
1906
|
-
const
|
|
1907
|
-
if (
|
|
1908
|
-
throw new
|
|
1909
|
-
}
|
|
1910
|
-
const resolved = resolve(targetDir, ...pathParts);
|
|
1911
|
-
const relativePath = relative(targetDir, resolved);
|
|
1912
|
-
if (relativePath.startsWith("..") || isAbsolute2(relativePath)) {
|
|
1913
|
-
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}".`);
|
|
1914
1211
|
}
|
|
1915
|
-
return
|
|
1212
|
+
return parsed;
|
|
1916
1213
|
}
|
|
1917
|
-
function
|
|
1918
|
-
|
|
1214
|
+
function parseOptionalPositiveInteger(raw, label) {
|
|
1215
|
+
if (raw == null || raw === "") return void 0;
|
|
1216
|
+
return parsePositiveInteger(raw, label);
|
|
1919
1217
|
}
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
try {
|
|
1926
|
-
const invocation = await resolvePullInvocation(context, workspacePath, options);
|
|
1927
|
-
const result = await pullSandboxWorkspace(context, invocation.targetDir, {
|
|
1928
|
-
force: Boolean(options.force),
|
|
1929
|
-
scopes: invocation.scopes
|
|
1930
|
-
});
|
|
1931
|
-
print(`Pulled ${result.fileCount} ${result.fileCount === 1 ? "file" : "files"} to ${result.targetDir}`);
|
|
1932
|
-
} catch (error) {
|
|
1933
|
-
handlePullError(error);
|
|
1934
|
-
}
|
|
1935
|
-
});
|
|
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).");
|
|
1936
1223
|
}
|
|
1937
|
-
|
|
1938
|
-
const
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
throw new Error("Specify either a workspace path or --all, not both");
|
|
1942
|
-
}
|
|
1943
|
-
if (options.scope.length > 0 && (workspacePath || options.all)) {
|
|
1944
|
-
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.");
|
|
1945
1228
|
}
|
|
1946
|
-
|
|
1947
|
-
throw new Error("Specify the local checkout directory only once");
|
|
1948
|
-
}
|
|
1949
|
-
const state = await loadCheckoutState();
|
|
1950
|
-
if (state) validateCheckoutStateContext(state, context);
|
|
1951
|
-
const targetDir = options.dir ?? legacyTargetDir ?? state?.checkoutRoot ?? ".";
|
|
1952
|
-
if (options.all || legacyTargetDir && options.scope.length === 0) {
|
|
1953
|
-
return { targetDir };
|
|
1954
|
-
}
|
|
1955
|
-
if (workspacePath) return { targetDir, scopes: [workspacePath] };
|
|
1956
|
-
if (options.scope.length > 0) return { targetDir, scopes: options.scope };
|
|
1957
|
-
if (!state || state.checkedOutScopes.length === 0) {
|
|
1958
|
-
throw new Error("Checkout is not initialized. Run moxt pull <workspace-path> or moxt pull --all first.");
|
|
1959
|
-
}
|
|
1960
|
-
return {
|
|
1961
|
-
targetDir: state.checkoutRoot,
|
|
1962
|
-
scopes: state.mode === "full" ? void 0 : state.checkedOutScopes
|
|
1963
|
-
};
|
|
1964
|
-
}
|
|
1965
|
-
function isLegacyTargetDirectory(value) {
|
|
1966
|
-
return value === "." || value.startsWith("./") || value.startsWith("../") || isAbsolute3(value);
|
|
1229
|
+
return value;
|
|
1967
1230
|
}
|
|
1968
|
-
function
|
|
1969
|
-
if (
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
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;
|
|
1979
1251
|
}
|
|
1252
|
+
throw new WorkflowOptionsError(
|
|
1253
|
+
'Error: --scope must be "all", "assigned_to_me", "subscribed_by_me", or "owner_is_me".'
|
|
1254
|
+
);
|
|
1980
1255
|
}
|
|
1981
|
-
function
|
|
1982
|
-
return
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
try {
|
|
1987
|
-
context = resolveRuntimeContext();
|
|
1988
|
-
} catch (error) {
|
|
1989
|
-
if (error instanceof RuntimeContextError) {
|
|
1990
|
-
printError(error.message);
|
|
1991
|
-
process.exit(1);
|
|
1992
|
-
}
|
|
1993
|
-
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.`);
|
|
1994
1261
|
}
|
|
1995
|
-
|
|
1996
|
-
printError("moxt pull is only available in sandbox mode");
|
|
1997
|
-
process.exit(1);
|
|
1998
|
-
}
|
|
1999
|
-
return context;
|
|
1262
|
+
return ids;
|
|
2000
1263
|
}
|
|
2001
|
-
function
|
|
2002
|
-
if (
|
|
2003
|
-
|
|
2004
|
-
|
|
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}`);
|
|
2005
1268
|
}
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
// src/utils/sandbox-push.ts
|
|
2010
|
-
import { createHash as createHash3 } from "crypto";
|
|
2011
|
-
import { lstat as lstat2, readFile as readFile3, readdir } from "fs/promises";
|
|
2012
|
-
import { isAbsolute as isAbsolute4, join as join2, relative as relative2, resolve as resolve2 } from "path";
|
|
2013
|
-
|
|
2014
|
-
// src/utils/concurrency.ts
|
|
2015
|
-
async function runWithConcurrency(items, concurrency, action) {
|
|
2016
|
-
if (!Number.isInteger(concurrency) || concurrency < 1) {
|
|
2017
|
-
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}`);
|
|
2018
1272
|
}
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
const item = items[nextIndex];
|
|
2023
|
-
nextIndex += 1;
|
|
2024
|
-
await action(item);
|
|
2025
|
-
}
|
|
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.");
|
|
2026
1276
|
}
|
|
2027
|
-
const
|
|
2028
|
-
|
|
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;
|
|
2029
1282
|
}
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
var MAX_CONCURRENT_UPLOADS = 8;
|
|
2034
|
-
var SandboxPushError = class extends Error {
|
|
2035
|
-
constructor(message, code) {
|
|
2036
|
-
super(message);
|
|
2037
|
-
this.code = code;
|
|
2038
|
-
this.name = "SandboxPushError";
|
|
1283
|
+
function readCommentContentFile2(path2) {
|
|
1284
|
+
if (!fs4.existsSync(path2)) {
|
|
1285
|
+
throw new WorkflowOptionsError(`Error: comment content file not found: ${path2}`);
|
|
2039
1286
|
}
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
const resolvedSourceDir = resolve2(sourceDir);
|
|
2044
|
-
await assertWorkspaceRoot(resolvedSourceDir);
|
|
2045
|
-
const repos = listRepoEntries(context.repoPayload);
|
|
2046
|
-
for (const repo of repos) {
|
|
2047
|
-
assertSafeRepoName(resolvedSourceDir, repo.name);
|
|
2048
|
-
}
|
|
2049
|
-
const state = await resolveCheckoutState(context, resolvedSourceDir, repos);
|
|
2050
|
-
const checkoutRepos = repos.filter(
|
|
2051
|
-
(repo) => state.repos[repo.repoId] && doesCheckoutPathIntersect(state, repo.name)
|
|
2052
|
-
);
|
|
2053
|
-
if (checkoutRepos.length === 0) {
|
|
2054
|
-
throw new SandboxPushError("Checkout state does not contain a pushable scope", "invalid-path");
|
|
2055
|
-
}
|
|
2056
|
-
const tree = await fetchWorkspaceTree2(context, checkoutRepos);
|
|
2057
|
-
const remoteEntries = indexRemoteEntries(tree, checkoutRepos);
|
|
2058
|
-
const localFiles = await collectLocalFiles(resolvedSourceDir, checkoutRepos, remoteEntries, state);
|
|
2059
|
-
const { changes, reconciledFiles, removedWorkspacePaths } = planChanges(
|
|
2060
|
-
localFiles,
|
|
2061
|
-
remoteEntries,
|
|
2062
|
-
state,
|
|
2063
|
-
checkoutRepos
|
|
2064
|
-
);
|
|
2065
|
-
let pushResponse;
|
|
2066
|
-
if (changes.length > 0) {
|
|
2067
|
-
await uploadChangedFiles(context, changes);
|
|
2068
|
-
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}`);
|
|
2069
1290
|
}
|
|
2070
|
-
if (
|
|
2071
|
-
|
|
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
|
+
);
|
|
2072
1295
|
}
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
};
|
|
2077
|
-
}
|
|
2078
|
-
async function resolveCheckoutState(context, sourceDir, repos) {
|
|
2079
|
-
const state = await loadCheckoutState();
|
|
2080
|
-
if (!state) {
|
|
2081
|
-
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.");
|
|
2082
1299
|
}
|
|
2083
|
-
if (
|
|
2084
|
-
throw new
|
|
2085
|
-
`Checkout state belongs to workspace ${state.workspaceId}, not ${context.workspaceId}`,
|
|
2086
|
-
"invalid-path"
|
|
2087
|
-
);
|
|
1300
|
+
if (isBinaryContent(buffer2)) {
|
|
1301
|
+
throw new WorkflowOptionsError("Error: comment content file must be UTF-8 text, not binary content.");
|
|
2088
1302
|
}
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
);
|
|
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.");
|
|
2094
1308
|
}
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
if (currentRepos.get(repoId) !== repo.name) {
|
|
2098
|
-
throw new SandboxPushError(`Checkout state contains unknown repo: ${repoId}`, "invalid-path");
|
|
2099
|
-
}
|
|
1309
|
+
if (content.length === 0) {
|
|
1310
|
+
throw new WorkflowOptionsError("Error: comment content file must not be empty.");
|
|
2100
1311
|
}
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
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.`
|
|
2104
1315
|
);
|
|
2105
|
-
if (!hasRepo) throw new SandboxPushError(`Checkout state contains invalid scope: ${scope}`, "invalid-path");
|
|
2106
1316
|
}
|
|
2107
|
-
return
|
|
1317
|
+
return content;
|
|
2108
1318
|
}
|
|
2109
|
-
|
|
2110
|
-
const
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
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 {};
|
|
2119
1339
|
}
|
|
2120
|
-
|
|
2121
|
-
const
|
|
2122
|
-
const
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
}
|
|
2138
|
-
const key = remoteEntryKey(entry.repoId, entry.path);
|
|
2139
|
-
if (entries.has(key)) {
|
|
2140
|
-
throw new SandboxPushError(`Remote tree returned duplicate path: ${entry.prefixedPath}`, "remote");
|
|
2141
|
-
}
|
|
2142
|
-
entries.set(key, entry);
|
|
2143
|
-
}
|
|
2144
|
-
return entries;
|
|
2145
|
-
}
|
|
2146
|
-
async function collectLocalFiles(sourceDir, repos, remoteEntries, state) {
|
|
2147
|
-
const files = [];
|
|
2148
|
-
for (const repo of repos) {
|
|
2149
|
-
const repoRoot = resolveSafeRepoRoot(sourceDir, repo.name);
|
|
2150
|
-
const stats = await lstat2(repoRoot).catch((error) => {
|
|
2151
|
-
if (isNodeError3(error) && error.code === "ENOENT") return void 0;
|
|
2152
|
-
throw error;
|
|
2153
|
-
});
|
|
2154
|
-
if (!stats) continue;
|
|
2155
|
-
if (!stats.isDirectory()) {
|
|
2156
|
-
throw new SandboxPushError(`Local repo path is not a directory: ${repo.name}`, "invalid-path");
|
|
2157
|
-
}
|
|
2158
|
-
await collectRepoFiles(repoRoot, repo, "", remoteEntries, state, files);
|
|
2159
|
-
}
|
|
2160
|
-
return files;
|
|
2161
|
-
}
|
|
2162
|
-
async function collectRepoFiles(repoRoot, repo, relativeDir, remoteEntries, state, files) {
|
|
2163
|
-
const localDir = relativeDir ? join2(repoRoot, ...relativeDir.split("/")) : repoRoot;
|
|
2164
|
-
const entries = await readdir(localDir, { withFileTypes: true });
|
|
2165
|
-
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
2166
|
-
for (const entry of entries) {
|
|
2167
|
-
if (entry.name === ".git") continue;
|
|
2168
|
-
const repoPath = relativeDir ? `${relativeDir}/${entry.name}` : entry.name;
|
|
2169
|
-
const workspacePath = `${repo.name}/${repoPath}`;
|
|
2170
|
-
assertSafePathPart(entry.name, workspacePath);
|
|
2171
|
-
if (!doesCheckoutPathIntersect(state, workspacePath)) continue;
|
|
2172
|
-
if (entry.isSymbolicLink()) {
|
|
2173
|
-
throw new SandboxPushError(`Refusing to push symbolic link: ${workspacePath}`, "invalid-file");
|
|
2174
|
-
}
|
|
2175
|
-
if (entry.isDirectory()) {
|
|
2176
|
-
const remote = remoteEntries.get(remoteEntryKey(repo.repoId, repoPath));
|
|
2177
|
-
if (remote?.type === "blob") {
|
|
2178
|
-
throw new SandboxPushError(`Local directory conflicts with remote file: ${workspacePath}`, "invalid-file");
|
|
2179
|
-
}
|
|
2180
|
-
await collectRepoFiles(repoRoot, repo, repoPath, remoteEntries, state, files);
|
|
2181
|
-
continue;
|
|
2182
|
-
}
|
|
2183
|
-
if (!entry.isFile()) {
|
|
2184
|
-
throw new SandboxPushError(`Refusing to push unsupported file: ${workspacePath}`, "invalid-file");
|
|
2185
|
-
}
|
|
2186
|
-
const localPath = join2(repoRoot, ...repoPath.split("/"));
|
|
2187
|
-
const stats = await lstat2(localPath);
|
|
2188
|
-
if (!stats.isFile()) {
|
|
2189
|
-
const kind = stats.isSymbolicLink() ? "symbolic link" : "unsupported file";
|
|
2190
|
-
throw new SandboxPushError(`Refusing to push ${kind}: ${workspacePath}`, "invalid-file");
|
|
2191
|
-
}
|
|
2192
|
-
if (stats.size > MFS_FILE_SIZE_LIMIT_BYTES) {
|
|
2193
|
-
throw new SandboxPushError(
|
|
2194
|
-
`File too large (max ${MFS_FILE_SIZE_LIMIT_LABEL}): ${workspacePath}`,
|
|
2195
|
-
"too-large"
|
|
2196
|
-
);
|
|
2197
|
-
}
|
|
2198
|
-
const content = await readFile3(localPath);
|
|
2199
|
-
if (content.length > MFS_FILE_SIZE_LIMIT_BYTES) {
|
|
2200
|
-
throw new SandboxPushError(
|
|
2201
|
-
`File too large (max ${MFS_FILE_SIZE_LIMIT_LABEL}): ${workspacePath}`,
|
|
2202
|
-
"too-large"
|
|
2203
|
-
);
|
|
2204
|
-
}
|
|
2205
|
-
if (!isCheckoutPathIncluded(state, workspacePath)) continue;
|
|
2206
|
-
files.push({
|
|
2207
|
-
repo,
|
|
2208
|
-
repoPath,
|
|
2209
|
-
workspacePath,
|
|
2210
|
-
localPath,
|
|
2211
|
-
contentHash: createHash3("sha256").update(content).digest("hex"),
|
|
2212
|
-
size: content.length
|
|
2213
|
-
});
|
|
2214
|
-
}
|
|
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 {};
|
|
2215
1357
|
}
|
|
2216
|
-
function
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
const removedWorkspacePaths = [];
|
|
2220
|
-
const localWorkspacePaths = new Set(localFiles.map((file) => file.workspacePath));
|
|
2221
|
-
const reposById = new Map(repos.map((repo) => [repo.repoId, repo]));
|
|
2222
|
-
for (const file of localFiles) {
|
|
2223
|
-
const baseline = state.files[file.workspacePath];
|
|
2224
|
-
if (baseline?.sha === file.contentHash) continue;
|
|
2225
|
-
const existing = remoteEntries.get(remoteEntryKey(file.repo.repoId, file.repoPath));
|
|
2226
|
-
if (existing?.type === "tree") {
|
|
2227
|
-
throw new SandboxPushError(`Local file conflicts with remote directory: ${file.workspacePath}`, "invalid-file");
|
|
2228
|
-
}
|
|
2229
|
-
if (existing?.sha === file.contentHash) {
|
|
2230
|
-
reconciledFiles.push({ ...file, remote: existing });
|
|
2231
|
-
continue;
|
|
2232
|
-
}
|
|
2233
|
-
if (existing && !existing.sha) {
|
|
2234
|
-
throw new SandboxPushError(`${file.workspacePath} does not have a base content hash`, "remote");
|
|
2235
|
-
}
|
|
2236
|
-
if (baseline && existing?.sha !== baseline.sha) {
|
|
2237
|
-
throw new SandboxPushError(`Remote file changed since checkout: ${file.workspacePath}`, "conflict");
|
|
2238
|
-
}
|
|
2239
|
-
if (!baseline && existing) {
|
|
2240
|
-
throw new SandboxPushError(`Remote file changed since checkout: ${file.workspacePath}`, "conflict");
|
|
2241
|
-
}
|
|
2242
|
-
changes.push({
|
|
2243
|
-
...file,
|
|
2244
|
-
action: existing ? "update" : "create",
|
|
2245
|
-
...existing ? { existing } : {}
|
|
2246
|
-
});
|
|
2247
|
-
}
|
|
2248
|
-
for (const [workspacePath, baseline] of Object.entries(state.files)) {
|
|
2249
|
-
if (localWorkspacePaths.has(workspacePath) || !isCheckoutPathIncluded(state, workspacePath)) continue;
|
|
2250
|
-
const repo = reposById.get(baseline.repoId);
|
|
2251
|
-
if (!repo) continue;
|
|
2252
|
-
const existing = remoteEntries.get(remoteEntryKey(baseline.repoId, baseline.repoPath));
|
|
2253
|
-
if (!existing) {
|
|
2254
|
-
removedWorkspacePaths.push(workspacePath);
|
|
2255
|
-
continue;
|
|
2256
|
-
}
|
|
2257
|
-
if (existing.type !== "blob" || existing.sha !== baseline.sha) {
|
|
2258
|
-
throw new SandboxPushError(`Remote file changed since checkout: ${workspacePath}`, "conflict");
|
|
2259
|
-
}
|
|
2260
|
-
changes.push({
|
|
2261
|
-
action: "delete",
|
|
2262
|
-
repo,
|
|
2263
|
-
repoPath: baseline.repoPath,
|
|
2264
|
-
workspacePath,
|
|
2265
|
-
fileId: validFileId(existing.fileId) ?? baseline.fileId
|
|
2266
|
-
});
|
|
1358
|
+
function assertNonEmptyBody(body) {
|
|
1359
|
+
if (Object.keys(body).length === 0) {
|
|
1360
|
+
throw new WorkflowOptionsError("Error: at least one update option must be provided.");
|
|
2267
1361
|
}
|
|
2268
|
-
return { changes, reconciledFiles, removedWorkspacePaths };
|
|
2269
|
-
}
|
|
2270
|
-
async function uploadChangedFiles(context, changes) {
|
|
2271
|
-
const writeChanges = changes.filter(isWriteChange);
|
|
2272
|
-
if (writeChanges.length === 0) return;
|
|
2273
|
-
const client = new SandboxMoxtClient(context);
|
|
2274
|
-
const changesByRepo = groupChangesByRepo(writeChanges);
|
|
2275
|
-
const uploadUrlsByRepo = await Promise.all(
|
|
2276
|
-
[...changesByRepo].map(async ([repoId, repoChanges]) => {
|
|
2277
|
-
const hashes = [...new Set(repoChanges.map((change) => change.contentHash))];
|
|
2278
|
-
const response = await client.request(
|
|
2279
|
-
"POST",
|
|
2280
|
-
`/agent/api/pipeline-sandbox/mfs/${encodeURIComponent(repoId)}/presign-upload`,
|
|
2281
|
-
{ hashes }
|
|
2282
|
-
);
|
|
2283
|
-
return [repoId, response.data.urls];
|
|
2284
|
-
})
|
|
2285
|
-
);
|
|
2286
|
-
const uploadUrls = new Map(uploadUrlsByRepo);
|
|
2287
|
-
await runWithConcurrency(writeChanges, MAX_CONCURRENT_UPLOADS, async (change) => {
|
|
2288
|
-
const uploadUrl = uploadUrls.get(change.repo.repoId)?.[change.contentHash];
|
|
2289
|
-
if (!uploadUrl) {
|
|
2290
|
-
throw new SandboxPushError(`No upload URL returned for ${change.workspacePath}`, "remote");
|
|
2291
|
-
}
|
|
2292
|
-
const content = await readFile3(change.localPath);
|
|
2293
|
-
const currentHash = createHash3("sha256").update(content).digest("hex");
|
|
2294
|
-
if (content.length !== change.size || currentHash !== change.contentHash) {
|
|
2295
|
-
throw new SandboxPushError(`Local file changed while pushing: ${change.workspacePath}`, "invalid-file");
|
|
2296
|
-
}
|
|
2297
|
-
const upload = await fetch(uploadUrl, {
|
|
2298
|
-
method: "PUT",
|
|
2299
|
-
headers: { "Content-Type": "application/octet-stream" },
|
|
2300
|
-
body: new Uint8Array(content)
|
|
2301
|
-
});
|
|
2302
|
-
if (!upload.ok) {
|
|
2303
|
-
throw new SandboxPushError(`Upload failed [${upload.status}]: ${change.workspacePath}`, "remote");
|
|
2304
|
-
}
|
|
2305
|
-
});
|
|
2306
1362
|
}
|
|
2307
|
-
|
|
2308
|
-
const
|
|
2309
|
-
const
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
"/agent/api/pipeline-sandbox/mfs-workspace/unified-push",
|
|
2313
|
-
{
|
|
2314
|
-
message,
|
|
2315
|
-
repoPushes: repos.flatMap((repo) => {
|
|
2316
|
-
const repoChanges = changesByRepo.get(repo.repoId);
|
|
2317
|
-
if (!repoChanges) return [];
|
|
2318
|
-
return [{
|
|
2319
|
-
repoId: repo.repoId,
|
|
2320
|
-
changes: repoChanges.map((change) => {
|
|
2321
|
-
if (change.action === "delete") {
|
|
2322
|
-
return {
|
|
2323
|
-
action: change.action,
|
|
2324
|
-
...change.fileId ? { fileId: change.fileId } : {},
|
|
2325
|
-
path: change.repoPath
|
|
2326
|
-
};
|
|
2327
|
-
}
|
|
2328
|
-
return {
|
|
2329
|
-
action: change.action,
|
|
2330
|
-
...change.existing?.fileId ? { fileId: change.existing.fileId } : {},
|
|
2331
|
-
path: change.repoPath,
|
|
2332
|
-
contentHash: change.contentHash,
|
|
2333
|
-
...change.existing?.sha ? { baseContentHash: change.existing.sha } : {},
|
|
2334
|
-
size: change.size,
|
|
2335
|
-
fileType: MFS_FILE_TYPE_REGULAR2
|
|
2336
|
-
};
|
|
2337
|
-
})
|
|
2338
|
-
}];
|
|
2339
|
-
}),
|
|
2340
|
-
crossRepoMoves: []
|
|
2341
|
-
}
|
|
2342
|
-
);
|
|
2343
|
-
return response.data;
|
|
2344
|
-
}
|
|
2345
|
-
async function updateCheckoutState(state, changes, reconciledFiles, removedWorkspacePaths, response) {
|
|
2346
|
-
const files = { ...state.files };
|
|
2347
|
-
for (const workspacePath of removedWorkspacePaths) delete files[workspacePath];
|
|
2348
|
-
for (const file of reconciledFiles) {
|
|
2349
|
-
files[file.workspacePath] = checkoutStateFile(
|
|
2350
|
-
file,
|
|
2351
|
-
validFileId(file.remote.fileId),
|
|
2352
|
-
validFileType(file.remote.fileType)
|
|
2353
|
-
);
|
|
2354
|
-
}
|
|
2355
|
-
for (const change of changes) {
|
|
2356
|
-
if (change.action === "delete") {
|
|
2357
|
-
delete files[change.workspacePath];
|
|
2358
|
-
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));
|
|
2359
1368
|
}
|
|
2360
|
-
const returnedFileId = validFileId(response?.fileMap?.[change.repo.repoId]?.[change.repoPath]);
|
|
2361
|
-
const fileId = returnedFileId ?? change.existing?.fileId ?? state.files[change.workspacePath]?.fileId ?? null;
|
|
2362
|
-
files[change.workspacePath] = checkoutStateFile(change, validFileId(fileId), validFileType(change.existing?.fileType));
|
|
2363
1369
|
}
|
|
2364
|
-
|
|
2365
|
-
}
|
|
2366
|
-
function isWriteChange(change) {
|
|
2367
|
-
return change.action !== "delete";
|
|
2368
|
-
}
|
|
2369
|
-
function checkoutStateFile(file, fileId, fileType) {
|
|
2370
|
-
return {
|
|
2371
|
-
repoId: file.repo.repoId,
|
|
2372
|
-
repoPath: file.repoPath,
|
|
2373
|
-
fileId,
|
|
2374
|
-
sha: file.contentHash,
|
|
2375
|
-
size: file.size,
|
|
2376
|
-
fileType: fileType ?? MFS_FILE_TYPE_REGULAR2
|
|
2377
|
-
};
|
|
1370
|
+
const text = query.toString();
|
|
1371
|
+
return text ? `?${text}` : "";
|
|
2378
1372
|
}
|
|
2379
|
-
function
|
|
2380
|
-
|
|
1373
|
+
function failResponse(response) {
|
|
1374
|
+
printError(`Error [${response.status}]: ${JSON.stringify(response.data)}`);
|
|
1375
|
+
process.exit(1);
|
|
2381
1376
|
}
|
|
2382
|
-
function
|
|
2383
|
-
|
|
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
|
+
);
|
|
2384
1425
|
}
|
|
2385
|
-
function
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
const
|
|
2389
|
-
|
|
2390
|
-
|
|
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;
|
|
2391
1433
|
}
|
|
2392
|
-
return
|
|
1434
|
+
return String(value);
|
|
2393
1435
|
}
|
|
2394
|
-
function
|
|
2395
|
-
|
|
1436
|
+
function printDetailField2(name, value, indent = 0) {
|
|
1437
|
+
print(`${indentation2(indent)}${name}: ${formatDetailScalar2(value)}`);
|
|
2396
1438
|
}
|
|
2397
|
-
function
|
|
2398
|
-
if (
|
|
2399
|
-
|
|
1439
|
+
function printDetailTextField2(name, value, indent = 0) {
|
|
1440
|
+
if (value === null || value === "") {
|
|
1441
|
+
printDetailField2(name, value, indent);
|
|
1442
|
+
return;
|
|
2400
1443
|
}
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
}
|
|
2405
|
-
function resolveSafeRepoRoot(sourceDir, repoName) {
|
|
2406
|
-
assertSafePathPart(repoName, repoName);
|
|
2407
|
-
const repoRoot = resolve2(sourceDir, repoName);
|
|
2408
|
-
const relativePath = relative2(sourceDir, repoRoot);
|
|
2409
|
-
if (relativePath.startsWith("..") || isAbsolute4(relativePath)) {
|
|
2410
|
-
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}`);
|
|
2411
1448
|
}
|
|
2412
|
-
return repoRoot;
|
|
2413
1449
|
}
|
|
2414
|
-
function
|
|
2415
|
-
|
|
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
|
+
);
|
|
2416
1520
|
}
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
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);
|
|
1573
|
+
}
|
|
1574
|
+
if (response.data.workflows.length === 0) {
|
|
1575
|
+
stopSpinner(true, "No workflows found.");
|
|
1576
|
+
return;
|
|
2435
1577
|
}
|
|
1578
|
+
stopSpinner(true, `Found ${response.data.workflows.length} workflow(s)`);
|
|
1579
|
+
for (const item of response.data.workflows) printWorkflow(item);
|
|
2436
1580
|
});
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
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);
|
|
2446
1600
|
}
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
}
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
// src/utils/checkout-status.ts
|
|
2464
|
-
import { createHash as createHash4 } from "crypto";
|
|
2465
|
-
import { createReadStream } from "fs";
|
|
2466
|
-
import { lstat as lstat3, readdir as readdir2 } from "fs/promises";
|
|
2467
|
-
import { join as join3 } from "path";
|
|
2468
|
-
var CheckoutStatusError = class extends Error {
|
|
2469
|
-
constructor(message) {
|
|
2470
|
-
super(message);
|
|
2471
|
-
this.name = "CheckoutStatusError";
|
|
2472
|
-
}
|
|
2473
|
-
};
|
|
2474
|
-
async function inspectCheckoutStatus(state) {
|
|
2475
|
-
await requireCheckoutDirectory(state.checkoutRoot);
|
|
2476
|
-
const localFiles = /* @__PURE__ */ new Map();
|
|
2477
|
-
for (const repo of Object.values(state.repos)) {
|
|
2478
|
-
await scanDirectory(state, repo.name, join3(state.checkoutRoot, repo.name), localFiles);
|
|
2479
|
-
}
|
|
2480
|
-
const changes = [];
|
|
2481
|
-
for (const [path2, sha] of localFiles) {
|
|
2482
|
-
const baseline = state.files[path2];
|
|
2483
|
-
if (!baseline) changes.push({ action: "added", path: path2 });
|
|
2484
|
-
else if (baseline.sha !== sha) changes.push({ action: "modified", path: path2 });
|
|
2485
|
-
}
|
|
2486
|
-
for (const path2 of Object.keys(state.files)) {
|
|
2487
|
-
if (isCheckoutPathIncluded(state, path2) && !localFiles.has(path2)) changes.push({ action: "deleted", path: path2 });
|
|
2488
|
-
}
|
|
2489
|
-
return changes.sort((left, right) => left.path.localeCompare(right.path));
|
|
2490
|
-
}
|
|
2491
|
-
async function requireCheckoutDirectory(checkoutRoot) {
|
|
2492
|
-
let stats;
|
|
2493
|
-
try {
|
|
2494
|
-
stats = await lstat3(checkoutRoot);
|
|
2495
|
-
} catch (error) {
|
|
2496
|
-
throw new CheckoutStatusError(`Checkout root is not available: ${errorMessage2(error)}`);
|
|
2497
|
-
}
|
|
2498
|
-
if (stats.isSymbolicLink()) throw new CheckoutStatusError(`Refusing to inspect symbolic link: ${checkoutRoot}`);
|
|
2499
|
-
if (!stats.isDirectory()) throw new CheckoutStatusError(`Checkout root is not a directory: ${checkoutRoot}`);
|
|
2500
|
-
}
|
|
2501
|
-
async function scanDirectory(state, workspacePath, localPath, files) {
|
|
2502
|
-
let entries;
|
|
2503
|
-
try {
|
|
2504
|
-
entries = await readdir2(localPath, { withFileTypes: true });
|
|
2505
|
-
} catch (error) {
|
|
2506
|
-
if (isNodeError4(error) && error.code === "ENOENT") return;
|
|
2507
|
-
throw new CheckoutStatusError(`Failed to inspect ${workspacePath}: ${errorMessage2(error)}`);
|
|
2508
|
-
}
|
|
2509
|
-
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
2510
|
-
if (entry.name === ".git") continue;
|
|
2511
|
-
const childWorkspacePath = `${workspacePath}/${entry.name}`;
|
|
2512
|
-
if (!doesCheckoutPathIntersect(state, childWorkspacePath)) continue;
|
|
2513
|
-
const childLocalPath = join3(localPath, entry.name);
|
|
2514
|
-
const stats = await lstat3(childLocalPath);
|
|
2515
|
-
if (stats.isSymbolicLink()) {
|
|
2516
|
-
throw new CheckoutStatusError(`Refusing to inspect symbolic link: ${childWorkspacePath}`);
|
|
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);
|
|
2517
1616
|
}
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
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);
|
|
2522
1635
|
}
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
// src/commands/status.ts
|
|
2539
|
-
function registerStatusCommand(program2) {
|
|
2540
|
-
program2.command("status").description("Show Moxt workspace checkout status").option("--json", "Print machine-readable status").action(async (options) => {
|
|
2541
|
-
const context = resolveRuntimeContextOrExit2();
|
|
2542
|
-
if (context.mode !== "sandbox") {
|
|
2543
|
-
printError("moxt status is only available in sandbox mode");
|
|
2544
|
-
process.exit(1);
|
|
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);
|
|
2545
1650
|
}
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
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);
|
|
2550
1666
|
}
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
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);
|
|
2568
1703
|
}
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
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);
|
|
2574
1738
|
}
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
}
|
|
2603
|
-
function buildUninitializedStatus(workspaceId, repos) {
|
|
2604
|
-
return {
|
|
2605
|
-
mode: "sandbox",
|
|
2606
|
-
workspaceId,
|
|
2607
|
-
checkout: {
|
|
2608
|
-
mode: "not_initialized",
|
|
2609
|
-
checkedOutScopes: [],
|
|
2610
|
-
localChangesTracked: false
|
|
2611
|
-
},
|
|
2612
|
-
repos
|
|
2613
|
-
};
|
|
2614
|
-
}
|
|
2615
|
-
function validateStateContext(state, context) {
|
|
2616
|
-
if (state.workspaceId !== context.workspaceId) {
|
|
2617
|
-
throw new Error(`Checkout state belongs to workspace ${state.workspaceId}, not ${context.workspaceId}`);
|
|
2618
|
-
}
|
|
2619
|
-
const currentRepos = new Map(listRepoEntries(context.repoPayload).map((repo) => [repo.repoId, repo.name]));
|
|
2620
|
-
for (const [repoId, repo] of Object.entries(state.repos)) {
|
|
2621
|
-
const currentName = currentRepos.get(repoId);
|
|
2622
|
-
if (!currentName) throw new Error(`Checkout state contains unknown repo: ${repoId}`);
|
|
2623
|
-
if (currentName !== repo.name) {
|
|
2624
|
-
throw new Error(`Checkout state repo name does not match current workspace: ${repo.name}`);
|
|
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");
|
|
2625
1766
|
}
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
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);
|
|
2635
1779
|
}
|
|
2636
|
-
|
|
2637
|
-
}
|
|
2638
|
-
}
|
|
2639
|
-
function formatCheckoutMode(mode) {
|
|
2640
|
-
return mode === "not_initialized" ? "not initialized" : `${mode} checkout`;
|
|
2641
|
-
}
|
|
2642
|
-
|
|
2643
|
-
// src/telemetry/config.ts
|
|
2644
|
-
import * as fs3 from "fs";
|
|
2645
|
-
import * as os from "os";
|
|
2646
|
-
import * as path from "path";
|
|
2647
|
-
function getConfigPath() {
|
|
2648
|
-
return path.join(os.homedir(), ".moxt", "config.json");
|
|
2649
|
-
}
|
|
2650
|
-
function readTelemetryConfig() {
|
|
2651
|
-
try {
|
|
2652
|
-
const raw = fs3.readFileSync(getConfigPath(), "utf8");
|
|
2653
|
-
const parsed = JSON.parse(raw);
|
|
2654
|
-
return parsed.telemetry ?? {};
|
|
2655
|
-
} catch {
|
|
2656
|
-
return {};
|
|
2657
|
-
}
|
|
2658
|
-
}
|
|
2659
|
-
function writeTelemetryConfig(update) {
|
|
2660
|
-
const configPath = getConfigPath();
|
|
2661
|
-
let existing = {};
|
|
2662
|
-
try {
|
|
2663
|
-
existing = JSON.parse(fs3.readFileSync(configPath, "utf8"));
|
|
2664
|
-
} catch {
|
|
2665
|
-
existing = {};
|
|
2666
|
-
}
|
|
2667
|
-
const currentTelemetry = existing.telemetry ?? {};
|
|
2668
|
-
const next = { ...existing, telemetry: { ...currentTelemetry, ...update } };
|
|
2669
|
-
fs3.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
2670
|
-
fs3.writeFileSync(configPath, JSON.stringify(next, null, 2));
|
|
2671
|
-
}
|
|
2672
|
-
|
|
2673
|
-
// src/telemetry/opt-out.ts
|
|
2674
|
-
function isCiEnvironment() {
|
|
2675
|
-
return process.env.CI === "true" || process.env.CI === "1";
|
|
1780
|
+
stopSpinner(true, `Deleted: ${response.data.scopedId}`);
|
|
1781
|
+
});
|
|
2676
1782
|
}
|
|
2677
|
-
function
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
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
|
+
});
|
|
2683
1808
|
}
|
|
2684
|
-
return readTelemetryConfig().disabled === true;
|
|
2685
1809
|
}
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
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}`);
|
|
2699
1835
|
}
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
// src/commands/whoami.ts
|
|
2712
|
-
function registerWhoamiCommand(program2) {
|
|
2713
|
-
program2.command("whoami").description("Show current user information").action(async () => {
|
|
2714
|
-
startSpinner("Fetching user info...");
|
|
2715
|
-
const response = await httpGet("/users/whoami");
|
|
2716
|
-
if (response.ok) {
|
|
2717
|
-
stopSpinner(true, "Done");
|
|
2718
|
-
print(`${colors.bold}Nickname${colors.reset}: ${response.data.nickname}`);
|
|
2719
|
-
print(`${colors.bold}Email${colors.reset}: ${response.data.email}`);
|
|
2720
|
-
} 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) {
|
|
2721
1846
|
stopSpinner(false, "Failed");
|
|
2722
|
-
|
|
2723
|
-
process.exit(1);
|
|
1847
|
+
failResponse(response);
|
|
2724
1848
|
}
|
|
1849
|
+
stopSpinner(true, "Task fetched");
|
|
1850
|
+
printTaskDetail(response.data);
|
|
2725
1851
|
});
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
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);
|
|
2743
1879
|
}
|
|
2744
|
-
stopSpinner(true,
|
|
2745
|
-
|
|
2746
|
-
|
|
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);
|
|
2747
1913
|
}
|
|
2748
|
-
|
|
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) {
|
|
2749
1994
|
stopSpinner(false, "Failed");
|
|
2750
|
-
|
|
2751
|
-
process.exit(1);
|
|
1995
|
+
failResponse(response);
|
|
2752
1996
|
}
|
|
1997
|
+
stopSpinner(true, `Deleted: ${response.data.scopedId}`);
|
|
2753
1998
|
});
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
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
|
+
);
|
|
2759
2031
|
if (!response.ok) {
|
|
2760
|
-
stopSpinner(false, "Failed
|
|
2761
|
-
|
|
2762
|
-
process.exit(1);
|
|
2032
|
+
stopSpinner(false, "Failed");
|
|
2033
|
+
failResponse(response);
|
|
2763
2034
|
}
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
stopSpinner(true, "No members found.");
|
|
2035
|
+
if (response.data.items.length === 0) {
|
|
2036
|
+
stopSpinner(true, "No comments found.");
|
|
2767
2037
|
return;
|
|
2768
2038
|
}
|
|
2769
|
-
stopSpinner(true, `Found ${
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
}
|
|
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
|
+
});
|
|
2774
2044
|
});
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
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);
|
|
2786
2060
|
}
|
|
2787
|
-
|
|
2061
|
+
stopSpinner(true, `Deleted: ${response.data.id}`);
|
|
2788
2062
|
}
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
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);
|
|
2795
2081
|
}
|
|
2796
|
-
stopSpinner(true,
|
|
2797
|
-
const
|
|
2798
|
-
|
|
2799
|
-
for (const email of emails) {
|
|
2800
|
-
const member = emailToMember.get(email.toLowerCase());
|
|
2801
|
-
if (!member) {
|
|
2802
|
-
print(`${colors.red}\u2717${colors.reset} ${email} - not found in workspace`);
|
|
2803
|
-
continue;
|
|
2804
|
-
}
|
|
2805
|
-
const deleteResponse = await httpDelete(`/workspaces/${workspaceId}/members/${member.userId}`);
|
|
2806
|
-
if (deleteResponse.ok) {
|
|
2807
|
-
print(`${colors.green}\u2713${colors.reset} ${email} - removed`);
|
|
2808
|
-
} else {
|
|
2809
|
-
print(`${colors.red}\u2717${colors.reset} ${email} - failed: ${JSON.stringify(deleteResponse.data)}`);
|
|
2810
|
-
}
|
|
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}`);
|
|
2811
2085
|
}
|
|
2812
2086
|
});
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
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
|
+
);
|
|
2818
2095
|
if (!response.ok) {
|
|
2819
|
-
stopSpinner(false, "Failed
|
|
2820
|
-
|
|
2821
|
-
process.exit(1);
|
|
2822
|
-
}
|
|
2823
|
-
const { spaces } = response.data;
|
|
2824
|
-
if (spaces.length === 0) {
|
|
2825
|
-
stopSpinner(true, "No spaces found.");
|
|
2826
|
-
return;
|
|
2096
|
+
stopSpinner(false, "Failed");
|
|
2097
|
+
failResponse(response);
|
|
2827
2098
|
}
|
|
2828
|
-
stopSpinner(true, `
|
|
2829
|
-
print(
|
|
2830
|
-
|
|
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
|
+
{}
|
|
2831
2110
|
);
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
print(`${s.type} ${teamSpaceId} ${teammateId} ${s.name}`);
|
|
2111
|
+
if (!response.ok) {
|
|
2112
|
+
stopSpinner(false, "Failed");
|
|
2113
|
+
failResponse(response);
|
|
2836
2114
|
}
|
|
2115
|
+
stopSpinner(true, `Abort accepted: ${id}`);
|
|
2837
2116
|
});
|
|
2838
2117
|
}
|
|
2839
2118
|
|
|
2840
2119
|
// src/program.ts
|
|
2841
2120
|
function createProgram(version) {
|
|
2842
|
-
const program2 = new
|
|
2121
|
+
const program2 = new Command();
|
|
2843
2122
|
program2.name("moxt").description("Moxt CLI - AI Workspace").version(version).action(() => {
|
|
2844
2123
|
program2.help();
|
|
2845
2124
|
});
|
|
2846
|
-
registerAuthCommand(program2);
|
|
2847
2125
|
registerWhoamiCommand(program2);
|
|
2848
2126
|
registerWorkspaceCommand(program2);
|
|
2849
2127
|
registerFileCommand(program2);
|
|
2850
2128
|
registerMemoryCommand(program2);
|
|
2129
|
+
registerWorkflowCommand(program2);
|
|
2851
2130
|
registerMiniappCommand(program2);
|
|
2852
|
-
registerPullCommand(program2);
|
|
2853
|
-
registerPushCommand(program2);
|
|
2854
|
-
registerStatusCommand(program2);
|
|
2855
2131
|
registerTelemetryCommand(program2);
|
|
2856
2132
|
return program2;
|
|
2857
2133
|
}
|
|
@@ -2863,7 +2139,7 @@ var FLUSH_TIMEOUT_MS = 3e3;
|
|
|
2863
2139
|
var MAX_BATCH_SIZE = 100;
|
|
2864
2140
|
function commonLabels() {
|
|
2865
2141
|
return {
|
|
2866
|
-
cli_version: "0.4.0
|
|
2142
|
+
cli_version: "0.4.0",
|
|
2867
2143
|
node_version: process.versions.node,
|
|
2868
2144
|
os: platform2(),
|
|
2869
2145
|
arch: arch2(),
|
|
@@ -2909,7 +2185,7 @@ async function flush() {
|
|
|
2909
2185
|
}
|
|
2910
2186
|
const batch = buffer.splice(0, MAX_BATCH_SIZE);
|
|
2911
2187
|
const payload = {
|
|
2912
|
-
release_id: "0.4.0
|
|
2188
|
+
release_id: "0.4.0",
|
|
2913
2189
|
client_type: "cli",
|
|
2914
2190
|
metric_data_list: batch.map((m) => ({
|
|
2915
2191
|
profile: "cli",
|
|
@@ -3050,9 +2326,9 @@ function installExitHandler() {
|
|
|
3050
2326
|
|
|
3051
2327
|
// src/index.ts
|
|
3052
2328
|
updateNotifier({
|
|
3053
|
-
pkg: { name: "@moxt-ai/cli", version: "0.4.0
|
|
2329
|
+
pkg: { name: "@moxt-ai/cli", version: "0.4.0" }
|
|
3054
2330
|
}).notify();
|
|
3055
|
-
var program = createProgram("0.4.0
|
|
2331
|
+
var program = createProgram("0.4.0");
|
|
3056
2332
|
instrumentProgram(program);
|
|
3057
2333
|
installExitHandler();
|
|
3058
2334
|
program.parseAsync(process.argv).then(async () => {
|