codeam-cli 2.15.2 → 2.15.6
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/CHANGELOG.md +11 -0
- package/README.md +7 -5
- package/dist/index.js +1055 -114
- package/package.json +14 -13
package/dist/index.js
CHANGED
|
@@ -386,8 +386,8 @@ var import_qrcode_terminal = __toESM(require("qrcode-terminal"));
|
|
|
386
386
|
// package.json
|
|
387
387
|
var package_default = {
|
|
388
388
|
name: "codeam-cli",
|
|
389
|
-
version: "2.15.
|
|
390
|
-
description: "
|
|
389
|
+
version: "2.15.6",
|
|
390
|
+
description: "Workflow-continuity bridge for AI coding agents. Wrap Claude Code or Codex in a PTY and supervise, approve, and redirect the session from any device \u2014 async. The terminal companion for CodeAgent Mobile.",
|
|
391
391
|
type: "commonjs",
|
|
392
392
|
main: "dist/index.js",
|
|
393
393
|
bin: {
|
|
@@ -410,29 +410,29 @@ var package_default = {
|
|
|
410
410
|
keywords: [
|
|
411
411
|
"claude",
|
|
412
412
|
"claude-code",
|
|
413
|
-
"claude-code-mobile",
|
|
414
413
|
"ai",
|
|
415
414
|
"ai-agent",
|
|
416
415
|
"ai-coding",
|
|
417
|
-
"ai-
|
|
416
|
+
"ai-workflow",
|
|
417
|
+
"ai-workflow-continuity",
|
|
418
|
+
"async-ai-productivity",
|
|
419
|
+
"remote-ai-supervision",
|
|
418
420
|
"cli",
|
|
419
|
-
"
|
|
420
|
-
"
|
|
421
|
-
"mobile-ide",
|
|
422
|
-
"mobile-coding",
|
|
423
|
-
"pair-programming",
|
|
421
|
+
"developer-tools",
|
|
422
|
+
"devtools",
|
|
424
423
|
"codeagent",
|
|
425
424
|
"codeam",
|
|
426
425
|
"anthropic",
|
|
426
|
+
"openai-codex",
|
|
427
427
|
"cursor",
|
|
428
428
|
"copilot",
|
|
429
429
|
"jetbrains",
|
|
430
430
|
"vscode",
|
|
431
|
+
"windsurf",
|
|
431
432
|
"mcp",
|
|
432
|
-
"
|
|
433
|
-
"
|
|
434
|
-
"
|
|
435
|
-
"agent-control"
|
|
433
|
+
"remote-development",
|
|
434
|
+
"agent-control",
|
|
435
|
+
"agent-operations"
|
|
436
436
|
],
|
|
437
437
|
homepage: "https://codeagent-mobile.com",
|
|
438
438
|
bugs: {
|
|
@@ -456,6 +456,7 @@ var package_default = {
|
|
|
456
456
|
},
|
|
457
457
|
dependencies: {
|
|
458
458
|
"@clack/prompts": "^1.2.0",
|
|
459
|
+
chokidar: "^3.6.0",
|
|
459
460
|
picocolors: "^1.1.0",
|
|
460
461
|
"qrcode-terminal": "^0.12.0",
|
|
461
462
|
ws: "^8.18.0",
|
|
@@ -520,6 +521,34 @@ function vercelBypassHeader() {
|
|
|
520
521
|
return token ? { "x-vercel-protection-bypass": token } : {};
|
|
521
522
|
}
|
|
522
523
|
|
|
524
|
+
// src/lib/git-branch.ts
|
|
525
|
+
var import_child_process = require("child_process");
|
|
526
|
+
function detectCurrentBranch(cwd = process.cwd()) {
|
|
527
|
+
try {
|
|
528
|
+
const raw = _execSeam.exec("git branch --show-current", {
|
|
529
|
+
cwd,
|
|
530
|
+
// 1 s ceiling is comfortably above normal git latency (<50 ms
|
|
531
|
+
// on a healthy repo) and well below the pair POST's 10 s budget.
|
|
532
|
+
timeout: 1e3,
|
|
533
|
+
// Swallow stderr — non-git directories print "fatal: not a git
|
|
534
|
+
// repository" to stderr and we don't want that on the CLI's
|
|
535
|
+
// own stderr while pairing.
|
|
536
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
537
|
+
encoding: "utf8"
|
|
538
|
+
});
|
|
539
|
+
const trimmed = raw.trim();
|
|
540
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
541
|
+
} catch {
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
var _execSeam = {
|
|
546
|
+
exec: (cmd, opts) => {
|
|
547
|
+
const out = (0, import_child_process.execSync)(cmd, opts);
|
|
548
|
+
return typeof out === "string" ? out : out.toString("utf8");
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
|
|
523
552
|
// src/lib/poll-delay.ts
|
|
524
553
|
var MAX_DELAY_MS = 3e4;
|
|
525
554
|
function computePollDelay({ baseMs, failures }) {
|
|
@@ -534,12 +563,14 @@ async function requestCode(pluginId) {
|
|
|
534
563
|
try {
|
|
535
564
|
const runtime = process.env.CODESPACES === "true" ? "github-codespaces" : "local";
|
|
536
565
|
const codespaceName = process.env.CODESPACE_NAME;
|
|
566
|
+
const branch = detectCurrentBranch();
|
|
537
567
|
const result = await _transport.postJson(`${API_BASE}/api/pairing/code`, {
|
|
538
568
|
pluginId,
|
|
539
569
|
ideName: "Terminal (codeam-cli)",
|
|
540
570
|
ideVersion: package_default.version,
|
|
541
571
|
hostname: os2.hostname(),
|
|
542
572
|
runtime,
|
|
573
|
+
branch,
|
|
543
574
|
...codespaceName ? { codespaceName } : {}
|
|
544
575
|
});
|
|
545
576
|
const data = result?.data;
|
|
@@ -982,7 +1013,7 @@ var CommandRelayService = class {
|
|
|
982
1013
|
};
|
|
983
1014
|
|
|
984
1015
|
// src/services/pty/unix.strategy.ts
|
|
985
|
-
var
|
|
1016
|
+
var import_child_process2 = require("child_process");
|
|
986
1017
|
var fs4 = __toESM(require("fs"));
|
|
987
1018
|
var os4 = __toESM(require("os"));
|
|
988
1019
|
var path4 = __toESM(require("path"));
|
|
@@ -1086,7 +1117,7 @@ var UnixPtyStrategy = class {
|
|
|
1086
1117
|
const rows = process.stdout.rows || 50;
|
|
1087
1118
|
this.helperPath = path4.join(os4.tmpdir(), "codeam-pty-helper.py");
|
|
1088
1119
|
fs4.writeFileSync(this.helperPath, PYTHON_PTY_HELPER, { mode: 420 });
|
|
1089
|
-
this.proc = (0,
|
|
1120
|
+
this.proc = (0, import_child_process2.spawn)(python, [this.helperPath, cmd, ...args2], {
|
|
1090
1121
|
stdio: ["pipe", "pipe", "inherit"],
|
|
1091
1122
|
cwd,
|
|
1092
1123
|
env: {
|
|
@@ -1151,7 +1182,7 @@ var UnixPtyStrategy = class {
|
|
|
1151
1182
|
* are NOT interpreted by /bin/sh.
|
|
1152
1183
|
*/
|
|
1153
1184
|
spawnDirect(cmd, cwd, args2 = []) {
|
|
1154
|
-
this.proc = (0,
|
|
1185
|
+
this.proc = (0, import_child_process2.spawn)(cmd, args2, {
|
|
1155
1186
|
stdio: ["pipe", "inherit", "inherit"],
|
|
1156
1187
|
cwd,
|
|
1157
1188
|
env: process.env,
|
|
@@ -1223,7 +1254,7 @@ var UnixPtyStrategy = class {
|
|
|
1223
1254
|
};
|
|
1224
1255
|
|
|
1225
1256
|
// src/services/pty/windows.strategy.ts
|
|
1226
|
-
var
|
|
1257
|
+
var import_child_process3 = require("child_process");
|
|
1227
1258
|
var WindowsPtyStrategy = class {
|
|
1228
1259
|
constructor(opts) {
|
|
1229
1260
|
this.opts = opts;
|
|
@@ -1231,7 +1262,7 @@ var WindowsPtyStrategy = class {
|
|
|
1231
1262
|
opts;
|
|
1232
1263
|
proc = null;
|
|
1233
1264
|
spawn(cmd, cwd, args2 = []) {
|
|
1234
|
-
this.proc = (0,
|
|
1265
|
+
this.proc = (0, import_child_process3.spawn)(cmd, args2, {
|
|
1235
1266
|
stdio: ["pipe", "pipe", "inherit"],
|
|
1236
1267
|
cwd,
|
|
1237
1268
|
env: {
|
|
@@ -1407,7 +1438,7 @@ var WindowsConPtyStrategy = class _WindowsConPtyStrategy {
|
|
|
1407
1438
|
};
|
|
1408
1439
|
|
|
1409
1440
|
// src/services/claude-installer.ts
|
|
1410
|
-
var
|
|
1441
|
+
var import_child_process4 = require("child_process");
|
|
1411
1442
|
var path6 = __toESM(require("path"));
|
|
1412
1443
|
var os5 = __toESM(require("os"));
|
|
1413
1444
|
|
|
@@ -3360,7 +3391,7 @@ function runInstaller() {
|
|
|
3360
3391
|
"irm https://claude.ai/install.ps1 | iex"
|
|
3361
3392
|
] : ["-c", "curl -fsSL https://claude.ai/install.sh | bash"];
|
|
3362
3393
|
return new Promise((resolve2) => {
|
|
3363
|
-
const proc = (0,
|
|
3394
|
+
const proc = (0, import_child_process4.spawn)(cmd, args2, { stdio: "inherit" });
|
|
3364
3395
|
proc.on("error", (err) => {
|
|
3365
3396
|
console.error(`
|
|
3366
3397
|
\u2717 Installer failed to launch: ${err.message}`);
|
|
@@ -3659,7 +3690,7 @@ var AgentService = class {
|
|
|
3659
3690
|
var fs5 = __toESM(require("fs"));
|
|
3660
3691
|
var os6 = __toESM(require("os"));
|
|
3661
3692
|
var path8 = __toESM(require("path"));
|
|
3662
|
-
var
|
|
3693
|
+
var import_child_process5 = require("child_process");
|
|
3663
3694
|
var HELPER_SCRIPT = `import os,pty,sys,select,signal,struct,fcntl,termios,errno
|
|
3664
3695
|
m,s=pty.openpty()
|
|
3665
3696
|
try:
|
|
@@ -3728,7 +3759,7 @@ async function fetchClaudeQuota() {
|
|
|
3728
3759
|
resolve2(null);
|
|
3729
3760
|
return;
|
|
3730
3761
|
}
|
|
3731
|
-
const proc = (0,
|
|
3762
|
+
const proc = (0, import_child_process5.spawn)(python, [helperPath, claudeCmd, "--tools", ""], {
|
|
3732
3763
|
stdio: ["pipe", "pipe", "ignore"],
|
|
3733
3764
|
cwd: process.cwd(),
|
|
3734
3765
|
env: { ...process.env, TERM: "dumb", COLUMNS: "120", LINES: "30" }
|
|
@@ -4191,12 +4222,12 @@ var os9 = __toESM(require("os"));
|
|
|
4191
4222
|
var path11 = __toESM(require("path"));
|
|
4192
4223
|
|
|
4193
4224
|
// src/agents/claude/credentials.ts
|
|
4194
|
-
var
|
|
4225
|
+
var import_child_process6 = require("child_process");
|
|
4195
4226
|
var fs7 = __toESM(require("fs"));
|
|
4196
4227
|
var os8 = __toESM(require("os"));
|
|
4197
4228
|
var path10 = __toESM(require("path"));
|
|
4198
4229
|
var import_util = require("util");
|
|
4199
|
-
var execFileP = (0, import_util.promisify)(
|
|
4230
|
+
var execFileP = (0, import_util.promisify)(import_child_process6.execFile);
|
|
4200
4231
|
async function detectLocalClaudeCredentials() {
|
|
4201
4232
|
const localClaudeDir = path10.join(os8.homedir(), ".claude");
|
|
4202
4233
|
const flat = path10.join(localClaudeDir, ".credentials.json");
|
|
@@ -6125,6 +6156,886 @@ var HistoryService = class _HistoryService {
|
|
|
6125
6156
|
}
|
|
6126
6157
|
};
|
|
6127
6158
|
|
|
6159
|
+
// src/services/file-watcher.service.ts
|
|
6160
|
+
var import_child_process7 = require("child_process");
|
|
6161
|
+
var path15 = __toESM(require("path"));
|
|
6162
|
+
|
|
6163
|
+
// src/services/file-watcher/diff-parser.ts
|
|
6164
|
+
var HUNK_HEADER_RE = /^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/;
|
|
6165
|
+
function parseUnifiedDiff(diff) {
|
|
6166
|
+
if (!diff || diff.trim().length === 0) {
|
|
6167
|
+
return {
|
|
6168
|
+
fileStatus: "modified",
|
|
6169
|
+
hunks: [],
|
|
6170
|
+
totalLinesAdded: 0,
|
|
6171
|
+
totalLinesRemoved: 0
|
|
6172
|
+
};
|
|
6173
|
+
}
|
|
6174
|
+
const rawLines = diff.split(/\r?\n/);
|
|
6175
|
+
const fileStatus = detectFileStatus(rawLines);
|
|
6176
|
+
const hunks = [];
|
|
6177
|
+
let current = null;
|
|
6178
|
+
let oldLine = 0;
|
|
6179
|
+
let newLine = 0;
|
|
6180
|
+
let totalAdded = 0;
|
|
6181
|
+
let totalRemoved = 0;
|
|
6182
|
+
for (const raw of rawLines) {
|
|
6183
|
+
if (raw.startsWith("@@")) {
|
|
6184
|
+
const match = raw.match(HUNK_HEADER_RE);
|
|
6185
|
+
if (!match) continue;
|
|
6186
|
+
if (current) hunks.push(current);
|
|
6187
|
+
oldLine = parseInt(match[1], 10);
|
|
6188
|
+
newLine = parseInt(match[2], 10);
|
|
6189
|
+
current = {
|
|
6190
|
+
header: raw,
|
|
6191
|
+
lines: [],
|
|
6192
|
+
linesAdded: 0,
|
|
6193
|
+
linesRemoved: 0
|
|
6194
|
+
};
|
|
6195
|
+
continue;
|
|
6196
|
+
}
|
|
6197
|
+
if (current === null) continue;
|
|
6198
|
+
if (raw.startsWith("\\ No newline")) continue;
|
|
6199
|
+
if (raw.startsWith("+")) {
|
|
6200
|
+
current.lines.push({ type: "add", lineNumber: newLine, text: raw.slice(1) });
|
|
6201
|
+
current.linesAdded += 1;
|
|
6202
|
+
totalAdded += 1;
|
|
6203
|
+
newLine += 1;
|
|
6204
|
+
continue;
|
|
6205
|
+
}
|
|
6206
|
+
if (raw.startsWith("-")) {
|
|
6207
|
+
current.lines.push({ type: "remove", lineNumber: oldLine, text: raw.slice(1) });
|
|
6208
|
+
current.linesRemoved += 1;
|
|
6209
|
+
totalRemoved += 1;
|
|
6210
|
+
oldLine += 1;
|
|
6211
|
+
continue;
|
|
6212
|
+
}
|
|
6213
|
+
if (raw.startsWith(" ")) {
|
|
6214
|
+
current.lines.push({ type: "context", lineNumber: newLine, text: raw.slice(1) });
|
|
6215
|
+
newLine += 1;
|
|
6216
|
+
oldLine += 1;
|
|
6217
|
+
continue;
|
|
6218
|
+
}
|
|
6219
|
+
}
|
|
6220
|
+
if (current) hunks.push(current);
|
|
6221
|
+
return {
|
|
6222
|
+
fileStatus,
|
|
6223
|
+
hunks,
|
|
6224
|
+
totalLinesAdded: totalAdded,
|
|
6225
|
+
totalLinesRemoved: totalRemoved
|
|
6226
|
+
};
|
|
6227
|
+
}
|
|
6228
|
+
function detectFileStatus(rawLines) {
|
|
6229
|
+
for (const line of rawLines) {
|
|
6230
|
+
if (line.startsWith("@@")) break;
|
|
6231
|
+
if (line.startsWith("new file mode")) return "added";
|
|
6232
|
+
if (line.startsWith("deleted file mode")) return "deleted";
|
|
6233
|
+
if (line.startsWith("rename from ") || line.startsWith("rename to ")) {
|
|
6234
|
+
return "renamed";
|
|
6235
|
+
}
|
|
6236
|
+
if (line.startsWith("--- /dev/null")) return "added";
|
|
6237
|
+
if (line.startsWith("+++ /dev/null")) return "deleted";
|
|
6238
|
+
}
|
|
6239
|
+
return "modified";
|
|
6240
|
+
}
|
|
6241
|
+
|
|
6242
|
+
// src/services/file-watcher/transport.ts
|
|
6243
|
+
var http5 = __toESM(require("http"));
|
|
6244
|
+
var https5 = __toESM(require("https"));
|
|
6245
|
+
var _transport3 = {
|
|
6246
|
+
post: _post2
|
|
6247
|
+
};
|
|
6248
|
+
function _post2(url, headers, payload) {
|
|
6249
|
+
return new Promise((resolve2, reject) => {
|
|
6250
|
+
let settled = false;
|
|
6251
|
+
const u2 = new URL(url);
|
|
6252
|
+
const lib = u2.protocol === "https:" ? https5 : http5;
|
|
6253
|
+
const req = lib.request(
|
|
6254
|
+
{
|
|
6255
|
+
hostname: u2.hostname,
|
|
6256
|
+
port: u2.port || (u2.protocol === "https:" ? 443 : 80),
|
|
6257
|
+
path: u2.pathname + u2.search,
|
|
6258
|
+
method: "POST",
|
|
6259
|
+
headers: {
|
|
6260
|
+
...headers,
|
|
6261
|
+
...vercelBypassHeader(),
|
|
6262
|
+
"Content-Length": Buffer.byteLength(payload)
|
|
6263
|
+
},
|
|
6264
|
+
timeout: 8e3
|
|
6265
|
+
},
|
|
6266
|
+
(res) => {
|
|
6267
|
+
let body = "";
|
|
6268
|
+
res.on("data", (c2) => {
|
|
6269
|
+
body += c2.toString();
|
|
6270
|
+
});
|
|
6271
|
+
res.on("end", () => {
|
|
6272
|
+
if (settled) return;
|
|
6273
|
+
settled = true;
|
|
6274
|
+
resolve2({ statusCode: res.statusCode ?? 0, body });
|
|
6275
|
+
});
|
|
6276
|
+
}
|
|
6277
|
+
);
|
|
6278
|
+
req.on("error", (err) => {
|
|
6279
|
+
if (settled) return;
|
|
6280
|
+
settled = true;
|
|
6281
|
+
reject(err);
|
|
6282
|
+
});
|
|
6283
|
+
req.on("timeout", () => {
|
|
6284
|
+
req.destroy();
|
|
6285
|
+
});
|
|
6286
|
+
req.write(payload);
|
|
6287
|
+
req.end();
|
|
6288
|
+
});
|
|
6289
|
+
}
|
|
6290
|
+
|
|
6291
|
+
// src/services/file-watcher.service.ts
|
|
6292
|
+
var API_BASE5 = process.env.CODEAM_API_URL ?? "https://codeagent-mobile-api.vercel.app";
|
|
6293
|
+
var DEBOUNCE_MS = 250;
|
|
6294
|
+
var MAX_RETRIES = 2;
|
|
6295
|
+
var RETRY_BACKOFF_MS = 300;
|
|
6296
|
+
var FileWatcherService = class {
|
|
6297
|
+
constructor(opts) {
|
|
6298
|
+
this.opts = opts;
|
|
6299
|
+
this.apiBase = opts.apiBaseUrl ?? API_BASE5;
|
|
6300
|
+
}
|
|
6301
|
+
opts;
|
|
6302
|
+
watcher = null;
|
|
6303
|
+
pending = /* @__PURE__ */ new Map();
|
|
6304
|
+
apiBase;
|
|
6305
|
+
stopped = false;
|
|
6306
|
+
/**
|
|
6307
|
+
* Start watching `opts.workingDir`. Idempotent (second call is a
|
|
6308
|
+
* no-op). Resolves once chokidar's initial scan completes; that
|
|
6309
|
+
* way the `start.ts` orchestrator can sequence "agent up → watcher
|
|
6310
|
+
* ready" deterministically if it wants to, though today it
|
|
6311
|
+
* doesn't await this.
|
|
6312
|
+
*/
|
|
6313
|
+
async start() {
|
|
6314
|
+
if (this.watcher) return;
|
|
6315
|
+
if (this.stopped) {
|
|
6316
|
+
throw new Error("FileWatcherService has already been stopped \u2014 re-instantiate to restart.");
|
|
6317
|
+
}
|
|
6318
|
+
let chokidar;
|
|
6319
|
+
try {
|
|
6320
|
+
chokidar = require("chokidar");
|
|
6321
|
+
} catch (err) {
|
|
6322
|
+
log.warn(
|
|
6323
|
+
"fileWatcher",
|
|
6324
|
+
`chokidar unavailable \u2014 file change emission disabled`,
|
|
6325
|
+
err
|
|
6326
|
+
);
|
|
6327
|
+
return;
|
|
6328
|
+
}
|
|
6329
|
+
const watcher = chokidar.watch(this.opts.workingDir, {
|
|
6330
|
+
ignored: [
|
|
6331
|
+
/(^|[\\/])\../,
|
|
6332
|
+
// dot-files & dot-dirs (.git, .next, .expo, .DS_Store, …)
|
|
6333
|
+
/node_modules/,
|
|
6334
|
+
/dist/,
|
|
6335
|
+
/build/,
|
|
6336
|
+
/out/,
|
|
6337
|
+
/coverage/,
|
|
6338
|
+
/\.turbo/,
|
|
6339
|
+
/\.cache/,
|
|
6340
|
+
/\.parcel-cache/,
|
|
6341
|
+
// Build outputs that aren't a typical "dist" target
|
|
6342
|
+
/target\//,
|
|
6343
|
+
/__pycache__/
|
|
6344
|
+
],
|
|
6345
|
+
ignoreInitial: true,
|
|
6346
|
+
// we only care about post-start changes
|
|
6347
|
+
persistent: true,
|
|
6348
|
+
awaitWriteFinish: {
|
|
6349
|
+
// Coalesces rapid sequential writes (npm install spam, build
|
|
6350
|
+
// tools emitting bursts). Lower than chokidar's default so
|
|
6351
|
+
// the user sees their Files screen update within 0.5 s of
|
|
6352
|
+
// saving.
|
|
6353
|
+
stabilityThreshold: 150,
|
|
6354
|
+
pollInterval: 50
|
|
6355
|
+
}
|
|
6356
|
+
});
|
|
6357
|
+
watcher.on("add", (filePath) => this.schedule(filePath, "add"));
|
|
6358
|
+
watcher.on("change", (filePath) => this.schedule(filePath, "change"));
|
|
6359
|
+
watcher.on("unlink", (filePath) => this.schedule(filePath, "unlink"));
|
|
6360
|
+
this.watcher = watcher;
|
|
6361
|
+
log.info(
|
|
6362
|
+
"fileWatcher",
|
|
6363
|
+
`watching ${this.opts.workingDir} for session=${this.opts.sessionId.slice(0, 8)}`
|
|
6364
|
+
);
|
|
6365
|
+
}
|
|
6366
|
+
/**
|
|
6367
|
+
* Stop watching. Idempotent — safe to call multiple times. After
|
|
6368
|
+
* stop, the instance is dead; create a new one to resume. (This
|
|
6369
|
+
* matches the `OutputService` / `CommandRelayService` style.)
|
|
6370
|
+
*/
|
|
6371
|
+
async stop() {
|
|
6372
|
+
if (this.stopped) return;
|
|
6373
|
+
this.stopped = true;
|
|
6374
|
+
for (const entry of this.pending.values()) {
|
|
6375
|
+
clearTimeout(entry.timer);
|
|
6376
|
+
}
|
|
6377
|
+
this.pending.clear();
|
|
6378
|
+
if (this.watcher) {
|
|
6379
|
+
try {
|
|
6380
|
+
await this.watcher.close();
|
|
6381
|
+
} catch (err) {
|
|
6382
|
+
log.warn("fileWatcher", "error closing chokidar", err);
|
|
6383
|
+
}
|
|
6384
|
+
this.watcher = null;
|
|
6385
|
+
}
|
|
6386
|
+
log.info("fileWatcher", `stopped (session=${this.opts.sessionId.slice(0, 8)})`);
|
|
6387
|
+
}
|
|
6388
|
+
/**
|
|
6389
|
+
* Coalesce rapid writes per-file. Each fresh event resets the
|
|
6390
|
+
* 250 ms debounce timer. When the timer fires, we compute the
|
|
6391
|
+
* diff once and emit.
|
|
6392
|
+
*
|
|
6393
|
+
* `unlink` events bypass the diff path — we emit a synthetic
|
|
6394
|
+
* deletion directly because `git diff <path>` for a removed file
|
|
6395
|
+
* produces a diff that's already encoded as a deletion in
|
|
6396
|
+
* `parseUnifiedDiff`, but in practice the file is gone and the
|
|
6397
|
+
* synthetic path is simpler and avoids a race with git's index.
|
|
6398
|
+
*/
|
|
6399
|
+
schedule(absPath, changeType) {
|
|
6400
|
+
if (this.stopped) return;
|
|
6401
|
+
const existing = this.pending.get(absPath);
|
|
6402
|
+
if (existing) clearTimeout(existing.timer);
|
|
6403
|
+
const timer = setTimeout(() => {
|
|
6404
|
+
this.pending.delete(absPath);
|
|
6405
|
+
void this.emitForFile(absPath, changeType);
|
|
6406
|
+
}, DEBOUNCE_MS);
|
|
6407
|
+
this.pending.set(absPath, {
|
|
6408
|
+
lastEventAt: Date.now(),
|
|
6409
|
+
timer,
|
|
6410
|
+
changeType
|
|
6411
|
+
});
|
|
6412
|
+
}
|
|
6413
|
+
/**
|
|
6414
|
+
* Visible for tests — lets vitest pump a synthetic file event
|
|
6415
|
+
* through the debounce + diff + emit pipeline without spinning up
|
|
6416
|
+
* a real chokidar watcher.
|
|
6417
|
+
*/
|
|
6418
|
+
/* @internal */
|
|
6419
|
+
_scheduleForTest(absPath, changeType) {
|
|
6420
|
+
this.schedule(absPath, changeType);
|
|
6421
|
+
}
|
|
6422
|
+
async emitForFile(absPath, changeType) {
|
|
6423
|
+
if (this.stopped) return;
|
|
6424
|
+
const relPath = path15.relative(this.opts.workingDir, absPath);
|
|
6425
|
+
if (!relPath || relPath.startsWith("..")) {
|
|
6426
|
+
return;
|
|
6427
|
+
}
|
|
6428
|
+
let diffText = "";
|
|
6429
|
+
let fileStatus = "modified";
|
|
6430
|
+
if (changeType === "unlink") {
|
|
6431
|
+
const diff = await this.gitDiff(relPath);
|
|
6432
|
+
if (diff !== null && diff.trim().length > 0) {
|
|
6433
|
+
diffText = diff;
|
|
6434
|
+
} else {
|
|
6435
|
+
await this.postFileChanged({
|
|
6436
|
+
sessionId: this.opts.sessionId,
|
|
6437
|
+
pluginId: this.opts.pluginId,
|
|
6438
|
+
filePath: relPath,
|
|
6439
|
+
fileStatus: "deleted",
|
|
6440
|
+
linesAdded: 0,
|
|
6441
|
+
linesRemoved: 0,
|
|
6442
|
+
hunkCount: 0
|
|
6443
|
+
});
|
|
6444
|
+
return;
|
|
6445
|
+
}
|
|
6446
|
+
fileStatus = "deleted";
|
|
6447
|
+
} else {
|
|
6448
|
+
const diff = await this.gitDiff(relPath);
|
|
6449
|
+
if (diff === null) {
|
|
6450
|
+
log.warn(
|
|
6451
|
+
"fileWatcher",
|
|
6452
|
+
`git diff failed for ${relPath} \u2014 emitting file-changed only`
|
|
6453
|
+
);
|
|
6454
|
+
await this.postFileChanged({
|
|
6455
|
+
sessionId: this.opts.sessionId,
|
|
6456
|
+
pluginId: this.opts.pluginId,
|
|
6457
|
+
filePath: relPath,
|
|
6458
|
+
fileStatus: changeType === "add" ? "added" : "modified",
|
|
6459
|
+
linesAdded: 0,
|
|
6460
|
+
linesRemoved: 0,
|
|
6461
|
+
hunkCount: 0
|
|
6462
|
+
});
|
|
6463
|
+
return;
|
|
6464
|
+
}
|
|
6465
|
+
diffText = diff;
|
|
6466
|
+
}
|
|
6467
|
+
const parsed = parseUnifiedDiff(diffText);
|
|
6468
|
+
const finalStatus = parsed.fileStatus !== "modified" ? parsed.fileStatus : changeType === "add" ? "added" : changeType === "unlink" ? "deleted" : fileStatus;
|
|
6469
|
+
const reviewStatus = parsed.hunks.length > 0 ? "awaiting_review" : void 0;
|
|
6470
|
+
await this.postFileChanged({
|
|
6471
|
+
sessionId: this.opts.sessionId,
|
|
6472
|
+
pluginId: this.opts.pluginId,
|
|
6473
|
+
filePath: relPath,
|
|
6474
|
+
fileStatus: finalStatus,
|
|
6475
|
+
linesAdded: parsed.totalLinesAdded,
|
|
6476
|
+
linesRemoved: parsed.totalLinesRemoved,
|
|
6477
|
+
hunkCount: parsed.hunks.length,
|
|
6478
|
+
reviewStatus
|
|
6479
|
+
});
|
|
6480
|
+
for (const hunk of parsed.hunks) {
|
|
6481
|
+
await this.postReviewHunk({
|
|
6482
|
+
sessionId: this.opts.sessionId,
|
|
6483
|
+
pluginId: this.opts.pluginId,
|
|
6484
|
+
filePath: relPath,
|
|
6485
|
+
fileStatus: finalStatus,
|
|
6486
|
+
hunkHeader: hunk.header,
|
|
6487
|
+
lines: hunk.lines,
|
|
6488
|
+
linesAdded: hunk.linesAdded,
|
|
6489
|
+
linesRemoved: hunk.linesRemoved
|
|
6490
|
+
});
|
|
6491
|
+
}
|
|
6492
|
+
}
|
|
6493
|
+
/**
|
|
6494
|
+
* Compute the unified diff for a single path relative to the
|
|
6495
|
+
* working dir. Returns `null` when git is unavailable or the cwd
|
|
6496
|
+
* is not a repo. Returns `''` when there's no diff (a touch that
|
|
6497
|
+
* didn't change content).
|
|
6498
|
+
*
|
|
6499
|
+
* For tracked files we use `git diff --no-color -- <path>` which
|
|
6500
|
+
* compares the worktree against HEAD's blob.
|
|
6501
|
+
* For untracked files (`git ls-files --error-unmatch` exits non-
|
|
6502
|
+
* zero) we use `git diff --no-color --no-index /dev/null <path>`,
|
|
6503
|
+
* which produces an "added"-shaped diff against an empty source.
|
|
6504
|
+
*/
|
|
6505
|
+
async gitDiff(relPath) {
|
|
6506
|
+
const tracked = await runGit(
|
|
6507
|
+
this.opts.workingDir,
|
|
6508
|
+
["diff", "--no-color", "--", relPath]
|
|
6509
|
+
);
|
|
6510
|
+
if (tracked === null) return null;
|
|
6511
|
+
if (tracked.trim().length > 0) return tracked;
|
|
6512
|
+
const devNull = process.platform === "win32" ? "NUL" : "/dev/null";
|
|
6513
|
+
const untracked = await runGit(
|
|
6514
|
+
this.opts.workingDir,
|
|
6515
|
+
["diff", "--no-color", "--no-index", "--", devNull, relPath],
|
|
6516
|
+
{ allowNonZeroExit: true }
|
|
6517
|
+
);
|
|
6518
|
+
return untracked ?? "";
|
|
6519
|
+
}
|
|
6520
|
+
async postFileChanged(body) {
|
|
6521
|
+
await this.postWithRetries(`${this.apiBase}/api/files/changed`, body);
|
|
6522
|
+
}
|
|
6523
|
+
async postReviewHunk(body) {
|
|
6524
|
+
await this.postWithRetries(`${this.apiBase}/api/review/hunks`, body);
|
|
6525
|
+
}
|
|
6526
|
+
async postWithRetries(url, body) {
|
|
6527
|
+
const payload = JSON.stringify(body);
|
|
6528
|
+
const headers = {
|
|
6529
|
+
"Content-Type": "application/json",
|
|
6530
|
+
"X-Codeam-Protocol-Version": "2.0.0",
|
|
6531
|
+
"X-Plugin-Auth-Token": this.opts.pluginAuthToken
|
|
6532
|
+
};
|
|
6533
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt += 1) {
|
|
6534
|
+
try {
|
|
6535
|
+
const { statusCode, body: resBody } = await _transport3.post(url, headers, payload);
|
|
6536
|
+
if (statusCode >= 200 && statusCode < 300) {
|
|
6537
|
+
log.trace(
|
|
6538
|
+
"fileWatcher",
|
|
6539
|
+
`post ok url=${url} status=${statusCode} path=${body.filePath}`
|
|
6540
|
+
);
|
|
6541
|
+
return;
|
|
6542
|
+
}
|
|
6543
|
+
if (statusCode === 410 || statusCode === 404) {
|
|
6544
|
+
log.warn(
|
|
6545
|
+
"fileWatcher",
|
|
6546
|
+
`session dead (status=${statusCode}) \u2014 dropping ${body.filePath}`
|
|
6547
|
+
);
|
|
6548
|
+
this.stopped = true;
|
|
6549
|
+
return;
|
|
6550
|
+
}
|
|
6551
|
+
log.warn(
|
|
6552
|
+
"fileWatcher",
|
|
6553
|
+
`post failed url=${url} status=${statusCode} attempt=${attempt + 1} body=${resBody.slice(0, 200)}`
|
|
6554
|
+
);
|
|
6555
|
+
} catch (err) {
|
|
6556
|
+
log.warn(
|
|
6557
|
+
"fileWatcher",
|
|
6558
|
+
`post error url=${url} attempt=${attempt + 1}`,
|
|
6559
|
+
err
|
|
6560
|
+
);
|
|
6561
|
+
}
|
|
6562
|
+
if (attempt < MAX_RETRIES) {
|
|
6563
|
+
await sleep(RETRY_BACKOFF_MS * (attempt + 1));
|
|
6564
|
+
}
|
|
6565
|
+
}
|
|
6566
|
+
log.warn(
|
|
6567
|
+
"fileWatcher",
|
|
6568
|
+
`giving up after ${MAX_RETRIES + 1} attempts \u2014 path=${body.filePath}`
|
|
6569
|
+
);
|
|
6570
|
+
}
|
|
6571
|
+
};
|
|
6572
|
+
function sleep(ms) {
|
|
6573
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
6574
|
+
}
|
|
6575
|
+
async function runGit(cwd, args2, opts = {}) {
|
|
6576
|
+
return _runGit(cwd, args2, opts);
|
|
6577
|
+
}
|
|
6578
|
+
var _gitSeam = {
|
|
6579
|
+
run: _runGitImpl
|
|
6580
|
+
};
|
|
6581
|
+
async function _runGitImpl(cwd, args2, opts = {}) {
|
|
6582
|
+
return new Promise((resolve2) => {
|
|
6583
|
+
let proc;
|
|
6584
|
+
try {
|
|
6585
|
+
proc = (0, import_child_process7.spawn)("git", args2, { cwd, env: process.env });
|
|
6586
|
+
} catch {
|
|
6587
|
+
resolve2(null);
|
|
6588
|
+
return;
|
|
6589
|
+
}
|
|
6590
|
+
let stdout = "";
|
|
6591
|
+
let stderr = "";
|
|
6592
|
+
proc.stdout?.on("data", (c2) => {
|
|
6593
|
+
stdout += c2.toString();
|
|
6594
|
+
});
|
|
6595
|
+
proc.stderr?.on("data", (c2) => {
|
|
6596
|
+
stderr += c2.toString();
|
|
6597
|
+
});
|
|
6598
|
+
proc.on("error", () => resolve2(null));
|
|
6599
|
+
proc.on("close", (code) => {
|
|
6600
|
+
if (code === 0 || opts.allowNonZeroExit) {
|
|
6601
|
+
resolve2(stdout);
|
|
6602
|
+
} else {
|
|
6603
|
+
log.trace("fileWatcher", `git ${args2.join(" ")} exited ${code} stderr=${stderr.slice(0, 200)}`);
|
|
6604
|
+
resolve2(null);
|
|
6605
|
+
}
|
|
6606
|
+
});
|
|
6607
|
+
});
|
|
6608
|
+
}
|
|
6609
|
+
function _runGit(cwd, args2, opts = {}) {
|
|
6610
|
+
return _gitSeam.run(cwd, args2, opts);
|
|
6611
|
+
}
|
|
6612
|
+
|
|
6613
|
+
// src/services/streaming-emitter.service.ts
|
|
6614
|
+
var import_crypto = require("crypto");
|
|
6615
|
+
|
|
6616
|
+
// src/services/streaming/transport.ts
|
|
6617
|
+
var http6 = __toESM(require("http"));
|
|
6618
|
+
var https6 = __toESM(require("https"));
|
|
6619
|
+
var _transport4 = {
|
|
6620
|
+
post: _post3,
|
|
6621
|
+
get: _get
|
|
6622
|
+
};
|
|
6623
|
+
function _post3(url, headers, payload) {
|
|
6624
|
+
return new Promise((resolve2, reject) => {
|
|
6625
|
+
let settled = false;
|
|
6626
|
+
const u2 = new URL(url);
|
|
6627
|
+
const lib = u2.protocol === "https:" ? https6 : http6;
|
|
6628
|
+
const req = lib.request(
|
|
6629
|
+
{
|
|
6630
|
+
hostname: u2.hostname,
|
|
6631
|
+
port: u2.port || (u2.protocol === "https:" ? 443 : 80),
|
|
6632
|
+
path: u2.pathname + u2.search,
|
|
6633
|
+
method: "POST",
|
|
6634
|
+
headers: {
|
|
6635
|
+
...headers,
|
|
6636
|
+
...vercelBypassHeader(),
|
|
6637
|
+
"Content-Length": Buffer.byteLength(payload)
|
|
6638
|
+
},
|
|
6639
|
+
timeout: 8e3
|
|
6640
|
+
},
|
|
6641
|
+
(res) => {
|
|
6642
|
+
let body = "";
|
|
6643
|
+
res.on("data", (c2) => {
|
|
6644
|
+
body += c2.toString();
|
|
6645
|
+
});
|
|
6646
|
+
res.on("end", () => {
|
|
6647
|
+
if (settled) return;
|
|
6648
|
+
settled = true;
|
|
6649
|
+
resolve2({ statusCode: res.statusCode ?? 0, body });
|
|
6650
|
+
});
|
|
6651
|
+
}
|
|
6652
|
+
);
|
|
6653
|
+
req.on("error", (err) => {
|
|
6654
|
+
if (settled) return;
|
|
6655
|
+
settled = true;
|
|
6656
|
+
reject(err);
|
|
6657
|
+
});
|
|
6658
|
+
req.on("timeout", () => {
|
|
6659
|
+
req.destroy();
|
|
6660
|
+
});
|
|
6661
|
+
req.write(payload);
|
|
6662
|
+
req.end();
|
|
6663
|
+
});
|
|
6664
|
+
}
|
|
6665
|
+
function _get(url, headers) {
|
|
6666
|
+
return new Promise((resolve2, reject) => {
|
|
6667
|
+
let settled = false;
|
|
6668
|
+
const u2 = new URL(url);
|
|
6669
|
+
const lib = u2.protocol === "https:" ? https6 : http6;
|
|
6670
|
+
const req = lib.request(
|
|
6671
|
+
{
|
|
6672
|
+
hostname: u2.hostname,
|
|
6673
|
+
port: u2.port || (u2.protocol === "https:" ? 443 : 80),
|
|
6674
|
+
path: u2.pathname + u2.search,
|
|
6675
|
+
method: "GET",
|
|
6676
|
+
headers: {
|
|
6677
|
+
...headers,
|
|
6678
|
+
...vercelBypassHeader()
|
|
6679
|
+
},
|
|
6680
|
+
timeout: 8e3
|
|
6681
|
+
},
|
|
6682
|
+
(res) => {
|
|
6683
|
+
let body = "";
|
|
6684
|
+
res.on("data", (c2) => {
|
|
6685
|
+
body += c2.toString();
|
|
6686
|
+
});
|
|
6687
|
+
res.on("end", () => {
|
|
6688
|
+
if (settled) return;
|
|
6689
|
+
settled = true;
|
|
6690
|
+
resolve2({ statusCode: res.statusCode ?? 0, body });
|
|
6691
|
+
});
|
|
6692
|
+
}
|
|
6693
|
+
);
|
|
6694
|
+
req.on("error", (err) => {
|
|
6695
|
+
if (settled) return;
|
|
6696
|
+
settled = true;
|
|
6697
|
+
reject(err);
|
|
6698
|
+
});
|
|
6699
|
+
req.on("timeout", () => {
|
|
6700
|
+
req.destroy();
|
|
6701
|
+
});
|
|
6702
|
+
req.end();
|
|
6703
|
+
});
|
|
6704
|
+
}
|
|
6705
|
+
|
|
6706
|
+
// src/services/streaming-emitter.service.ts
|
|
6707
|
+
var API_BASE6 = process.env.CODEAM_API_URL ?? "https://codeagent-mobile-api.vercel.app";
|
|
6708
|
+
var TICK_MS = 50;
|
|
6709
|
+
var ANSWER_POLL_MS = 1500;
|
|
6710
|
+
var SELECTOR_STABLE_MS = 800;
|
|
6711
|
+
var MAX_RETRIES2 = 1;
|
|
6712
|
+
var RETRY_BACKOFF_MS2 = 200;
|
|
6713
|
+
var StreamingEmitterService = class {
|
|
6714
|
+
constructor(opts) {
|
|
6715
|
+
this.opts = opts;
|
|
6716
|
+
this.apiBase = opts.apiBaseUrl ?? API_BASE6;
|
|
6717
|
+
this.headers = {
|
|
6718
|
+
"Content-Type": "application/json",
|
|
6719
|
+
"X-Codeam-Protocol-Version": "2.0.0",
|
|
6720
|
+
"X-Plugin-Auth-Token": opts.pluginAuthToken
|
|
6721
|
+
};
|
|
6722
|
+
}
|
|
6723
|
+
opts;
|
|
6724
|
+
apiBase;
|
|
6725
|
+
headers;
|
|
6726
|
+
rawBuffer = "";
|
|
6727
|
+
active = false;
|
|
6728
|
+
tickTimer = null;
|
|
6729
|
+
answerPollTimer = null;
|
|
6730
|
+
/** Open chunk we're appending bytes to; cleared on kind change or stop. */
|
|
6731
|
+
activeChunk = null;
|
|
6732
|
+
/** Outstanding answer we're polling the backend for. Null when idle. */
|
|
6733
|
+
pendingAnswer = null;
|
|
6734
|
+
/**
|
|
6735
|
+
* Memoised selector signature so we don't fire `awaiting-answer` on
|
|
6736
|
+
* every tick while the user thinks. Cleared once an answer resolves.
|
|
6737
|
+
*/
|
|
6738
|
+
lastSelectorSignature = null;
|
|
6739
|
+
selectorFirstSeenAt = 0;
|
|
6740
|
+
// ─── Lifecycle ─────────────────────────────────────────────────────
|
|
6741
|
+
start() {
|
|
6742
|
+
if (this.active) return;
|
|
6743
|
+
this.active = true;
|
|
6744
|
+
this.tickTimer = setInterval(() => this.tick(), TICK_MS);
|
|
6745
|
+
this.answerPollTimer = setInterval(() => {
|
|
6746
|
+
void this.pollPendingAnswer();
|
|
6747
|
+
}, ANSWER_POLL_MS);
|
|
6748
|
+
log.trace("streamingEmitter", `started session=${this.opts.sessionId.slice(0, 8)}`);
|
|
6749
|
+
}
|
|
6750
|
+
/**
|
|
6751
|
+
* Stop the emitter. Finalises the open chunk (if any) so the
|
|
6752
|
+
* backend doesn't leave a half-streamed message in the SSE
|
|
6753
|
+
* buffer. Idempotent.
|
|
6754
|
+
*/
|
|
6755
|
+
async stop() {
|
|
6756
|
+
if (!this.active) return;
|
|
6757
|
+
this.active = false;
|
|
6758
|
+
if (this.tickTimer) {
|
|
6759
|
+
clearInterval(this.tickTimer);
|
|
6760
|
+
this.tickTimer = null;
|
|
6761
|
+
}
|
|
6762
|
+
if (this.answerPollTimer) {
|
|
6763
|
+
clearInterval(this.answerPollTimer);
|
|
6764
|
+
this.answerPollTimer = null;
|
|
6765
|
+
}
|
|
6766
|
+
if (this.activeChunk) {
|
|
6767
|
+
const finalContent = this.activeChunk.currentContent;
|
|
6768
|
+
const chunk = this.activeChunk;
|
|
6769
|
+
this.activeChunk = null;
|
|
6770
|
+
await this.postChunk({
|
|
6771
|
+
chunkId: chunk.chunkId,
|
|
6772
|
+
kind: chunk.kind,
|
|
6773
|
+
content: finalContent,
|
|
6774
|
+
isFinal: true
|
|
6775
|
+
});
|
|
6776
|
+
}
|
|
6777
|
+
log.trace("streamingEmitter", `stopped session=${this.opts.sessionId.slice(0, 8)}`);
|
|
6778
|
+
}
|
|
6779
|
+
// ─── Input pump ────────────────────────────────────────────────────
|
|
6780
|
+
/**
|
|
6781
|
+
* Forward a chunk of raw PTY bytes into the emitter. Safe to call
|
|
6782
|
+
* before `start()` — bytes accumulate in the internal buffer and the
|
|
6783
|
+
* first tick processes the backlog.
|
|
6784
|
+
*/
|
|
6785
|
+
push(raw) {
|
|
6786
|
+
if (raw.length === 0) return;
|
|
6787
|
+
this.rawBuffer += raw;
|
|
6788
|
+
}
|
|
6789
|
+
// ─── Tick ──────────────────────────────────────────────────────────
|
|
6790
|
+
/**
|
|
6791
|
+
* Visible for tests. Runs one classify-and-emit pass against the
|
|
6792
|
+
* current PTY buffer.
|
|
6793
|
+
*/
|
|
6794
|
+
/* @internal */
|
|
6795
|
+
_tickForTest() {
|
|
6796
|
+
this.tick();
|
|
6797
|
+
}
|
|
6798
|
+
tick() {
|
|
6799
|
+
if (!this.active) return;
|
|
6800
|
+
const lines = (this.opts.runtime.renderToLines ?? renderToLines)(this.rawBuffer);
|
|
6801
|
+
const selector = this.opts.runtime.detectInteractivePrompt(lines);
|
|
6802
|
+
if (selector) {
|
|
6803
|
+
this.maybeEmitAwaitingAnswer(selector);
|
|
6804
|
+
return;
|
|
6805
|
+
}
|
|
6806
|
+
this.lastSelectorSignature = null;
|
|
6807
|
+
this.selectorFirstSeenAt = 0;
|
|
6808
|
+
const groups = this.classifyLines(lines, this.opts.runtime);
|
|
6809
|
+
if (groups.length === 0) return;
|
|
6810
|
+
const last = groups[groups.length - 1];
|
|
6811
|
+
if (this.activeChunk && this.activeChunk.kind === last.kind) {
|
|
6812
|
+
this.activeChunk.currentContent = last.content;
|
|
6813
|
+
this.maybeFlushActive(false);
|
|
6814
|
+
return;
|
|
6815
|
+
}
|
|
6816
|
+
if (this.activeChunk) {
|
|
6817
|
+
const closing = this.activeChunk;
|
|
6818
|
+
this.activeChunk = null;
|
|
6819
|
+
void this.postChunk({
|
|
6820
|
+
chunkId: closing.chunkId,
|
|
6821
|
+
kind: closing.kind,
|
|
6822
|
+
content: closing.currentContent,
|
|
6823
|
+
isFinal: true
|
|
6824
|
+
});
|
|
6825
|
+
}
|
|
6826
|
+
this.activeChunk = {
|
|
6827
|
+
chunkId: (0, import_crypto.randomUUID)(),
|
|
6828
|
+
kind: last.kind,
|
|
6829
|
+
emittedContent: "",
|
|
6830
|
+
currentContent: last.content,
|
|
6831
|
+
lastEmitAt: 0
|
|
6832
|
+
};
|
|
6833
|
+
this.maybeFlushActive(false);
|
|
6834
|
+
}
|
|
6835
|
+
maybeFlushActive(force) {
|
|
6836
|
+
const chunk = this.activeChunk;
|
|
6837
|
+
if (!chunk) return;
|
|
6838
|
+
const now = Date.now();
|
|
6839
|
+
if (!force && now - chunk.lastEmitAt < TICK_MS) return;
|
|
6840
|
+
if (chunk.currentContent === chunk.emittedContent) return;
|
|
6841
|
+
chunk.emittedContent = chunk.currentContent;
|
|
6842
|
+
chunk.lastEmitAt = now;
|
|
6843
|
+
void this.postChunk({
|
|
6844
|
+
chunkId: chunk.chunkId,
|
|
6845
|
+
kind: chunk.kind,
|
|
6846
|
+
content: chunk.currentContent,
|
|
6847
|
+
isFinal: false
|
|
6848
|
+
});
|
|
6849
|
+
}
|
|
6850
|
+
// ─── Line classification ───────────────────────────────────────────
|
|
6851
|
+
/**
|
|
6852
|
+
* Walk the rendered screen lines and bucket them into kind-groups.
|
|
6853
|
+
*
|
|
6854
|
+
* Visible for tests so we can assert discrimination against fixture
|
|
6855
|
+
* inputs without standing up the full POST loop.
|
|
6856
|
+
*/
|
|
6857
|
+
/* @internal */
|
|
6858
|
+
_classifyForTest(lines) {
|
|
6859
|
+
return this.classifyLines(lines, this.opts.runtime);
|
|
6860
|
+
}
|
|
6861
|
+
classifyLines(lines, runtime) {
|
|
6862
|
+
const parseLine2 = runtime.parseTuiChrome?.bind(runtime);
|
|
6863
|
+
const groups = [];
|
|
6864
|
+
let current = null;
|
|
6865
|
+
const flush = () => {
|
|
6866
|
+
if (!current) return;
|
|
6867
|
+
const text = current.lines.join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
6868
|
+
if (text.length > 0) groups.push({ kind: current.kind, content: text });
|
|
6869
|
+
current = null;
|
|
6870
|
+
};
|
|
6871
|
+
for (const rawLine of lines) {
|
|
6872
|
+
const kind = classifyLine(rawLine, parseLine2, runtime);
|
|
6873
|
+
if (kind === null) continue;
|
|
6874
|
+
if (!current || current.kind !== kind) {
|
|
6875
|
+
flush();
|
|
6876
|
+
current = { kind, lines: [rawLine] };
|
|
6877
|
+
} else {
|
|
6878
|
+
current.lines.push(rawLine);
|
|
6879
|
+
}
|
|
6880
|
+
}
|
|
6881
|
+
flush();
|
|
6882
|
+
return groups;
|
|
6883
|
+
}
|
|
6884
|
+
// ─── Awaiting-answer + answer subscription ─────────────────────────
|
|
6885
|
+
maybeEmitAwaitingAnswer(selector) {
|
|
6886
|
+
const signature = `${selector.question}::${selector.options.join("|")}`;
|
|
6887
|
+
const now = Date.now();
|
|
6888
|
+
if (this.lastSelectorSignature !== signature) {
|
|
6889
|
+
this.lastSelectorSignature = signature;
|
|
6890
|
+
this.selectorFirstSeenAt = now;
|
|
6891
|
+
return;
|
|
6892
|
+
}
|
|
6893
|
+
if (this.pendingAnswer) return;
|
|
6894
|
+
if (now - this.selectorFirstSeenAt < SELECTOR_STABLE_MS) return;
|
|
6895
|
+
const questionId = (0, import_crypto.randomUUID)();
|
|
6896
|
+
this.pendingAnswer = {
|
|
6897
|
+
questionId,
|
|
6898
|
+
options: selector.options.length > 0 ? selector.options : void 0,
|
|
6899
|
+
fromIndex: selector.currentIndex
|
|
6900
|
+
};
|
|
6901
|
+
const event = {
|
|
6902
|
+
questionId,
|
|
6903
|
+
prompt: selector.question,
|
|
6904
|
+
...selector.options.length > 0 ? { options: selector.options } : {}
|
|
6905
|
+
};
|
|
6906
|
+
void this.postAwaitingAnswer(event);
|
|
6907
|
+
}
|
|
6908
|
+
async pollPendingAnswer() {
|
|
6909
|
+
if (!this.active) return;
|
|
6910
|
+
if (!this.pendingAnswer) return;
|
|
6911
|
+
const questionId = this.pendingAnswer.questionId;
|
|
6912
|
+
try {
|
|
6913
|
+
const { statusCode, body } = await _transport4.get(
|
|
6914
|
+
`${this.apiBase}/api/sessions/${encodeURIComponent(this.opts.sessionId)}/pending-answer?questionId=${encodeURIComponent(questionId)}`,
|
|
6915
|
+
this.headers
|
|
6916
|
+
);
|
|
6917
|
+
if (statusCode === 204 || statusCode === 404) {
|
|
6918
|
+
return;
|
|
6919
|
+
}
|
|
6920
|
+
if (statusCode < 200 || statusCode >= 300) {
|
|
6921
|
+
log.warn("streamingEmitter", `pending-answer status=${statusCode} body=${body.slice(0, 200)}`);
|
|
6922
|
+
return;
|
|
6923
|
+
}
|
|
6924
|
+
const parsed = parsePendingAnswerResponse(body);
|
|
6925
|
+
if (!parsed || parsed.questionId !== questionId) return;
|
|
6926
|
+
const pending = this.pendingAnswer;
|
|
6927
|
+
this.pendingAnswer = null;
|
|
6928
|
+
this.lastSelectorSignature = null;
|
|
6929
|
+
this.selectorFirstSeenAt = 0;
|
|
6930
|
+
this.deliverAnswer(parsed, pending);
|
|
6931
|
+
} catch (err) {
|
|
6932
|
+
log.trace("streamingEmitter", "pending-answer poll failed", err);
|
|
6933
|
+
}
|
|
6934
|
+
}
|
|
6935
|
+
deliverAnswer(answer, pending) {
|
|
6936
|
+
if (pending.options && pending.options.length > 0) {
|
|
6937
|
+
let targetIndex = answer.optionIndex ?? -1;
|
|
6938
|
+
if (targetIndex < 0) {
|
|
6939
|
+
targetIndex = pending.options.findIndex((o) => o === answer.answer);
|
|
6940
|
+
}
|
|
6941
|
+
if (targetIndex < 0 || targetIndex >= pending.options.length) {
|
|
6942
|
+
log.warn(
|
|
6943
|
+
"streamingEmitter",
|
|
6944
|
+
`answer label "${answer.answer}" not found in selector options \u2014 defaulting to 0`
|
|
6945
|
+
);
|
|
6946
|
+
targetIndex = 0;
|
|
6947
|
+
}
|
|
6948
|
+
this.opts.ptyInput.selectOption(targetIndex, pending.fromIndex ?? 0);
|
|
6949
|
+
return;
|
|
6950
|
+
}
|
|
6951
|
+
this.opts.ptyInput.sendCommand(answer.answer);
|
|
6952
|
+
}
|
|
6953
|
+
// ─── POSTs ─────────────────────────────────────────────────────────
|
|
6954
|
+
async postChunk(event) {
|
|
6955
|
+
await this.postWithRetries(
|
|
6956
|
+
`${this.apiBase}/api/sessions/${encodeURIComponent(this.opts.sessionId)}/streaming-chunk`,
|
|
6957
|
+
event
|
|
6958
|
+
);
|
|
6959
|
+
}
|
|
6960
|
+
async postAwaitingAnswer(event) {
|
|
6961
|
+
await this.postWithRetries(
|
|
6962
|
+
`${this.apiBase}/api/sessions/${encodeURIComponent(this.opts.sessionId)}/awaiting-answer`,
|
|
6963
|
+
event
|
|
6964
|
+
);
|
|
6965
|
+
}
|
|
6966
|
+
async postWithRetries(url, body) {
|
|
6967
|
+
const payload = JSON.stringify(body);
|
|
6968
|
+
for (let attempt = 0; attempt <= MAX_RETRIES2; attempt += 1) {
|
|
6969
|
+
try {
|
|
6970
|
+
const { statusCode, body: resBody } = await _transport4.post(url, this.headers, payload);
|
|
6971
|
+
if (statusCode >= 200 && statusCode < 300) {
|
|
6972
|
+
log.trace("streamingEmitter", `post ok url=${url} status=${statusCode}`);
|
|
6973
|
+
return;
|
|
6974
|
+
}
|
|
6975
|
+
if (statusCode === 410 || statusCode === 404) {
|
|
6976
|
+
log.warn("streamingEmitter", `session dead (status=${statusCode}) \u2014 disabling emitter`);
|
|
6977
|
+
this.active = false;
|
|
6978
|
+
if (this.tickTimer) {
|
|
6979
|
+
clearInterval(this.tickTimer);
|
|
6980
|
+
this.tickTimer = null;
|
|
6981
|
+
}
|
|
6982
|
+
if (this.answerPollTimer) {
|
|
6983
|
+
clearInterval(this.answerPollTimer);
|
|
6984
|
+
this.answerPollTimer = null;
|
|
6985
|
+
}
|
|
6986
|
+
return;
|
|
6987
|
+
}
|
|
6988
|
+
log.warn(
|
|
6989
|
+
"streamingEmitter",
|
|
6990
|
+
`post failed url=${url} status=${statusCode} attempt=${attempt + 1} body=${resBody.slice(0, 200)}`
|
|
6991
|
+
);
|
|
6992
|
+
} catch (err) {
|
|
6993
|
+
log.warn("streamingEmitter", `post error url=${url} attempt=${attempt + 1}`, err);
|
|
6994
|
+
}
|
|
6995
|
+
if (attempt < MAX_RETRIES2) {
|
|
6996
|
+
await sleep2(RETRY_BACKOFF_MS2 * (attempt + 1));
|
|
6997
|
+
}
|
|
6998
|
+
}
|
|
6999
|
+
}
|
|
7000
|
+
};
|
|
7001
|
+
function sleep2(ms) {
|
|
7002
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
7003
|
+
}
|
|
7004
|
+
var TREE_CONTINUATION_RE = /^\s*└/;
|
|
7005
|
+
function classifyLine(line, parseChrome, runtime) {
|
|
7006
|
+
if (line.trim().length === 0) return null;
|
|
7007
|
+
if (TREE_CONTINUATION_RE.test(line)) return "tool_result";
|
|
7008
|
+
const step = parseChrome?.(line) ?? null;
|
|
7009
|
+
if (step) {
|
|
7010
|
+
if (step.tool === "thinking") return "thinking";
|
|
7011
|
+
return "tool_use";
|
|
7012
|
+
}
|
|
7013
|
+
const filtered = runtime.filterTuiOutput([line]);
|
|
7014
|
+
if (filtered.length === 0) return null;
|
|
7015
|
+
return "text";
|
|
7016
|
+
}
|
|
7017
|
+
function parsePendingAnswerResponse(body) {
|
|
7018
|
+
if (!body) return null;
|
|
7019
|
+
let parsed;
|
|
7020
|
+
try {
|
|
7021
|
+
parsed = JSON.parse(body);
|
|
7022
|
+
} catch {
|
|
7023
|
+
return null;
|
|
7024
|
+
}
|
|
7025
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
7026
|
+
const root = parsed;
|
|
7027
|
+
const candidate = root.data && typeof root.data === "object" ? root.data : root;
|
|
7028
|
+
const questionId = candidate.questionId;
|
|
7029
|
+
const answer = candidate.answer;
|
|
7030
|
+
const optionIndex = candidate.optionIndex;
|
|
7031
|
+
if (typeof questionId !== "string" || typeof answer !== "string") return null;
|
|
7032
|
+
const result = { questionId, answer };
|
|
7033
|
+
if (typeof optionIndex === "number" && Number.isInteger(optionIndex)) {
|
|
7034
|
+
result.optionIndex = optionIndex;
|
|
7035
|
+
}
|
|
7036
|
+
return result;
|
|
7037
|
+
}
|
|
7038
|
+
|
|
6128
7039
|
// src/commands/start/quota-fetcher.ts
|
|
6129
7040
|
var inProgress = false;
|
|
6130
7041
|
function fetchQuotaUsage(runtime, historySvc) {
|
|
@@ -6140,13 +7051,13 @@ function fetchQuotaUsage(runtime, historySvc) {
|
|
|
6140
7051
|
}
|
|
6141
7052
|
|
|
6142
7053
|
// src/commands/start/keep-alive.ts
|
|
6143
|
-
var
|
|
7054
|
+
var import_child_process8 = require("child_process");
|
|
6144
7055
|
function buildKeepAlive(ctx) {
|
|
6145
7056
|
let timer = null;
|
|
6146
7057
|
async function setIdleTimeout(minutes) {
|
|
6147
7058
|
if (!ctx.inCodespace || !ctx.codespaceName) return;
|
|
6148
7059
|
await new Promise((resolve2) => {
|
|
6149
|
-
const proc = (0,
|
|
7060
|
+
const proc = (0, import_child_process8.spawn)(
|
|
6150
7061
|
"gh",
|
|
6151
7062
|
[
|
|
6152
7063
|
"api",
|
|
@@ -6185,9 +7096,9 @@ function buildKeepAlive(ctx) {
|
|
|
6185
7096
|
// src/commands/start/handlers.ts
|
|
6186
7097
|
var fs14 = __toESM(require("fs"));
|
|
6187
7098
|
var os13 = __toESM(require("os"));
|
|
6188
|
-
var
|
|
6189
|
-
var
|
|
6190
|
-
var
|
|
7099
|
+
var path19 = __toESM(require("path"));
|
|
7100
|
+
var import_crypto3 = require("crypto");
|
|
7101
|
+
var import_child_process11 = require("child_process");
|
|
6191
7102
|
|
|
6192
7103
|
// src/lib/payload.ts
|
|
6193
7104
|
var import_zod2 = require("zod");
|
|
@@ -6244,7 +7155,7 @@ function parsePayload(schema, raw) {
|
|
|
6244
7155
|
|
|
6245
7156
|
// src/services/file-ops.service.ts
|
|
6246
7157
|
var fs12 = __toESM(require("fs/promises"));
|
|
6247
|
-
var
|
|
7158
|
+
var path16 = __toESM(require("path"));
|
|
6248
7159
|
var MAX_FILE_BYTES = 5 * 1024 * 1024;
|
|
6249
7160
|
var MAX_WALK_DEPTH = 6;
|
|
6250
7161
|
var MAX_VISITED_DIRS = 5e3;
|
|
@@ -6279,8 +7190,8 @@ var SUBDIR_IGNORE = /* @__PURE__ */ new Set([
|
|
|
6279
7190
|
"__pycache__"
|
|
6280
7191
|
]);
|
|
6281
7192
|
function isUnder(parent, candidate) {
|
|
6282
|
-
const rel =
|
|
6283
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
7193
|
+
const rel = path16.relative(parent, candidate);
|
|
7194
|
+
return rel === "" || !rel.startsWith("..") && !path16.isAbsolute(rel);
|
|
6284
7195
|
}
|
|
6285
7196
|
async function isExistingFile(absPath) {
|
|
6286
7197
|
try {
|
|
@@ -6303,7 +7214,7 @@ async function walkForSuffix(dir, needleVariants, depth, ctx) {
|
|
|
6303
7214
|
}
|
|
6304
7215
|
for (const e of entries) {
|
|
6305
7216
|
if (!e.isFile()) continue;
|
|
6306
|
-
const full =
|
|
7217
|
+
const full = path16.join(dir, e.name);
|
|
6307
7218
|
if (needleVariants.some((needle) => full.endsWith(needle))) {
|
|
6308
7219
|
ctx.matches.push(full);
|
|
6309
7220
|
if (ctx.matches.length >= ctx.cap) return;
|
|
@@ -6313,21 +7224,21 @@ async function walkForSuffix(dir, needleVariants, depth, ctx) {
|
|
|
6313
7224
|
if (!e.isDirectory()) continue;
|
|
6314
7225
|
if (SUBDIR_IGNORE.has(e.name)) continue;
|
|
6315
7226
|
if (e.name.startsWith(".") && SUBDIR_IGNORE.has(e.name)) continue;
|
|
6316
|
-
await walkForSuffix(
|
|
7227
|
+
await walkForSuffix(path16.join(dir, e.name), needleVariants, depth + 1, ctx);
|
|
6317
7228
|
if (ctx.matches.length >= ctx.cap) return;
|
|
6318
7229
|
}
|
|
6319
7230
|
}
|
|
6320
7231
|
async function findFile(rawPath) {
|
|
6321
7232
|
const cwd = process.cwd();
|
|
6322
|
-
if (
|
|
6323
|
-
const abs =
|
|
7233
|
+
if (path16.isAbsolute(rawPath)) {
|
|
7234
|
+
const abs = path16.normalize(rawPath);
|
|
6324
7235
|
if (isUnder(cwd, abs) && await isExistingFile(abs)) return abs;
|
|
6325
7236
|
}
|
|
6326
|
-
const direct =
|
|
7237
|
+
const direct = path16.resolve(cwd, rawPath);
|
|
6327
7238
|
if (isUnder(cwd, direct) && await isExistingFile(direct)) return direct;
|
|
6328
|
-
const normalized =
|
|
7239
|
+
const normalized = path16.normalize(rawPath).replace(/^[./\\]+/, "");
|
|
6329
7240
|
const needles = [
|
|
6330
|
-
`${
|
|
7241
|
+
`${path16.sep}${normalized}`,
|
|
6331
7242
|
`/${normalized}`
|
|
6332
7243
|
].filter((v, i, a) => a.indexOf(v) === i);
|
|
6333
7244
|
const ctx = { visited: 0, matches: [], cap: 16 };
|
|
@@ -6341,7 +7252,7 @@ async function findWriteTarget(rawPath) {
|
|
|
6341
7252
|
const found = await findFile(rawPath);
|
|
6342
7253
|
if (found) return found;
|
|
6343
7254
|
const cwd = process.cwd();
|
|
6344
|
-
const fallback =
|
|
7255
|
+
const fallback = path16.isAbsolute(rawPath) ? path16.normalize(rawPath) : path16.resolve(cwd, rawPath);
|
|
6345
7256
|
if (!isUnder(cwd, fallback)) return null;
|
|
6346
7257
|
return fallback;
|
|
6347
7258
|
}
|
|
@@ -6381,7 +7292,7 @@ async function writeProjectFile(rawPath, content) {
|
|
|
6381
7292
|
if (Buffer.byteLength(content, "utf-8") > MAX_FILE_BYTES) {
|
|
6382
7293
|
return { error: "Content too large." };
|
|
6383
7294
|
}
|
|
6384
|
-
await fs12.mkdir(
|
|
7295
|
+
await fs12.mkdir(path16.dirname(abs), { recursive: true });
|
|
6385
7296
|
await fs12.writeFile(abs, content, "utf-8");
|
|
6386
7297
|
return { ok: true };
|
|
6387
7298
|
} catch (e) {
|
|
@@ -6391,11 +7302,11 @@ async function writeProjectFile(rawPath, content) {
|
|
|
6391
7302
|
}
|
|
6392
7303
|
|
|
6393
7304
|
// src/services/project-ops.service.ts
|
|
6394
|
-
var
|
|
7305
|
+
var import_child_process9 = require("child_process");
|
|
6395
7306
|
var import_util2 = require("util");
|
|
6396
7307
|
var fs13 = __toESM(require("fs/promises"));
|
|
6397
|
-
var
|
|
6398
|
-
var execFileP2 = (0, import_util2.promisify)(
|
|
7308
|
+
var path17 = __toESM(require("path"));
|
|
7309
|
+
var execFileP2 = (0, import_util2.promisify)(import_child_process9.execFile);
|
|
6399
7310
|
var PROJECT_IGNORE = /* @__PURE__ */ new Set([
|
|
6400
7311
|
"node_modules",
|
|
6401
7312
|
".git",
|
|
@@ -6452,12 +7363,12 @@ async function listProjectFiles(opts = {}) {
|
|
|
6452
7363
|
return;
|
|
6453
7364
|
}
|
|
6454
7365
|
if (PROJECT_IGNORE.has(e.name)) continue;
|
|
6455
|
-
const full =
|
|
7366
|
+
const full = path17.join(dir, e.name);
|
|
6456
7367
|
if (e.isDirectory()) {
|
|
6457
7368
|
if (depth >= 12) continue;
|
|
6458
7369
|
await walk(full, depth + 1);
|
|
6459
7370
|
} else if (e.isFile()) {
|
|
6460
|
-
const rel =
|
|
7371
|
+
const rel = path17.relative(root, full);
|
|
6461
7372
|
if (q2 && !rel.toLowerCase().includes(q2) && !e.name.toLowerCase().includes(q2)) {
|
|
6462
7373
|
continue;
|
|
6463
7374
|
}
|
|
@@ -6565,7 +7476,7 @@ async function gitStatus(cwd) {
|
|
|
6565
7476
|
let hasMergeInProgress = false;
|
|
6566
7477
|
try {
|
|
6567
7478
|
const gitDir = (await git(["rev-parse", "--git-dir"], root)).stdout.trim();
|
|
6568
|
-
const mergeHead =
|
|
7479
|
+
const mergeHead = path17.isAbsolute(gitDir) ? path17.join(gitDir, "MERGE_HEAD") : path17.join(root, gitDir, "MERGE_HEAD");
|
|
6569
7480
|
await fs13.access(mergeHead);
|
|
6570
7481
|
hasMergeInProgress = true;
|
|
6571
7482
|
} catch {
|
|
@@ -6712,7 +7623,7 @@ async function jsSearchFiles(opts, cwd, cap) {
|
|
|
6712
7623
|
}
|
|
6713
7624
|
let content = "";
|
|
6714
7625
|
try {
|
|
6715
|
-
content = await fs13.readFile(
|
|
7626
|
+
content = await fs13.readFile(path17.join(cwd, f.path), "utf8");
|
|
6716
7627
|
} catch {
|
|
6717
7628
|
continue;
|
|
6718
7629
|
}
|
|
@@ -6735,8 +7646,8 @@ async function jsSearchFiles(opts, cwd, cap) {
|
|
|
6735
7646
|
}
|
|
6736
7647
|
|
|
6737
7648
|
// src/services/terminal-ops.service.ts
|
|
6738
|
-
var
|
|
6739
|
-
var
|
|
7649
|
+
var import_child_process10 = require("child_process");
|
|
7650
|
+
var import_crypto2 = require("crypto");
|
|
6740
7651
|
var import_path = __toESM(require("path"));
|
|
6741
7652
|
var MAX_CONCURRENT_SESSIONS = 4;
|
|
6742
7653
|
var nodePtyModule;
|
|
@@ -6857,7 +7768,7 @@ function createPythonSession(id, shell, cwd, env, cols, rows) {
|
|
|
6857
7768
|
}
|
|
6858
7769
|
let child;
|
|
6859
7770
|
try {
|
|
6860
|
-
child = (0,
|
|
7771
|
+
child = (0, import_child_process10.spawn)(python, ["-c", PYTHON_TERMINAL_HELPER, shell], {
|
|
6861
7772
|
cwd,
|
|
6862
7773
|
env: { ...env, COLUMNS: String(cols), LINES: String(rows) },
|
|
6863
7774
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -6912,7 +7823,7 @@ function openTerminal(opts) {
|
|
|
6912
7823
|
};
|
|
6913
7824
|
const cols = Math.max(1, Math.min(opts.cols ?? 80, 500));
|
|
6914
7825
|
const rows = Math.max(1, Math.min(opts.rows ?? 24, 200));
|
|
6915
|
-
const id = (0,
|
|
7826
|
+
const id = (0, import_crypto2.randomUUID)();
|
|
6916
7827
|
const ptyMod = loadNodePty2();
|
|
6917
7828
|
if (ptyMod) {
|
|
6918
7829
|
try {
|
|
@@ -6992,7 +7903,7 @@ function closeTerminal(sessionId) {
|
|
|
6992
7903
|
function saveFilesTemp(files) {
|
|
6993
7904
|
return files.filter(({ base64 }) => base64 && base64.length > 0).map(({ filename, base64 }) => {
|
|
6994
7905
|
const safeName = filename.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 80);
|
|
6995
|
-
const tmpPath =
|
|
7906
|
+
const tmpPath = path19.join(os13.tmpdir(), `codeam-${(0, import_crypto3.randomUUID)()}-${safeName}`);
|
|
6996
7907
|
fs14.writeFileSync(tmpPath, Buffer.from(base64, "base64"));
|
|
6997
7908
|
return tmpPath;
|
|
6998
7909
|
});
|
|
@@ -7124,7 +8035,7 @@ var sessionTerminated = (ctx) => {
|
|
|
7124
8035
|
} catch {
|
|
7125
8036
|
}
|
|
7126
8037
|
try {
|
|
7127
|
-
const proc = (0,
|
|
8038
|
+
const proc = (0, import_child_process11.spawn)("bash", ["-lc", "pm2 delete codeam-pair >/dev/null 2>&1 || true"], {
|
|
7128
8039
|
detached: true,
|
|
7129
8040
|
stdio: "ignore"
|
|
7130
8041
|
});
|
|
@@ -7146,7 +8057,7 @@ var shutdownSession = async (ctx, cmd) => {
|
|
|
7146
8057
|
}
|
|
7147
8058
|
if (ctx.keepAliveCtx.inCodespace && ctx.keepAliveCtx.codespaceName) {
|
|
7148
8059
|
try {
|
|
7149
|
-
const stopProc = (0,
|
|
8060
|
+
const stopProc = (0, import_child_process11.spawn)(
|
|
7150
8061
|
"bash",
|
|
7151
8062
|
["-lc", `sleep 1; gh codespace stop -c ${JSON.stringify(ctx.keepAliveCtx.codespaceName)} >/dev/null 2>&1 || true`],
|
|
7152
8063
|
{ detached: true, stdio: "ignore" }
|
|
@@ -7156,7 +8067,7 @@ var shutdownSession = async (ctx, cmd) => {
|
|
|
7156
8067
|
}
|
|
7157
8068
|
}
|
|
7158
8069
|
try {
|
|
7159
|
-
const proc = (0,
|
|
8070
|
+
const proc = (0, import_child_process11.spawn)("bash", ["-lc", "pm2 delete codeam-pair >/dev/null 2>&1 || true"], {
|
|
7160
8071
|
detached: true,
|
|
7161
8072
|
stdio: "ignore"
|
|
7162
8073
|
});
|
|
@@ -7375,21 +8286,40 @@ async function start(requestedAgent) {
|
|
|
7375
8286
|
session.pluginAuthToken,
|
|
7376
8287
|
runtime
|
|
7377
8288
|
);
|
|
8289
|
+
const fileWatcher = session.pluginAuthToken ? new FileWatcherService({
|
|
8290
|
+
workingDir: cwd,
|
|
8291
|
+
sessionId: session.id,
|
|
8292
|
+
pluginId,
|
|
8293
|
+
pluginAuthToken: session.pluginAuthToken
|
|
8294
|
+
}) : null;
|
|
8295
|
+
let streamingEmitter = null;
|
|
7378
8296
|
const claude = new AgentService(
|
|
7379
8297
|
runtime,
|
|
7380
8298
|
{
|
|
7381
8299
|
cwd,
|
|
7382
8300
|
onData(raw) {
|
|
7383
8301
|
outputSvc.push(raw);
|
|
8302
|
+
streamingEmitter?.push(raw);
|
|
7384
8303
|
},
|
|
7385
8304
|
onExit(code) {
|
|
7386
8305
|
process.removeListener("SIGINT", sigintHandler);
|
|
7387
8306
|
outputSvc.dispose();
|
|
7388
8307
|
relay.stop();
|
|
8308
|
+
void fileWatcher?.stop();
|
|
8309
|
+
void streamingEmitter?.stop();
|
|
7389
8310
|
process.exit(code);
|
|
7390
8311
|
}
|
|
7391
8312
|
}
|
|
7392
8313
|
);
|
|
8314
|
+
if (session.pluginAuthToken) {
|
|
8315
|
+
streamingEmitter = new StreamingEmitterService({
|
|
8316
|
+
sessionId: session.id,
|
|
8317
|
+
pluginId,
|
|
8318
|
+
pluginAuthToken: session.pluginAuthToken,
|
|
8319
|
+
runtime,
|
|
8320
|
+
ptyInput: claude
|
|
8321
|
+
});
|
|
8322
|
+
}
|
|
7393
8323
|
const ctx = {
|
|
7394
8324
|
outputSvc,
|
|
7395
8325
|
claude,
|
|
@@ -7415,12 +8345,19 @@ async function start(requestedAgent) {
|
|
|
7415
8345
|
claude.kill();
|
|
7416
8346
|
outputSvc.dispose();
|
|
7417
8347
|
relay.stop();
|
|
8348
|
+
void fileWatcher?.stop();
|
|
8349
|
+
void streamingEmitter?.stop();
|
|
7418
8350
|
process.exit(0);
|
|
7419
8351
|
}
|
|
7420
8352
|
process.once("SIGINT", sigintHandler);
|
|
7421
8353
|
await claude.spawn();
|
|
7422
8354
|
await outputSvc.startTerminalTurn();
|
|
7423
8355
|
relay.start();
|
|
8356
|
+
if (fileWatcher) {
|
|
8357
|
+
fileWatcher.start().catch(() => {
|
|
8358
|
+
});
|
|
8359
|
+
}
|
|
8360
|
+
streamingEmitter?.start();
|
|
7424
8361
|
setTimeout(() => {
|
|
7425
8362
|
historySvc.load().catch(() => {
|
|
7426
8363
|
});
|
|
@@ -7429,7 +8366,7 @@ async function start(requestedAgent) {
|
|
|
7429
8366
|
}
|
|
7430
8367
|
|
|
7431
8368
|
// src/commands/pair.ts
|
|
7432
|
-
var
|
|
8369
|
+
var import_crypto4 = require("crypto");
|
|
7433
8370
|
var import_picocolors3 = __toESM(require("picocolors"));
|
|
7434
8371
|
|
|
7435
8372
|
// src/ui/prompts.ts
|
|
@@ -7492,7 +8429,7 @@ async function pair(args2 = []) {
|
|
|
7492
8429
|
const flagAgent = parseAgentFlag(args2);
|
|
7493
8430
|
const agentId = flagAgent ?? await promptForAgent(config.preferredAgent ?? "claude");
|
|
7494
8431
|
showIntro();
|
|
7495
|
-
const pluginId = (0,
|
|
8432
|
+
const pluginId = (0, import_crypto4.randomUUID)();
|
|
7496
8433
|
const spin = dist_exports.spinner();
|
|
7497
8434
|
spin.start("Requesting pairing code...");
|
|
7498
8435
|
const result = await requestCode(pluginId);
|
|
@@ -7548,8 +8485,8 @@ async function pair(args2 = []) {
|
|
|
7548
8485
|
// src/commands/pair-auto.ts
|
|
7549
8486
|
var fs15 = __toESM(require("fs"));
|
|
7550
8487
|
var os14 = __toESM(require("os"));
|
|
7551
|
-
var
|
|
7552
|
-
var
|
|
8488
|
+
var import_crypto5 = require("crypto");
|
|
8489
|
+
var API_BASE7 = process.env.CODEAM_API_URL ?? "https://codeagent-mobile-api.vercel.app";
|
|
7553
8490
|
function fail(msg) {
|
|
7554
8491
|
console.error(`
|
|
7555
8492
|
${msg}
|
|
@@ -7564,12 +8501,12 @@ function readTokenFromArgs(args2) {
|
|
|
7564
8501
|
}
|
|
7565
8502
|
const fileFlag = args2.find((a) => a.startsWith("--token-file="));
|
|
7566
8503
|
if (fileFlag) {
|
|
7567
|
-
const
|
|
8504
|
+
const path25 = fileFlag.slice("--token-file=".length);
|
|
7568
8505
|
try {
|
|
7569
|
-
const content = fs15.readFileSync(
|
|
7570
|
-
if (content.length === 0) fail(`--token-file ${
|
|
8506
|
+
const content = fs15.readFileSync(path25, "utf8").trim();
|
|
8507
|
+
if (content.length === 0) fail(`--token-file ${path25} is empty`);
|
|
7571
8508
|
try {
|
|
7572
|
-
fs15.unlinkSync(
|
|
8509
|
+
fs15.unlinkSync(path25);
|
|
7573
8510
|
} catch {
|
|
7574
8511
|
}
|
|
7575
8512
|
return content;
|
|
@@ -7581,14 +8518,18 @@ function readTokenFromArgs(args2) {
|
|
|
7581
8518
|
fail("codeam pair-auto requires --token-file=<path>, --token=<value>, or CODEAM_AUTO_TOKEN env");
|
|
7582
8519
|
}
|
|
7583
8520
|
async function claim(token, pluginId) {
|
|
7584
|
-
const url = `${
|
|
8521
|
+
const url = `${API_BASE7}/api/pairing/claim-auto-token`;
|
|
7585
8522
|
const body = {
|
|
7586
8523
|
token,
|
|
7587
8524
|
pluginId,
|
|
7588
8525
|
ideName: "codeam-cli (codespace)",
|
|
7589
8526
|
ideVersion: process.env.npm_package_version ?? "unknown",
|
|
7590
8527
|
hostname: os14.hostname(),
|
|
7591
|
-
codespaceName: process.env.CODESPACE_NAME ?? ""
|
|
8528
|
+
codespaceName: process.env.CODESPACE_NAME ?? "",
|
|
8529
|
+
// Current git branch of the codespace's working directory, so the
|
|
8530
|
+
// backend can populate `PairedSession.branch` for the codespace pair.
|
|
8531
|
+
// `null` when detached HEAD / not a git repo.
|
|
8532
|
+
branch: detectCurrentBranch()
|
|
7592
8533
|
};
|
|
7593
8534
|
const res = await fetch(url, {
|
|
7594
8535
|
method: "POST",
|
|
@@ -7610,7 +8551,7 @@ async function claim(token, pluginId) {
|
|
|
7610
8551
|
}
|
|
7611
8552
|
async function pairAuto(args2) {
|
|
7612
8553
|
const token = readTokenFromArgs(args2);
|
|
7613
|
-
const pluginId = (0,
|
|
8554
|
+
const pluginId = (0, import_crypto5.randomUUID)();
|
|
7614
8555
|
console.log(" Claiming pairing token\u2026");
|
|
7615
8556
|
const claimed = await claim(token, pluginId);
|
|
7616
8557
|
if (!isKnownAgentId(claimed.agent)) {
|
|
@@ -7736,11 +8677,11 @@ async function logout() {
|
|
|
7736
8677
|
var import_picocolors9 = __toESM(require("picocolors"));
|
|
7737
8678
|
|
|
7738
8679
|
// src/services/providers/github-codespaces.ts
|
|
7739
|
-
var
|
|
8680
|
+
var import_child_process12 = require("child_process");
|
|
7740
8681
|
var import_util3 = require("util");
|
|
7741
8682
|
var import_picocolors7 = __toESM(require("picocolors"));
|
|
7742
|
-
var
|
|
7743
|
-
var execFileP3 = (0, import_util3.promisify)(
|
|
8683
|
+
var path20 = __toESM(require("path"));
|
|
8684
|
+
var execFileP3 = (0, import_util3.promisify)(import_child_process12.execFile);
|
|
7744
8685
|
var MAX_BUFFER = 8 * 1024 * 1024;
|
|
7745
8686
|
function resetStdinForChild() {
|
|
7746
8687
|
if (process.stdin.isTTY) {
|
|
@@ -7784,7 +8725,7 @@ var GitHubCodespacesProvider = class {
|
|
|
7784
8725
|
if (!isAuthed) {
|
|
7785
8726
|
resetStdinForChild();
|
|
7786
8727
|
await new Promise((resolve2, reject) => {
|
|
7787
|
-
const proc = (0,
|
|
8728
|
+
const proc = (0, import_child_process12.spawn)("gh", ["auth", "login", "-s", "codespace,repo,read:user"], {
|
|
7788
8729
|
stdio: "inherit"
|
|
7789
8730
|
});
|
|
7790
8731
|
proc.on("exit", (code) => {
|
|
@@ -7818,7 +8759,7 @@ var GitHubCodespacesProvider = class {
|
|
|
7818
8759
|
wt(noteLines.join("\n"), "One more permission needed");
|
|
7819
8760
|
resetStdinForChild();
|
|
7820
8761
|
const refreshCode = await new Promise((resolve2, reject) => {
|
|
7821
|
-
const proc = (0,
|
|
8762
|
+
const proc = (0, import_child_process12.spawn)(
|
|
7822
8763
|
"gh",
|
|
7823
8764
|
["auth", "refresh", "-h", "github.com", "-s", "codespace"],
|
|
7824
8765
|
{ stdio: "inherit" }
|
|
@@ -7968,7 +8909,7 @@ var GitHubCodespacesProvider = class {
|
|
|
7968
8909
|
O2.step(`Installing gh via ${installCmd.describe}\u2026`);
|
|
7969
8910
|
resetStdinForChild();
|
|
7970
8911
|
const ok = await new Promise((resolve2) => {
|
|
7971
|
-
const proc = (0,
|
|
8912
|
+
const proc = (0, import_child_process12.spawn)(installCmd.exe, installCmd.args, { stdio: "inherit" });
|
|
7972
8913
|
proc.on("exit", (code) => resolve2(code === 0));
|
|
7973
8914
|
proc.on("error", () => resolve2(false));
|
|
7974
8915
|
});
|
|
@@ -7995,7 +8936,7 @@ var GitHubCodespacesProvider = class {
|
|
|
7995
8936
|
);
|
|
7996
8937
|
resetStdinForChild();
|
|
7997
8938
|
await new Promise((resolve2, reject) => {
|
|
7998
|
-
const proc = (0,
|
|
8939
|
+
const proc = (0, import_child_process12.spawn)(
|
|
7999
8940
|
"gh",
|
|
8000
8941
|
["auth", "refresh", "-h", "github.com", "-s", "repo,read:org"],
|
|
8001
8942
|
{ stdio: "inherit" }
|
|
@@ -8173,7 +9114,7 @@ var GitHubCodespacesProvider = class {
|
|
|
8173
9114
|
async streamCommand(workspaceId, command2) {
|
|
8174
9115
|
resetStdinForChild();
|
|
8175
9116
|
return new Promise((resolve2, reject) => {
|
|
8176
|
-
const proc = (0,
|
|
9117
|
+
const proc = (0, import_child_process12.spawn)(
|
|
8177
9118
|
"gh",
|
|
8178
9119
|
["codespace", "ssh", "-c", workspaceId, "--", "-tt", command2],
|
|
8179
9120
|
{ stdio: "inherit" }
|
|
@@ -8200,11 +9141,11 @@ var GitHubCodespacesProvider = class {
|
|
|
8200
9141
|
`mkdir -p ${shellQuote(remoteDir)} && tar -xzf - -C ${shellQuote(remoteDir)}`
|
|
8201
9142
|
];
|
|
8202
9143
|
await new Promise((resolve2, reject) => {
|
|
8203
|
-
const tar = (0,
|
|
9144
|
+
const tar = (0, import_child_process12.spawn)("tar", tarArgs, {
|
|
8204
9145
|
stdio: ["ignore", "pipe", "pipe"],
|
|
8205
9146
|
env: tarEnv
|
|
8206
9147
|
});
|
|
8207
|
-
const ssh = (0,
|
|
9148
|
+
const ssh = (0, import_child_process12.spawn)("gh", sshArgs, {
|
|
8208
9149
|
stdio: [tar.stdout, "pipe", "pipe"]
|
|
8209
9150
|
});
|
|
8210
9151
|
let tarErr = "";
|
|
@@ -8228,7 +9169,7 @@ var GitHubCodespacesProvider = class {
|
|
|
8228
9169
|
});
|
|
8229
9170
|
}
|
|
8230
9171
|
async uploadFile(workspaceId, remotePath, contents, options = {}) {
|
|
8231
|
-
const remoteDir =
|
|
9172
|
+
const remoteDir = path20.posix.dirname(remotePath);
|
|
8232
9173
|
const parts = [
|
|
8233
9174
|
`mkdir -p ${shellQuote(remoteDir)}`,
|
|
8234
9175
|
`cat > ${shellQuote(remotePath)}`
|
|
@@ -8238,7 +9179,7 @@ var GitHubCodespacesProvider = class {
|
|
|
8238
9179
|
}
|
|
8239
9180
|
const cmd = parts.join(" && ");
|
|
8240
9181
|
await new Promise((resolve2, reject) => {
|
|
8241
|
-
const proc = (0,
|
|
9182
|
+
const proc = (0, import_child_process12.spawn)(
|
|
8242
9183
|
"gh",
|
|
8243
9184
|
["codespace", "ssh", "-c", workspaceId, "--", cmd],
|
|
8244
9185
|
{ stdio: ["pipe", "pipe", "pipe"] }
|
|
@@ -8296,11 +9237,11 @@ function shellQuote(s) {
|
|
|
8296
9237
|
}
|
|
8297
9238
|
|
|
8298
9239
|
// src/services/providers/gitpod.ts
|
|
8299
|
-
var
|
|
9240
|
+
var import_child_process13 = require("child_process");
|
|
8300
9241
|
var import_util4 = require("util");
|
|
8301
|
-
var
|
|
9242
|
+
var path21 = __toESM(require("path"));
|
|
8302
9243
|
var import_picocolors8 = __toESM(require("picocolors"));
|
|
8303
|
-
var execFileP4 = (0, import_util4.promisify)(
|
|
9244
|
+
var execFileP4 = (0, import_util4.promisify)(import_child_process13.execFile);
|
|
8304
9245
|
var MAX_BUFFER2 = 8 * 1024 * 1024;
|
|
8305
9246
|
function resetStdinForChild2() {
|
|
8306
9247
|
if (process.stdin.isTTY) {
|
|
@@ -8340,7 +9281,7 @@ var GitpodProvider = class {
|
|
|
8340
9281
|
);
|
|
8341
9282
|
resetStdinForChild2();
|
|
8342
9283
|
await new Promise((resolve2, reject) => {
|
|
8343
|
-
const proc = (0,
|
|
9284
|
+
const proc = (0, import_child_process13.spawn)("gitpod", ["login"], { stdio: "inherit" });
|
|
8344
9285
|
proc.on("exit", (code) => {
|
|
8345
9286
|
if (code === 0) resolve2();
|
|
8346
9287
|
else reject(new Error("gitpod login failed."));
|
|
@@ -8492,7 +9433,7 @@ var GitpodProvider = class {
|
|
|
8492
9433
|
async streamCommand(workspaceId, command2) {
|
|
8493
9434
|
resetStdinForChild2();
|
|
8494
9435
|
return new Promise((resolve2, reject) => {
|
|
8495
|
-
const proc = (0,
|
|
9436
|
+
const proc = (0, import_child_process13.spawn)(
|
|
8496
9437
|
"gitpod",
|
|
8497
9438
|
["workspace", "ssh", workspaceId, "--", "-tt", command2],
|
|
8498
9439
|
{ stdio: "inherit" }
|
|
@@ -8512,11 +9453,11 @@ var GitpodProvider = class {
|
|
|
8512
9453
|
const tarEnv = { ...process.env, COPYFILE_DISABLE: "1" };
|
|
8513
9454
|
const remoteCmd = `mkdir -p ${shellQuote2(remoteDir)} && tar -xzf - -C ${shellQuote2(remoteDir)}`;
|
|
8514
9455
|
await new Promise((resolve2, reject) => {
|
|
8515
|
-
const tar = (0,
|
|
9456
|
+
const tar = (0, import_child_process13.spawn)("tar", tarArgs, {
|
|
8516
9457
|
stdio: ["ignore", "pipe", "pipe"],
|
|
8517
9458
|
env: tarEnv
|
|
8518
9459
|
});
|
|
8519
|
-
const ssh = (0,
|
|
9460
|
+
const ssh = (0, import_child_process13.spawn)(
|
|
8520
9461
|
"gitpod",
|
|
8521
9462
|
["workspace", "ssh", workspaceId, "--", remoteCmd],
|
|
8522
9463
|
{ stdio: [tar.stdout, "pipe", "pipe"] }
|
|
@@ -8538,7 +9479,7 @@ var GitpodProvider = class {
|
|
|
8538
9479
|
});
|
|
8539
9480
|
}
|
|
8540
9481
|
async uploadFile(workspaceId, remotePath, contents, options = {}) {
|
|
8541
|
-
const remoteDir =
|
|
9482
|
+
const remoteDir = path21.posix.dirname(remotePath);
|
|
8542
9483
|
const parts = [
|
|
8543
9484
|
`mkdir -p ${shellQuote2(remoteDir)}`,
|
|
8544
9485
|
`cat > ${shellQuote2(remotePath)}`
|
|
@@ -8548,7 +9489,7 @@ var GitpodProvider = class {
|
|
|
8548
9489
|
}
|
|
8549
9490
|
const cmd = parts.join(" && ");
|
|
8550
9491
|
await new Promise((resolve2, reject) => {
|
|
8551
|
-
const proc = (0,
|
|
9492
|
+
const proc = (0, import_child_process13.spawn)(
|
|
8552
9493
|
"gitpod",
|
|
8553
9494
|
["workspace", "ssh", workspaceId, "--", cmd],
|
|
8554
9495
|
{ stdio: ["pipe", "pipe", "pipe"] }
|
|
@@ -8572,10 +9513,10 @@ function shellQuote2(s) {
|
|
|
8572
9513
|
}
|
|
8573
9514
|
|
|
8574
9515
|
// src/services/providers/gitlab-workspaces.ts
|
|
8575
|
-
var
|
|
9516
|
+
var import_child_process14 = require("child_process");
|
|
8576
9517
|
var import_util5 = require("util");
|
|
8577
|
-
var
|
|
8578
|
-
var execFileP5 = (0, import_util5.promisify)(
|
|
9518
|
+
var path22 = __toESM(require("path"));
|
|
9519
|
+
var execFileP5 = (0, import_util5.promisify)(import_child_process14.execFile);
|
|
8579
9520
|
var MAX_BUFFER3 = 8 * 1024 * 1024;
|
|
8580
9521
|
var GITLAB_API_BASE = process.env.CODEAM_GITLAB_API_URL ?? "https://gitlab.com/api/v4";
|
|
8581
9522
|
function resetStdinForChild3() {
|
|
@@ -8617,7 +9558,7 @@ var GitLabWorkspacesProvider = class {
|
|
|
8617
9558
|
);
|
|
8618
9559
|
resetStdinForChild3();
|
|
8619
9560
|
await new Promise((resolve2, reject) => {
|
|
8620
|
-
const proc = (0,
|
|
9561
|
+
const proc = (0, import_child_process14.spawn)(
|
|
8621
9562
|
"glab",
|
|
8622
9563
|
["auth", "login", "--scopes", "api,read_user,read_repository"],
|
|
8623
9564
|
{ stdio: "inherit" }
|
|
@@ -8789,7 +9730,7 @@ Docs: https://docs.gitlab.com/ee/user/workspace/configuration.html`
|
|
|
8789
9730
|
const sshHost = process.env.CODEAM_GITLAB_SSH_HOST ?? "workspaces.gitlab.com";
|
|
8790
9731
|
resetStdinForChild3();
|
|
8791
9732
|
return new Promise((resolve2, reject) => {
|
|
8792
|
-
const proc = (0,
|
|
9733
|
+
const proc = (0, import_child_process14.spawn)(
|
|
8793
9734
|
"ssh",
|
|
8794
9735
|
["-tt", "-o", "StrictHostKeyChecking=accept-new", `${workspaceId}@${sshHost}`, command2],
|
|
8795
9736
|
{ stdio: "inherit" }
|
|
@@ -8810,8 +9751,8 @@ Docs: https://docs.gitlab.com/ee/user/workspace/configuration.html`
|
|
|
8810
9751
|
const tarEnv = { ...process.env, COPYFILE_DISABLE: "1" };
|
|
8811
9752
|
const remoteCmd = `mkdir -p ${shellQuote3(remoteDir)} && tar -xzf - -C ${shellQuote3(remoteDir)}`;
|
|
8812
9753
|
await new Promise((resolve2, reject) => {
|
|
8813
|
-
const tar = (0,
|
|
8814
|
-
const ssh = (0,
|
|
9754
|
+
const tar = (0, import_child_process14.spawn)("tar", tarArgs, { stdio: ["ignore", "pipe", "pipe"], env: tarEnv });
|
|
9755
|
+
const ssh = (0, import_child_process14.spawn)(
|
|
8815
9756
|
"ssh",
|
|
8816
9757
|
["-o", "StrictHostKeyChecking=accept-new", `${workspaceId}@${sshHost}`, remoteCmd],
|
|
8817
9758
|
{ stdio: [tar.stdout, "pipe", "pipe"] }
|
|
@@ -8834,14 +9775,14 @@ Docs: https://docs.gitlab.com/ee/user/workspace/configuration.html`
|
|
|
8834
9775
|
}
|
|
8835
9776
|
async uploadFile(workspaceId, remotePath, contents, options = {}) {
|
|
8836
9777
|
const sshHost = process.env.CODEAM_GITLAB_SSH_HOST ?? "workspaces.gitlab.com";
|
|
8837
|
-
const remoteDir =
|
|
9778
|
+
const remoteDir = path22.posix.dirname(remotePath);
|
|
8838
9779
|
const parts = [`mkdir -p ${shellQuote3(remoteDir)}`, `cat > ${shellQuote3(remotePath)}`];
|
|
8839
9780
|
if (options.mode != null) {
|
|
8840
9781
|
parts.push(`chmod ${options.mode.toString(8)} ${shellQuote3(remotePath)}`);
|
|
8841
9782
|
}
|
|
8842
9783
|
const cmd = parts.join(" && ");
|
|
8843
9784
|
await new Promise((resolve2, reject) => {
|
|
8844
|
-
const proc = (0,
|
|
9785
|
+
const proc = (0, import_child_process14.spawn)(
|
|
8845
9786
|
"ssh",
|
|
8846
9787
|
["-o", "StrictHostKeyChecking=accept-new", `${workspaceId}@${sshHost}`, cmd],
|
|
8847
9788
|
{ stdio: ["pipe", "pipe", "pipe"] }
|
|
@@ -8900,10 +9841,10 @@ function shellQuote3(s) {
|
|
|
8900
9841
|
}
|
|
8901
9842
|
|
|
8902
9843
|
// src/services/providers/railway.ts
|
|
8903
|
-
var
|
|
9844
|
+
var import_child_process15 = require("child_process");
|
|
8904
9845
|
var import_util6 = require("util");
|
|
8905
|
-
var
|
|
8906
|
-
var execFileP6 = (0, import_util6.promisify)(
|
|
9846
|
+
var path23 = __toESM(require("path"));
|
|
9847
|
+
var execFileP6 = (0, import_util6.promisify)(import_child_process15.execFile);
|
|
8907
9848
|
var MAX_BUFFER4 = 8 * 1024 * 1024;
|
|
8908
9849
|
function resetStdinForChild4() {
|
|
8909
9850
|
if (process.stdin.isTTY) {
|
|
@@ -8944,7 +9885,7 @@ var RailwayProvider = class {
|
|
|
8944
9885
|
);
|
|
8945
9886
|
resetStdinForChild4();
|
|
8946
9887
|
await new Promise((resolve2, reject) => {
|
|
8947
|
-
const proc = (0,
|
|
9888
|
+
const proc = (0, import_child_process15.spawn)("railway", ["login"], { stdio: "inherit" });
|
|
8948
9889
|
proc.on("exit", (code) => {
|
|
8949
9890
|
if (code === 0) resolve2();
|
|
8950
9891
|
else reject(new Error("railway login failed."));
|
|
@@ -9087,7 +10028,7 @@ var RailwayProvider = class {
|
|
|
9087
10028
|
}
|
|
9088
10029
|
resetStdinForChild4();
|
|
9089
10030
|
return new Promise((resolve2, reject) => {
|
|
9090
|
-
const proc = (0,
|
|
10031
|
+
const proc = (0, import_child_process15.spawn)(
|
|
9091
10032
|
"railway",
|
|
9092
10033
|
["shell", "--project", projectId, "--service", serviceId, "--command", command2],
|
|
9093
10034
|
{ stdio: "inherit" }
|
|
@@ -9111,8 +10052,8 @@ var RailwayProvider = class {
|
|
|
9111
10052
|
const tarEnv = { ...process.env, COPYFILE_DISABLE: "1" };
|
|
9112
10053
|
const remoteCmd = `mkdir -p ${shellQuote4(remoteDir)} && tar -xzf - -C ${shellQuote4(remoteDir)}`;
|
|
9113
10054
|
await new Promise((resolve2, reject) => {
|
|
9114
|
-
const tar = (0,
|
|
9115
|
-
const sh = (0,
|
|
10055
|
+
const tar = (0, import_child_process15.spawn)("tar", tarArgs, { stdio: ["ignore", "pipe", "pipe"], env: tarEnv });
|
|
10056
|
+
const sh = (0, import_child_process15.spawn)(
|
|
9116
10057
|
"railway",
|
|
9117
10058
|
["shell", "--project", projectId, "--service", serviceId, "--command", remoteCmd],
|
|
9118
10059
|
{ stdio: [tar.stdout, "pipe", "pipe"] }
|
|
@@ -9138,14 +10079,14 @@ var RailwayProvider = class {
|
|
|
9138
10079
|
if (!projectId || !serviceId) {
|
|
9139
10080
|
throw new Error("Invalid Railway workspace id (expected projectId/serviceId).");
|
|
9140
10081
|
}
|
|
9141
|
-
const remoteDir =
|
|
10082
|
+
const remoteDir = path23.posix.dirname(remotePath);
|
|
9142
10083
|
const parts = [`mkdir -p ${shellQuote4(remoteDir)}`, `cat > ${shellQuote4(remotePath)}`];
|
|
9143
10084
|
if (options.mode != null) {
|
|
9144
10085
|
parts.push(`chmod ${options.mode.toString(8)} ${shellQuote4(remotePath)}`);
|
|
9145
10086
|
}
|
|
9146
10087
|
const cmd = parts.join(" && ");
|
|
9147
10088
|
await new Promise((resolve2, reject) => {
|
|
9148
|
-
const proc = (0,
|
|
10089
|
+
const proc = (0, import_child_process15.spawn)(
|
|
9149
10090
|
"railway",
|
|
9150
10091
|
["shell", "--project", projectId, "--service", serviceId, "--command", cmd],
|
|
9151
10092
|
{ stdio: ["pipe", "pipe", "pipe"] }
|
|
@@ -9680,7 +10621,7 @@ async function stopWorkspaceFromLocal(target) {
|
|
|
9680
10621
|
// src/commands/version.ts
|
|
9681
10622
|
var import_picocolors11 = __toESM(require("picocolors"));
|
|
9682
10623
|
function version() {
|
|
9683
|
-
const v = true ? "2.15.
|
|
10624
|
+
const v = true ? "2.15.6" : "unknown";
|
|
9684
10625
|
console.log(`${import_picocolors11.default.bold("codeam-cli")} ${import_picocolors11.default.cyan(v)}`);
|
|
9685
10626
|
}
|
|
9686
10627
|
|
|
@@ -9723,16 +10664,16 @@ function help() {
|
|
|
9723
10664
|
// src/lib/updateNotifier.ts
|
|
9724
10665
|
var fs16 = __toESM(require("fs"));
|
|
9725
10666
|
var os15 = __toESM(require("os"));
|
|
9726
|
-
var
|
|
9727
|
-
var
|
|
10667
|
+
var path24 = __toESM(require("path"));
|
|
10668
|
+
var https7 = __toESM(require("https"));
|
|
9728
10669
|
var import_picocolors13 = __toESM(require("picocolors"));
|
|
9729
10670
|
var PKG_NAME = "codeam-cli";
|
|
9730
10671
|
var REGISTRY_URL = `https://registry.npmjs.org/${PKG_NAME}/latest`;
|
|
9731
10672
|
var TTL_MS = 24 * 60 * 60 * 1e3;
|
|
9732
10673
|
var REQUEST_TIMEOUT_MS = 1500;
|
|
9733
10674
|
function cachePath() {
|
|
9734
|
-
const dir =
|
|
9735
|
-
return
|
|
10675
|
+
const dir = path24.join(os15.homedir(), ".codeam");
|
|
10676
|
+
return path24.join(dir, "update-check.json");
|
|
9736
10677
|
}
|
|
9737
10678
|
function readCache() {
|
|
9738
10679
|
try {
|
|
@@ -9747,7 +10688,7 @@ function readCache() {
|
|
|
9747
10688
|
function writeCache(cache) {
|
|
9748
10689
|
try {
|
|
9749
10690
|
const file = cachePath();
|
|
9750
|
-
fs16.mkdirSync(
|
|
10691
|
+
fs16.mkdirSync(path24.dirname(file), { recursive: true });
|
|
9751
10692
|
fs16.writeFileSync(file, JSON.stringify(cache));
|
|
9752
10693
|
} catch {
|
|
9753
10694
|
}
|
|
@@ -9767,7 +10708,7 @@ function compareSemver(a, b) {
|
|
|
9767
10708
|
}
|
|
9768
10709
|
function fetchLatest() {
|
|
9769
10710
|
return new Promise((resolve2) => {
|
|
9770
|
-
const req =
|
|
10711
|
+
const req = https7.get(
|
|
9771
10712
|
REGISTRY_URL,
|
|
9772
10713
|
{ headers: { Accept: "application/json" }, timeout: REQUEST_TIMEOUT_MS },
|
|
9773
10714
|
(res) => {
|
|
@@ -9819,7 +10760,7 @@ function checkForUpdates() {
|
|
|
9819
10760
|
if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
|
|
9820
10761
|
if (process.env.CI) return;
|
|
9821
10762
|
if (!process.stdout.isTTY) return;
|
|
9822
|
-
const current = true ? "2.15.
|
|
10763
|
+
const current = true ? "2.15.6" : null;
|
|
9823
10764
|
if (!current) return;
|
|
9824
10765
|
const cache = readCache();
|
|
9825
10766
|
const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;
|