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