@moxt-ai/cli 0.3.3-moxt-run.1 → 0.4.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -38
- package/dist/index.js +1723 -1990
- package/dist/index.js.map +1 -1
- package/package.json +4 -6
package/dist/index.js
CHANGED
|
@@ -4,10 +4,203 @@
|
|
|
4
4
|
import updateNotifier from "update-notifier";
|
|
5
5
|
|
|
6
6
|
// src/program.ts
|
|
7
|
-
import { Command } from "commander";
|
|
7
|
+
import { Command as Command2 } from "commander";
|
|
8
|
+
|
|
9
|
+
// src/utils/output.ts
|
|
10
|
+
var colors = {
|
|
11
|
+
bold: "\x1B[1m",
|
|
12
|
+
blue: "\x1B[1;34m",
|
|
13
|
+
green: "\x1B[32m",
|
|
14
|
+
red: "\x1B[31m",
|
|
15
|
+
cyan: "\x1B[36m",
|
|
16
|
+
dim: "\x1B[2m",
|
|
17
|
+
reset: "\x1B[0m"
|
|
18
|
+
};
|
|
19
|
+
function print(message) {
|
|
20
|
+
console.log(message);
|
|
21
|
+
}
|
|
22
|
+
function printError(message) {
|
|
23
|
+
console.error(`${colors.red}${message}${colors.reset}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// src/utils/runtime-context.ts
|
|
27
|
+
var DEFAULT_USER_HOST = "api.moxt.ai";
|
|
28
|
+
var SANDBOX_ENV_KEYS = [
|
|
29
|
+
"BASE_API_HOST",
|
|
30
|
+
"SANDBOX_TOOL_API_TOKEN",
|
|
31
|
+
"MOXT_REPO_PAYLOAD",
|
|
32
|
+
"MOXT_WORKSPACE_ID"
|
|
33
|
+
];
|
|
34
|
+
var RuntimeContextError = class extends Error {
|
|
35
|
+
constructor(message) {
|
|
36
|
+
super(message);
|
|
37
|
+
this.name = "RuntimeContextError";
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
function resolveRuntimeContext(env = process.env) {
|
|
41
|
+
if (hasAnySandboxEnv(env)) {
|
|
42
|
+
return resolveSandboxRuntimeContext(env);
|
|
43
|
+
}
|
|
44
|
+
const apiKey = readNonEmptyEnv(env, "MOXT_API_KEY");
|
|
45
|
+
if (apiKey) {
|
|
46
|
+
return {
|
|
47
|
+
mode: "user",
|
|
48
|
+
host: readNonEmptyEnv(env, "MOXT_HOST") ?? DEFAULT_USER_HOST,
|
|
49
|
+
apiKey
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
throw new RuntimeContextError("Missing auth. Set sandbox environment variables or MOXT_API_KEY.");
|
|
53
|
+
}
|
|
54
|
+
function resolveSandboxRuntimeContext(env = process.env) {
|
|
55
|
+
const missingKeys = SANDBOX_ENV_KEYS.filter((key) => !readNonEmptyEnv(env, key));
|
|
56
|
+
if (missingKeys.length > 0) {
|
|
57
|
+
throw new RuntimeContextError(`Incomplete sandbox environment. Missing: ${missingKeys.join(", ")}.`);
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
mode: "sandbox",
|
|
61
|
+
baseApiHost: normalizeBaseApiHost(readRequiredEnv(env, "BASE_API_HOST")),
|
|
62
|
+
sandboxToolApiToken: readRequiredEnv(env, "SANDBOX_TOOL_API_TOKEN"),
|
|
63
|
+
workspaceId: readRequiredEnv(env, "MOXT_WORKSPACE_ID"),
|
|
64
|
+
repoPayload: parseRepoPayload(readRequiredEnv(env, "MOXT_REPO_PAYLOAD"))
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function parseRepoPayload(raw) {
|
|
68
|
+
let parsed;
|
|
69
|
+
try {
|
|
70
|
+
parsed = JSON.parse(raw);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
throw new RuntimeContextError(`MOXT_REPO_PAYLOAD must be valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
73
|
+
}
|
|
74
|
+
if (!isRecord(parsed)) {
|
|
75
|
+
throw new RuntimeContextError("MOXT_REPO_PAYLOAD must be a JSON object.");
|
|
76
|
+
}
|
|
77
|
+
const personal = parseRepoEntry(parsed.personal, "personal");
|
|
78
|
+
const teamsValue = parsed.teams;
|
|
79
|
+
if (!Array.isArray(teamsValue)) {
|
|
80
|
+
throw new RuntimeContextError("MOXT_REPO_PAYLOAD.teams must be an array.");
|
|
81
|
+
}
|
|
82
|
+
const teams = teamsValue.map((entry, index) => parseRepoEntry(entry, `teams[${index}]`));
|
|
83
|
+
const workspace = parsed.workspace == null ? null : parseRepoEntry(parsed.workspace, "workspace");
|
|
84
|
+
return { personal, teams, workspace };
|
|
85
|
+
}
|
|
86
|
+
function normalizeBaseApiHost(value) {
|
|
87
|
+
const trimmed = value.trim();
|
|
88
|
+
if (!trimmed) {
|
|
89
|
+
throw new RuntimeContextError("BASE_API_HOST must not be empty.");
|
|
90
|
+
}
|
|
91
|
+
return trimmed.replace(/\/+$/, "");
|
|
92
|
+
}
|
|
93
|
+
function hasAnySandboxEnv(env) {
|
|
94
|
+
return SANDBOX_ENV_KEYS.some((key) => readNonEmptyEnv(env, key) !== void 0);
|
|
95
|
+
}
|
|
96
|
+
function readRequiredEnv(env, key) {
|
|
97
|
+
const value = readNonEmptyEnv(env, key);
|
|
98
|
+
if (!value) {
|
|
99
|
+
throw new RuntimeContextError(`${key} is required.`);
|
|
100
|
+
}
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
103
|
+
function readNonEmptyEnv(env, key) {
|
|
104
|
+
const value = env[key];
|
|
105
|
+
if (typeof value !== "string") return void 0;
|
|
106
|
+
const trimmed = value.trim();
|
|
107
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
108
|
+
}
|
|
109
|
+
function parseRepoEntry(value, path2) {
|
|
110
|
+
if (!isRecord(value)) {
|
|
111
|
+
throw new RuntimeContextError(`MOXT_REPO_PAYLOAD.${path2} must be an object.`);
|
|
112
|
+
}
|
|
113
|
+
const repoId = readStringField(value, "repoId", path2);
|
|
114
|
+
const name = readStringField(value, "name", path2);
|
|
115
|
+
return { repoId, name };
|
|
116
|
+
}
|
|
117
|
+
function readStringField(record2, field, path2) {
|
|
118
|
+
const value = record2[field];
|
|
119
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
120
|
+
throw new RuntimeContextError(`MOXT_REPO_PAYLOAD.${path2}.${field} must be a non-empty string.`);
|
|
121
|
+
}
|
|
122
|
+
return value;
|
|
123
|
+
}
|
|
124
|
+
function isRecord(value) {
|
|
125
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// src/utils/sandbox-path.ts
|
|
129
|
+
var WorkspacePathError = class extends Error {
|
|
130
|
+
constructor(message) {
|
|
131
|
+
super(message);
|
|
132
|
+
this.name = "WorkspacePathError";
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
function resolveWorkspacePath(repoPayload, workspacePath) {
|
|
136
|
+
const normalizedPath = normalizeWorkspacePath(workspacePath);
|
|
137
|
+
const [repoName, ...repoPathParts] = normalizedPath.split("/");
|
|
138
|
+
const repoEntries = listRepoEntries(repoPayload);
|
|
139
|
+
const repo = repoEntries.find((entry) => entry.name === repoName);
|
|
140
|
+
if (!repo) {
|
|
141
|
+
throw new WorkspacePathError(`Workspace path must start with one of: ${repoEntries.map((entry) => entry.name).join(", ")}.`);
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
repoId: repo.repoId,
|
|
145
|
+
repoName: repo.name,
|
|
146
|
+
repoPath: repoPathParts.join("/")
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function listRepoEntries(repoPayload) {
|
|
150
|
+
return [
|
|
151
|
+
repoPayload.personal,
|
|
152
|
+
...repoPayload.teams,
|
|
153
|
+
...repoPayload.workspace ? [repoPayload.workspace] : []
|
|
154
|
+
];
|
|
155
|
+
}
|
|
156
|
+
function normalizeWorkspacePath(workspacePath) {
|
|
157
|
+
const trimmed = workspacePath.trim();
|
|
158
|
+
if (!trimmed) {
|
|
159
|
+
throw new WorkspacePathError("Workspace path must not be empty.");
|
|
160
|
+
}
|
|
161
|
+
if (trimmed.startsWith("/")) {
|
|
162
|
+
throw new WorkspacePathError("Workspace path must be relative.");
|
|
163
|
+
}
|
|
164
|
+
const parts = trimmed.split("/").filter((part) => part.length > 0 && part !== ".");
|
|
165
|
+
if (parts.length === 0) {
|
|
166
|
+
throw new WorkspacePathError("Workspace path must include a repo name.");
|
|
167
|
+
}
|
|
168
|
+
if (parts.some((part) => part === "..")) {
|
|
169
|
+
throw new WorkspacePathError('Workspace path must not contain "..".');
|
|
170
|
+
}
|
|
171
|
+
return parts.join("/");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// src/commands/auth.ts
|
|
175
|
+
function registerAuthCommand(program2) {
|
|
176
|
+
const auth = program2.command("auth").description("Authentication utilities");
|
|
177
|
+
auth.command("status").description("Show CLI authentication mode").action(() => {
|
|
178
|
+
try {
|
|
179
|
+
const context = resolveRuntimeContext();
|
|
180
|
+
if (context.mode === "sandbox") {
|
|
181
|
+
print(`${colors.bold}Mode${colors.reset}: sandbox`);
|
|
182
|
+
print(`${colors.bold}Workspace${colors.reset}: ${context.workspaceId}`);
|
|
183
|
+
print(`${colors.bold}Base API${colors.reset}: ${context.baseApiHost}`);
|
|
184
|
+
print(`${colors.bold}Repos${colors.reset}:`);
|
|
185
|
+
for (const repo of listRepoEntries(context.repoPayload)) {
|
|
186
|
+
print(` ${repo.name} ${repo.repoId}`);
|
|
187
|
+
}
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
print(`${colors.bold}Mode${colors.reset}: user`);
|
|
191
|
+
print(`${colors.bold}Host${colors.reset}: ${context.host}`);
|
|
192
|
+
} catch (error) {
|
|
193
|
+
if (error instanceof RuntimeContextError) {
|
|
194
|
+
printError(error.message);
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
}
|
|
8
201
|
|
|
9
202
|
// src/commands/file.ts
|
|
10
|
-
import * as
|
|
203
|
+
import * as fs2 from "fs";
|
|
11
204
|
|
|
12
205
|
// src/utils/file-selector.ts
|
|
13
206
|
var SpaceOptionsError = class extends Error {
|
|
@@ -149,7 +342,7 @@ function getApiKeyOrUndefined() {
|
|
|
149
342
|
|
|
150
343
|
// src/utils/http.ts
|
|
151
344
|
function getUserAgent() {
|
|
152
|
-
return `moxt-cli/${"0.
|
|
345
|
+
return `moxt-cli/${"0.4.0-beta.0"} (${platform()}/${arch()}) node/${process.versions.node}`;
|
|
153
346
|
}
|
|
154
347
|
async function fetchApi(method, path2, body) {
|
|
155
348
|
const apiKey = getApiKey();
|
|
@@ -202,23 +395,6 @@ async function httpRaw(method, path2, body) {
|
|
|
202
395
|
};
|
|
203
396
|
}
|
|
204
397
|
|
|
205
|
-
// src/utils/output.ts
|
|
206
|
-
var colors = {
|
|
207
|
-
bold: "\x1B[1m",
|
|
208
|
-
blue: "\x1B[1;34m",
|
|
209
|
-
green: "\x1B[32m",
|
|
210
|
-
red: "\x1B[31m",
|
|
211
|
-
cyan: "\x1B[36m",
|
|
212
|
-
dim: "\x1B[2m",
|
|
213
|
-
reset: "\x1B[0m"
|
|
214
|
-
};
|
|
215
|
-
function print(message) {
|
|
216
|
-
console.log(message);
|
|
217
|
-
}
|
|
218
|
-
function printError(message) {
|
|
219
|
-
console.error(`${colors.red}${message}${colors.reset}`);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
398
|
// src/utils/search-options.ts
|
|
223
399
|
var SearchOptionsError = class extends Error {
|
|
224
400
|
constructor(message) {
|
|
@@ -272,6 +448,209 @@ function runOrExit(fn) {
|
|
|
272
448
|
}
|
|
273
449
|
}
|
|
274
450
|
|
|
451
|
+
// src/utils/sandbox-client.ts
|
|
452
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
453
|
+
var SandboxApiError = class extends Error {
|
|
454
|
+
status;
|
|
455
|
+
body;
|
|
456
|
+
constructor(status, body) {
|
|
457
|
+
super(`Sandbox API request failed with ${status}: ${body}`);
|
|
458
|
+
this.name = "SandboxApiError";
|
|
459
|
+
this.status = status;
|
|
460
|
+
this.body = body;
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
var SandboxMoxtClient = class {
|
|
464
|
+
constructor(context) {
|
|
465
|
+
this.context = context;
|
|
466
|
+
}
|
|
467
|
+
context;
|
|
468
|
+
async request(method, path2, body, options = {}) {
|
|
469
|
+
const signal = AbortSignal.timeout(options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
470
|
+
const response = await fetch(`${this.context.baseApiHost}${path2}`, {
|
|
471
|
+
method,
|
|
472
|
+
headers: {
|
|
473
|
+
"Content-Type": "application/json",
|
|
474
|
+
Authorization: `Bearer ${this.context.sandboxToolApiToken}`
|
|
475
|
+
},
|
|
476
|
+
body: body === void 0 ? void 0 : JSON.stringify(body),
|
|
477
|
+
signal
|
|
478
|
+
});
|
|
479
|
+
const contentType = response.headers.get("content-type");
|
|
480
|
+
const data = contentType?.includes("application/json") ? await response.json() : await response.text();
|
|
481
|
+
if (!response.ok) {
|
|
482
|
+
throw new SandboxApiError(response.status, typeof data === "string" ? data : JSON.stringify(data));
|
|
483
|
+
}
|
|
484
|
+
return {
|
|
485
|
+
status: response.status,
|
|
486
|
+
data
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
// src/utils/sandbox-file.ts
|
|
492
|
+
import { createHash } from "crypto";
|
|
493
|
+
var MFS_FILE_TYPE_REGULAR = 1;
|
|
494
|
+
var MFS_FILE_SIZE_LIMIT_BYTES = 10 * 1024 * 1024;
|
|
495
|
+
var MFS_FILE_SIZE_LIMIT_LABEL = `${MFS_FILE_SIZE_LIMIT_BYTES / (1024 * 1024)}MB`;
|
|
496
|
+
var SandboxFileError = class extends Error {
|
|
497
|
+
constructor(message, code) {
|
|
498
|
+
super(message);
|
|
499
|
+
this.code = code;
|
|
500
|
+
this.name = "SandboxFileError";
|
|
501
|
+
}
|
|
502
|
+
code;
|
|
503
|
+
};
|
|
504
|
+
async function readSandboxFile(context, workspacePath) {
|
|
505
|
+
const resolved = resolveSandboxFilePath(context, workspacePath);
|
|
506
|
+
const tree = await fetchRepoTree(context, resolved);
|
|
507
|
+
const entry = findTreeEntry(tree, resolved);
|
|
508
|
+
if (!entry) {
|
|
509
|
+
throw new SandboxFileError(`${workspacePath} does not exist`, "not-found");
|
|
510
|
+
}
|
|
511
|
+
if (entry.type === "tree") {
|
|
512
|
+
throw new SandboxFileError(`${workspacePath} is a directory`, "directory");
|
|
513
|
+
}
|
|
514
|
+
if (!entry.sha) {
|
|
515
|
+
throw new SandboxFileError(`${workspacePath} does not have downloadable content`, "remote");
|
|
516
|
+
}
|
|
517
|
+
if (entry.size !== null && entry.size > MFS_FILE_SIZE_LIMIT_BYTES) {
|
|
518
|
+
throw new SandboxFileError(`File too large (max ${MFS_FILE_SIZE_LIMIT_LABEL}): ${workspacePath}`, "too-large");
|
|
519
|
+
}
|
|
520
|
+
const client = new SandboxMoxtClient(context);
|
|
521
|
+
const presign = await client.request(
|
|
522
|
+
"POST",
|
|
523
|
+
`/agent/api/pipeline-sandbox/mfs/${encodeURIComponent(resolved.repoId)}/presign-download`,
|
|
524
|
+
{ hashes: [entry.sha] }
|
|
525
|
+
);
|
|
526
|
+
const url = presign.data.urls[entry.sha];
|
|
527
|
+
if (!url) {
|
|
528
|
+
throw new SandboxFileError(`No download URL returned for ${workspacePath}`, "remote");
|
|
529
|
+
}
|
|
530
|
+
const response = await fetch(url);
|
|
531
|
+
if (!response.ok) {
|
|
532
|
+
throw new SandboxFileError(`Download failed [${response.status}]`, "remote");
|
|
533
|
+
}
|
|
534
|
+
const content = await response.text();
|
|
535
|
+
const data = Buffer.from(content, "utf8");
|
|
536
|
+
if (data.length > MFS_FILE_SIZE_LIMIT_BYTES) {
|
|
537
|
+
throw new SandboxFileError(`File too large (max ${MFS_FILE_SIZE_LIMIT_LABEL}): ${workspacePath}`, "too-large");
|
|
538
|
+
}
|
|
539
|
+
const actualSha = createHash("sha256").update(data).digest("hex");
|
|
540
|
+
if (actualSha !== entry.sha) {
|
|
541
|
+
throw new SandboxFileError(`Downloaded content hash mismatch: ${workspacePath}`, "remote");
|
|
542
|
+
}
|
|
543
|
+
return {
|
|
544
|
+
path: workspacePath,
|
|
545
|
+
content,
|
|
546
|
+
sha: entry.sha,
|
|
547
|
+
size: data.length,
|
|
548
|
+
fileId: entry.fileId,
|
|
549
|
+
fileType: entry.fileType
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
async function writeSandboxFile(context, workspacePath, content, policy) {
|
|
553
|
+
const data = Buffer.from(content, "utf8");
|
|
554
|
+
if (data.length > MFS_FILE_SIZE_LIMIT_BYTES) {
|
|
555
|
+
throw new SandboxFileError(`File too large (max ${MFS_FILE_SIZE_LIMIT_LABEL}): ${workspacePath}`, "too-large");
|
|
556
|
+
}
|
|
557
|
+
const resolved = resolveSandboxFilePath(context, workspacePath);
|
|
558
|
+
const tree = await fetchRepoTree(context, resolved);
|
|
559
|
+
const existing = findTreeEntry(tree, resolved);
|
|
560
|
+
if (existing?.type === "tree") {
|
|
561
|
+
throw new SandboxFileError(`${workspacePath} is a directory`, "directory");
|
|
562
|
+
}
|
|
563
|
+
if (existing && !existing.sha) {
|
|
564
|
+
throw new SandboxFileError(`${workspacePath} does not have base content hash`, "remote");
|
|
565
|
+
}
|
|
566
|
+
const baseContentHash = resolveBaseContentHash(workspacePath, existing, policy);
|
|
567
|
+
const contentHash = createHash("sha256").update(data).digest("hex");
|
|
568
|
+
const client = new SandboxMoxtClient(context);
|
|
569
|
+
const presign = await client.request(
|
|
570
|
+
"POST",
|
|
571
|
+
`/agent/api/pipeline-sandbox/mfs/${encodeURIComponent(resolved.repoId)}/presign-upload`,
|
|
572
|
+
{ hashes: [contentHash] }
|
|
573
|
+
);
|
|
574
|
+
const uploadUrl = presign.data.urls[contentHash];
|
|
575
|
+
if (!uploadUrl) {
|
|
576
|
+
throw new SandboxFileError(`No upload URL returned for ${workspacePath}`, "remote");
|
|
577
|
+
}
|
|
578
|
+
const upload = await fetch(uploadUrl, {
|
|
579
|
+
method: "PUT",
|
|
580
|
+
headers: { "Content-Type": "application/octet-stream" },
|
|
581
|
+
body: new Uint8Array(data)
|
|
582
|
+
});
|
|
583
|
+
if (!upload.ok) {
|
|
584
|
+
throw new SandboxFileError(`Upload failed [${upload.status}]`, "remote");
|
|
585
|
+
}
|
|
586
|
+
const action = existing ? "update" : "create";
|
|
587
|
+
await client.request(
|
|
588
|
+
"POST",
|
|
589
|
+
"/agent/api/pipeline-sandbox/mfs-workspace/unified-push",
|
|
590
|
+
{
|
|
591
|
+
message: `moxt file write ${workspacePath}`,
|
|
592
|
+
repoPushes: [
|
|
593
|
+
{
|
|
594
|
+
repoId: resolved.repoId,
|
|
595
|
+
changes: [
|
|
596
|
+
{
|
|
597
|
+
action,
|
|
598
|
+
...existing?.fileId ? { fileId: existing.fileId } : {},
|
|
599
|
+
path: resolved.repoPath,
|
|
600
|
+
contentHash,
|
|
601
|
+
...baseContentHash ? { baseContentHash } : {},
|
|
602
|
+
size: data.length,
|
|
603
|
+
fileType: MFS_FILE_TYPE_REGULAR
|
|
604
|
+
}
|
|
605
|
+
]
|
|
606
|
+
}
|
|
607
|
+
],
|
|
608
|
+
crossRepoMoves: []
|
|
609
|
+
}
|
|
610
|
+
);
|
|
611
|
+
return { path: workspacePath, created: !existing };
|
|
612
|
+
}
|
|
613
|
+
function resolveBaseContentHash(workspacePath, existing, policy) {
|
|
614
|
+
if (policy.type === "force") return void 0;
|
|
615
|
+
if (policy.type === "use-current-sha") return existing?.sha ?? void 0;
|
|
616
|
+
if (policy.type === "create-only") {
|
|
617
|
+
if (!existing) return void 0;
|
|
618
|
+
throw new SandboxFileError(
|
|
619
|
+
`Existing file requires --base-sha or --force: ${workspacePath} (actual ${existing.sha})`,
|
|
620
|
+
"conflict"
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
if (existing?.sha === policy.sha) return policy.sha;
|
|
624
|
+
throw new SandboxFileError(
|
|
625
|
+
`File version conflict: ${workspacePath} (expected ${policy.sha}, actual ${existing?.sha ?? "missing"})`,
|
|
626
|
+
"conflict"
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
function resolveSandboxFilePath(context, workspacePath) {
|
|
630
|
+
let resolved;
|
|
631
|
+
try {
|
|
632
|
+
resolved = resolveWorkspacePath(context.repoPayload, workspacePath);
|
|
633
|
+
} catch (error) {
|
|
634
|
+
throw new SandboxFileError(error instanceof Error ? error.message : String(error), "invalid-path");
|
|
635
|
+
}
|
|
636
|
+
if (!resolved.repoPath) {
|
|
637
|
+
throw new SandboxFileError(`${workspacePath} is a directory`, "directory");
|
|
638
|
+
}
|
|
639
|
+
return resolved;
|
|
640
|
+
}
|
|
641
|
+
async function fetchRepoTree(context, resolved) {
|
|
642
|
+
const client = new SandboxMoxtClient(context);
|
|
643
|
+
const params = new URLSearchParams({ entryFilter: "paths", path: resolved.repoPath });
|
|
644
|
+
const response = await client.request(
|
|
645
|
+
"GET",
|
|
646
|
+
`/agent/api/pipeline-sandbox/mfs/${encodeURIComponent(resolved.repoId)}/tree?${params.toString()}`
|
|
647
|
+
);
|
|
648
|
+
return response.data;
|
|
649
|
+
}
|
|
650
|
+
function findTreeEntry(tree, resolved) {
|
|
651
|
+
return tree.entries.find((entry) => entry.path === resolved.repoPath);
|
|
652
|
+
}
|
|
653
|
+
|
|
275
654
|
// src/utils/spinner.ts
|
|
276
655
|
var frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
277
656
|
var intervalId = null;
|
|
@@ -294,6 +673,23 @@ function stopSpinner(success, message) {
|
|
|
294
673
|
`);
|
|
295
674
|
}
|
|
296
675
|
|
|
676
|
+
// src/utils/write-content.ts
|
|
677
|
+
import * as fs from "fs";
|
|
678
|
+
function readWriteCommandContentOrExit(options, readContentFile, readStdin = () => fs.readFileSync(0, "utf8")) {
|
|
679
|
+
const inputModes = [options.content !== void 0, options.contentFile !== void 0, options.stdin === true].filter(Boolean);
|
|
680
|
+
if (inputModes.length > 1) {
|
|
681
|
+
printError("Only one of --content, --content-file, or --stdin can be used");
|
|
682
|
+
process.exit(1);
|
|
683
|
+
}
|
|
684
|
+
if (options.content !== void 0) {
|
|
685
|
+
return options.content;
|
|
686
|
+
}
|
|
687
|
+
if (options.contentFile !== void 0) {
|
|
688
|
+
return readContentFile(options.contentFile);
|
|
689
|
+
}
|
|
690
|
+
return readStdin();
|
|
691
|
+
}
|
|
692
|
+
|
|
297
693
|
// src/commands/file.ts
|
|
298
694
|
function addSpaceOptions(cmd) {
|
|
299
695
|
return cmd.requiredOption("-w, --workspace <workspaceId>", "Workspace ID").option("-s, --space <name>", "Space name (personal or teamspace name)").option("--personal", "Use personal space (default)").option("--team-space-id <teamSpaceId>", "Team space ID").option("--teammate-id <teammateId>", "Teammate ID (teammate space)");
|
|
@@ -392,7 +788,39 @@ function registerFileCommand(program2) {
|
|
|
392
788
|
}
|
|
393
789
|
}
|
|
394
790
|
});
|
|
395
|
-
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) => {
|
|
791
|
+
file.command("read").description("Read file content").argument("[workspacePath]", "Workspace path in sandbox mode, e.g. personal/docs/readme.md").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})").option("--json", "Print sandbox file content and metadata as JSON").action(async (workspacePath, options) => {
|
|
792
|
+
const sandboxRuntime = resolveSandboxRuntimeContextOrExit();
|
|
793
|
+
if (sandboxRuntime) {
|
|
794
|
+
if (options.url) {
|
|
795
|
+
printError("--url cannot be used in sandbox mode");
|
|
796
|
+
process.exit(1);
|
|
797
|
+
}
|
|
798
|
+
const path2 = workspacePath ?? options.path;
|
|
799
|
+
if (!path2) {
|
|
800
|
+
printError("Workspace path is required in sandbox mode");
|
|
801
|
+
process.exit(1);
|
|
802
|
+
}
|
|
803
|
+
if (!options.json) startSpinner("Reading file...");
|
|
804
|
+
try {
|
|
805
|
+
const result = await readSandboxFile(sandboxRuntime, path2);
|
|
806
|
+
if (options.json) {
|
|
807
|
+
print(JSON.stringify(result, null, 2));
|
|
808
|
+
} else {
|
|
809
|
+
stopSpinner(true, "Done");
|
|
810
|
+
print(result.content);
|
|
811
|
+
}
|
|
812
|
+
} catch (error) {
|
|
813
|
+
handleSandboxFileError(error, path2);
|
|
814
|
+
}
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
if (options.json) {
|
|
818
|
+
printError("--json is only available for sandbox workspace paths");
|
|
819
|
+
process.exit(1);
|
|
820
|
+
}
|
|
821
|
+
if (workspacePath && !options.path) {
|
|
822
|
+
options.path = workspacePath;
|
|
823
|
+
}
|
|
396
824
|
const mode = runOrExit(() => resolveReadMode(options));
|
|
397
825
|
startSpinner("Reading file...");
|
|
398
826
|
let response;
|
|
@@ -428,29 +856,37 @@ function registerFileCommand(program2) {
|
|
|
428
856
|
stopSpinner(true, "Done");
|
|
429
857
|
print(content ?? "");
|
|
430
858
|
});
|
|
431
|
-
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(
|
|
432
|
-
async (options) => {
|
|
433
|
-
const
|
|
434
|
-
if (
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
859
|
+
file.command("put").description("Upload a file").argument("[workspacePath]", "Workspace path in sandbox mode, e.g. personal/docs/readme.md").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(
|
|
860
|
+
async (workspacePath, options) => {
|
|
861
|
+
const sandboxRuntime = resolveSandboxRuntimeContextOrExit();
|
|
862
|
+
if (sandboxRuntime) {
|
|
863
|
+
if (options.url) {
|
|
864
|
+
printError("--url cannot be used in sandbox mode");
|
|
865
|
+
process.exit(1);
|
|
866
|
+
}
|
|
867
|
+
const path2 = workspacePath ?? options.path;
|
|
868
|
+
if (!path2) {
|
|
869
|
+
printError("Workspace path is required in sandbox mode");
|
|
870
|
+
process.exit(1);
|
|
871
|
+
}
|
|
872
|
+
const { content: content2 } = readLocalTextFileOrExit(options.localPath);
|
|
873
|
+
startSpinner("Uploading...");
|
|
874
|
+
try {
|
|
875
|
+
const response2 = await writeSandboxFile(sandboxRuntime, path2, content2, {
|
|
876
|
+
type: "use-current-sha"
|
|
877
|
+
});
|
|
878
|
+
const action2 = response2.created ? "Created" : "Updated";
|
|
879
|
+
stopSpinner(true, `${action2}: ${response2.path}`);
|
|
880
|
+
} catch (error) {
|
|
881
|
+
handleSandboxFileError(error, path2);
|
|
882
|
+
}
|
|
883
|
+
return;
|
|
447
884
|
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
print(`${colors.red}\u2620 Binary files are not allowed. Only text files can be uploaded.${colors.reset}`);
|
|
451
|
-
process.exit(1);
|
|
885
|
+
if (workspacePath && !options.path) {
|
|
886
|
+
options.path = workspacePath;
|
|
452
887
|
}
|
|
453
|
-
const
|
|
888
|
+
const mode = runOrExit(() => resolveWriteMode(options));
|
|
889
|
+
const { content } = readLocalTextFileOrExit(options.localPath);
|
|
454
890
|
startSpinner("Uploading...");
|
|
455
891
|
let response;
|
|
456
892
|
let displayPath;
|
|
@@ -492,6 +928,23 @@ function registerFileCommand(program2) {
|
|
|
492
928
|
stopSpinner(true, `${action}: ${response.data.path}`);
|
|
493
929
|
}
|
|
494
930
|
);
|
|
931
|
+
file.command("write").description("Write file content in sandbox mode").argument("<workspacePath>", "Workspace path, e.g. personal/docs/readme.md").option("--content <content>", "Text content to write").option("--content-file <contentFile>", "Local text file to write").option("--stdin", "Read text content from stdin (default)").option("--base-sha <sha>", "Write only if the remote file still has this SHA").option("--force", "Overwrite an existing remote file without a version condition").action(async (workspacePath, options) => {
|
|
932
|
+
const runtime = resolveSandboxRuntimeContextOrExit();
|
|
933
|
+
if (!runtime) {
|
|
934
|
+
printError("file write is only available in sandbox mode");
|
|
935
|
+
process.exit(1);
|
|
936
|
+
}
|
|
937
|
+
const policy = resolveSandboxFileWritePolicyOrExit(options);
|
|
938
|
+
const content = readWriteCommandContentOrExit(options, (path2) => readLocalTextFileOrExit(path2).content);
|
|
939
|
+
startSpinner("Writing file...");
|
|
940
|
+
try {
|
|
941
|
+
const response = await writeSandboxFile(runtime, workspacePath, content, policy);
|
|
942
|
+
const action = response.created ? "Created" : "Updated";
|
|
943
|
+
stopSpinner(true, `${action}: ${response.path}`);
|
|
944
|
+
} catch (error) {
|
|
945
|
+
handleSandboxFileError(error, workspacePath);
|
|
946
|
+
}
|
|
947
|
+
});
|
|
495
948
|
addSpaceOptions(
|
|
496
949
|
file.command("mkdir").description("Create a directory").requiredOption("-p, --path <path>", "Directory path").option("-r, --recursive", "Create parent directories if needed")
|
|
497
950
|
).action(
|
|
@@ -549,6 +1002,82 @@ function registerFileCommand(program2) {
|
|
|
549
1002
|
}
|
|
550
1003
|
);
|
|
551
1004
|
}
|
|
1005
|
+
function resolveSandboxFileWritePolicyOrExit(options) {
|
|
1006
|
+
if (options.baseSha && options.force) {
|
|
1007
|
+
printError("Only one of --base-sha or --force can be used");
|
|
1008
|
+
process.exit(1);
|
|
1009
|
+
}
|
|
1010
|
+
if (options.baseSha) {
|
|
1011
|
+
if (!/^[a-f0-9]{64}$/.test(options.baseSha)) {
|
|
1012
|
+
printError("--base-sha must be a lowercase SHA-256 hash");
|
|
1013
|
+
process.exit(1);
|
|
1014
|
+
}
|
|
1015
|
+
return { type: "base-sha", sha: options.baseSha };
|
|
1016
|
+
}
|
|
1017
|
+
return options.force ? { type: "force" } : { type: "create-only" };
|
|
1018
|
+
}
|
|
1019
|
+
function resolveRuntimeContextOrExit() {
|
|
1020
|
+
try {
|
|
1021
|
+
return resolveRuntimeContext();
|
|
1022
|
+
} catch (error) {
|
|
1023
|
+
if (error instanceof RuntimeContextError) {
|
|
1024
|
+
printError(error.message);
|
|
1025
|
+
process.exit(1);
|
|
1026
|
+
}
|
|
1027
|
+
throw error;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
function resolveSandboxRuntimeContextOrExit() {
|
|
1031
|
+
if (!SANDBOX_ENV_KEYS.some((key) => (process.env[key] ?? "").trim().length > 0)) {
|
|
1032
|
+
return void 0;
|
|
1033
|
+
}
|
|
1034
|
+
const context = resolveRuntimeContextOrExit();
|
|
1035
|
+
return context.mode === "sandbox" ? context : void 0;
|
|
1036
|
+
}
|
|
1037
|
+
function readLocalTextFileOrExit(localPath) {
|
|
1038
|
+
if (!fs2.existsSync(localPath)) {
|
|
1039
|
+
print(`${colors.red}\u2620 Local file not found: ${localPath}${colors.reset}`);
|
|
1040
|
+
process.exit(1);
|
|
1041
|
+
}
|
|
1042
|
+
const stats = fs2.statSync(localPath);
|
|
1043
|
+
if (!stats.isFile()) {
|
|
1044
|
+
print(`${colors.red}\u2620 Not a file: ${localPath}${colors.reset}`);
|
|
1045
|
+
process.exit(1);
|
|
1046
|
+
}
|
|
1047
|
+
if (stats.size > MFS_FILE_SIZE_LIMIT_BYTES) {
|
|
1048
|
+
print(`${colors.red}\u2620 File too large (max ${MFS_FILE_SIZE_LIMIT_LABEL}): ${localPath}${colors.reset}`);
|
|
1049
|
+
process.exit(1);
|
|
1050
|
+
}
|
|
1051
|
+
const buffer2 = fs2.readFileSync(localPath);
|
|
1052
|
+
if (isBinaryContent(buffer2)) {
|
|
1053
|
+
print(`${colors.red}\u2620 Binary files are not allowed. Only text files can be uploaded.${colors.reset}`);
|
|
1054
|
+
process.exit(1);
|
|
1055
|
+
}
|
|
1056
|
+
return { content: buffer2.toString("utf8") };
|
|
1057
|
+
}
|
|
1058
|
+
function handleSandboxFileError(error, displayPath) {
|
|
1059
|
+
stopSpinner(false, "Failed");
|
|
1060
|
+
if (error instanceof SandboxFileError) {
|
|
1061
|
+
if (error.code === "not-found") {
|
|
1062
|
+
print(`${colors.red}\u2620 ${displayPath} does not exist${colors.reset}`);
|
|
1063
|
+
} else if (error.code === "directory") {
|
|
1064
|
+
print(`${colors.red}\u2620 ${displayPath} is a directory${colors.reset}`);
|
|
1065
|
+
} else {
|
|
1066
|
+
printError(error.message);
|
|
1067
|
+
}
|
|
1068
|
+
process.exit(1);
|
|
1069
|
+
}
|
|
1070
|
+
if (error instanceof SandboxApiError) {
|
|
1071
|
+
printError(`Sandbox API error [${error.status}]: ${error.body}`);
|
|
1072
|
+
process.exit(1);
|
|
1073
|
+
}
|
|
1074
|
+
if (error instanceof Error) {
|
|
1075
|
+
printError(`Sandbox file operation failed: ${error.message}`);
|
|
1076
|
+
process.exit(1);
|
|
1077
|
+
}
|
|
1078
|
+
printError(`Sandbox file operation failed: ${String(error)}`);
|
|
1079
|
+
process.exit(1);
|
|
1080
|
+
}
|
|
552
1081
|
|
|
553
1082
|
// src/commands/memory.ts
|
|
554
1083
|
function registerMemoryCommand(program2) {
|
|
@@ -744,2142 +1273,1343 @@ function registerMiniappCommand(program2) {
|
|
|
744
1273
|
});
|
|
745
1274
|
}
|
|
746
1275
|
|
|
747
|
-
// src/
|
|
748
|
-
import {
|
|
749
|
-
import {
|
|
750
|
-
import { homedir as homedir2 } from "os";
|
|
1276
|
+
// src/commands/pull.ts
|
|
1277
|
+
import { isAbsolute as isAbsolute3 } from "path";
|
|
1278
|
+
import { Option } from "commander";
|
|
751
1279
|
|
|
752
|
-
// src/
|
|
1280
|
+
// src/utils/sandbox-pull.ts
|
|
1281
|
+
import { createHash as createHash2 } from "crypto";
|
|
1282
|
+
import { lstat, mkdir as mkdir2, readFile as readFile2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
1283
|
+
import { dirname as dirname2, isAbsolute as isAbsolute2, relative, resolve } from "path";
|
|
1284
|
+
|
|
1285
|
+
// src/utils/checkout-state.ts
|
|
753
1286
|
import { randomUUID } from "crypto";
|
|
754
|
-
import { access, mkdir, readdir, readFile, rm, writeFile } from "fs/promises";
|
|
755
1287
|
import { homedir } from "os";
|
|
756
|
-
import { join, posix
|
|
757
|
-
|
|
758
|
-
var
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
RUNLOG_DIRECTORY_MARKER,
|
|
763
|
-
"events",
|
|
764
|
-
"events.jsonl",
|
|
765
|
-
"run.json",
|
|
766
|
-
"state.json",
|
|
767
|
-
"transcript",
|
|
768
|
-
"transcript.jsonl",
|
|
769
|
-
"upload.json"
|
|
770
|
-
]);
|
|
771
|
-
var RUNLOG_METADATA_SCHEMAS = {
|
|
772
|
-
"run.json": "runlog.capture.v1",
|
|
773
|
-
"state.json": "runlog.state.v1",
|
|
774
|
-
"upload.json": "runlog.upload.v1"
|
|
775
|
-
};
|
|
776
|
-
var RUNLOG_METADATA_FILE_NAMES = Object.keys(RUNLOG_METADATA_SCHEMAS);
|
|
777
|
-
var CaptureOutputPrepareError = class extends Error {
|
|
778
|
-
path;
|
|
779
|
-
constructor(path2, cause) {
|
|
780
|
-
super(errorMessage(cause));
|
|
781
|
-
this.name = "CaptureOutputPrepareError";
|
|
782
|
-
this.path = path2;
|
|
783
|
-
this.cause = cause;
|
|
1288
|
+
import { dirname, isAbsolute, join, posix } from "path";
|
|
1289
|
+
import { mkdir, readFile, rename, rm, writeFile } from "fs/promises";
|
|
1290
|
+
var CheckoutStateError = class extends Error {
|
|
1291
|
+
constructor(message) {
|
|
1292
|
+
super(message);
|
|
1293
|
+
this.name = "CheckoutStateError";
|
|
784
1294
|
}
|
|
785
1295
|
};
|
|
786
|
-
function
|
|
787
|
-
|
|
788
|
-
if (nodePlatform === "darwin") {
|
|
789
|
-
return pathJoin(home, "Library", "Application Support", "moxt", "runs");
|
|
790
|
-
}
|
|
791
|
-
if (nodePlatform === "win32") {
|
|
792
|
-
return pathJoin(env.LOCALAPPDATA ?? pathJoin(home, "AppData", "Local"), "Moxt", "runs");
|
|
793
|
-
}
|
|
794
|
-
return pathJoin(env.XDG_DATA_HOME ?? pathJoin(home, ".local", "share"), "moxt", "runs");
|
|
795
|
-
}
|
|
796
|
-
function withDefaultCaptureOutput(env, home = homedir()) {
|
|
797
|
-
const { outputDir, outputRoot } = captureEnvPaths(env);
|
|
798
|
-
if (hasValue(outputDir) || hasValue(outputRoot)) {
|
|
799
|
-
return env;
|
|
800
|
-
}
|
|
801
|
-
return {
|
|
802
|
-
...env,
|
|
803
|
-
MOXT_RUN_OUTPUT_ROOT: defaultCaptureOutputRoot(home, env)
|
|
804
|
-
};
|
|
1296
|
+
function getCheckoutStatePath() {
|
|
1297
|
+
return join(homedir(), ".moxt", "checkout-state.json");
|
|
805
1298
|
}
|
|
806
|
-
async function
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
await
|
|
1299
|
+
async function loadCheckoutState(statePath = getCheckoutStatePath()) {
|
|
1300
|
+
let raw;
|
|
1301
|
+
try {
|
|
1302
|
+
raw = await readFile(statePath, "utf8");
|
|
1303
|
+
} catch (error) {
|
|
1304
|
+
if (isNodeError(error) && error.code === "ENOENT") return void 0;
|
|
1305
|
+
throw new CheckoutStateError(`Failed to read checkout state: ${errorMessage(error)}`);
|
|
810
1306
|
}
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
1307
|
+
let value;
|
|
1308
|
+
try {
|
|
1309
|
+
value = JSON.parse(raw);
|
|
1310
|
+
} catch {
|
|
1311
|
+
throw new CheckoutStateError(`Checkout state is not valid JSON: ${statePath}`);
|
|
814
1312
|
}
|
|
1313
|
+
return validateCheckoutState(value);
|
|
815
1314
|
}
|
|
816
|
-
function
|
|
817
|
-
const
|
|
818
|
-
const
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
1315
|
+
async function saveCheckoutState(state, statePath = getCheckoutStatePath()) {
|
|
1316
|
+
const validState = validateCheckoutState(state);
|
|
1317
|
+
const stateDir = dirname(statePath);
|
|
1318
|
+
const temporaryPath = `${statePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
1319
|
+
await mkdir(stateDir, { recursive: true });
|
|
1320
|
+
try {
|
|
1321
|
+
await writeFile(temporaryPath, `${JSON.stringify(validState, null, 2)}
|
|
1322
|
+
`, { mode: 384 });
|
|
1323
|
+
await rename(temporaryPath, statePath);
|
|
1324
|
+
} catch (error) {
|
|
1325
|
+
await rm(temporaryPath, { force: true });
|
|
1326
|
+
throw new CheckoutStateError(`Failed to save checkout state: ${errorMessage(error)}`);
|
|
824
1327
|
}
|
|
825
|
-
return [...dirs];
|
|
826
1328
|
}
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
await writeCaptureStateFile(outputDir, runId, "open", capturedAt);
|
|
832
|
-
})
|
|
1329
|
+
function isCheckoutPathIncluded(state, workspacePath) {
|
|
1330
|
+
if (state.mode === "full") return true;
|
|
1331
|
+
return state.checkedOutScopes.some(
|
|
1332
|
+
(scope) => workspacePath === scope || workspacePath.startsWith(`${scope}/`)
|
|
833
1333
|
);
|
|
834
1334
|
}
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
await writeCaptureUploadFile(outputDir, runId);
|
|
840
|
-
await writeCaptureStateFile(outputDir, runId, "closed", capturedAt);
|
|
841
|
-
})
|
|
1335
|
+
function doesCheckoutPathIntersect(state, workspacePath) {
|
|
1336
|
+
if (state.mode === "full") return true;
|
|
1337
|
+
return state.checkedOutScopes.some(
|
|
1338
|
+
(scope) => workspacePath === scope || workspacePath.startsWith(`${scope}/`) || scope.startsWith(`${workspacePath}/`)
|
|
842
1339
|
);
|
|
843
1340
|
}
|
|
844
|
-
function
|
|
845
|
-
const
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
const
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
}
|
|
861
|
-
await writeCaptureRunFile(outputDir, payload, runId, {
|
|
862
|
-
events: eventChunks.map(withoutContent),
|
|
863
|
-
transcript: transcriptChunks.map(withoutContent)
|
|
864
|
-
});
|
|
865
|
-
await writeCaptureUploadFile(outputDir, runId);
|
|
866
|
-
await writeCaptureStateFile(outputDir, runId, "closed", capturedAt);
|
|
867
|
-
})
|
|
1341
|
+
function validateCheckoutState(value) {
|
|
1342
|
+
const state = requireRecord(value, "Checkout state");
|
|
1343
|
+
if (state.version !== 1) {
|
|
1344
|
+
throw new CheckoutStateError(`Unsupported checkout state version: ${String(state.version)}`);
|
|
1345
|
+
}
|
|
1346
|
+
const workspaceId = requireNonEmptyString(state.workspaceId, "workspaceId");
|
|
1347
|
+
const checkoutRoot = requireNonEmptyString(state.checkoutRoot, "checkoutRoot");
|
|
1348
|
+
if (!isAbsolute(checkoutRoot)) throw new CheckoutStateError("checkoutRoot must be an absolute path");
|
|
1349
|
+
if (state.mode !== "partial" && state.mode !== "full") {
|
|
1350
|
+
throw new CheckoutStateError("mode must be partial or full");
|
|
1351
|
+
}
|
|
1352
|
+
if (!Array.isArray(state.checkedOutScopes)) {
|
|
1353
|
+
throw new CheckoutStateError("checkedOutScopes must be an array");
|
|
1354
|
+
}
|
|
1355
|
+
const checkedOutScopes = state.checkedOutScopes.map(
|
|
1356
|
+
(scope, index) => requireWorkspacePath(scope, `checkedOutScopes[${index}]`)
|
|
868
1357
|
);
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
1358
|
+
if (new Set(checkedOutScopes).size !== checkedOutScopes.length) {
|
|
1359
|
+
throw new CheckoutStateError("checkedOutScopes must not contain duplicates");
|
|
1360
|
+
}
|
|
1361
|
+
const reposValue = requireRecord(state.repos, "repos");
|
|
1362
|
+
const repos = {};
|
|
1363
|
+
for (const [repoId, rawRepo] of Object.entries(reposValue)) {
|
|
1364
|
+
requireNonEmptyString(repoId, "repo id");
|
|
1365
|
+
const repo = requireRecord(rawRepo, `repos.${repoId}`);
|
|
1366
|
+
const name = requireRepoName(repo.name, `repos.${repoId}.name`);
|
|
1367
|
+
repos[repoId] = {
|
|
1368
|
+
name,
|
|
1369
|
+
treeSha: requireNonEmptyString(repo.treeSha, `repos.${repoId}.treeSha`)
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
const filesValue = requireRecord(state.files, "files");
|
|
1373
|
+
const files = {};
|
|
1374
|
+
for (const [workspacePath, rawFile] of Object.entries(filesValue)) {
|
|
1375
|
+
requireWorkspacePath(workspacePath, `files.${workspacePath}`);
|
|
1376
|
+
const file = requireRecord(rawFile, `files.${workspacePath}`);
|
|
1377
|
+
const repoId = requireNonEmptyString(file.repoId, `files.${workspacePath}.repoId`);
|
|
1378
|
+
const repo = repos[repoId];
|
|
1379
|
+
if (!repo) throw new CheckoutStateError(`File references unknown repo: ${workspacePath}`);
|
|
1380
|
+
const repoPath = requireWorkspacePath(file.repoPath, `files.${workspacePath}.repoPath`);
|
|
1381
|
+
if (workspacePath !== `${repo.name}/${repoPath}`) {
|
|
1382
|
+
throw new CheckoutStateError(`File path does not match its repo: ${workspacePath}`);
|
|
1383
|
+
}
|
|
1384
|
+
const fileId = file.fileId === null ? null : requireNonEmptyString(file.fileId, `files.${workspacePath}.fileId`);
|
|
1385
|
+
const sha = requireNonEmptyString(file.sha, `files.${workspacePath}.sha`);
|
|
1386
|
+
if (!/^[a-f0-9]{64}$/.test(sha)) throw new CheckoutStateError(`Invalid file SHA: ${workspacePath}`);
|
|
1387
|
+
files[workspacePath] = {
|
|
1388
|
+
repoId,
|
|
1389
|
+
repoPath,
|
|
1390
|
+
fileId,
|
|
1391
|
+
sha,
|
|
1392
|
+
size: requireNonNegativeInteger(file.size, `files.${workspacePath}.size`),
|
|
1393
|
+
fileType: requireNonNegativeInteger(file.fileType, `files.${workspacePath}.fileType`)
|
|
1394
|
+
};
|
|
1395
|
+
}
|
|
872
1396
|
return {
|
|
873
|
-
|
|
874
|
-
|
|
1397
|
+
version: 1,
|
|
1398
|
+
workspaceId,
|
|
1399
|
+
checkoutRoot,
|
|
1400
|
+
mode: state.mode,
|
|
1401
|
+
checkedOutScopes,
|
|
1402
|
+
repos,
|
|
1403
|
+
files
|
|
875
1404
|
};
|
|
876
1405
|
}
|
|
877
|
-
function
|
|
878
|
-
if (
|
|
879
|
-
|
|
880
|
-
}
|
|
881
|
-
return text.endsWith("\n") ? text : `${text}
|
|
882
|
-
`;
|
|
883
|
-
}
|
|
884
|
-
async function prepareCaptureDirectory(outputDir) {
|
|
885
|
-
await mkdir(outputDir, { recursive: true });
|
|
886
|
-
await assertMoxtManagedOutputDir(outputDir);
|
|
887
|
-
await Promise.all([
|
|
888
|
-
rm(join(outputDir, RUNLOG_DIRECTORY_MARKER), { force: true }),
|
|
889
|
-
rm(join(outputDir, "run.json"), { force: true }),
|
|
890
|
-
rm(join(outputDir, "state.json"), { force: true }),
|
|
891
|
-
rm(join(outputDir, "upload.json"), { force: true }),
|
|
892
|
-
rm(join(outputDir, "events.jsonl"), { force: true }),
|
|
893
|
-
rm(join(outputDir, "transcript.jsonl"), { force: true }),
|
|
894
|
-
rm(join(outputDir, "events"), { recursive: true, force: true }),
|
|
895
|
-
rm(join(outputDir, "transcript"), { recursive: true, force: true })
|
|
896
|
-
]);
|
|
897
|
-
await writeRunlogDirectoryMarker(outputDir);
|
|
898
|
-
await mkdir(join(outputDir, "events"), { recursive: true });
|
|
899
|
-
await mkdir(join(outputDir, "transcript"), { recursive: true });
|
|
900
|
-
}
|
|
901
|
-
async function assertMoxtManagedOutputDir(outputDir) {
|
|
902
|
-
const entries = await readdir(outputDir);
|
|
903
|
-
const unmanaged = entries.filter((entry) => !RUNLOG_MANAGED_OUTPUT_NAMES.has(entry));
|
|
904
|
-
if (unmanaged.length > 0) {
|
|
905
|
-
throw new Error(`refusing to use run output directory with unmanaged files: ${unmanaged.slice(0, 5).join(", ")}`);
|
|
906
|
-
}
|
|
907
|
-
const meaningfulEntries = entries.filter((entry) => entry !== ".DS_Store");
|
|
908
|
-
if (meaningfulEntries.length === 0) {
|
|
909
|
-
return;
|
|
1406
|
+
function requireRecord(value, field) {
|
|
1407
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
1408
|
+
throw new CheckoutStateError(`${field} must be an object`);
|
|
910
1409
|
}
|
|
911
|
-
|
|
912
|
-
|
|
1410
|
+
return value;
|
|
1411
|
+
}
|
|
1412
|
+
function requireNonEmptyString(value, field) {
|
|
1413
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
1414
|
+
throw new CheckoutStateError(`${field} must be a non-empty string`);
|
|
913
1415
|
}
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
1416
|
+
return value;
|
|
1417
|
+
}
|
|
1418
|
+
function requireWorkspacePath(value, field) {
|
|
1419
|
+
const path2 = requireNonEmptyString(value, field);
|
|
1420
|
+
if (path2.includes("\\") || path2.includes("\0") || posix.isAbsolute(path2) || posix.normalize(path2) !== path2) {
|
|
1421
|
+
throw new CheckoutStateError(`${field} must be a normalized relative path`);
|
|
918
1422
|
}
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
const
|
|
923
|
-
if (
|
|
924
|
-
|
|
1423
|
+
return path2;
|
|
1424
|
+
}
|
|
1425
|
+
function requireRepoName(value, field) {
|
|
1426
|
+
const name = requireWorkspacePath(value, field);
|
|
1427
|
+
if (name.includes("/")) throw new CheckoutStateError(`${field} must be a single path segment`);
|
|
1428
|
+
return name;
|
|
1429
|
+
}
|
|
1430
|
+
function requireNonNegativeInteger(value, field) {
|
|
1431
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
|
|
1432
|
+
throw new CheckoutStateError(`${field} must be a non-negative integer`);
|
|
925
1433
|
}
|
|
1434
|
+
return value;
|
|
926
1435
|
}
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
try {
|
|
930
|
-
parsed = JSON.parse(await readFile(join(outputDir, fileName), "utf8"));
|
|
931
|
-
} catch (error) {
|
|
932
|
-
throw new Error(`invalid ${fileName}: ${errorMessage(error)}`);
|
|
933
|
-
}
|
|
934
|
-
const value = objectValue(parsed);
|
|
935
|
-
if (value === null) {
|
|
936
|
-
throw new Error(`invalid ${fileName}: expected object`);
|
|
937
|
-
}
|
|
938
|
-
if (value.schema_version !== RUNLOG_METADATA_SCHEMAS[fileName]) {
|
|
939
|
-
throw new Error(`invalid ${fileName}: unexpected schema_version`);
|
|
940
|
-
}
|
|
941
|
-
if (typeof value.run_id !== "string" || value.run_id.trim() === "") {
|
|
942
|
-
throw new Error(`invalid ${fileName}: missing run_id`);
|
|
943
|
-
}
|
|
944
|
-
return value.run_id;
|
|
945
|
-
}
|
|
946
|
-
async function assertRunlogDirectoryMarker(outputDir) {
|
|
947
|
-
let parsed;
|
|
948
|
-
try {
|
|
949
|
-
parsed = JSON.parse(await readFile(join(outputDir, RUNLOG_DIRECTORY_MARKER), "utf8"));
|
|
950
|
-
} catch (error) {
|
|
951
|
-
throw new Error(`invalid ${RUNLOG_DIRECTORY_MARKER}: ${errorMessage(error)}`);
|
|
952
|
-
}
|
|
953
|
-
const value = objectValue(parsed);
|
|
954
|
-
if (value?.schema_version !== RUNLOG_DIRECTORY_MARKER_SCHEMA) {
|
|
955
|
-
throw new Error(`invalid ${RUNLOG_DIRECTORY_MARKER}: unexpected schema_version`);
|
|
956
|
-
}
|
|
1436
|
+
function isNodeError(error) {
|
|
1437
|
+
return error instanceof Error && "code" in error;
|
|
957
1438
|
}
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
}) {
|
|
961
|
-
try {
|
|
962
|
-
await prepare();
|
|
963
|
-
} catch (error) {
|
|
964
|
-
throw new CaptureOutputPrepareError(path2, error);
|
|
965
|
-
}
|
|
966
|
-
}
|
|
967
|
-
async function writeCaptureRunFile(outputDir, payload, runId, files) {
|
|
968
|
-
const runFile = {
|
|
969
|
-
schema_version: "runlog.capture.v1",
|
|
970
|
-
run_id: runId,
|
|
971
|
-
run: payload.run,
|
|
972
|
-
recording: payload.recording,
|
|
973
|
-
repo: payload.repo,
|
|
974
|
-
...payload.redaction_failed ? { redaction_failed: true } : {},
|
|
975
|
-
files: {
|
|
976
|
-
state: "state.json",
|
|
977
|
-
upload: "upload.json",
|
|
978
|
-
events: files.events,
|
|
979
|
-
transcript: files.transcript
|
|
980
|
-
}
|
|
981
|
-
};
|
|
982
|
-
await writeFile(join(outputDir, "run.json"), `${JSON.stringify(runFile, null, 2)}
|
|
983
|
-
`, "utf8");
|
|
984
|
-
}
|
|
985
|
-
async function writeRunlogDirectoryMarker(outputDir) {
|
|
986
|
-
await writeFile(
|
|
987
|
-
join(outputDir, RUNLOG_DIRECTORY_MARKER),
|
|
988
|
-
`${JSON.stringify({ schema_version: RUNLOG_DIRECTORY_MARKER_SCHEMA }, null, 2)}
|
|
989
|
-
`,
|
|
990
|
-
"utf8"
|
|
991
|
-
);
|
|
1439
|
+
function errorMessage(error) {
|
|
1440
|
+
return error instanceof Error ? error.message : String(error);
|
|
992
1441
|
}
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1442
|
+
|
|
1443
|
+
// src/utils/sandbox-pull.ts
|
|
1444
|
+
var SandboxPullError = class extends Error {
|
|
1445
|
+
constructor(message, code) {
|
|
1446
|
+
super(message);
|
|
1447
|
+
this.code = code;
|
|
1448
|
+
this.name = "SandboxPullError";
|
|
999
1449
|
}
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
const
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
bytes: chunk.bytes,
|
|
1026
|
-
lines: chunk.lines,
|
|
1027
|
-
oversized_line: chunk.oversized_line
|
|
1028
|
-
};
|
|
1029
|
-
}
|
|
1030
|
-
function captureEnvPaths(env) {
|
|
1031
|
-
const { MOXT_RUN_OUTPUT_DIR: outputDir, MOXT_RUN_OUTPUT_ROOT: outputRoot } = env;
|
|
1032
|
-
return { outputDir, outputRoot };
|
|
1033
|
-
}
|
|
1034
|
-
function chunkJsonl(text, dir, prefix) {
|
|
1035
|
-
if (text === "") {
|
|
1036
|
-
return [];
|
|
1037
|
-
}
|
|
1038
|
-
const chunks = [];
|
|
1039
|
-
let content = "";
|
|
1040
|
-
let bytes = 0;
|
|
1041
|
-
let lines = 0;
|
|
1042
|
-
let oversizedLine = false;
|
|
1043
|
-
const flush2 = () => {
|
|
1044
|
-
if (content === "") {
|
|
1045
|
-
return;
|
|
1450
|
+
code;
|
|
1451
|
+
};
|
|
1452
|
+
async function pullSandboxWorkspace(context, targetDir, options = {}) {
|
|
1453
|
+
const resolvedTargetDir = resolve(targetDir);
|
|
1454
|
+
const repos = listRepoEntries(context.repoPayload);
|
|
1455
|
+
const previousState = await loadCheckoutState();
|
|
1456
|
+
const compatibleState = resolveCompatibleState(previousState, context, resolvedTargetDir, repos);
|
|
1457
|
+
const previousFiles = compatibleState?.files ?? {};
|
|
1458
|
+
const selection = resolvePullSelection(context, options.scopes);
|
|
1459
|
+
const repoById = new Map(selection.repos.map((repo) => [repo.repoId, repo]));
|
|
1460
|
+
const tree = await fetchWorkspaceTree(context, selection);
|
|
1461
|
+
const stateRepos = resolveStateRepos(tree, selection.repos);
|
|
1462
|
+
const entries = [...tree.entries].sort((a, b) => a.prefixedPath.localeCompare(b.prefixedPath));
|
|
1463
|
+
const directoryPaths = selection.repos.map((repo) => ({
|
|
1464
|
+
workspacePath: repo.name,
|
|
1465
|
+
localPath: resolveSafeLocalPath(resolvedTargetDir, repo.name, "")
|
|
1466
|
+
}));
|
|
1467
|
+
const blobEntries = [];
|
|
1468
|
+
const remoteBlobPaths = /* @__PURE__ */ new Set();
|
|
1469
|
+
const remotePaths = /* @__PURE__ */ new Set();
|
|
1470
|
+
const matchedScopes = /* @__PURE__ */ new Set();
|
|
1471
|
+
for (const entry of entries) {
|
|
1472
|
+
const repo = repoById.get(entry.repoId);
|
|
1473
|
+
if (!repo) {
|
|
1474
|
+
throw new SandboxPullError(`Remote tree returned unknown repo: ${entry.repoId}`, "remote");
|
|
1046
1475
|
}
|
|
1047
|
-
const
|
|
1048
|
-
|
|
1049
|
-
path:
|
|
1050
|
-
content,
|
|
1051
|
-
bytes,
|
|
1052
|
-
lines,
|
|
1053
|
-
oversized_line: oversizedLine
|
|
1054
|
-
});
|
|
1055
|
-
content = "";
|
|
1056
|
-
bytes = 0;
|
|
1057
|
-
lines = 0;
|
|
1058
|
-
oversizedLine = false;
|
|
1059
|
-
};
|
|
1060
|
-
for (const line of jsonlLines(text)) {
|
|
1061
|
-
const lineBytes = Buffer.byteLength(line);
|
|
1062
|
-
if (lineBytes > DATA_FILE_TARGET_BYTES) {
|
|
1063
|
-
flush2();
|
|
1064
|
-
content = line;
|
|
1065
|
-
bytes = lineBytes;
|
|
1066
|
-
lines = 1;
|
|
1067
|
-
oversizedLine = true;
|
|
1068
|
-
flush2();
|
|
1069
|
-
continue;
|
|
1476
|
+
const expectedWorkspacePath = entry.path ? `${repo.name}/${entry.path}` : repo.name;
|
|
1477
|
+
if (entry.prefixedPath !== expectedWorkspacePath) {
|
|
1478
|
+
throw new SandboxPullError(`Remote tree returned inconsistent path: ${entry.prefixedPath}`, "remote");
|
|
1070
1479
|
}
|
|
1071
|
-
if (
|
|
1072
|
-
|
|
1480
|
+
if (remotePaths.has(expectedWorkspacePath)) {
|
|
1481
|
+
throw new SandboxPullError(`Remote tree returned duplicate path: ${expectedWorkspacePath}`, "remote");
|
|
1073
1482
|
}
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
const normalized = normalizeJsonl(text);
|
|
1083
|
-
if (normalized === "") {
|
|
1084
|
-
return [];
|
|
1085
|
-
}
|
|
1086
|
-
return normalized.slice(0, -1).split("\n").map((line) => `${line}
|
|
1087
|
-
`);
|
|
1088
|
-
}
|
|
1089
|
-
function serializeEvents(events) {
|
|
1090
|
-
if (events.length === 0) {
|
|
1091
|
-
return "";
|
|
1092
|
-
}
|
|
1093
|
-
return `${events.map((event) => JSON.stringify(event)).join("\n")}
|
|
1094
|
-
`;
|
|
1095
|
-
}
|
|
1096
|
-
function normalizeEvents(transcript, startSeq, agent) {
|
|
1097
|
-
const events = [];
|
|
1098
|
-
for (const line of transcript.split("\n")) {
|
|
1099
|
-
if (line.trim() === "") {
|
|
1483
|
+
remotePaths.add(expectedWorkspacePath);
|
|
1484
|
+
const localPath = resolveSafeLocalPath(resolvedTargetDir, repo.name, entry.path);
|
|
1485
|
+
for (const scope of selection.scopes) {
|
|
1486
|
+
if (pathIsIncluded(scope, expectedWorkspacePath)) matchedScopes.add(scope);
|
|
1487
|
+
}
|
|
1488
|
+
if (!selection.scopes.some((scope) => pathsIntersect(scope, expectedWorkspacePath))) continue;
|
|
1489
|
+
if (entry.type === "tree") {
|
|
1490
|
+
directoryPaths.push({ workspacePath: entry.prefixedPath, localPath });
|
|
1100
1491
|
continue;
|
|
1101
1492
|
}
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
if (event !== null) {
|
|
1105
|
-
events.push(event);
|
|
1493
|
+
if (!entry.sha) {
|
|
1494
|
+
throw new SandboxPullError(`${entry.prefixedPath} does not have downloadable content`, "remote");
|
|
1106
1495
|
}
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
}
|
|
1110
|
-
function normalizeClaudeEvent(raw, seq) {
|
|
1111
|
-
if (raw === null) {
|
|
1112
|
-
return null;
|
|
1113
|
-
}
|
|
1114
|
-
const timestamp = stringValue(raw.timestamp);
|
|
1115
|
-
if (raw.type === "user") {
|
|
1116
|
-
const message = messageValue(raw.message);
|
|
1117
|
-
const text = textFromContent(message?.content);
|
|
1118
|
-
if (text === "") {
|
|
1119
|
-
return null;
|
|
1496
|
+
if (entry.fileId !== null && (typeof entry.fileId !== "string" || entry.fileId.length === 0)) {
|
|
1497
|
+
throw new SandboxPullError(`${entry.prefixedPath} has invalid file metadata`, "remote");
|
|
1120
1498
|
}
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
seq,
|
|
1124
|
-
type: "user_message",
|
|
1125
|
-
...timestamp === void 0 ? {} : { timestamp },
|
|
1126
|
-
text
|
|
1127
|
-
};
|
|
1128
|
-
}
|
|
1129
|
-
if (raw.type === "assistant") {
|
|
1130
|
-
const message = messageValue(raw.message);
|
|
1131
|
-
const text = textFromContent(message?.content);
|
|
1132
|
-
if (text === "") {
|
|
1133
|
-
return null;
|
|
1499
|
+
if (entry.fileType !== null && (!Number.isInteger(entry.fileType) || entry.fileType < 0)) {
|
|
1500
|
+
throw new SandboxPullError(`${entry.prefixedPath} has invalid file metadata`, "remote");
|
|
1134
1501
|
}
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
const inputTokens = numberValue(usage?.input_tokens);
|
|
1138
|
-
const outputTokens = numberValue(usage?.output_tokens);
|
|
1139
|
-
return {
|
|
1140
|
-
schema_version: "runlog.events.v1",
|
|
1141
|
-
seq,
|
|
1142
|
-
type: "assistant_message",
|
|
1143
|
-
...timestamp === void 0 ? {} : { timestamp },
|
|
1144
|
-
...model === void 0 ? {} : { model },
|
|
1145
|
-
text,
|
|
1146
|
-
...inputTokens === void 0 ? {} : { input_tokens: inputTokens },
|
|
1147
|
-
...outputTokens === void 0 ? {} : { output_tokens: outputTokens }
|
|
1148
|
-
};
|
|
1149
|
-
}
|
|
1150
|
-
if (raw.type === "system" && raw.subtype === "turn_duration") {
|
|
1151
|
-
const durationMs = numberValue(raw.durationMs);
|
|
1152
|
-
const messageCount = numberValue(raw.messageCount);
|
|
1153
|
-
return {
|
|
1154
|
-
schema_version: "runlog.events.v1",
|
|
1155
|
-
seq,
|
|
1156
|
-
type: "turn_summary",
|
|
1157
|
-
...timestamp === void 0 ? {} : { timestamp },
|
|
1158
|
-
...durationMs === void 0 ? {} : { duration_ms: durationMs },
|
|
1159
|
-
...messageCount === void 0 ? {} : { message_count: messageCount }
|
|
1160
|
-
};
|
|
1161
|
-
}
|
|
1162
|
-
return null;
|
|
1163
|
-
}
|
|
1164
|
-
function normalizeCodexEvent(raw, seq) {
|
|
1165
|
-
if (raw === null || raw.type !== "event_msg") {
|
|
1166
|
-
return null;
|
|
1167
|
-
}
|
|
1168
|
-
const timestamp = stringValue(raw.timestamp);
|
|
1169
|
-
const payload = codexPayloadValue(raw.payload);
|
|
1170
|
-
const payloadType = stringValue(payload?.type);
|
|
1171
|
-
if (payloadType === "user_message") {
|
|
1172
|
-
const text = stringValue(payload?.message) ?? "";
|
|
1173
|
-
if (text === "") {
|
|
1174
|
-
return null;
|
|
1502
|
+
if (entry.size !== null && (!Number.isInteger(entry.size) || entry.size < 0)) {
|
|
1503
|
+
throw new SandboxPullError(`${entry.prefixedPath} has invalid file metadata`, "remote");
|
|
1175
1504
|
}
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
seq,
|
|
1179
|
-
type: "user_message",
|
|
1180
|
-
...timestamp === void 0 ? {} : { timestamp },
|
|
1181
|
-
text
|
|
1182
|
-
};
|
|
1183
|
-
}
|
|
1184
|
-
if (payloadType === "agent_message") {
|
|
1185
|
-
const text = stringValue(payload?.message) ?? "";
|
|
1186
|
-
if (text === "") {
|
|
1187
|
-
return null;
|
|
1505
|
+
if (entry.size !== null && entry.size > MFS_FILE_SIZE_LIMIT_BYTES) {
|
|
1506
|
+
throw new SandboxPullError(`File too large (max ${MFS_FILE_SIZE_LIMIT_LABEL}): ${entry.prefixedPath}`, "too-large");
|
|
1188
1507
|
}
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
seq,
|
|
1192
|
-
type: "assistant_message",
|
|
1193
|
-
...timestamp === void 0 ? {} : { timestamp },
|
|
1194
|
-
text
|
|
1195
|
-
};
|
|
1508
|
+
remoteBlobPaths.add(entry.prefixedPath);
|
|
1509
|
+
blobEntries.push({ entry, localPath });
|
|
1196
1510
|
}
|
|
1197
|
-
if (
|
|
1198
|
-
const
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
};
|
|
1511
|
+
if (selection.mode === "partial") {
|
|
1512
|
+
for (const scope of selection.scopes) {
|
|
1513
|
+
const repoRoot = selection.repos.some((repo) => repo.name === scope);
|
|
1514
|
+
const previouslyTracked = Object.keys(previousFiles).some((workspacePath) => pathIsIncluded(scope, workspacePath));
|
|
1515
|
+
if (!repoRoot && !matchedScopes.has(scope) && !previouslyTracked) {
|
|
1516
|
+
throw new SandboxPullError(`Workspace path does not exist: ${scope}`, "invalid-path");
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1206
1519
|
}
|
|
1207
|
-
|
|
1208
|
-
}
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1520
|
+
const plannedBlobs = [];
|
|
1521
|
+
for (const { entry, localPath } of blobEntries) {
|
|
1522
|
+
plannedBlobs.push({
|
|
1523
|
+
entry,
|
|
1524
|
+
localPath,
|
|
1525
|
+
shouldWrite: await shouldWritePulledFile(
|
|
1526
|
+
localPath,
|
|
1527
|
+
requireBlobSha(entry),
|
|
1528
|
+
entry.prefixedPath,
|
|
1529
|
+
previousFiles[entry.prefixedPath],
|
|
1530
|
+
Boolean(options.force)
|
|
1531
|
+
)
|
|
1532
|
+
});
|
|
1215
1533
|
}
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1534
|
+
const plannedDeletions = await planRemoteDeletions(
|
|
1535
|
+
compatibleState,
|
|
1536
|
+
selection,
|
|
1537
|
+
resolvedTargetDir,
|
|
1538
|
+
remoteBlobPaths,
|
|
1539
|
+
Boolean(options.force)
|
|
1540
|
+
);
|
|
1541
|
+
for (const directory of directoryPaths) {
|
|
1542
|
+
await assertCanCreateDirectory(directory.localPath, directory.workspacePath);
|
|
1220
1543
|
}
|
|
1221
|
-
|
|
1222
|
-
|
|
1544
|
+
const blobsToDownload = plannedBlobs.filter((planned) => planned.shouldWrite);
|
|
1545
|
+
const downloadUrls = await presignDownloads(context, blobsToDownload.map(({ entry }) => entry));
|
|
1546
|
+
const downloadedEntries = [];
|
|
1547
|
+
for (const planned of blobsToDownload) {
|
|
1548
|
+
const { entry, localPath } = planned;
|
|
1549
|
+
const remoteContent = await downloadBlob(entry, downloadUrls);
|
|
1550
|
+
downloadedEntries.push({ ...planned, content: remoteContent });
|
|
1223
1551
|
}
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
if (contentPart?.type !== "text") {
|
|
1227
|
-
return "";
|
|
1228
|
-
}
|
|
1229
|
-
return stringValue(contentPart.text) ?? "";
|
|
1230
|
-
}).filter((text) => text !== "").join("\n");
|
|
1231
|
-
}
|
|
1232
|
-
function messageValue(value) {
|
|
1233
|
-
return isObject(value) ? value : void 0;
|
|
1234
|
-
}
|
|
1235
|
-
function codexPayloadValue(value) {
|
|
1236
|
-
return isObject(value) ? value : void 0;
|
|
1237
|
-
}
|
|
1238
|
-
function usageValue(value) {
|
|
1239
|
-
return isObject(value) ? value : void 0;
|
|
1240
|
-
}
|
|
1241
|
-
function contentPartValue(value) {
|
|
1242
|
-
return isObject(value) ? value : void 0;
|
|
1243
|
-
}
|
|
1244
|
-
function stringValue(value) {
|
|
1245
|
-
return typeof value === "string" ? value : void 0;
|
|
1246
|
-
}
|
|
1247
|
-
function numberValue(value) {
|
|
1248
|
-
return typeof value === "number" ? value : void 0;
|
|
1249
|
-
}
|
|
1250
|
-
function isObject(value) {
|
|
1251
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1252
|
-
}
|
|
1253
|
-
function hasValue(value) {
|
|
1254
|
-
return value !== void 0 && value !== "";
|
|
1255
|
-
}
|
|
1256
|
-
function errorMessage(error) {
|
|
1257
|
-
if (error instanceof Error && error.message !== "") {
|
|
1258
|
-
return error.message;
|
|
1552
|
+
for (const deletion of plannedDeletions) {
|
|
1553
|
+
await rm2(deletion.localPath);
|
|
1259
1554
|
}
|
|
1260
|
-
|
|
1261
|
-
}
|
|
1262
|
-
function objectValue(value) {
|
|
1263
|
-
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
1264
|
-
}
|
|
1265
|
-
|
|
1266
|
-
// src/run/redaction.ts
|
|
1267
|
-
import { createHmac } from "crypto";
|
|
1268
|
-
var SECRET_RULES = [
|
|
1269
|
-
{
|
|
1270
|
-
name: "private_key",
|
|
1271
|
-
pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
|
|
1272
|
-
replacement: (value, salt) => `<REDACTED:private_key:${tag(value, salt)}>`
|
|
1273
|
-
},
|
|
1274
|
-
{
|
|
1275
|
-
name: "bearer",
|
|
1276
|
-
pattern: /Bearer\s+[A-Za-z0-9._~+/=-]+/gi,
|
|
1277
|
-
replacement: (value, salt) => `<REDACTED:bearer:${tag(value, salt)}>`
|
|
1278
|
-
},
|
|
1279
|
-
{
|
|
1280
|
-
name: "basic",
|
|
1281
|
-
pattern: /Basic\s+[A-Za-z0-9+/=-]+/gi,
|
|
1282
|
-
replacement: (value, salt) => `<REDACTED:basic:${tag(value, salt)}>`
|
|
1283
|
-
},
|
|
1284
|
-
{
|
|
1285
|
-
name: "jwt",
|
|
1286
|
-
pattern: /eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g,
|
|
1287
|
-
replacement: (value, salt) => `<REDACTED:jwt:${tag(value, salt)}>`
|
|
1288
|
-
},
|
|
1289
|
-
{
|
|
1290
|
-
name: "github",
|
|
1291
|
-
pattern: /github_pat_[A-Za-z0-9_]{20,}/g,
|
|
1292
|
-
replacement: (value, salt) => `<REDACTED:github:${tag(value, salt)}>`
|
|
1293
|
-
},
|
|
1294
|
-
{
|
|
1295
|
-
name: "github",
|
|
1296
|
-
pattern: /github_pat_[A-Za-z0-9_]{4,}(?:\u2026|\.{3})/g,
|
|
1297
|
-
replacement: (value, salt) => `<REDACTED:github:${tag(value, salt)}>`
|
|
1298
|
-
},
|
|
1299
|
-
{
|
|
1300
|
-
name: "github",
|
|
1301
|
-
pattern: /gh[opsu]_[A-Za-z0-9_]{8,}/g,
|
|
1302
|
-
replacement: (value, salt) => `<REDACTED:github:${tag(value, salt)}>`
|
|
1303
|
-
},
|
|
1304
|
-
{
|
|
1305
|
-
name: "github",
|
|
1306
|
-
pattern: /gh[opsu]_[A-Za-z0-9_]{4,}(?:\u2026|\.{3})/g,
|
|
1307
|
-
replacement: (value, salt) => `<REDACTED:github:${tag(value, salt)}>`
|
|
1308
|
-
},
|
|
1309
|
-
{
|
|
1310
|
-
name: "openai",
|
|
1311
|
-
pattern: /sk-(?:proj-)?[A-Za-z0-9_-]{20,}/g,
|
|
1312
|
-
replacement: (value, salt) => `<REDACTED:openai:${tag(value, salt)}>`
|
|
1313
|
-
},
|
|
1314
|
-
{
|
|
1315
|
-
name: "openai",
|
|
1316
|
-
pattern: /sk-(?:proj-)?[A-Za-z0-9_-]{4,}(?:\u2026|\.{3})/g,
|
|
1317
|
-
replacement: (value, salt) => `<REDACTED:openai:${tag(value, salt)}>`
|
|
1318
|
-
},
|
|
1319
|
-
{
|
|
1320
|
-
name: "slack",
|
|
1321
|
-
pattern: /xox[baprs]-[A-Za-z0-9-]{10,}/g,
|
|
1322
|
-
replacement: (value, salt) => `<REDACTED:slack:${tag(value, salt)}>`
|
|
1323
|
-
},
|
|
1324
|
-
{
|
|
1325
|
-
name: "stripe",
|
|
1326
|
-
pattern: /(?:sk|rk)_(?:live|test)_[A-Za-z0-9]{10,}/g,
|
|
1327
|
-
replacement: (value, salt) => `<REDACTED:stripe:${tag(value, salt)}>`
|
|
1328
|
-
},
|
|
1329
|
-
{
|
|
1330
|
-
name: "aws_key",
|
|
1331
|
-
pattern: /(?:AKIA|ASIA)[0-9A-Z]{16}/g,
|
|
1332
|
-
replacement: (value, salt) => `<REDACTED:aws_key:${tag(value, salt)}>`
|
|
1333
|
-
},
|
|
1334
|
-
{
|
|
1335
|
-
name: "aws_key",
|
|
1336
|
-
pattern: /(?:AKIA|ASIA)[0-9A-Z]{4,}(?:\u2026|\.{3})/g,
|
|
1337
|
-
replacement: (value, salt) => `<REDACTED:aws_key:${tag(value, salt)}>`
|
|
1555
|
+
for (const directory of directoryPaths) {
|
|
1556
|
+
await mkdir2(directory.localPath, { recursive: true });
|
|
1338
1557
|
}
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
var FIELD_UNQUOTED_PATTERN = /(["']?(?:password|passwd|pwd|secret|client_secret|token|access_token|refresh_token|cookie|set-cookie|api_key|apikey|private_key)["']?\s*[:=]\s*)[^"',\n}\s]+/gi;
|
|
1342
|
-
function redactText(text, salt = "moxt-run-local-salt") {
|
|
1343
|
-
const hitCounts = /* @__PURE__ */ new Map();
|
|
1344
|
-
let redacted = text;
|
|
1345
|
-
for (const rule of SECRET_RULES) {
|
|
1346
|
-
redacted = redacted.replace(rule.pattern, (value) => {
|
|
1347
|
-
addHit(hitCounts, rule.name);
|
|
1348
|
-
return rule.replacement(value, salt);
|
|
1349
|
-
});
|
|
1558
|
+
for (const downloaded of downloadedEntries) {
|
|
1559
|
+
await writePulledFile(downloaded.localPath, downloaded.content);
|
|
1350
1560
|
}
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
return
|
|
1357
|
-
|
|
1561
|
+
const downloadedContentByPath = new Map(
|
|
1562
|
+
downloadedEntries.map(({ entry, content }) => [entry.prefixedPath, content])
|
|
1563
|
+
);
|
|
1564
|
+
const pulledFiles = Object.fromEntries(await Promise.all(plannedBlobs.map(async ({ entry, localPath }) => {
|
|
1565
|
+
const content = downloadedContentByPath.get(entry.prefixedPath);
|
|
1566
|
+
return [
|
|
1567
|
+
entry.prefixedPath,
|
|
1568
|
+
{
|
|
1569
|
+
repoId: entry.repoId,
|
|
1570
|
+
repoPath: entry.path,
|
|
1571
|
+
fileId: entry.fileId,
|
|
1572
|
+
sha: requireBlobSha(entry),
|
|
1573
|
+
size: content?.length ?? entry.size ?? previousFiles[entry.prefixedPath]?.size ?? await localFileSize(localPath, entry.prefixedPath),
|
|
1574
|
+
fileType: entry.fileType ?? 1
|
|
1575
|
+
}
|
|
1576
|
+
];
|
|
1577
|
+
})));
|
|
1578
|
+
await saveCheckoutState(buildNextCheckoutState({
|
|
1579
|
+
context,
|
|
1580
|
+
checkoutRoot: resolvedTargetDir,
|
|
1581
|
+
allRepos: repos,
|
|
1582
|
+
selection,
|
|
1583
|
+
previousState: compatibleState,
|
|
1584
|
+
pulledRepos: stateRepos,
|
|
1585
|
+
pulledFiles
|
|
1586
|
+
}));
|
|
1358
1587
|
return {
|
|
1359
|
-
|
|
1360
|
-
|
|
1588
|
+
fileCount: blobEntries.length,
|
|
1589
|
+
targetDir: resolvedTargetDir
|
|
1361
1590
|
};
|
|
1362
1591
|
}
|
|
1363
|
-
function
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1592
|
+
function resolveCompatibleState(state, context, checkoutRoot, repos) {
|
|
1593
|
+
if (!state || state.workspaceId !== context.workspaceId || state.checkoutRoot !== checkoutRoot) return void 0;
|
|
1594
|
+
const currentRepos = new Map(repos.map((repo) => [repo.repoId, repo.name]));
|
|
1595
|
+
const stateMatchesRepos = Object.entries(state.repos).every(
|
|
1596
|
+
([repoId, repo]) => currentRepos.get(repoId) === repo.name
|
|
1597
|
+
);
|
|
1598
|
+
return stateMatchesRepos ? state : void 0;
|
|
1368
1599
|
}
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
return remote;
|
|
1374
|
-
}
|
|
1375
|
-
try {
|
|
1376
|
-
const url = new URL(remote);
|
|
1377
|
-
url.username = "";
|
|
1378
|
-
url.password = "";
|
|
1379
|
-
return url.toString();
|
|
1380
|
-
} catch {
|
|
1381
|
-
return remote;
|
|
1600
|
+
function resolvePullSelection(context, requestedScopes) {
|
|
1601
|
+
const repos = listRepoEntries(context.repoPayload);
|
|
1602
|
+
if (!requestedScopes || requestedScopes.length === 0) {
|
|
1603
|
+
return { mode: "full", scopes: repos.map((repo) => repo.name), repos };
|
|
1382
1604
|
}
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1605
|
+
const resolvedScopes = requestedScopes.map((scope) => {
|
|
1606
|
+
const resolved = resolveWorkspacePath(context.repoPayload, scope);
|
|
1607
|
+
return {
|
|
1608
|
+
path: resolved.repoPath ? `${resolved.repoName}/${resolved.repoPath}` : resolved.repoName,
|
|
1609
|
+
repoId: resolved.repoId
|
|
1610
|
+
};
|
|
1611
|
+
});
|
|
1612
|
+
const scopes = collapseScopes(resolvedScopes.map((scope) => scope.path));
|
|
1613
|
+
const repoIds = new Set(resolvedScopes.map((scope) => scope.repoId));
|
|
1386
1614
|
return {
|
|
1387
|
-
|
|
1388
|
-
|
|
1615
|
+
mode: "partial",
|
|
1616
|
+
scopes,
|
|
1617
|
+
repos: repos.filter((repo) => repoIds.has(repo.repoId))
|
|
1389
1618
|
};
|
|
1390
1619
|
}
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
}
|
|
1404
|
-
function matchSession(before, after, startedAtMs, options = {}) {
|
|
1405
|
-
const candidates = [];
|
|
1406
|
-
for (const [path2, current2] of after.entries()) {
|
|
1407
|
-
const previous = before.get(path2);
|
|
1408
|
-
if (previous === void 0) {
|
|
1409
|
-
if (wasChangedAfterStart(current2, startedAtMs)) {
|
|
1410
|
-
candidates.push({ path: path2, startOffset: 0 });
|
|
1411
|
-
}
|
|
1412
|
-
continue;
|
|
1413
|
-
}
|
|
1414
|
-
if (!options.newFilesOnly && current2.size > previous.size) {
|
|
1415
|
-
candidates.push({ path: path2, startOffset: previous.size });
|
|
1416
|
-
}
|
|
1417
|
-
}
|
|
1418
|
-
if (candidates.length === 0) {
|
|
1419
|
-
return { match: "none", sessionFile: null, startOffset: 0 };
|
|
1420
|
-
}
|
|
1421
|
-
const trustedCandidates = options.isCandidate === void 0 ? candidates : candidates.filter((candidate) => options.isCandidate?.(candidate.path));
|
|
1422
|
-
if (trustedCandidates.length === 0) {
|
|
1423
|
-
return { match: "none", sessionFile: null, startOffset: 0 };
|
|
1424
|
-
}
|
|
1425
|
-
if (trustedCandidates.length === 1) {
|
|
1426
|
-
const [candidate] = trustedCandidates;
|
|
1427
|
-
if (candidate !== void 0) {
|
|
1428
|
-
return {
|
|
1429
|
-
match: "unique",
|
|
1430
|
-
sessionFile: candidate.path,
|
|
1431
|
-
startOffset: candidate.startOffset
|
|
1432
|
-
};
|
|
1433
|
-
}
|
|
1434
|
-
}
|
|
1435
|
-
return { match: "ambiguous", sessionFile: null, startOffset: 0 };
|
|
1436
|
-
}
|
|
1437
|
-
function matchKnownSessionFile(before, after, sessionFile) {
|
|
1438
|
-
const current2 = after.get(sessionFile);
|
|
1439
|
-
if (current2 === void 0) {
|
|
1440
|
-
return { match: "none", sessionFile: null, startOffset: 0 };
|
|
1441
|
-
}
|
|
1442
|
-
const previous = before.get(sessionFile);
|
|
1443
|
-
if (previous !== void 0 && current2.size <= previous.size) {
|
|
1444
|
-
return { match: "none", sessionFile: null, startOffset: 0 };
|
|
1620
|
+
function buildNextCheckoutState(input) {
|
|
1621
|
+
const { context, checkoutRoot, allRepos, selection, previousState, pulledRepos, pulledFiles } = input;
|
|
1622
|
+
if (selection.mode === "full") {
|
|
1623
|
+
return {
|
|
1624
|
+
version: 1,
|
|
1625
|
+
workspaceId: context.workspaceId,
|
|
1626
|
+
checkoutRoot,
|
|
1627
|
+
mode: "full",
|
|
1628
|
+
checkedOutScopes: selection.scopes,
|
|
1629
|
+
repos: pulledRepos,
|
|
1630
|
+
files: pulledFiles
|
|
1631
|
+
};
|
|
1445
1632
|
}
|
|
1633
|
+
const previousFiles = previousState?.files ?? {};
|
|
1634
|
+
const preservedFiles = Object.fromEntries(
|
|
1635
|
+
Object.entries(previousFiles).filter(
|
|
1636
|
+
([workspacePath]) => !selection.scopes.some((scope) => pathIsIncluded(scope, workspacePath))
|
|
1637
|
+
)
|
|
1638
|
+
);
|
|
1639
|
+
const checkedOutScopes = collapseScopes([...previousState?.checkedOutScopes ?? [], ...selection.scopes]);
|
|
1640
|
+
const mode = allRepos.every((repo) => checkedOutScopes.includes(repo.name)) ? "full" : "partial";
|
|
1446
1641
|
return {
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1642
|
+
version: 1,
|
|
1643
|
+
workspaceId: context.workspaceId,
|
|
1644
|
+
checkoutRoot,
|
|
1645
|
+
mode,
|
|
1646
|
+
checkedOutScopes,
|
|
1647
|
+
repos: { ...previousState?.repos ?? {}, ...pulledRepos },
|
|
1648
|
+
files: { ...preservedFiles, ...pulledFiles }
|
|
1450
1649
|
};
|
|
1451
1650
|
}
|
|
1452
|
-
function
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
case "ambiguous":
|
|
1459
|
-
return { found_chat: false, quality: "uncertain" };
|
|
1460
|
-
case "prep_failed":
|
|
1461
|
-
return { found_chat: false, quality: "unavailable" };
|
|
1462
|
-
}
|
|
1463
|
-
}
|
|
1464
|
-
function collectJsonl(dir, snapshot) {
|
|
1465
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
1466
|
-
const path2 = join2(dir, entry.name);
|
|
1467
|
-
if (entry.isDirectory()) {
|
|
1468
|
-
collectJsonl(path2, snapshot);
|
|
1469
|
-
continue;
|
|
1651
|
+
function collapseScopes(scopes) {
|
|
1652
|
+
const result = [];
|
|
1653
|
+
for (const scope of scopes) {
|
|
1654
|
+
if (result.some((existing) => pathIsIncluded(existing, scope))) continue;
|
|
1655
|
+
for (let index = result.length - 1; index >= 0; index--) {
|
|
1656
|
+
if (pathIsIncluded(scope, result[index])) result.splice(index, 1);
|
|
1470
1657
|
}
|
|
1471
|
-
|
|
1472
|
-
continue;
|
|
1473
|
-
}
|
|
1474
|
-
const stat2 = statSync2(path2);
|
|
1475
|
-
snapshot.set(path2, {
|
|
1476
|
-
mtimeMs: stat2.mtimeMs,
|
|
1477
|
-
size: stat2.size
|
|
1478
|
-
});
|
|
1658
|
+
result.push(scope);
|
|
1479
1659
|
}
|
|
1660
|
+
return result;
|
|
1480
1661
|
}
|
|
1481
|
-
function
|
|
1482
|
-
return
|
|
1662
|
+
function pathsIntersect(left, right) {
|
|
1663
|
+
return pathIsIncluded(left, right) || pathIsIncluded(right, left);
|
|
1483
1664
|
}
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
var SIGNAL_NUMBERS = /* @__PURE__ */ new Map([
|
|
1487
|
-
["SIGHUP", 1],
|
|
1488
|
-
["SIGINT", 2],
|
|
1489
|
-
["SIGQUIT", 3],
|
|
1490
|
-
["SIGKILL", 9],
|
|
1491
|
-
["SIGTERM", 15]
|
|
1492
|
-
]);
|
|
1493
|
-
function signalNumber(signal) {
|
|
1494
|
-
return SIGNAL_NUMBERS.get(signal) ?? 0;
|
|
1495
|
-
}
|
|
1496
|
-
function classifyExit(event, observedSignals) {
|
|
1497
|
-
if (event.errorCode !== void 0) {
|
|
1498
|
-
return {
|
|
1499
|
-
code: event.errorCode === "ENOENT" ? 127 : 126,
|
|
1500
|
-
status: "unknown"
|
|
1501
|
-
};
|
|
1502
|
-
}
|
|
1503
|
-
if (event.signal !== null) {
|
|
1504
|
-
return {
|
|
1505
|
-
code: 128 + signalNumber(event.signal),
|
|
1506
|
-
status: "cancelled"
|
|
1507
|
-
};
|
|
1508
|
-
}
|
|
1509
|
-
if (event.code !== null) {
|
|
1510
|
-
if (event.code === 0) {
|
|
1511
|
-
return { code: 0, status: "completed" };
|
|
1512
|
-
}
|
|
1513
|
-
const signal = event.code - 128;
|
|
1514
|
-
return {
|
|
1515
|
-
code: event.code,
|
|
1516
|
-
status: event.code > 128 && observedSignals.has(signal) ? "cancelled" : "failed"
|
|
1517
|
-
};
|
|
1518
|
-
}
|
|
1519
|
-
return { code: 1, status: "unknown" };
|
|
1665
|
+
function pathIsIncluded(scope, workspacePath) {
|
|
1666
|
+
return workspacePath === scope || workspacePath.startsWith(`${scope}/`);
|
|
1520
1667
|
}
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
var LIVE_CAPTURE_INTERVAL_MS = 250;
|
|
1527
|
-
function createLiveCapture(options) {
|
|
1528
|
-
return new LiveCaptureRecorder(options);
|
|
1529
|
-
}
|
|
1530
|
-
var LiveCaptureRecorder = class {
|
|
1531
|
-
enabled;
|
|
1532
|
-
outputs;
|
|
1533
|
-
env;
|
|
1534
|
-
runId;
|
|
1535
|
-
agent;
|
|
1536
|
-
startedAt;
|
|
1537
|
-
sessions;
|
|
1538
|
-
before;
|
|
1539
|
-
baselineOk;
|
|
1540
|
-
startedAtMs;
|
|
1541
|
-
knownSessionFile;
|
|
1542
|
-
isSessionCandidate;
|
|
1543
|
-
newSessionFilesOnly;
|
|
1544
|
-
openSessionFile;
|
|
1545
|
-
timer;
|
|
1546
|
-
pollInFlight = null;
|
|
1547
|
-
sessionFile = null;
|
|
1548
|
-
readOffset = 0;
|
|
1549
|
-
pendingText = "";
|
|
1550
|
-
nextSeq = 1;
|
|
1551
|
-
sessionMatch;
|
|
1552
|
-
captureFailed = false;
|
|
1553
|
-
constructor(options) {
|
|
1554
|
-
const outputDirs = captureOutputDirectories(options.env, options.runId);
|
|
1555
|
-
this.enabled = outputDirs.length > 0;
|
|
1556
|
-
this.outputs = outputDirs.map((outputDir) => new CaptureDirectoryOutput(outputDir));
|
|
1557
|
-
this.env = options.env;
|
|
1558
|
-
this.runId = options.runId;
|
|
1559
|
-
this.agent = options.agent;
|
|
1560
|
-
this.startedAt = options.startedAt;
|
|
1561
|
-
this.sessions = options.sessions;
|
|
1562
|
-
this.before = options.before;
|
|
1563
|
-
this.baselineOk = options.baselineOk;
|
|
1564
|
-
this.startedAtMs = options.startedAtMs;
|
|
1565
|
-
this.knownSessionFile = options.knownSessionFile;
|
|
1566
|
-
this.isSessionCandidate = options.isSessionCandidate;
|
|
1567
|
-
this.newSessionFilesOnly = options.newSessionFilesOnly ?? false;
|
|
1568
|
-
this.openSessionFile = options.openSessionFile;
|
|
1569
|
-
this.sessionMatch = options.baselineOk ? "none" : "prep_failed";
|
|
1570
|
-
}
|
|
1571
|
-
async start() {
|
|
1572
|
-
if (!this.enabled) {
|
|
1573
|
-
return;
|
|
1574
|
-
}
|
|
1575
|
-
await emitOpenCaptureDirectories(this.env, this.runId, this.startedAt);
|
|
1576
|
-
if (this.baselineOk) {
|
|
1577
|
-
this.timer = setInterval(() => {
|
|
1578
|
-
this.queuePoll();
|
|
1579
|
-
}, LIVE_CAPTURE_INTERVAL_MS);
|
|
1668
|
+
function resolveStateRepos(tree, repos) {
|
|
1669
|
+
const knownRepoIds = new Set(repos.map((repo) => repo.repoId));
|
|
1670
|
+
for (const repoId of Object.keys(tree.repos)) {
|
|
1671
|
+
if (!knownRepoIds.has(repoId)) {
|
|
1672
|
+
throw new SandboxPullError(`Remote tree returned unknown repo: ${repoId}`, "remote");
|
|
1580
1673
|
}
|
|
1581
1674
|
}
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
if (this.timer !== void 0) {
|
|
1587
|
-
clearInterval(this.timer);
|
|
1588
|
-
this.timer = void 0;
|
|
1675
|
+
return Object.fromEntries(repos.map((repo) => {
|
|
1676
|
+
const remoteRepo = tree.repos[repo.repoId];
|
|
1677
|
+
if (!remoteRepo) {
|
|
1678
|
+
throw new SandboxPullError(`Remote tree did not return repo: ${repo.repoId}`, "remote");
|
|
1589
1679
|
}
|
|
1590
|
-
if (
|
|
1591
|
-
|
|
1680
|
+
if (remoteRepo.prefix !== repo.name) {
|
|
1681
|
+
throw new SandboxPullError(`Remote tree returned unexpected prefix for repo ${repo.repoId}`, "remote");
|
|
1592
1682
|
}
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
}
|
|
1596
|
-
async finalize(payload, capturedAt) {
|
|
1597
|
-
if (!this.enabled) {
|
|
1598
|
-
return;
|
|
1683
|
+
if (typeof remoteRepo.sha !== "string" || remoteRepo.sha.length === 0) {
|
|
1684
|
+
throw new SandboxPullError(`Remote tree did not return a version for repo ${repo.repoId}`, "remote");
|
|
1599
1685
|
}
|
|
1600
|
-
|
|
1601
|
-
|
|
1686
|
+
return [repo.repoId, { name: repo.name, treeSha: remoteRepo.sha }];
|
|
1687
|
+
}));
|
|
1688
|
+
}
|
|
1689
|
+
async function fetchWorkspaceTree(context, selection) {
|
|
1690
|
+
const client = new SandboxMoxtClient(context);
|
|
1691
|
+
const results = await Promise.all(selection.repos.map(async (repo) => {
|
|
1692
|
+
const filter = resolveRepoTreeFilter(repo, selection);
|
|
1693
|
+
const params = new URLSearchParams({ entryFilter: filter.type });
|
|
1694
|
+
if (filter.type === "paths") {
|
|
1695
|
+
for (const path2 of filter.paths) params.append("path", path2);
|
|
1602
1696
|
}
|
|
1603
|
-
|
|
1697
|
+
const response = await client.request(
|
|
1698
|
+
"GET",
|
|
1699
|
+
`/agent/api/pipeline-sandbox/mfs/${encodeURIComponent(repo.repoId)}/tree?${params.toString()}`
|
|
1700
|
+
);
|
|
1701
|
+
return { repo, tree: response.data };
|
|
1702
|
+
}));
|
|
1703
|
+
return {
|
|
1704
|
+
repos: Object.fromEntries(
|
|
1705
|
+
results.map(({ repo, tree }) => [repo.repoId, { sha: tree.sha, prefix: repo.name }])
|
|
1706
|
+
),
|
|
1707
|
+
entries: results.flatMap(
|
|
1708
|
+
({ repo, tree }) => tree.entries.map((entry) => ({
|
|
1709
|
+
...entry,
|
|
1710
|
+
repoId: repo.repoId,
|
|
1711
|
+
prefixedPath: entry.path ? `${repo.name}/${entry.path}` : repo.name
|
|
1712
|
+
}))
|
|
1713
|
+
)
|
|
1714
|
+
};
|
|
1715
|
+
}
|
|
1716
|
+
function resolveRepoTreeFilter(repo, selection) {
|
|
1717
|
+
if (selection.mode === "full") return { type: "all" };
|
|
1718
|
+
const repoPaths = [];
|
|
1719
|
+
for (const scope of selection.scopes) {
|
|
1720
|
+
if (scope === repo.name) return { type: "all" };
|
|
1721
|
+
if (scope.startsWith(`${repo.name}/`)) repoPaths.push(scope.slice(repo.name.length + 1));
|
|
1604
1722
|
}
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
return;
|
|
1608
|
-
}
|
|
1609
|
-
this.pollInFlight = this.poll().finally(() => {
|
|
1610
|
-
this.pollInFlight = null;
|
|
1611
|
-
});
|
|
1723
|
+
if (repoPaths.length === 0) {
|
|
1724
|
+
throw new SandboxPullError(`No pull scope resolved for repo: ${repo.name}`, "invalid-path");
|
|
1612
1725
|
}
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
const matched2 = matchKnownSessionFile(this.before, snapshot, opened.sessionFile);
|
|
1637
|
-
this.sessionMatch = matched2.match;
|
|
1638
|
-
if (matched2.match === "unique" && matched2.sessionFile !== null) {
|
|
1639
|
-
this.sessionFile = matched2.sessionFile;
|
|
1640
|
-
this.readOffset = matched2.startOffset;
|
|
1641
|
-
await this.captureNewText();
|
|
1642
|
-
}
|
|
1643
|
-
return;
|
|
1644
|
-
}
|
|
1645
|
-
const matched = matchSession(this.before, snapshot, this.startedAtMs, this.matchOptions());
|
|
1646
|
-
this.sessionMatch = matched.match;
|
|
1647
|
-
if (matched.match !== "unique" || matched.sessionFile === null) {
|
|
1648
|
-
return;
|
|
1649
|
-
}
|
|
1650
|
-
this.sessionFile = matched.sessionFile;
|
|
1651
|
-
this.readOffset = matched.startOffset;
|
|
1726
|
+
return { type: "paths", paths: repoPaths };
|
|
1727
|
+
}
|
|
1728
|
+
async function presignDownloads(context, entries) {
|
|
1729
|
+
const client = new SandboxMoxtClient(context);
|
|
1730
|
+
const entriesByRepo = /* @__PURE__ */ new Map();
|
|
1731
|
+
for (const entry of entries) {
|
|
1732
|
+
const existing = entriesByRepo.get(entry.repoId) ?? [];
|
|
1733
|
+
existing.push(entry);
|
|
1734
|
+
entriesByRepo.set(entry.repoId, existing);
|
|
1735
|
+
}
|
|
1736
|
+
const urls = /* @__PURE__ */ new Map();
|
|
1737
|
+
for (const [repoId, repoEntries] of entriesByRepo) {
|
|
1738
|
+
const hashes = [...new Set(repoEntries.map((entry) => entry.sha).filter((sha) => Boolean(sha)))];
|
|
1739
|
+
if (hashes.length === 0) continue;
|
|
1740
|
+
const response = await client.request(
|
|
1741
|
+
"POST",
|
|
1742
|
+
`/agent/api/pipeline-sandbox/mfs/${encodeURIComponent(repoId)}/presign-download`,
|
|
1743
|
+
{ hashes }
|
|
1744
|
+
);
|
|
1745
|
+
for (const hash of hashes) {
|
|
1746
|
+
const url = response.data.urls[hash];
|
|
1747
|
+
if (!url) {
|
|
1748
|
+
throw new SandboxPullError(`No download URL returned for ${hash}`, "remote");
|
|
1652
1749
|
}
|
|
1653
|
-
|
|
1654
|
-
} catch {
|
|
1655
|
-
this.captureFailed = true;
|
|
1656
|
-
}
|
|
1657
|
-
}
|
|
1658
|
-
async captureNewText() {
|
|
1659
|
-
if (this.sessionFile === null) {
|
|
1660
|
-
return;
|
|
1661
|
-
}
|
|
1662
|
-
const next = await readFileFromOffset(this.sessionFile, this.readOffset);
|
|
1663
|
-
if (next.bytes === 0) {
|
|
1664
|
-
return;
|
|
1665
|
-
}
|
|
1666
|
-
this.readOffset += next.bytes;
|
|
1667
|
-
await this.captureCompleteLines(next.text);
|
|
1668
|
-
}
|
|
1669
|
-
async captureCompleteLines(text) {
|
|
1670
|
-
const combined = `${this.pendingText}${text}`;
|
|
1671
|
-
const lastNewline = combined.lastIndexOf("\n");
|
|
1672
|
-
if (lastNewline === -1) {
|
|
1673
|
-
this.pendingText = combined;
|
|
1674
|
-
return;
|
|
1750
|
+
urls.set(hash, url);
|
|
1675
1751
|
}
|
|
1676
|
-
const complete = combined.slice(0, lastNewline + 1);
|
|
1677
|
-
this.pendingText = combined.slice(lastNewline + 1);
|
|
1678
|
-
await this.captureTranscriptText(complete);
|
|
1679
1752
|
}
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
this.pendingText = "";
|
|
1687
|
-
await this.captureTranscriptText(text);
|
|
1688
|
-
}
|
|
1689
|
-
async captureTranscriptText(text) {
|
|
1690
|
-
const redacted = redactText(text).text;
|
|
1691
|
-
const events = serializeEventsFromTranscript(redacted, this.nextSeq, this.agent);
|
|
1692
|
-
this.nextSeq = events.nextSeq;
|
|
1693
|
-
await Promise.all(
|
|
1694
|
-
this.outputs.map(async (output) => {
|
|
1695
|
-
await output.appendTranscript(redacted);
|
|
1696
|
-
await output.appendEvents(events.text);
|
|
1697
|
-
})
|
|
1698
|
-
);
|
|
1699
|
-
}
|
|
1700
|
-
directoryPayload(payload) {
|
|
1701
|
-
const recording = isCompleteRecording(payload) ? this.sessionFile === null ? payload.recording : recordingFromSessionMatch(this.sessionMatch) : payload.recording;
|
|
1702
|
-
const keepRedactionFailure = this.captureFailed || this.files().transcript.length === 0 && payload.redaction_failed;
|
|
1703
|
-
const { redaction_failed: _redactionFailed, ...rest } = payload;
|
|
1704
|
-
return {
|
|
1705
|
-
...rest,
|
|
1706
|
-
...keepRedactionFailure ? { redaction_failed: true } : {},
|
|
1707
|
-
recording
|
|
1708
|
-
};
|
|
1709
|
-
}
|
|
1710
|
-
files() {
|
|
1711
|
-
const [output] = this.outputs;
|
|
1712
|
-
if (output === void 0) {
|
|
1713
|
-
return { events: [], transcript: [] };
|
|
1714
|
-
}
|
|
1715
|
-
return output.files();
|
|
1753
|
+
return urls;
|
|
1754
|
+
}
|
|
1755
|
+
async function downloadBlob(entry, urls) {
|
|
1756
|
+
const hash = entry.sha;
|
|
1757
|
+
if (!hash) {
|
|
1758
|
+
throw new SandboxPullError(`${entry.prefixedPath} does not have downloadable content`, "remote");
|
|
1716
1759
|
}
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
...this.newSessionFilesOnly ? { newFilesOnly: true } : {}
|
|
1721
|
-
};
|
|
1760
|
+
const url = urls.get(hash);
|
|
1761
|
+
if (!url) {
|
|
1762
|
+
throw new SandboxPullError(`No download URL returned for ${entry.prefixedPath}`, "remote");
|
|
1722
1763
|
}
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
events;
|
|
1727
|
-
constructor(outputDir) {
|
|
1728
|
-
this.transcript = new JsonlStreamWriter(outputDir, "transcript", "transcript");
|
|
1729
|
-
this.events = new JsonlStreamWriter(outputDir, "events", "events");
|
|
1764
|
+
const response = await fetch(url);
|
|
1765
|
+
if (!response.ok) {
|
|
1766
|
+
throw new SandboxPullError(`Download failed [${response.status}]: ${entry.prefixedPath}`, "remote");
|
|
1730
1767
|
}
|
|
1731
|
-
|
|
1732
|
-
|
|
1768
|
+
const content = Buffer.from(await response.arrayBuffer());
|
|
1769
|
+
if (content.length > MFS_FILE_SIZE_LIMIT_BYTES) {
|
|
1770
|
+
throw new SandboxPullError(`File too large (max ${MFS_FILE_SIZE_LIMIT_LABEL}): ${entry.prefixedPath}`, "too-large");
|
|
1733
1771
|
}
|
|
1734
|
-
|
|
1735
|
-
|
|
1772
|
+
const contentHash = createHash2("sha256").update(content).digest("hex");
|
|
1773
|
+
if (contentHash !== hash) {
|
|
1774
|
+
throw new SandboxPullError(`Downloaded content hash mismatch: ${entry.prefixedPath}`, "remote");
|
|
1736
1775
|
}
|
|
1737
|
-
|
|
1738
|
-
|
|
1776
|
+
return content;
|
|
1777
|
+
}
|
|
1778
|
+
async function shouldWritePulledFile(localPath, remoteContentHash, workspacePath, previousFile, force) {
|
|
1779
|
+
const existing = await lstat(localPath).catch((error) => {
|
|
1780
|
+
if (isNodeError2(error) && error.code === "ENOENT") return void 0;
|
|
1781
|
+
throw error;
|
|
1782
|
+
});
|
|
1783
|
+
if (existing && !existing.isFile()) {
|
|
1784
|
+
throw new SandboxPullError(`Refusing to overwrite local path: ${workspacePath}`, "directory-conflict");
|
|
1785
|
+
}
|
|
1786
|
+
if (!existing) {
|
|
1787
|
+
if (force || !previousFile) return true;
|
|
1788
|
+
if (remoteContentHash === previousFile.sha) return false;
|
|
1789
|
+
throw new SandboxPullError(
|
|
1790
|
+
`Local file was deleted but remote file changed: ${workspacePath}`,
|
|
1791
|
+
"file-conflict"
|
|
1792
|
+
);
|
|
1739
1793
|
}
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1794
|
+
if (force) return true;
|
|
1795
|
+
const localContentHash = await hashLocalFile(localPath);
|
|
1796
|
+
if (localContentHash === remoteContentHash) return false;
|
|
1797
|
+
if (!previousFile) {
|
|
1798
|
+
throw new SandboxPullError(
|
|
1799
|
+
`Refusing to overwrite modified local file: ${workspacePath}. Use --force to overwrite.`,
|
|
1800
|
+
"file-conflict"
|
|
1801
|
+
);
|
|
1745
1802
|
}
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
this.outputDir = outputDir;
|
|
1755
|
-
this.dir = dir;
|
|
1756
|
-
this.prefix = prefix;
|
|
1757
|
-
}
|
|
1758
|
-
async appendText(text) {
|
|
1759
|
-
for (const line of jsonlLines2(text)) {
|
|
1760
|
-
await this.appendLine(line);
|
|
1761
|
-
}
|
|
1762
|
-
}
|
|
1763
|
-
async invalidate() {
|
|
1764
|
-
await Promise.all(
|
|
1765
|
-
this.writtenFiles.map(async (file) => {
|
|
1766
|
-
file.bytes = 0;
|
|
1767
|
-
file.lines = 0;
|
|
1768
|
-
file.oversized_line = false;
|
|
1769
|
-
await writeFile2(join3(this.outputDir, file.path), "", "utf8");
|
|
1770
|
-
})
|
|
1803
|
+
const localChanged = localContentHash !== previousFile.sha;
|
|
1804
|
+
const remoteChanged = remoteContentHash !== previousFile.sha;
|
|
1805
|
+
if (!localChanged && remoteChanged) return true;
|
|
1806
|
+
if (localChanged && !remoteChanged) return false;
|
|
1807
|
+
if (localChanged && remoteChanged) {
|
|
1808
|
+
throw new SandboxPullError(
|
|
1809
|
+
`Local and remote file both changed since checkout: ${workspacePath}`,
|
|
1810
|
+
"file-conflict"
|
|
1771
1811
|
);
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
if (
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
return;
|
|
1812
|
+
}
|
|
1813
|
+
return false;
|
|
1814
|
+
}
|
|
1815
|
+
async function planRemoteDeletions(previousState, selection, checkoutRoot, remoteBlobPaths, force) {
|
|
1816
|
+
if (!previousState) return [];
|
|
1817
|
+
const deletions = [];
|
|
1818
|
+
for (const [workspacePath, previousFile] of Object.entries(previousState.files)) {
|
|
1819
|
+
if (!selection.scopes.some((scope) => pathIsIncluded(scope, workspacePath))) continue;
|
|
1820
|
+
if (remoteBlobPaths.has(workspacePath)) continue;
|
|
1821
|
+
const repo = previousState.repos[previousFile.repoId];
|
|
1822
|
+
if (!repo) {
|
|
1823
|
+
throw new SandboxPullError(`Checkout baseline references unknown repo: ${workspacePath}`, "remote");
|
|
1824
|
+
}
|
|
1825
|
+
const localPath = resolveSafeLocalPath(checkoutRoot, repo.name, previousFile.repoPath);
|
|
1826
|
+
const existing = await lstat(localPath).catch((error) => {
|
|
1827
|
+
if (isNodeError2(error) && error.code === "ENOENT") return void 0;
|
|
1828
|
+
throw error;
|
|
1829
|
+
});
|
|
1830
|
+
if (!existing) continue;
|
|
1831
|
+
if (!existing.isFile()) {
|
|
1832
|
+
throw new SandboxPullError(`Refusing to overwrite local path: ${workspacePath}`, "directory-conflict");
|
|
1833
|
+
}
|
|
1834
|
+
if (!force && await hashLocalFile(localPath) !== previousFile.sha) {
|
|
1835
|
+
throw new SandboxPullError(
|
|
1836
|
+
`Remote file was deleted but local file changed: ${workspacePath}`,
|
|
1837
|
+
"file-conflict"
|
|
1838
|
+
);
|
|
1788
1839
|
}
|
|
1789
|
-
|
|
1790
|
-
this.current = null;
|
|
1791
|
-
}
|
|
1792
|
-
const file = this.current ?? this.createFile();
|
|
1793
|
-
file.bytes += lineBytes;
|
|
1794
|
-
file.lines += 1;
|
|
1795
|
-
await appendFile(join3(this.outputDir, file.path), line, "utf8");
|
|
1796
|
-
}
|
|
1797
|
-
createFile() {
|
|
1798
|
-
const index = String(this.writtenFiles.length + 1).padStart(6, "0");
|
|
1799
|
-
const file = {
|
|
1800
|
-
path: `${this.dir}/${this.prefix}-${index}.jsonl`,
|
|
1801
|
-
bytes: 0,
|
|
1802
|
-
lines: 0,
|
|
1803
|
-
oversized_line: false
|
|
1804
|
-
};
|
|
1805
|
-
this.writtenFiles.push(file);
|
|
1806
|
-
this.current = file;
|
|
1807
|
-
return file;
|
|
1840
|
+
deletions.push({ localPath });
|
|
1808
1841
|
}
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1842
|
+
return deletions;
|
|
1843
|
+
}
|
|
1844
|
+
async function hashLocalFile(localPath) {
|
|
1845
|
+
return createHash2("sha256").update(await readFile2(localPath)).digest("hex");
|
|
1812
1846
|
}
|
|
1813
|
-
function
|
|
1814
|
-
|
|
1815
|
-
|
|
1847
|
+
async function localFileSize(localPath, workspacePath) {
|
|
1848
|
+
const stats = await lstat(localPath);
|
|
1849
|
+
if (!stats.isFile()) {
|
|
1850
|
+
throw new SandboxPullError(`Cannot determine local file size: ${workspacePath}`, "directory-conflict");
|
|
1816
1851
|
}
|
|
1817
|
-
|
|
1818
|
-
`;
|
|
1819
|
-
return normalized.slice(0, -1).split("\n").map((line) => `${line}
|
|
1820
|
-
`);
|
|
1852
|
+
return stats.size;
|
|
1821
1853
|
}
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
const stat2 = await file.stat();
|
|
1826
|
-
const length = Math.max(0, stat2.size - offset);
|
|
1827
|
-
if (length === 0) {
|
|
1828
|
-
return { text: "", bytes: 0 };
|
|
1829
|
-
}
|
|
1830
|
-
const buffer2 = Buffer.alloc(length);
|
|
1831
|
-
const result = await file.read(buffer2, 0, length, offset);
|
|
1832
|
-
return { text: buffer2.subarray(0, result.bytesRead).toString("utf8"), bytes: result.bytesRead };
|
|
1833
|
-
} finally {
|
|
1834
|
-
await file.close();
|
|
1854
|
+
function requireBlobSha(entry) {
|
|
1855
|
+
if (!entry.sha) {
|
|
1856
|
+
throw new SandboxPullError(`${entry.prefixedPath} does not have downloadable content`, "remote");
|
|
1835
1857
|
}
|
|
1858
|
+
return entry.sha;
|
|
1836
1859
|
}
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1860
|
+
async function assertCanCreateDirectory(localPath, workspacePath) {
|
|
1861
|
+
const existing = await lstat(localPath).catch((error) => {
|
|
1862
|
+
if (isNodeError2(error) && error.code === "ENOENT") return void 0;
|
|
1863
|
+
throw error;
|
|
1864
|
+
});
|
|
1865
|
+
if (existing && !existing.isDirectory()) {
|
|
1866
|
+
throw new SandboxPullError(`Refusing to overwrite local path: ${workspacePath}`, "directory-conflict");
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
async function writePulledFile(localPath, remoteContent) {
|
|
1870
|
+
await mkdir2(dirname2(localPath), { recursive: true });
|
|
1871
|
+
await writeFile2(localPath, remoteContent);
|
|
1872
|
+
}
|
|
1873
|
+
function resolveSafeLocalPath(targetDir, repoName, repoPath) {
|
|
1874
|
+
const pathParts = [repoName, ...repoPath.split("/").filter((part) => part.length > 0)];
|
|
1875
|
+
if (pathParts.some((part) => part === "." || part === ".." || part.includes("/") || part.includes("\\") || part.includes("\0"))) {
|
|
1876
|
+
throw new SandboxPullError(`Remote path is unsafe: ${[repoName, repoPath].filter(Boolean).join("/")}`, "invalid-path");
|
|
1850
1877
|
}
|
|
1851
|
-
const
|
|
1852
|
-
const
|
|
1853
|
-
if (
|
|
1854
|
-
|
|
1855
|
-
headers["CF-Access-Client-Secret"] = clientSecret;
|
|
1878
|
+
const resolved = resolve(targetDir, ...pathParts);
|
|
1879
|
+
const relativePath = relative(targetDir, resolved);
|
|
1880
|
+
if (relativePath.startsWith("..") || isAbsolute2(relativePath)) {
|
|
1881
|
+
throw new SandboxPullError(`Remote path escapes target directory: ${repoName}/${repoPath}`, "invalid-path");
|
|
1856
1882
|
}
|
|
1857
|
-
return
|
|
1883
|
+
return resolved;
|
|
1858
1884
|
}
|
|
1859
|
-
function
|
|
1860
|
-
return
|
|
1885
|
+
function isNodeError2(error) {
|
|
1886
|
+
return error instanceof Error && "code" in error;
|
|
1861
1887
|
}
|
|
1862
1888
|
|
|
1863
|
-
// src/
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
const outputDirs = captureOutputDirectories(options.env, options.runId);
|
|
1868
|
-
const failures = [];
|
|
1869
|
-
for (const outputDir of outputDirs) {
|
|
1889
|
+
// src/commands/pull.ts
|
|
1890
|
+
function registerPullCommand(program2) {
|
|
1891
|
+
program2.command("pull").description("Pull workspace files into the local checkout").argument("[workspacePath]", "Workspace file or directory to pull").option("--all", "Pull the entire workspace").option("--force", "Overwrite changed local files").addOption(new Option("--dir <dir>").hideHelp()).addOption(new Option("--scope <workspacePath>").argParser(collectScope).default([]).hideHelp()).action(async (workspacePath, options) => {
|
|
1892
|
+
const context = resolveSandboxRuntimeContextOrExit2();
|
|
1870
1893
|
try {
|
|
1871
|
-
await
|
|
1894
|
+
const invocation = await resolvePullInvocation(context, workspacePath, options);
|
|
1895
|
+
const result = await pullSandboxWorkspace(context, invocation.targetDir, {
|
|
1896
|
+
force: Boolean(options.force),
|
|
1897
|
+
scopes: invocation.scopes
|
|
1898
|
+
});
|
|
1899
|
+
print(`Pulled ${result.fileCount} ${result.fileCount === 1 ? "file" : "files"} to ${result.targetDir}`);
|
|
1872
1900
|
} catch (error) {
|
|
1873
|
-
|
|
1901
|
+
handlePullError(error);
|
|
1874
1902
|
}
|
|
1903
|
+
});
|
|
1904
|
+
}
|
|
1905
|
+
async function resolvePullInvocation(context, argument, options) {
|
|
1906
|
+
const legacyTargetDir = argument && isLegacyTargetDirectory(argument) ? argument : void 0;
|
|
1907
|
+
const workspacePath = legacyTargetDir ? void 0 : argument;
|
|
1908
|
+
if (workspacePath && options.all) {
|
|
1909
|
+
throw new Error("Specify either a workspace path or --all, not both");
|
|
1875
1910
|
}
|
|
1876
|
-
if (
|
|
1877
|
-
throw new Error(
|
|
1911
|
+
if (options.scope.length > 0 && (workspacePath || options.all)) {
|
|
1912
|
+
throw new Error("Legacy --scope cannot be combined with a workspace path or --all");
|
|
1878
1913
|
}
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
const files = await discoverUploadFiles(outputDir);
|
|
1882
|
-
const state = await readUploadState(outputDir, runId);
|
|
1883
|
-
for (const file of files) {
|
|
1884
|
-
const previous = state.files[file.artifactPath];
|
|
1885
|
-
if (previous?.status === "uploaded" && previous.sha256 === file.sha256 && previous.bytes === file.bytes) {
|
|
1886
|
-
continue;
|
|
1887
|
-
}
|
|
1888
|
-
const attempts = (previous?.attempts ?? 0) + 1;
|
|
1889
|
-
state.files[file.artifactPath] = {
|
|
1890
|
-
status: "uploading",
|
|
1891
|
-
bytes: file.bytes,
|
|
1892
|
-
sha256: file.sha256,
|
|
1893
|
-
attempts
|
|
1894
|
-
};
|
|
1895
|
-
state.status = "uploading";
|
|
1896
|
-
state.updated_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
1897
|
-
await writeUploadState(outputDir, state);
|
|
1898
|
-
try {
|
|
1899
|
-
const response = await uploadFile(runId, file, config, env);
|
|
1900
|
-
state.files[file.artifactPath] = {
|
|
1901
|
-
status: "uploaded",
|
|
1902
|
-
bytes: file.bytes,
|
|
1903
|
-
sha256: file.sha256,
|
|
1904
|
-
attempts,
|
|
1905
|
-
uploaded_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1906
|
-
...response.etag === void 0 ? {} : { etag: response.etag }
|
|
1907
|
-
};
|
|
1908
|
-
} catch (error) {
|
|
1909
|
-
state.files[file.artifactPath] = {
|
|
1910
|
-
status: "failed",
|
|
1911
|
-
bytes: file.bytes,
|
|
1912
|
-
sha256: file.sha256,
|
|
1913
|
-
attempts,
|
|
1914
|
-
failed_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1915
|
-
error: errorMessage2(error)
|
|
1916
|
-
};
|
|
1917
|
-
}
|
|
1918
|
-
state.status = uploadStatus(
|
|
1919
|
-
state.files,
|
|
1920
|
-
files.map((candidate) => candidate.artifactPath)
|
|
1921
|
-
);
|
|
1922
|
-
state.updated_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
1923
|
-
await writeUploadState(outputDir, state);
|
|
1914
|
+
if (legacyTargetDir && options.dir) {
|
|
1915
|
+
throw new Error("Specify the local checkout directory only once");
|
|
1924
1916
|
}
|
|
1925
|
-
state
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
)
|
|
1929
|
-
|
|
1930
|
-
await writeUploadState(outputDir, state);
|
|
1931
|
-
if (state.status === "failed") {
|
|
1932
|
-
throw new Error("one or more run files failed to upload");
|
|
1933
|
-
}
|
|
1934
|
-
}
|
|
1935
|
-
async function discoverUploadFiles(outputDir) {
|
|
1936
|
-
const paths = await uploadPaths(outputDir);
|
|
1937
|
-
const files = await Promise.all(
|
|
1938
|
-
paths.map(async (path2) => {
|
|
1939
|
-
const absolute = join4(outputDir, path2);
|
|
1940
|
-
const content = await readFile2(absolute);
|
|
1941
|
-
return {
|
|
1942
|
-
artifactPath: path2,
|
|
1943
|
-
bytes: content.byteLength,
|
|
1944
|
-
sha256: createHash("sha256").update(content).digest("hex"),
|
|
1945
|
-
contentText: content.toString("utf8")
|
|
1946
|
-
};
|
|
1947
|
-
})
|
|
1948
|
-
);
|
|
1949
|
-
return files.sort((a, b) => a.artifactPath.localeCompare(b.artifactPath));
|
|
1950
|
-
}
|
|
1951
|
-
async function uploadPaths(outputDir) {
|
|
1952
|
-
const paths = [];
|
|
1953
|
-
for (const name of ["run.json", "state.json"]) {
|
|
1954
|
-
const absolute = join4(outputDir, name);
|
|
1955
|
-
try {
|
|
1956
|
-
const fileStat = await stat(absolute);
|
|
1957
|
-
if (fileStat.isFile()) {
|
|
1958
|
-
paths.push(name);
|
|
1959
|
-
}
|
|
1960
|
-
} catch {
|
|
1961
|
-
}
|
|
1917
|
+
const state = await loadCheckoutState();
|
|
1918
|
+
if (state) validateCheckoutStateContext(state, context);
|
|
1919
|
+
const targetDir = options.dir ?? legacyTargetDir ?? state?.checkoutRoot ?? ".";
|
|
1920
|
+
if (options.all || legacyTargetDir && options.scope.length === 0) {
|
|
1921
|
+
return { targetDir };
|
|
1962
1922
|
}
|
|
1963
|
-
|
|
1964
|
-
|
|
1923
|
+
if (workspacePath) return { targetDir, scopes: [workspacePath] };
|
|
1924
|
+
if (options.scope.length > 0) return { targetDir, scopes: options.scope };
|
|
1925
|
+
if (!state || state.checkedOutScopes.length === 0) {
|
|
1926
|
+
throw new Error("Checkout is not initialized. Run moxt pull <workspace-path> or moxt pull --all first.");
|
|
1965
1927
|
}
|
|
1966
|
-
return
|
|
1928
|
+
return {
|
|
1929
|
+
targetDir: state.checkoutRoot,
|
|
1930
|
+
scopes: state.mode === "full" ? void 0 : state.checkedOutScopes
|
|
1931
|
+
};
|
|
1967
1932
|
}
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1933
|
+
function isLegacyTargetDirectory(value) {
|
|
1934
|
+
return value === "." || value.startsWith("./") || value.startsWith("../") || isAbsolute3(value);
|
|
1935
|
+
}
|
|
1936
|
+
function validateCheckoutStateContext(state, context) {
|
|
1937
|
+
if (state.workspaceId !== context.workspaceId) {
|
|
1938
|
+
throw new Error(`Checkout state belongs to workspace ${state.workspaceId}, not ${context.workspaceId}`);
|
|
1974
1939
|
}
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1940
|
+
const currentRepos = new Map(listRepoEntries(context.repoPayload).map((repo) => [repo.repoId, repo.name]));
|
|
1941
|
+
for (const [repoId, repo] of Object.entries(state.repos)) {
|
|
1942
|
+
const currentName = currentRepos.get(repoId);
|
|
1943
|
+
if (!currentName) throw new Error(`Checkout state contains unknown repo: ${repoId}`);
|
|
1944
|
+
if (currentName !== repo.name) {
|
|
1945
|
+
throw new Error(`Checkout state repo name does not match current workspace: ${repo.name}`);
|
|
1980
1946
|
}
|
|
1981
|
-
if (!entry.isFile()) {
|
|
1982
|
-
continue;
|
|
1983
|
-
}
|
|
1984
|
-
const fileStat = await stat(absolute);
|
|
1985
|
-
if (fileStat.size === 0) {
|
|
1986
|
-
continue;
|
|
1987
|
-
}
|
|
1988
|
-
paths.push(relative(outputDir, absolute).replaceAll("\\", "/"));
|
|
1989
1947
|
}
|
|
1990
1948
|
}
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
const timeoutMs = config.timeoutMs ?? DEFAULT_UPLOAD_TIMEOUT_MS;
|
|
1997
|
-
const controller = new AbortController();
|
|
1998
|
-
const timeout = setTimeout(() => {
|
|
1999
|
-
controller.abort();
|
|
2000
|
-
}, timeoutMs);
|
|
1949
|
+
function collectScope(value, previous) {
|
|
1950
|
+
return [...previous, value];
|
|
1951
|
+
}
|
|
1952
|
+
function resolveSandboxRuntimeContextOrExit2() {
|
|
1953
|
+
let context;
|
|
2001
1954
|
try {
|
|
2002
|
-
|
|
2003
|
-
`${apiBaseUrl(env)}/workspaces/${encodeURIComponent(config.workspaceId)}/local-agent-runs/${encodeURIComponent(runId)}/files`,
|
|
2004
|
-
{
|
|
2005
|
-
method: "PUT",
|
|
2006
|
-
headers: {
|
|
2007
|
-
"Content-Type": "application/json",
|
|
2008
|
-
Authorization: `Bearer ${apiKey}`,
|
|
2009
|
-
"User-Agent": userAgent(),
|
|
2010
|
-
...cloudflareAccessHeaders(env)
|
|
2011
|
-
},
|
|
2012
|
-
signal: controller.signal,
|
|
2013
|
-
body: JSON.stringify({
|
|
2014
|
-
schema_version: "local_agent_run.upload_file.v1",
|
|
2015
|
-
artifact_path: file.artifactPath,
|
|
2016
|
-
bytes: file.bytes,
|
|
2017
|
-
sha256: file.sha256,
|
|
2018
|
-
encoding: "utf8",
|
|
2019
|
-
content_text: file.contentText
|
|
2020
|
-
})
|
|
2021
|
-
}
|
|
2022
|
-
);
|
|
2023
|
-
if (!response.ok) {
|
|
2024
|
-
throw new Error(`HTTP ${response.status}`);
|
|
2025
|
-
}
|
|
2026
|
-
const headerEtag = response.headers.get("etag") ?? void 0;
|
|
2027
|
-
const body = await responseJson(response);
|
|
2028
|
-
const { etag: bodyEtagValue } = isObject2(body) ? body : {};
|
|
2029
|
-
const bodyEtag = typeof bodyEtagValue === "string" ? bodyEtagValue : void 0;
|
|
2030
|
-
const etag = bodyEtag ?? headerEtag;
|
|
2031
|
-
return etag === void 0 ? {} : { etag };
|
|
1955
|
+
context = resolveRuntimeContext();
|
|
2032
1956
|
} catch (error) {
|
|
2033
|
-
if (
|
|
2034
|
-
|
|
1957
|
+
if (error instanceof RuntimeContextError) {
|
|
1958
|
+
printError(error.message);
|
|
1959
|
+
process.exit(1);
|
|
2035
1960
|
}
|
|
2036
1961
|
throw error;
|
|
2037
|
-
} finally {
|
|
2038
|
-
clearTimeout(timeout);
|
|
2039
1962
|
}
|
|
1963
|
+
if (context.mode !== "sandbox") {
|
|
1964
|
+
printError("moxt pull is only available in sandbox mode");
|
|
1965
|
+
process.exit(1);
|
|
1966
|
+
}
|
|
1967
|
+
return context;
|
|
2040
1968
|
}
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
1969
|
+
function handlePullError(error) {
|
|
1970
|
+
if (error instanceof Error) {
|
|
1971
|
+
printError(error.message);
|
|
1972
|
+
process.exit(1);
|
|
1973
|
+
}
|
|
1974
|
+
throw error;
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
// src/utils/sandbox-push.ts
|
|
1978
|
+
import { createHash as createHash3 } from "crypto";
|
|
1979
|
+
import { lstat as lstat2, readFile as readFile3, readdir } from "fs/promises";
|
|
1980
|
+
import { isAbsolute as isAbsolute4, join as join2, relative as relative2, resolve as resolve2 } from "path";
|
|
1981
|
+
|
|
1982
|
+
// src/utils/concurrency.ts
|
|
1983
|
+
async function runWithConcurrency(items, concurrency, action) {
|
|
1984
|
+
if (!Number.isInteger(concurrency) || concurrency < 1) {
|
|
1985
|
+
throw new RangeError("Concurrency must be a positive integer");
|
|
1986
|
+
}
|
|
1987
|
+
let nextIndex = 0;
|
|
1988
|
+
async function worker() {
|
|
1989
|
+
while (nextIndex < items.length) {
|
|
1990
|
+
const item = items[nextIndex];
|
|
1991
|
+
nextIndex += 1;
|
|
1992
|
+
await action(item);
|
|
2046
1993
|
}
|
|
2047
|
-
}
|
|
1994
|
+
}
|
|
1995
|
+
const workerCount = Math.min(concurrency, items.length);
|
|
1996
|
+
await Promise.all(Array.from({ length: workerCount }, () => worker()));
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
// src/utils/sandbox-push.ts
|
|
2000
|
+
var MFS_FILE_TYPE_REGULAR2 = 1;
|
|
2001
|
+
var MAX_CONCURRENT_UPLOADS = 8;
|
|
2002
|
+
var SandboxPushError = class extends Error {
|
|
2003
|
+
constructor(message, code) {
|
|
2004
|
+
super(message);
|
|
2005
|
+
this.code = code;
|
|
2006
|
+
this.name = "SandboxPushError";
|
|
2007
|
+
}
|
|
2008
|
+
code;
|
|
2009
|
+
};
|
|
2010
|
+
async function pushSandboxWorkspace(context, sourceDir, options) {
|
|
2011
|
+
const resolvedSourceDir = resolve2(sourceDir);
|
|
2012
|
+
await assertWorkspaceRoot(resolvedSourceDir);
|
|
2013
|
+
const repos = listRepoEntries(context.repoPayload);
|
|
2014
|
+
for (const repo of repos) {
|
|
2015
|
+
assertSafeRepoName(resolvedSourceDir, repo.name);
|
|
2016
|
+
}
|
|
2017
|
+
const state = await resolveCheckoutState(context, resolvedSourceDir, repos);
|
|
2018
|
+
const checkoutRepos = repos.filter(
|
|
2019
|
+
(repo) => state.repos[repo.repoId] && doesCheckoutPathIntersect(state, repo.name)
|
|
2020
|
+
);
|
|
2021
|
+
if (checkoutRepos.length === 0) {
|
|
2022
|
+
throw new SandboxPushError("Checkout state does not contain a pushable scope", "invalid-path");
|
|
2023
|
+
}
|
|
2024
|
+
const tree = await fetchWorkspaceTree2(context, checkoutRepos);
|
|
2025
|
+
const remoteEntries = indexRemoteEntries(tree, checkoutRepos);
|
|
2026
|
+
const localFiles = await collectLocalFiles(resolvedSourceDir, checkoutRepos, remoteEntries, state);
|
|
2027
|
+
const { changes, reconciledFiles, removedWorkspacePaths } = planChanges(
|
|
2028
|
+
localFiles,
|
|
2029
|
+
remoteEntries,
|
|
2030
|
+
state,
|
|
2031
|
+
checkoutRepos
|
|
2032
|
+
);
|
|
2033
|
+
let pushResponse;
|
|
2034
|
+
if (changes.length > 0) {
|
|
2035
|
+
await uploadChangedFiles(context, changes);
|
|
2036
|
+
pushResponse = await pushChanges(context, checkoutRepos, changes, options.message);
|
|
2037
|
+
}
|
|
2038
|
+
if (changes.length > 0 || reconciledFiles.length > 0 || removedWorkspacePaths.length > 0) {
|
|
2039
|
+
await updateCheckoutState(state, changes, reconciledFiles, removedWorkspacePaths, pushResponse);
|
|
2048
2040
|
}
|
|
2049
2041
|
return {
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
status: "pending",
|
|
2053
|
-
updated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2054
|
-
files: {}
|
|
2042
|
+
changes: changes.map((change) => ({ action: change.action, workspacePath: change.workspacePath })),
|
|
2043
|
+
sourceDir: resolvedSourceDir
|
|
2055
2044
|
};
|
|
2056
2045
|
}
|
|
2057
|
-
async function
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
function uploadStatus(files, expectedPaths) {
|
|
2062
|
-
if (expectedPaths.length === 0) {
|
|
2063
|
-
return "pending";
|
|
2046
|
+
async function resolveCheckoutState(context, sourceDir, repos) {
|
|
2047
|
+
const state = await loadCheckoutState();
|
|
2048
|
+
if (!state) {
|
|
2049
|
+
throw new SandboxPushError("Checkout is not initialized. Run moxt pull first.", "invalid-path");
|
|
2064
2050
|
}
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
return "uploaded";
|
|
2051
|
+
if (state.workspaceId !== context.workspaceId) {
|
|
2052
|
+
throw new SandboxPushError(
|
|
2053
|
+
`Checkout state belongs to workspace ${state.workspaceId}, not ${context.workspaceId}`,
|
|
2054
|
+
"invalid-path"
|
|
2055
|
+
);
|
|
2071
2056
|
}
|
|
2072
|
-
if (
|
|
2073
|
-
|
|
2057
|
+
if (state.checkoutRoot !== sourceDir) {
|
|
2058
|
+
throw new SandboxPushError(
|
|
2059
|
+
`Checkout root is ${state.checkoutRoot}, not ${sourceDir}`,
|
|
2060
|
+
"invalid-path"
|
|
2061
|
+
);
|
|
2074
2062
|
}
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2063
|
+
const currentRepos = new Map(repos.map((repo) => [repo.repoId, repo.name]));
|
|
2064
|
+
for (const [repoId, repo] of Object.entries(state.repos)) {
|
|
2065
|
+
if (currentRepos.get(repoId) !== repo.name) {
|
|
2066
|
+
throw new SandboxPushError(`Checkout state contains unknown repo: ${repoId}`, "invalid-path");
|
|
2067
|
+
}
|
|
2080
2068
|
}
|
|
2081
|
-
const
|
|
2082
|
-
|
|
2083
|
-
}
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
return await response.json();
|
|
2087
|
-
} catch {
|
|
2088
|
-
return null;
|
|
2069
|
+
for (const scope of state.checkedOutScopes) {
|
|
2070
|
+
const hasRepo = Object.values(state.repos).some(
|
|
2071
|
+
(repo) => scope === repo.name || scope.startsWith(`${repo.name}/`)
|
|
2072
|
+
);
|
|
2073
|
+
if (!hasRepo) throw new SandboxPushError(`Checkout state contains invalid scope: ${scope}`, "invalid-path");
|
|
2089
2074
|
}
|
|
2075
|
+
return state;
|
|
2090
2076
|
}
|
|
2091
|
-
function
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
if (
|
|
2099
|
-
|
|
2077
|
+
async function assertWorkspaceRoot(sourceDir) {
|
|
2078
|
+
const stats = await lstat2(sourceDir).catch((error) => {
|
|
2079
|
+
if (isNodeError3(error) && error.code === "ENOENT") {
|
|
2080
|
+
throw new SandboxPushError(`Local workspace directory does not exist: ${sourceDir}`, "invalid-path");
|
|
2081
|
+
}
|
|
2082
|
+
throw error;
|
|
2083
|
+
});
|
|
2084
|
+
if (!stats.isDirectory()) {
|
|
2085
|
+
throw new SandboxPushError(`Local workspace path is not a directory: ${sourceDir}`, "invalid-path");
|
|
2100
2086
|
}
|
|
2101
|
-
return String(error);
|
|
2102
|
-
}
|
|
2103
|
-
function isObject2(value) {
|
|
2104
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2105
2087
|
}
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
}
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2088
|
+
async function fetchWorkspaceTree2(context, repos) {
|
|
2089
|
+
const client = new SandboxMoxtClient(context);
|
|
2090
|
+
const response = await client.request(
|
|
2091
|
+
"POST",
|
|
2092
|
+
"/agent/api/pipeline-sandbox/mfs-workspace/batch-trees",
|
|
2093
|
+
{
|
|
2094
|
+
repos: repos.map((repo) => ({ repoId: repo.repoId, prefix: repo.name }))
|
|
2095
|
+
}
|
|
2096
|
+
);
|
|
2097
|
+
return response.data;
|
|
2098
|
+
}
|
|
2099
|
+
function indexRemoteEntries(tree, repos) {
|
|
2100
|
+
const knownRepoIds = new Set(repos.map((repo) => repo.repoId));
|
|
2101
|
+
const entries = /* @__PURE__ */ new Map();
|
|
2102
|
+
for (const entry of tree.entries) {
|
|
2103
|
+
if (!knownRepoIds.has(entry.repoId)) {
|
|
2104
|
+
throw new SandboxPushError(`Remote tree returned unknown repo: ${entry.repoId}`, "remote");
|
|
2105
|
+
}
|
|
2106
|
+
const key = remoteEntryKey(entry.repoId, entry.path);
|
|
2107
|
+
if (entries.has(key)) {
|
|
2108
|
+
throw new SandboxPushError(`Remote tree returned duplicate path: ${entry.prefixedPath}`, "remote");
|
|
2109
|
+
}
|
|
2110
|
+
entries.set(key, entry);
|
|
2111
|
+
}
|
|
2112
|
+
return entries;
|
|
2113
|
+
}
|
|
2114
|
+
async function collectLocalFiles(sourceDir, repos, remoteEntries, state) {
|
|
2115
|
+
const files = [];
|
|
2116
|
+
for (const repo of repos) {
|
|
2117
|
+
const repoRoot = resolveSafeRepoRoot(sourceDir, repo.name);
|
|
2118
|
+
const stats = await lstat2(repoRoot).catch((error) => {
|
|
2119
|
+
if (isNodeError3(error) && error.code === "ENOENT") return void 0;
|
|
2120
|
+
throw error;
|
|
2127
2121
|
});
|
|
2128
|
-
|
|
2129
|
-
|
|
2122
|
+
if (!stats) continue;
|
|
2123
|
+
if (!stats.isDirectory()) {
|
|
2124
|
+
throw new SandboxPushError(`Local repo path is not a directory: ${repo.name}`, "invalid-path");
|
|
2125
|
+
}
|
|
2126
|
+
await collectRepoFiles(repoRoot, repo, "", remoteEntries, state, files);
|
|
2130
2127
|
}
|
|
2128
|
+
return files;
|
|
2131
2129
|
}
|
|
2132
|
-
function
|
|
2133
|
-
const
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2130
|
+
async function collectRepoFiles(repoRoot, repo, relativeDir, remoteEntries, state, files) {
|
|
2131
|
+
const localDir = relativeDir ? join2(repoRoot, ...relativeDir.split("/")) : repoRoot;
|
|
2132
|
+
const entries = await readdir(localDir, { withFileTypes: true });
|
|
2133
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
2134
|
+
for (const entry of entries) {
|
|
2135
|
+
if (entry.name === ".git") continue;
|
|
2136
|
+
const repoPath = relativeDir ? `${relativeDir}/${entry.name}` : entry.name;
|
|
2137
|
+
const workspacePath = `${repo.name}/${repoPath}`;
|
|
2138
|
+
assertSafePathPart(entry.name, workspacePath);
|
|
2139
|
+
if (!doesCheckoutPathIntersect(state, workspacePath)) continue;
|
|
2140
|
+
if (entry.isSymbolicLink()) {
|
|
2141
|
+
throw new SandboxPushError(`Refusing to push symbolic link: ${workspacePath}`, "invalid-file");
|
|
2140
2142
|
}
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
const queue = [rootPid];
|
|
2148
|
-
while (queue.length > 0) {
|
|
2149
|
-
const pid = queue.shift();
|
|
2150
|
-
if (seen.has(pid)) {
|
|
2143
|
+
if (entry.isDirectory()) {
|
|
2144
|
+
const remote = remoteEntries.get(remoteEntryKey(repo.repoId, repoPath));
|
|
2145
|
+
if (remote?.type === "blob") {
|
|
2146
|
+
throw new SandboxPushError(`Local directory conflicts with remote file: ${workspacePath}`, "invalid-file");
|
|
2147
|
+
}
|
|
2148
|
+
await collectRepoFiles(repoRoot, repo, repoPath, remoteEntries, state, files);
|
|
2151
2149
|
continue;
|
|
2152
2150
|
}
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2151
|
+
if (!entry.isFile()) {
|
|
2152
|
+
throw new SandboxPushError(`Refusing to push unsupported file: ${workspacePath}`, "invalid-file");
|
|
2153
|
+
}
|
|
2154
|
+
const localPath = join2(repoRoot, ...repoPath.split("/"));
|
|
2155
|
+
const stats = await lstat2(localPath);
|
|
2156
|
+
if (!stats.isFile()) {
|
|
2157
|
+
const kind = stats.isSymbolicLink() ? "symbolic link" : "unsupported file";
|
|
2158
|
+
throw new SandboxPushError(`Refusing to push ${kind}: ${workspacePath}`, "invalid-file");
|
|
2159
|
+
}
|
|
2160
|
+
if (stats.size > MFS_FILE_SIZE_LIMIT_BYTES) {
|
|
2161
|
+
throw new SandboxPushError(
|
|
2162
|
+
`File too large (max ${MFS_FILE_SIZE_LIMIT_LABEL}): ${workspacePath}`,
|
|
2163
|
+
"too-large"
|
|
2164
|
+
);
|
|
2165
|
+
}
|
|
2166
|
+
const content = await readFile3(localPath);
|
|
2167
|
+
if (content.length > MFS_FILE_SIZE_LIMIT_BYTES) {
|
|
2168
|
+
throw new SandboxPushError(
|
|
2169
|
+
`File too large (max ${MFS_FILE_SIZE_LIMIT_LABEL}): ${workspacePath}`,
|
|
2170
|
+
"too-large"
|
|
2171
|
+
);
|
|
2172
|
+
}
|
|
2173
|
+
if (!isCheckoutPathIncluded(state, workspacePath)) continue;
|
|
2174
|
+
files.push({
|
|
2175
|
+
repo,
|
|
2176
|
+
repoPath,
|
|
2177
|
+
workspacePath,
|
|
2178
|
+
localPath,
|
|
2179
|
+
contentHash: createHash3("sha256").update(content).digest("hex"),
|
|
2180
|
+
size: content.length
|
|
2181
|
+
});
|
|
2156
2182
|
}
|
|
2157
|
-
return pids;
|
|
2158
2183
|
}
|
|
2159
|
-
function
|
|
2160
|
-
const
|
|
2161
|
-
|
|
2162
|
-
|
|
2184
|
+
function planChanges(localFiles, remoteEntries, state, repos) {
|
|
2185
|
+
const changes = [];
|
|
2186
|
+
const reconciledFiles = [];
|
|
2187
|
+
const removedWorkspacePaths = [];
|
|
2188
|
+
const localWorkspacePaths = new Set(localFiles.map((file) => file.workspacePath));
|
|
2189
|
+
const reposById = new Map(repos.map((repo) => [repo.repoId, repo]));
|
|
2190
|
+
for (const file of localFiles) {
|
|
2191
|
+
const baseline = state.files[file.workspacePath];
|
|
2192
|
+
if (baseline?.sha === file.contentHash) continue;
|
|
2193
|
+
const existing = remoteEntries.get(remoteEntryKey(file.repo.repoId, file.repoPath));
|
|
2194
|
+
if (existing?.type === "tree") {
|
|
2195
|
+
throw new SandboxPushError(`Local file conflicts with remote directory: ${file.workspacePath}`, "invalid-file");
|
|
2196
|
+
}
|
|
2197
|
+
if (existing?.sha === file.contentHash) {
|
|
2198
|
+
reconciledFiles.push({ ...file, remote: existing });
|
|
2163
2199
|
continue;
|
|
2164
2200
|
}
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
continue;
|
|
2201
|
+
if (existing && !existing.sha) {
|
|
2202
|
+
throw new SandboxPushError(`${file.workspacePath} does not have a base content hash`, "remote");
|
|
2168
2203
|
}
|
|
2169
|
-
if (
|
|
2170
|
-
|
|
2204
|
+
if (baseline && existing?.sha !== baseline.sha) {
|
|
2205
|
+
throw new SandboxPushError(`Remote file changed since checkout: ${file.workspacePath}`, "conflict");
|
|
2171
2206
|
}
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2207
|
+
if (!baseline && existing) {
|
|
2208
|
+
throw new SandboxPushError(`Remote file changed since checkout: ${file.workspacePath}`, "conflict");
|
|
2209
|
+
}
|
|
2210
|
+
changes.push({
|
|
2211
|
+
...file,
|
|
2212
|
+
action: existing ? "update" : "create",
|
|
2213
|
+
...existing ? { existing } : {}
|
|
2214
|
+
});
|
|
2179
2215
|
}
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2216
|
+
for (const [workspacePath, baseline] of Object.entries(state.files)) {
|
|
2217
|
+
if (localWorkspacePaths.has(workspacePath) || !isCheckoutPathIncluded(state, workspacePath)) continue;
|
|
2218
|
+
const repo = reposById.get(baseline.repoId);
|
|
2219
|
+
if (!repo) continue;
|
|
2220
|
+
const existing = remoteEntries.get(remoteEntryKey(baseline.repoId, baseline.repoPath));
|
|
2221
|
+
if (!existing) {
|
|
2222
|
+
removedWorkspacePaths.push(workspacePath);
|
|
2223
|
+
continue;
|
|
2224
|
+
}
|
|
2225
|
+
if (existing.type !== "blob" || existing.sha !== baseline.sha) {
|
|
2226
|
+
throw new SandboxPushError(`Remote file changed since checkout: ${workspacePath}`, "conflict");
|
|
2227
|
+
}
|
|
2228
|
+
changes.push({
|
|
2229
|
+
action: "delete",
|
|
2230
|
+
repo,
|
|
2231
|
+
repoPath: baseline.repoPath,
|
|
2232
|
+
workspacePath,
|
|
2233
|
+
fileId: validFileId(existing.fileId) ?? baseline.fileId
|
|
2234
|
+
});
|
|
2185
2235
|
}
|
|
2186
|
-
return
|
|
2187
|
-
}
|
|
2188
|
-
function
|
|
2189
|
-
const
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2236
|
+
return { changes, reconciledFiles, removedWorkspacePaths };
|
|
2237
|
+
}
|
|
2238
|
+
async function uploadChangedFiles(context, changes) {
|
|
2239
|
+
const writeChanges = changes.filter(isWriteChange);
|
|
2240
|
+
if (writeChanges.length === 0) return;
|
|
2241
|
+
const client = new SandboxMoxtClient(context);
|
|
2242
|
+
const changesByRepo = groupChangesByRepo(writeChanges);
|
|
2243
|
+
const uploadUrlsByRepo = await Promise.all(
|
|
2244
|
+
[...changesByRepo].map(async ([repoId, repoChanges]) => {
|
|
2245
|
+
const hashes = [...new Set(repoChanges.map((change) => change.contentHash))];
|
|
2246
|
+
const response = await client.request(
|
|
2247
|
+
"POST",
|
|
2248
|
+
`/agent/api/pipeline-sandbox/mfs/${encodeURIComponent(repoId)}/presign-upload`,
|
|
2249
|
+
{ hashes }
|
|
2250
|
+
);
|
|
2251
|
+
return [repoId, response.data.urls];
|
|
2252
|
+
})
|
|
2253
|
+
);
|
|
2254
|
+
const uploadUrls = new Map(uploadUrlsByRepo);
|
|
2255
|
+
await runWithConcurrency(writeChanges, MAX_CONCURRENT_UPLOADS, async (change) => {
|
|
2256
|
+
const uploadUrl = uploadUrls.get(change.repo.repoId)?.[change.contentHash];
|
|
2257
|
+
if (!uploadUrl) {
|
|
2258
|
+
throw new SandboxPushError(`No upload URL returned for ${change.workspacePath}`, "remote");
|
|
2259
|
+
}
|
|
2260
|
+
const content = await readFile3(change.localPath);
|
|
2261
|
+
const currentHash = createHash3("sha256").update(content).digest("hex");
|
|
2262
|
+
if (content.length !== change.size || currentHash !== change.contentHash) {
|
|
2263
|
+
throw new SandboxPushError(`Local file changed while pushing: ${change.workspacePath}`, "invalid-file");
|
|
2264
|
+
}
|
|
2265
|
+
const upload = await fetch(uploadUrl, {
|
|
2266
|
+
method: "PUT",
|
|
2267
|
+
headers: { "Content-Type": "application/octet-stream" },
|
|
2268
|
+
body: new Uint8Array(content)
|
|
2269
|
+
});
|
|
2270
|
+
if (!upload.ok) {
|
|
2271
|
+
throw new SandboxPushError(`Upload failed [${upload.status}]: ${change.workspacePath}`, "remote");
|
|
2272
|
+
}
|
|
2273
|
+
});
|
|
2194
2274
|
}
|
|
2195
|
-
function
|
|
2196
|
-
|
|
2197
|
-
|
|
2275
|
+
async function pushChanges(context, repos, changes, message) {
|
|
2276
|
+
const client = new SandboxMoxtClient(context);
|
|
2277
|
+
const changesByRepo = groupChangesByRepo(changes);
|
|
2278
|
+
const response = await client.request(
|
|
2279
|
+
"POST",
|
|
2280
|
+
"/agent/api/pipeline-sandbox/mfs-workspace/unified-push",
|
|
2281
|
+
{
|
|
2282
|
+
message,
|
|
2283
|
+
repoPushes: repos.flatMap((repo) => {
|
|
2284
|
+
const repoChanges = changesByRepo.get(repo.repoId);
|
|
2285
|
+
if (!repoChanges) return [];
|
|
2286
|
+
return [{
|
|
2287
|
+
repoId: repo.repoId,
|
|
2288
|
+
changes: repoChanges.map((change) => {
|
|
2289
|
+
if (change.action === "delete") {
|
|
2290
|
+
return {
|
|
2291
|
+
action: change.action,
|
|
2292
|
+
...change.fileId ? { fileId: change.fileId } : {},
|
|
2293
|
+
path: change.repoPath
|
|
2294
|
+
};
|
|
2295
|
+
}
|
|
2296
|
+
return {
|
|
2297
|
+
action: change.action,
|
|
2298
|
+
...change.existing?.fileId ? { fileId: change.existing.fileId } : {},
|
|
2299
|
+
path: change.repoPath,
|
|
2300
|
+
contentHash: change.contentHash,
|
|
2301
|
+
...change.existing?.sha ? { baseContentHash: change.existing.sha } : {},
|
|
2302
|
+
size: change.size,
|
|
2303
|
+
fileType: MFS_FILE_TYPE_REGULAR2
|
|
2304
|
+
};
|
|
2305
|
+
})
|
|
2306
|
+
}];
|
|
2307
|
+
}),
|
|
2308
|
+
crossRepoMoves: []
|
|
2309
|
+
}
|
|
2310
|
+
);
|
|
2311
|
+
return response.data;
|
|
2312
|
+
}
|
|
2313
|
+
async function updateCheckoutState(state, changes, reconciledFiles, removedWorkspacePaths, response) {
|
|
2314
|
+
const files = { ...state.files };
|
|
2315
|
+
for (const workspacePath of removedWorkspacePaths) delete files[workspacePath];
|
|
2316
|
+
for (const file of reconciledFiles) {
|
|
2317
|
+
files[file.workspacePath] = checkoutStateFile(
|
|
2318
|
+
file,
|
|
2319
|
+
validFileId(file.remote.fileId),
|
|
2320
|
+
validFileType(file.remote.fileType)
|
|
2321
|
+
);
|
|
2198
2322
|
}
|
|
2199
|
-
|
|
2200
|
-
|
|
2323
|
+
for (const change of changes) {
|
|
2324
|
+
if (change.action === "delete") {
|
|
2325
|
+
delete files[change.workspacePath];
|
|
2326
|
+
continue;
|
|
2327
|
+
}
|
|
2328
|
+
const returnedFileId = validFileId(response?.fileMap?.[change.repo.repoId]?.[change.repoPath]);
|
|
2329
|
+
const fileId = returnedFileId ?? change.existing?.fileId ?? state.files[change.workspacePath]?.fileId ?? null;
|
|
2330
|
+
files[change.workspacePath] = checkoutStateFile(change, validFileId(fileId), validFileType(change.existing?.fileType));
|
|
2201
2331
|
}
|
|
2202
|
-
|
|
2332
|
+
await saveCheckoutState({ ...state, files });
|
|
2203
2333
|
}
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
function
|
|
2208
|
-
const root = git(["rev-parse", "--show-toplevel"], cwd);
|
|
2209
|
-
if (root === null) {
|
|
2210
|
-
return null;
|
|
2211
|
-
}
|
|
2334
|
+
function isWriteChange(change) {
|
|
2335
|
+
return change.action !== "delete";
|
|
2336
|
+
}
|
|
2337
|
+
function checkoutStateFile(file, fileId, fileType) {
|
|
2212
2338
|
return {
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2339
|
+
repoId: file.repo.repoId,
|
|
2340
|
+
repoPath: file.repoPath,
|
|
2341
|
+
fileId,
|
|
2342
|
+
sha: file.contentHash,
|
|
2343
|
+
size: file.size,
|
|
2344
|
+
fileType: fileType ?? MFS_FILE_TYPE_REGULAR2
|
|
2217
2345
|
};
|
|
2218
2346
|
}
|
|
2219
|
-
function
|
|
2220
|
-
|
|
2221
|
-
return execFileSync2("git", args, {
|
|
2222
|
-
cwd,
|
|
2223
|
-
encoding: "utf8",
|
|
2224
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
2225
|
-
}).trim();
|
|
2226
|
-
} catch {
|
|
2227
|
-
return null;
|
|
2228
|
-
}
|
|
2347
|
+
function validFileId(fileId) {
|
|
2348
|
+
return typeof fileId === "string" && fileId.length > 0 ? fileId : null;
|
|
2229
2349
|
}
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
import { closeSync, openSync, readSync, realpathSync } from "fs";
|
|
2233
|
-
import { join as join5, resolve } from "path";
|
|
2234
|
-
var CODEX_METADATA_READ_BYTES = 256 * 1024;
|
|
2235
|
-
function claudeProjectDir(cwd, home) {
|
|
2236
|
-
return join5(home, ".claude", "projects", encodeClaudeProjectPath(cwd));
|
|
2237
|
-
}
|
|
2238
|
-
function encodeClaudeProjectPath(cwd) {
|
|
2239
|
-
return cwd.replaceAll(/[^A-Za-z0-9]/g, "-");
|
|
2240
|
-
}
|
|
2241
|
-
function agentSessionDir(options) {
|
|
2242
|
-
switch (options.agent) {
|
|
2243
|
-
case "claude":
|
|
2244
|
-
return claudeProjectDir(options.cwd, options.home);
|
|
2245
|
-
case "codex":
|
|
2246
|
-
return codexSessionsDir(options.home, options.env);
|
|
2247
|
-
}
|
|
2248
|
-
}
|
|
2249
|
-
function agentSessionCandidateFilter(options) {
|
|
2250
|
-
switch (options.agent) {
|
|
2251
|
-
case "claude":
|
|
2252
|
-
return void 0;
|
|
2253
|
-
case "codex": {
|
|
2254
|
-
const expectedCwd = expectedCodexCwd(options.cwd, options.agentArgs);
|
|
2255
|
-
return (path2) => {
|
|
2256
|
-
try {
|
|
2257
|
-
const sessionCwd = codexSessionCwd(path2);
|
|
2258
|
-
return sessionCwd !== null && sameCodexCwd(sessionCwd, expectedCwd);
|
|
2259
|
-
} catch {
|
|
2260
|
-
return false;
|
|
2261
|
-
}
|
|
2262
|
-
};
|
|
2263
|
-
}
|
|
2264
|
-
}
|
|
2350
|
+
function validFileType(fileType) {
|
|
2351
|
+
return typeof fileType === "number" && Number.isInteger(fileType) && fileType >= 0 ? fileType : null;
|
|
2265
2352
|
}
|
|
2266
|
-
function
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
case "codex":
|
|
2273
|
-
return void 0;
|
|
2353
|
+
function groupChangesByRepo(changes) {
|
|
2354
|
+
const result = /* @__PURE__ */ new Map();
|
|
2355
|
+
for (const change of changes) {
|
|
2356
|
+
const repoChanges = result.get(change.repo.repoId) ?? [];
|
|
2357
|
+
repoChanges.push(change);
|
|
2358
|
+
result.set(change.repo.repoId, repoChanges);
|
|
2274
2359
|
}
|
|
2360
|
+
return result;
|
|
2275
2361
|
}
|
|
2276
|
-
function
|
|
2277
|
-
return
|
|
2362
|
+
function remoteEntryKey(repoId, repoPath) {
|
|
2363
|
+
return `${repoId}\0${repoPath}`;
|
|
2278
2364
|
}
|
|
2279
|
-
function
|
|
2280
|
-
|
|
2281
|
-
|
|
2365
|
+
function assertSafePathPart(part, workspacePath) {
|
|
2366
|
+
if (part === "." || part === ".." || part.includes("/") || part.includes("\\") || part.includes("\0")) {
|
|
2367
|
+
throw new SandboxPushError(`Local path is unsafe: ${workspacePath}`, "invalid-path");
|
|
2368
|
+
}
|
|
2282
2369
|
}
|
|
2283
|
-
function
|
|
2284
|
-
|
|
2285
|
-
return cd === void 0 ? resolve(cwd) : resolve(cwd, cd);
|
|
2370
|
+
function assertSafeRepoName(sourceDir, repoName) {
|
|
2371
|
+
resolveSafeRepoRoot(sourceDir, repoName);
|
|
2286
2372
|
}
|
|
2287
|
-
function
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
}
|
|
2294
|
-
if (arg.startsWith("--session-id=")) {
|
|
2295
|
-
return arg.slice("--session-id=".length);
|
|
2296
|
-
}
|
|
2373
|
+
function resolveSafeRepoRoot(sourceDir, repoName) {
|
|
2374
|
+
assertSafePathPart(repoName, repoName);
|
|
2375
|
+
const repoRoot = resolve2(sourceDir, repoName);
|
|
2376
|
+
const relativePath = relative2(sourceDir, repoRoot);
|
|
2377
|
+
if (relativePath.startsWith("..") || isAbsolute4(relativePath)) {
|
|
2378
|
+
throw new SandboxPushError(`Repo path escapes local workspace: ${repoName}`, "invalid-path");
|
|
2297
2379
|
}
|
|
2298
|
-
return
|
|
2380
|
+
return repoRoot;
|
|
2299
2381
|
}
|
|
2300
|
-
function
|
|
2301
|
-
|
|
2302
|
-
for (let index = 0; index < optionArgs.length; index += 1) {
|
|
2303
|
-
const arg = optionArgs[index];
|
|
2304
|
-
if (arg === "--resume" || arg === "-r") {
|
|
2305
|
-
const value = optionArgs[index + 1];
|
|
2306
|
-
return isUuid(value) ? value : void 0;
|
|
2307
|
-
}
|
|
2308
|
-
if (arg.startsWith("--resume=")) {
|
|
2309
|
-
const value = arg.slice("--resume=".length);
|
|
2310
|
-
return isUuid(value) ? value : void 0;
|
|
2311
|
-
}
|
|
2312
|
-
}
|
|
2313
|
-
return void 0;
|
|
2382
|
+
function isNodeError3(error) {
|
|
2383
|
+
return error instanceof Error && "code" in error;
|
|
2314
2384
|
}
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2385
|
+
|
|
2386
|
+
// src/commands/push.ts
|
|
2387
|
+
function registerPushCommand(program2) {
|
|
2388
|
+
program2.command("push").description("Push local workspace files to the sandbox workspace").argument("[dir]", "Local workspace directory", ".").option("-m, --message <message>", "Push message", "moxt push").action(async (sourceDir, options) => {
|
|
2389
|
+
const context = resolveSandboxRuntimeContextOrExit3();
|
|
2390
|
+
try {
|
|
2391
|
+
const result = await pushSandboxWorkspace(context, sourceDir, { message: options.message });
|
|
2392
|
+
for (const change of result.changes) {
|
|
2393
|
+
print(`${change.action} ${change.workspacePath}`);
|
|
2394
|
+
}
|
|
2395
|
+
if (result.changes.length === 0) {
|
|
2396
|
+
print("No changes to push");
|
|
2397
|
+
return;
|
|
2398
|
+
}
|
|
2399
|
+
const fileLabel = result.changes.length === 1 ? "file" : "files";
|
|
2400
|
+
print(`Pushed ${result.changes.length} ${fileLabel} from ${result.sourceDir}`);
|
|
2401
|
+
} catch (error) {
|
|
2402
|
+
handlePushError(error);
|
|
2321
2403
|
}
|
|
2322
|
-
}
|
|
2323
|
-
return null;
|
|
2404
|
+
});
|
|
2324
2405
|
}
|
|
2325
|
-
function
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
return arg.slice("--cd=".length);
|
|
2406
|
+
function resolveSandboxRuntimeContextOrExit3() {
|
|
2407
|
+
let context;
|
|
2408
|
+
try {
|
|
2409
|
+
context = resolveRuntimeContext();
|
|
2410
|
+
} catch (error) {
|
|
2411
|
+
if (error instanceof RuntimeContextError) {
|
|
2412
|
+
printError(error.message);
|
|
2413
|
+
process.exit(1);
|
|
2334
2414
|
}
|
|
2415
|
+
throw error;
|
|
2335
2416
|
}
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
const separatorIndex = agentArgs.indexOf("--");
|
|
2340
|
-
return separatorIndex === -1 ? agentArgs : agentArgs.slice(0, separatorIndex);
|
|
2341
|
-
}
|
|
2342
|
-
function sameCodexCwd(left, right) {
|
|
2343
|
-
return normalizeCodexCwd(left) === normalizeCodexCwd(right);
|
|
2344
|
-
}
|
|
2345
|
-
function normalizeCodexCwd(path2) {
|
|
2346
|
-
try {
|
|
2347
|
-
return realpathSync(path2);
|
|
2348
|
-
} catch {
|
|
2349
|
-
return resolve(path2);
|
|
2417
|
+
if (context.mode !== "sandbox") {
|
|
2418
|
+
printError("moxt push is only available in sandbox mode");
|
|
2419
|
+
process.exit(1);
|
|
2350
2420
|
}
|
|
2421
|
+
return context;
|
|
2351
2422
|
}
|
|
2352
|
-
function
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
}
|
|
2359
|
-
function isUuid(value) {
|
|
2360
|
-
return value !== void 0 && /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{12}$/i.test(value);
|
|
2423
|
+
function handlePushError(error) {
|
|
2424
|
+
if (error instanceof Error) {
|
|
2425
|
+
printError(error.message);
|
|
2426
|
+
process.exit(1);
|
|
2427
|
+
}
|
|
2428
|
+
throw error;
|
|
2361
2429
|
}
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2430
|
+
|
|
2431
|
+
// src/utils/checkout-status.ts
|
|
2432
|
+
import { createHash as createHash4 } from "crypto";
|
|
2433
|
+
import { createReadStream } from "fs";
|
|
2434
|
+
import { lstat as lstat3, readdir as readdir2 } from "fs/promises";
|
|
2435
|
+
import { join as join3 } from "path";
|
|
2436
|
+
var CheckoutStatusError = class extends Error {
|
|
2437
|
+
constructor(message) {
|
|
2438
|
+
super(message);
|
|
2439
|
+
this.name = "CheckoutStatusError";
|
|
2365
2440
|
}
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2441
|
+
};
|
|
2442
|
+
async function inspectCheckoutStatus(state) {
|
|
2443
|
+
await requireCheckoutDirectory(state.checkoutRoot);
|
|
2444
|
+
const localFiles = /* @__PURE__ */ new Map();
|
|
2445
|
+
for (const repo of Object.values(state.repos)) {
|
|
2446
|
+
await scanDirectory(state, repo.name, join3(state.checkoutRoot, repo.name), localFiles);
|
|
2371
2447
|
}
|
|
2372
|
-
const
|
|
2373
|
-
|
|
2374
|
-
|
|
2448
|
+
const changes = [];
|
|
2449
|
+
for (const [path2, sha] of localFiles) {
|
|
2450
|
+
const baseline = state.files[path2];
|
|
2451
|
+
if (!baseline) changes.push({ action: "added", path: path2 });
|
|
2452
|
+
else if (baseline.sha !== sha) changes.push({ action: "modified", path: path2 });
|
|
2375
2453
|
}
|
|
2376
|
-
const
|
|
2377
|
-
|
|
2378
|
-
return null;
|
|
2454
|
+
for (const path2 of Object.keys(state.files)) {
|
|
2455
|
+
if (isCheckoutPathIncluded(state, path2) && !localFiles.has(path2)) changes.push({ action: "deleted", path: path2 });
|
|
2379
2456
|
}
|
|
2380
|
-
|
|
2381
|
-
const cwd = payload?.cwd;
|
|
2382
|
-
return typeof cwd === "string" ? cwd : null;
|
|
2457
|
+
return changes.sort((left, right) => left.path.localeCompare(right.path));
|
|
2383
2458
|
}
|
|
2384
|
-
function
|
|
2385
|
-
|
|
2459
|
+
async function requireCheckoutDirectory(checkoutRoot) {
|
|
2460
|
+
let stats;
|
|
2386
2461
|
try {
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
} finally {
|
|
2391
|
-
closeSync(fd);
|
|
2462
|
+
stats = await lstat3(checkoutRoot);
|
|
2463
|
+
} catch (error) {
|
|
2464
|
+
throw new CheckoutStatusError(`Checkout root is not available: ${errorMessage2(error)}`);
|
|
2392
2465
|
}
|
|
2466
|
+
if (stats.isSymbolicLink()) throw new CheckoutStatusError(`Refusing to inspect symbolic link: ${checkoutRoot}`);
|
|
2467
|
+
if (!stats.isDirectory()) throw new CheckoutStatusError(`Checkout root is not a directory: ${checkoutRoot}`);
|
|
2393
2468
|
}
|
|
2394
|
-
function
|
|
2395
|
-
|
|
2396
|
-
}
|
|
2397
|
-
function rawCodexSessionLine(value) {
|
|
2398
|
-
return objectValue2(value);
|
|
2399
|
-
}
|
|
2400
|
-
function rawCodexSessionPayload(value) {
|
|
2401
|
-
return objectValue2(value);
|
|
2402
|
-
}
|
|
2403
|
-
function hasValue2(value) {
|
|
2404
|
-
return value !== void 0 && value !== "";
|
|
2405
|
-
}
|
|
2406
|
-
|
|
2407
|
-
// src/run/run-agent.ts
|
|
2408
|
-
var MAX_TRANSCRIPT_CAPTURE_BYTES = 1024 * 1024;
|
|
2409
|
-
var SESSION_SETTLE_TIMEOUT_MS = 1500;
|
|
2410
|
-
var SESSION_SETTLE_INTERVAL_MS = 100;
|
|
2411
|
-
var PROCESS_SESSION_POLL_INTERVAL_MS = 100;
|
|
2412
|
-
var CODEX_OPTIONS_WITH_VALUE = /* @__PURE__ */ new Set([
|
|
2413
|
-
"-a",
|
|
2414
|
-
"--add-dir",
|
|
2415
|
-
"--ask-for-approval",
|
|
2416
|
-
"-c",
|
|
2417
|
-
"-C",
|
|
2418
|
-
"--cd",
|
|
2419
|
-
"--color",
|
|
2420
|
-
"--config",
|
|
2421
|
-
"-i",
|
|
2422
|
-
"--image",
|
|
2423
|
-
"--local-provider",
|
|
2424
|
-
"-m",
|
|
2425
|
-
"--model",
|
|
2426
|
-
"-o",
|
|
2427
|
-
"--output-last-message",
|
|
2428
|
-
"--output-schema",
|
|
2429
|
-
"-p",
|
|
2430
|
-
"--profile",
|
|
2431
|
-
"--remote",
|
|
2432
|
-
"--remote-auth-token-env",
|
|
2433
|
-
"-s",
|
|
2434
|
-
"--sandbox"
|
|
2435
|
-
]);
|
|
2436
|
-
async function runLocalAgent(options) {
|
|
2437
|
-
const cwd = options.cwd ?? process.cwd();
|
|
2438
|
-
const env = options.env ?? process.env;
|
|
2439
|
-
const home = options.home ?? env.HOME ?? homedir2();
|
|
2440
|
-
const startedAtMs = Date.now();
|
|
2441
|
-
const startedAt = new Date(startedAtMs).toISOString();
|
|
2442
|
-
const runId = createCaptureRunId(startedAt, options.agent);
|
|
2443
|
-
const captureEnv = withDefaultCaptureOutput(env, home);
|
|
2469
|
+
async function scanDirectory(state, workspacePath, localPath, files) {
|
|
2470
|
+
let entries;
|
|
2444
2471
|
try {
|
|
2445
|
-
await
|
|
2472
|
+
entries = await readdir2(localPath, { withFileTypes: true });
|
|
2446
2473
|
} catch (error) {
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
}
|
|
2450
|
-
const repo = collectRepoFingerprint(cwd);
|
|
2451
|
-
let baselineOk = false;
|
|
2452
|
-
let before = /* @__PURE__ */ new Map();
|
|
2453
|
-
let sessions = "";
|
|
2454
|
-
let isSessionCandidate;
|
|
2455
|
-
let knownSessionFile;
|
|
2456
|
-
let childRootPid;
|
|
2457
|
-
let openSessionFileMatch = { match: "none" };
|
|
2458
|
-
let processSessionTimer;
|
|
2459
|
-
const newSessionFilesOnly = shouldUseNewSessionFilesOnly(options.agent, options.agentArgs);
|
|
2460
|
-
try {
|
|
2461
|
-
const sessionOptions = {
|
|
2462
|
-
agent: options.agent,
|
|
2463
|
-
cwd,
|
|
2464
|
-
home,
|
|
2465
|
-
env: captureEnv,
|
|
2466
|
-
agentArgs: options.agentArgs
|
|
2467
|
-
};
|
|
2468
|
-
sessions = agentSessionDir(sessionOptions);
|
|
2469
|
-
isSessionCandidate = agentSessionCandidateFilter(sessionOptions);
|
|
2470
|
-
knownSessionFile = agentKnownSessionFile(sessionOptions);
|
|
2471
|
-
before = snapshotJsonl(sessions);
|
|
2472
|
-
baselineOk = true;
|
|
2473
|
-
} catch {
|
|
2474
|
-
baselineOk = false;
|
|
2474
|
+
if (isNodeError4(error) && error.code === "ENOENT") return;
|
|
2475
|
+
throw new CheckoutStatusError(`Failed to inspect ${workspacePath}: ${errorMessage2(error)}`);
|
|
2475
2476
|
}
|
|
2476
|
-
const
|
|
2477
|
-
if (
|
|
2478
|
-
|
|
2477
|
+
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
2478
|
+
if (entry.name === ".git") continue;
|
|
2479
|
+
const childWorkspacePath = `${workspacePath}/${entry.name}`;
|
|
2480
|
+
if (!doesCheckoutPathIntersect(state, childWorkspacePath)) continue;
|
|
2481
|
+
const childLocalPath = join3(localPath, entry.name);
|
|
2482
|
+
const stats = await lstat3(childLocalPath);
|
|
2483
|
+
if (stats.isSymbolicLink()) {
|
|
2484
|
+
throw new CheckoutStatusError(`Refusing to inspect symbolic link: ${childWorkspacePath}`);
|
|
2479
2485
|
}
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
});
|
|
2485
|
-
if (matched.match !== "none") {
|
|
2486
|
-
openSessionFileMatch = matched;
|
|
2486
|
+
if (stats.isDirectory()) {
|
|
2487
|
+
await scanDirectory(state, childWorkspacePath, childLocalPath, files);
|
|
2488
|
+
} else if (stats.isFile() && isCheckoutPathIncluded(state, childWorkspacePath)) {
|
|
2489
|
+
files.set(childWorkspacePath, await hashFile(childLocalPath));
|
|
2487
2490
|
}
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
async function hashFile(path2) {
|
|
2494
|
+
const hash = createHash4("sha256");
|
|
2495
|
+
const stream = createReadStream(path2);
|
|
2496
|
+
for await (const chunk of stream) hash.update(chunk);
|
|
2497
|
+
return hash.digest("hex");
|
|
2498
|
+
}
|
|
2499
|
+
function isNodeError4(error) {
|
|
2500
|
+
return error instanceof Error && "code" in error;
|
|
2501
|
+
}
|
|
2502
|
+
function errorMessage2(error) {
|
|
2503
|
+
return error instanceof Error ? error.message : String(error);
|
|
2504
|
+
}
|
|
2505
|
+
|
|
2506
|
+
// src/commands/status.ts
|
|
2507
|
+
function registerStatusCommand(program2) {
|
|
2508
|
+
program2.command("status").description("Show Moxt workspace checkout status").option("--json", "Print machine-readable status").action(async (options) => {
|
|
2509
|
+
const context = resolveRuntimeContextOrExit2();
|
|
2510
|
+
if (context.mode !== "sandbox") {
|
|
2511
|
+
printError("moxt status is only available in sandbox mode");
|
|
2512
|
+
process.exit(1);
|
|
2494
2513
|
}
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
runId,
|
|
2499
|
-
agent: options.agent,
|
|
2500
|
-
startedAt,
|
|
2501
|
-
sessions,
|
|
2502
|
-
before,
|
|
2503
|
-
baselineOk,
|
|
2504
|
-
startedAtMs,
|
|
2505
|
-
...knownSessionFile === void 0 ? {} : { knownSessionFile },
|
|
2506
|
-
...isSessionCandidate === void 0 ? {} : { isSessionCandidate },
|
|
2507
|
-
...newSessionFilesOnly ? { newSessionFilesOnly: true } : {},
|
|
2508
|
-
openSessionFile: rememberOpenSessionFile
|
|
2509
|
-
});
|
|
2510
|
-
try {
|
|
2511
|
-
await liveCapture.start();
|
|
2512
|
-
} catch (error) {
|
|
2513
|
-
printError(
|
|
2514
|
-
[
|
|
2515
|
-
"moxt: cannot prepare run output",
|
|
2516
|
-
`reason: ${errorMessage3(error)}`,
|
|
2517
|
-
"",
|
|
2518
|
-
`${options.agent} was not started because this run cannot be recorded.`
|
|
2519
|
-
].join("\n")
|
|
2520
|
-
);
|
|
2521
|
-
return 1;
|
|
2522
|
-
}
|
|
2523
|
-
const observedSignals = /* @__PURE__ */ new Set();
|
|
2524
|
-
let child = null;
|
|
2525
|
-
let pendingForward = null;
|
|
2526
|
-
const observe = (signal) => {
|
|
2527
|
-
const number = signalNumber(signal);
|
|
2528
|
-
if (number !== 0) observedSignals.add(number);
|
|
2529
|
-
};
|
|
2530
|
-
const terminalSignalsReachChild = process.stdin.isTTY === true || process.stdout.isTTY === true || process.stderr.isTTY === true;
|
|
2531
|
-
const forwardInteractiveSignal = (signal) => {
|
|
2532
|
-
observe(signal);
|
|
2533
|
-
if (terminalSignalsReachChild) {
|
|
2514
|
+
const status = await buildSandboxStatusOrExit(context);
|
|
2515
|
+
if (options.json) {
|
|
2516
|
+
print(JSON.stringify(status, null, 2));
|
|
2534
2517
|
return;
|
|
2535
2518
|
}
|
|
2536
|
-
|
|
2537
|
-
|
|
2519
|
+
print(`${colors.bold}Workspace${colors.reset}: ${status.workspaceId}`);
|
|
2520
|
+
print(`${colors.bold}Mode${colors.reset}: sandbox`);
|
|
2521
|
+
print(`${colors.bold}Checkout${colors.reset}: ${formatCheckoutMode(status.checkout.mode)}`);
|
|
2522
|
+
if (status.checkout.mode !== "not_initialized") {
|
|
2523
|
+
print(`${colors.bold}Root${colors.reset}: ${status.checkout.root}`);
|
|
2524
|
+
}
|
|
2525
|
+
print("");
|
|
2526
|
+
print(`${colors.bold}Repos${colors.reset}:`);
|
|
2527
|
+
const repoNameWidth = status.repos.reduce((width, repo) => Math.max(width, repo.name.length), 0);
|
|
2528
|
+
for (const repo of status.repos) {
|
|
2529
|
+
print(` ${repo.name.padEnd(repoNameWidth)} ${repo.repoId}`);
|
|
2530
|
+
}
|
|
2531
|
+
print("");
|
|
2532
|
+
if (status.checkout.mode === "not_initialized") {
|
|
2533
|
+
print(`${colors.bold}Checked out scopes${colors.reset}: none`);
|
|
2534
|
+
print(`${colors.bold}Local changes${colors.reset}: not tracked`);
|
|
2538
2535
|
return;
|
|
2539
2536
|
}
|
|
2540
|
-
|
|
2541
|
-
};
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
observe("SIGTERM");
|
|
2546
|
-
if (child === null) pendingForward = "SIGTERM";
|
|
2547
|
-
else safeKill(child, "SIGTERM");
|
|
2548
|
-
};
|
|
2549
|
-
const onHup = () => {
|
|
2550
|
-
observe("SIGHUP");
|
|
2551
|
-
if (child === null) pendingForward = "SIGHUP";
|
|
2552
|
-
else safeKill(child, "SIGHUP");
|
|
2553
|
-
};
|
|
2554
|
-
process.on("SIGINT", onInt);
|
|
2555
|
-
process.on("SIGQUIT", onQuit);
|
|
2556
|
-
process.on("SIGTERM", onTerm);
|
|
2557
|
-
process.on("SIGHUP", onHup);
|
|
2558
|
-
const spawned = spawn(options.agent, [...options.agentArgs], {
|
|
2559
|
-
cwd,
|
|
2560
|
-
env: captureEnv,
|
|
2561
|
-
stdio: "inherit"
|
|
2562
|
-
});
|
|
2563
|
-
child = spawned;
|
|
2564
|
-
childRootPid = spawned.pid;
|
|
2565
|
-
if (childRootPid !== void 0 && baselineOk) {
|
|
2566
|
-
rememberOpenSessionFile();
|
|
2567
|
-
processSessionTimer = setInterval(() => {
|
|
2568
|
-
rememberOpenSessionFile();
|
|
2569
|
-
}, PROCESS_SESSION_POLL_INTERVAL_MS);
|
|
2570
|
-
}
|
|
2571
|
-
if (pendingForward !== null) {
|
|
2572
|
-
safeKill(spawned, pendingForward);
|
|
2573
|
-
}
|
|
2574
|
-
const exit = await waitForExit(spawned, options.agent);
|
|
2575
|
-
stopProcessSessionWatcher();
|
|
2576
|
-
const onCaptureSignal = () => {
|
|
2577
|
-
};
|
|
2578
|
-
process.on("SIGINT", onCaptureSignal);
|
|
2579
|
-
process.on("SIGQUIT", onCaptureSignal);
|
|
2580
|
-
process.on("SIGTERM", onCaptureSignal);
|
|
2581
|
-
process.on("SIGHUP", onCaptureSignal);
|
|
2582
|
-
process.off("SIGINT", onInt);
|
|
2583
|
-
process.off("SIGQUIT", onQuit);
|
|
2584
|
-
process.off("SIGTERM", onTerm);
|
|
2585
|
-
process.off("SIGHUP", onHup);
|
|
2586
|
-
const classified = classifyExit(exit, observedSignals);
|
|
2587
|
-
const endedAtMs = exit.waitAt;
|
|
2588
|
-
const endedAt = new Date(endedAtMs).toISOString();
|
|
2589
|
-
const durationMs = endedAtMs - startedAtMs;
|
|
2590
|
-
let after;
|
|
2591
|
-
if (baselineOk) {
|
|
2592
|
-
try {
|
|
2593
|
-
after = await waitForSessionSettle(
|
|
2594
|
-
sessions,
|
|
2595
|
-
before,
|
|
2596
|
-
startedAtMs,
|
|
2597
|
-
isSessionCandidate,
|
|
2598
|
-
newSessionFilesOnly
|
|
2599
|
-
);
|
|
2600
|
-
} catch {
|
|
2537
|
+
print(`${colors.bold}Checked out scopes${colors.reset}:`);
|
|
2538
|
+
for (const scope of status.checkout.checkedOutScopes) print(` ${scope}`);
|
|
2539
|
+
print(`${colors.bold}Local changes${colors.reset}:${status.checkout.changes.length === 0 ? " none" : ""}`);
|
|
2540
|
+
for (const change of status.checkout.changes) {
|
|
2541
|
+
print(` ${change.action.padEnd(8)} ${change.path}`);
|
|
2601
2542
|
}
|
|
2602
|
-
}
|
|
2603
|
-
try {
|
|
2604
|
-
await liveCapture.stop();
|
|
2605
|
-
} catch {
|
|
2606
|
-
}
|
|
2607
|
-
const payload = buildPayload({
|
|
2608
|
-
agent: options.agent,
|
|
2609
|
-
cwd,
|
|
2610
|
-
startedAt,
|
|
2611
|
-
endedAt,
|
|
2612
|
-
durationMs,
|
|
2613
|
-
exitCode: classified.code,
|
|
2614
|
-
status: classified.status,
|
|
2615
|
-
repo: sanitizeRepo(repo),
|
|
2616
|
-
baselineOk,
|
|
2617
|
-
sessions,
|
|
2618
|
-
before,
|
|
2619
|
-
after,
|
|
2620
|
-
startedAtMs,
|
|
2621
|
-
knownSessionFile,
|
|
2622
|
-
isSessionCandidate,
|
|
2623
|
-
newSessionFilesOnly,
|
|
2624
|
-
openSessionFileMatch
|
|
2625
2543
|
});
|
|
2626
|
-
let finalized = false;
|
|
2627
|
-
try {
|
|
2628
|
-
if (liveCapture.enabled) {
|
|
2629
|
-
await liveCapture.finalize(payload, (/* @__PURE__ */ new Date()).toISOString());
|
|
2630
|
-
} else {
|
|
2631
|
-
await writeCaptureDirectoryPayload(payload, captureEnv, runId, (/* @__PURE__ */ new Date()).toISOString());
|
|
2632
|
-
}
|
|
2633
|
-
finalized = true;
|
|
2634
|
-
} catch (error) {
|
|
2635
|
-
printError(`moxt: cannot finalize run artifact: ${errorMessage3(error)}`);
|
|
2636
|
-
} finally {
|
|
2637
|
-
process.off("SIGINT", onCaptureSignal);
|
|
2638
|
-
process.off("SIGQUIT", onCaptureSignal);
|
|
2639
|
-
process.off("SIGTERM", onCaptureSignal);
|
|
2640
|
-
process.off("SIGHUP", onCaptureSignal);
|
|
2641
|
-
}
|
|
2642
|
-
if (finalized && options.upload !== void 0) {
|
|
2643
|
-
try {
|
|
2644
|
-
await uploadRunArtifacts({
|
|
2645
|
-
env: captureEnv,
|
|
2646
|
-
runId,
|
|
2647
|
-
upload: options.upload
|
|
2648
|
-
});
|
|
2649
|
-
} catch (error) {
|
|
2650
|
-
printError(`moxt: cannot upload run artifact: ${errorMessage3(error)}`);
|
|
2651
|
-
}
|
|
2652
|
-
}
|
|
2653
|
-
return classified.code;
|
|
2654
2544
|
}
|
|
2655
|
-
async function
|
|
2656
|
-
const
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
after = snapshotJsonl(sessions);
|
|
2661
|
-
}
|
|
2662
|
-
return after;
|
|
2663
|
-
}
|
|
2664
|
-
function hasSessionChange(before, after, startedAtMs, isSessionCandidate, newSessionFilesOnly) {
|
|
2665
|
-
const matched = matchSession(before, after, startedAtMs, matchOptions(isSessionCandidate, newSessionFilesOnly));
|
|
2666
|
-
if (matched.match !== "unique") {
|
|
2667
|
-
return matched.match !== "none";
|
|
2668
|
-
}
|
|
2669
|
-
if (matched.sessionFile === null) {
|
|
2670
|
-
return false;
|
|
2671
|
-
}
|
|
2672
|
-
const snapshot = after.get(matched.sessionFile);
|
|
2673
|
-
return snapshot !== void 0 && snapshot.size > matched.startOffset;
|
|
2674
|
-
}
|
|
2675
|
-
function sleep(ms) {
|
|
2676
|
-
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
2677
|
-
}
|
|
2678
|
-
function safeKill(child, signal) {
|
|
2545
|
+
async function buildSandboxStatusOrExit(context) {
|
|
2546
|
+
const repos = listRepoEntries(context.repoPayload).map((repo) => ({
|
|
2547
|
+
name: repo.name,
|
|
2548
|
+
repoId: repo.repoId
|
|
2549
|
+
}));
|
|
2679
2550
|
try {
|
|
2680
|
-
|
|
2681
|
-
|
|
2551
|
+
const state = await loadCheckoutState();
|
|
2552
|
+
if (!state) return buildUninitializedStatus(context.workspaceId, repos);
|
|
2553
|
+
validateStateContext(state, context);
|
|
2554
|
+
return {
|
|
2555
|
+
mode: "sandbox",
|
|
2556
|
+
workspaceId: context.workspaceId,
|
|
2557
|
+
checkout: {
|
|
2558
|
+
mode: state.mode,
|
|
2559
|
+
root: state.checkoutRoot,
|
|
2560
|
+
checkedOutScopes: state.checkedOutScopes,
|
|
2561
|
+
localChangesTracked: true,
|
|
2562
|
+
changes: await inspectCheckoutStatus(state)
|
|
2563
|
+
},
|
|
2564
|
+
repos
|
|
2565
|
+
};
|
|
2566
|
+
} catch (error) {
|
|
2567
|
+
printError(error instanceof Error ? error.message : String(error));
|
|
2568
|
+
process.exit(1);
|
|
2682
2569
|
}
|
|
2683
2570
|
}
|
|
2684
|
-
function
|
|
2685
|
-
return new Promise((resolve2) => {
|
|
2686
|
-
child.once("error", (error) => {
|
|
2687
|
-
const errorCode = error.code ?? "UNKNOWN";
|
|
2688
|
-
const reason = errorCode === "ENOENT" ? "command not found" : error.message;
|
|
2689
|
-
printError(`moxt: cannot start ${agent}: ${reason}`);
|
|
2690
|
-
resolve2({
|
|
2691
|
-
code: null,
|
|
2692
|
-
signal: null,
|
|
2693
|
-
waitAt: Date.now(),
|
|
2694
|
-
errorCode
|
|
2695
|
-
});
|
|
2696
|
-
});
|
|
2697
|
-
child.once("exit", (code, signal) => {
|
|
2698
|
-
resolve2({ code, signal, waitAt: Date.now() });
|
|
2699
|
-
});
|
|
2700
|
-
});
|
|
2701
|
-
}
|
|
2702
|
-
function buildPayload(input) {
|
|
2703
|
-
let sessionMatch = input.baselineOk ? "none" : "prep_failed";
|
|
2704
|
-
let transcript;
|
|
2705
|
-
let redactionFailed = false;
|
|
2706
|
-
if (input.baselineOk) {
|
|
2707
|
-
try {
|
|
2708
|
-
const after = input.after ?? snapshotJsonl(input.sessions);
|
|
2709
|
-
const knownMatched = input.knownSessionFile === void 0 ? null : matchKnownSessionFile(input.before, after, input.knownSessionFile);
|
|
2710
|
-
const matched = knownMatched?.match === "unique" ? knownMatched : input.openSessionFileMatch.match === "unique" ? matchKnownSessionFile(input.before, after, input.openSessionFileMatch.sessionFile) : input.openSessionFileMatch.match === "ambiguous" ? {
|
|
2711
|
-
match: "ambiguous",
|
|
2712
|
-
sessionFile: null,
|
|
2713
|
-
startOffset: 0
|
|
2714
|
-
} : matchSession(
|
|
2715
|
-
input.before,
|
|
2716
|
-
after,
|
|
2717
|
-
input.startedAtMs,
|
|
2718
|
-
matchOptions(input.isSessionCandidate, input.newSessionFilesOnly)
|
|
2719
|
-
);
|
|
2720
|
-
sessionMatch = matched.match;
|
|
2721
|
-
if (matched.match === "unique" && matched.sessionFile !== null) {
|
|
2722
|
-
try {
|
|
2723
|
-
transcript = redactText(readFileFromOffset2(matched.sessionFile, matched.startOffset)).text;
|
|
2724
|
-
} catch {
|
|
2725
|
-
redactionFailed = true;
|
|
2726
|
-
}
|
|
2727
|
-
}
|
|
2728
|
-
} catch {
|
|
2729
|
-
sessionMatch = "prep_failed";
|
|
2730
|
-
}
|
|
2731
|
-
}
|
|
2571
|
+
function buildUninitializedStatus(workspaceId, repos) {
|
|
2732
2572
|
return {
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
started_at: input.startedAt,
|
|
2740
|
-
ended_at: input.endedAt,
|
|
2741
|
-
duration_ms: input.durationMs,
|
|
2742
|
-
status: input.status,
|
|
2743
|
-
exit_code: input.exitCode
|
|
2573
|
+
mode: "sandbox",
|
|
2574
|
+
workspaceId,
|
|
2575
|
+
checkout: {
|
|
2576
|
+
mode: "not_initialized",
|
|
2577
|
+
checkedOutScopes: [],
|
|
2578
|
+
localChangesTracked: false
|
|
2744
2579
|
},
|
|
2745
|
-
|
|
2746
|
-
repo: input.repo
|
|
2580
|
+
repos
|
|
2747
2581
|
};
|
|
2748
2582
|
}
|
|
2749
|
-
function
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
...newSessionFilesOnly ? { newFilesOnly: true } : {}
|
|
2753
|
-
};
|
|
2754
|
-
}
|
|
2755
|
-
function shouldUseNewSessionFilesOnly(agent, agentArgs) {
|
|
2756
|
-
return agent === "codex" && !isCodexResumeInvocation(agentArgs);
|
|
2757
|
-
}
|
|
2758
|
-
function isCodexResumeInvocation(agentArgs) {
|
|
2759
|
-
const command = nextCodexCommandArg(agentArgs, 0);
|
|
2760
|
-
if (command === null) {
|
|
2761
|
-
return false;
|
|
2762
|
-
}
|
|
2763
|
-
if (command.value === "resume") {
|
|
2764
|
-
return true;
|
|
2583
|
+
function validateStateContext(state, context) {
|
|
2584
|
+
if (state.workspaceId !== context.workspaceId) {
|
|
2585
|
+
throw new Error(`Checkout state belongs to workspace ${state.workspaceId}, not ${context.workspaceId}`);
|
|
2765
2586
|
}
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
for (let index = startIndex; index < agentArgs.length; index += 1) {
|
|
2773
|
-
const arg = agentArgs[index];
|
|
2774
|
-
if (arg === void 0) {
|
|
2775
|
-
continue;
|
|
2587
|
+
const currentRepos = new Map(listRepoEntries(context.repoPayload).map((repo) => [repo.repoId, repo.name]));
|
|
2588
|
+
for (const [repoId, repo] of Object.entries(state.repos)) {
|
|
2589
|
+
const currentName = currentRepos.get(repoId);
|
|
2590
|
+
if (!currentName) throw new Error(`Checkout state contains unknown repo: ${repoId}`);
|
|
2591
|
+
if (currentName !== repo.name) {
|
|
2592
|
+
throw new Error(`Checkout state repo name does not match current workspace: ${repo.name}`);
|
|
2776
2593
|
}
|
|
2777
|
-
if (arg === "--") {
|
|
2778
|
-
return null;
|
|
2779
|
-
}
|
|
2780
|
-
if (arg.startsWith("-")) {
|
|
2781
|
-
if (!arg.includes("=") && CODEX_OPTIONS_WITH_VALUE.has(arg)) {
|
|
2782
|
-
index += 1;
|
|
2783
|
-
}
|
|
2784
|
-
continue;
|
|
2785
|
-
}
|
|
2786
|
-
return { value: arg, index };
|
|
2787
2594
|
}
|
|
2788
|
-
return null;
|
|
2789
2595
|
}
|
|
2790
|
-
function
|
|
2791
|
-
const fd = openSync2(path2, "r");
|
|
2596
|
+
function resolveRuntimeContextOrExit2() {
|
|
2792
2597
|
try {
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
if (
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
return "";
|
|
2801
|
-
}
|
|
2802
|
-
readSync2(fd, buffer2, 0, length, offset);
|
|
2803
|
-
return buffer2.toString("utf8");
|
|
2804
|
-
} finally {
|
|
2805
|
-
closeSync2(fd);
|
|
2806
|
-
}
|
|
2807
|
-
}
|
|
2808
|
-
function printPrepareError(agent, error) {
|
|
2809
|
-
if (error instanceof CaptureOutputPrepareError) {
|
|
2810
|
-
printError(
|
|
2811
|
-
[
|
|
2812
|
-
"moxt: cannot create run output directory",
|
|
2813
|
-
`path: ${error.path}`,
|
|
2814
|
-
`reason: ${error.message}`,
|
|
2815
|
-
"",
|
|
2816
|
-
`${agent} was not started because this run cannot be recorded.`
|
|
2817
|
-
].join("\n")
|
|
2818
|
-
);
|
|
2819
|
-
return;
|
|
2820
|
-
}
|
|
2821
|
-
printError(
|
|
2822
|
-
[
|
|
2823
|
-
"moxt: cannot prepare run output",
|
|
2824
|
-
`reason: ${errorMessage3(error)}`,
|
|
2825
|
-
"",
|
|
2826
|
-
`${agent} was not started because this run cannot be recorded.`
|
|
2827
|
-
].join("\n")
|
|
2828
|
-
);
|
|
2829
|
-
}
|
|
2830
|
-
function errorMessage3(error) {
|
|
2831
|
-
if (error instanceof Error && error.message !== "") {
|
|
2832
|
-
return error.message;
|
|
2833
|
-
}
|
|
2834
|
-
return String(error);
|
|
2835
|
-
}
|
|
2836
|
-
|
|
2837
|
-
// src/commands/run.ts
|
|
2838
|
-
function registerRunCommand(program2) {
|
|
2839
|
-
program2.command("run").description("Run a local agent and capture its work as Moxt context").option("-w, --workspace <workspaceId>", "Workspace ID for upload mode").argument("<agent>", "Local agent to run: claude or codex").argument("[agentArgs...]", "Arguments passed to the local agent after --").allowUnknownOption(true).allowExcessArguments(true).action(async (agentArg, agentArgs, options) => {
|
|
2840
|
-
const agent = parseAgent(agentArg);
|
|
2841
|
-
const upload = resolveUploadOptions(options);
|
|
2842
|
-
const exitCode = await runLocalAgentOrExit(agent, agentArgs ?? [], upload);
|
|
2843
|
-
process.exit(exitCode);
|
|
2844
|
-
});
|
|
2845
|
-
}
|
|
2846
|
-
function parseAgent(agent) {
|
|
2847
|
-
if (agent === "claude" || agent === "codex") {
|
|
2848
|
-
return agent;
|
|
2849
|
-
}
|
|
2850
|
-
printError(`Error: moxt run supports only "claude" or "codex", got "${agent}".`);
|
|
2851
|
-
process.exit(1);
|
|
2852
|
-
}
|
|
2853
|
-
function resolveUploadOptions(options) {
|
|
2854
|
-
const workspaceId = resolveWorkspaceId(options);
|
|
2855
|
-
if (workspaceId === void 0) {
|
|
2856
|
-
return void 0;
|
|
2857
|
-
}
|
|
2858
|
-
if (!process.env.MOXT_API_KEY) {
|
|
2859
|
-
printError("Error: MOXT_API_KEY is required when using moxt run upload mode with -w/--workspace or MOXT_WORKSPACE_ID.");
|
|
2860
|
-
process.exit(1);
|
|
2598
|
+
return resolveRuntimeContext();
|
|
2599
|
+
} catch (error) {
|
|
2600
|
+
if (error instanceof RuntimeContextError) {
|
|
2601
|
+
printError(error.message);
|
|
2602
|
+
process.exit(1);
|
|
2603
|
+
}
|
|
2604
|
+
throw error;
|
|
2861
2605
|
}
|
|
2862
|
-
return {
|
|
2863
|
-
workspaceId
|
|
2864
|
-
};
|
|
2865
2606
|
}
|
|
2866
|
-
function
|
|
2867
|
-
return
|
|
2868
|
-
}
|
|
2869
|
-
function normalizeWorkspaceId(value) {
|
|
2870
|
-
const trimmed = value?.trim();
|
|
2871
|
-
return trimmed === void 0 || trimmed === "" ? void 0 : trimmed;
|
|
2872
|
-
}
|
|
2873
|
-
async function runLocalAgentOrExit(agent, agentArgs, upload) {
|
|
2874
|
-
return await runLocalAgent({
|
|
2875
|
-
agent,
|
|
2876
|
-
agentArgs,
|
|
2877
|
-
...upload === void 0 ? {} : { upload }
|
|
2878
|
-
});
|
|
2607
|
+
function formatCheckoutMode(mode) {
|
|
2608
|
+
return mode === "not_initialized" ? "not initialized" : `${mode} checkout`;
|
|
2879
2609
|
}
|
|
2880
2610
|
|
|
2881
2611
|
// src/telemetry/config.ts
|
|
2882
|
-
import * as
|
|
2612
|
+
import * as fs3 from "fs";
|
|
2883
2613
|
import * as os from "os";
|
|
2884
2614
|
import * as path from "path";
|
|
2885
2615
|
function getConfigPath() {
|
|
@@ -2887,7 +2617,7 @@ function getConfigPath() {
|
|
|
2887
2617
|
}
|
|
2888
2618
|
function readTelemetryConfig() {
|
|
2889
2619
|
try {
|
|
2890
|
-
const raw =
|
|
2620
|
+
const raw = fs3.readFileSync(getConfigPath(), "utf8");
|
|
2891
2621
|
const parsed = JSON.parse(raw);
|
|
2892
2622
|
return parsed.telemetry ?? {};
|
|
2893
2623
|
} catch {
|
|
@@ -2898,14 +2628,14 @@ function writeTelemetryConfig(update) {
|
|
|
2898
2628
|
const configPath = getConfigPath();
|
|
2899
2629
|
let existing = {};
|
|
2900
2630
|
try {
|
|
2901
|
-
existing = JSON.parse(
|
|
2631
|
+
existing = JSON.parse(fs3.readFileSync(configPath, "utf8"));
|
|
2902
2632
|
} catch {
|
|
2903
2633
|
existing = {};
|
|
2904
2634
|
}
|
|
2905
2635
|
const currentTelemetry = existing.telemetry ?? {};
|
|
2906
2636
|
const next = { ...existing, telemetry: { ...currentTelemetry, ...update } };
|
|
2907
|
-
|
|
2908
|
-
|
|
2637
|
+
fs3.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
2638
|
+
fs3.writeFileSync(configPath, JSON.stringify(next, null, 2));
|
|
2909
2639
|
}
|
|
2910
2640
|
|
|
2911
2641
|
// src/telemetry/opt-out.ts
|
|
@@ -3077,31 +2807,34 @@ function registerWorkspaceCommand(program2) {
|
|
|
3077
2807
|
|
|
3078
2808
|
// src/program.ts
|
|
3079
2809
|
function createProgram(version) {
|
|
3080
|
-
const program2 = new
|
|
2810
|
+
const program2 = new Command2();
|
|
3081
2811
|
program2.name("moxt").description("Moxt CLI - AI Workspace").version(version).action(() => {
|
|
3082
2812
|
program2.help();
|
|
3083
2813
|
});
|
|
2814
|
+
registerAuthCommand(program2);
|
|
3084
2815
|
registerWhoamiCommand(program2);
|
|
3085
2816
|
registerWorkspaceCommand(program2);
|
|
3086
2817
|
registerFileCommand(program2);
|
|
3087
2818
|
registerMemoryCommand(program2);
|
|
3088
|
-
registerRunCommand(program2);
|
|
3089
2819
|
registerMiniappCommand(program2);
|
|
2820
|
+
registerPullCommand(program2);
|
|
2821
|
+
registerPushCommand(program2);
|
|
2822
|
+
registerStatusCommand(program2);
|
|
3090
2823
|
registerTelemetryCommand(program2);
|
|
3091
2824
|
return program2;
|
|
3092
2825
|
}
|
|
3093
2826
|
|
|
3094
2827
|
// src/telemetry/client.ts
|
|
3095
|
-
import { arch as
|
|
2828
|
+
import { arch as arch2, platform as platform2 } from "os";
|
|
3096
2829
|
var buffer = [];
|
|
3097
2830
|
var FLUSH_TIMEOUT_MS = 3e3;
|
|
3098
2831
|
var MAX_BATCH_SIZE = 100;
|
|
3099
2832
|
function commonLabels() {
|
|
3100
2833
|
return {
|
|
3101
|
-
cli_version: "0.
|
|
2834
|
+
cli_version: "0.4.0-beta.0",
|
|
3102
2835
|
node_version: process.versions.node,
|
|
3103
|
-
os:
|
|
3104
|
-
arch:
|
|
2836
|
+
os: platform2(),
|
|
2837
|
+
arch: arch2(),
|
|
3105
2838
|
is_ci: isCiEnvironment() ? "true" : "false"
|
|
3106
2839
|
};
|
|
3107
2840
|
}
|
|
@@ -3144,7 +2877,7 @@ async function flush() {
|
|
|
3144
2877
|
}
|
|
3145
2878
|
const batch = buffer.splice(0, MAX_BATCH_SIZE);
|
|
3146
2879
|
const payload = {
|
|
3147
|
-
release_id: "0.
|
|
2880
|
+
release_id: "0.4.0-beta.0",
|
|
3148
2881
|
client_type: "cli",
|
|
3149
2882
|
metric_data_list: batch.map((m) => ({
|
|
3150
2883
|
profile: "cli",
|
|
@@ -3285,9 +3018,9 @@ function installExitHandler() {
|
|
|
3285
3018
|
|
|
3286
3019
|
// src/index.ts
|
|
3287
3020
|
updateNotifier({
|
|
3288
|
-
pkg: { name: "@moxt-ai/cli", version: "0.
|
|
3021
|
+
pkg: { name: "@moxt-ai/cli", version: "0.4.0-beta.0" }
|
|
3289
3022
|
}).notify();
|
|
3290
|
-
var program = createProgram("0.
|
|
3023
|
+
var program = createProgram("0.4.0-beta.0");
|
|
3291
3024
|
instrumentProgram(program);
|
|
3292
3025
|
installExitHandler();
|
|
3293
3026
|
program.parseAsync(process.argv).then(async () => {
|