coderemote 0.3.7 → 0.3.9
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 +1 -1
- package/dist/coderemote.js +1945 -367
- package/package.json +1 -1
package/dist/coderemote.js
CHANGED
|
@@ -1,27 +1,28 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// bin/coderemote.js
|
|
4
|
-
import { execFile as
|
|
5
|
-
import { randomBytes as
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import
|
|
4
|
+
import { execFile as execFile11, execFileSync, spawn as spawn5 } from "node:child_process";
|
|
5
|
+
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
6
|
+
import fs19 from "node:fs";
|
|
7
|
+
import net3 from "node:net";
|
|
8
|
+
import os7 from "node:os";
|
|
9
|
+
import path16 from "node:path";
|
|
10
10
|
import { fileURLToPath } from "node:url";
|
|
11
11
|
import { promisify as promisify4 } from "node:util";
|
|
12
12
|
import qrcode from "qrcode-terminal";
|
|
13
13
|
|
|
14
14
|
// ../../packages/agent-core/src/server.js
|
|
15
|
-
import { createReadStream, createWriteStream as createWriteStream2 } from "node:fs";
|
|
16
|
-
import
|
|
17
|
-
import
|
|
18
|
-
import
|
|
19
|
-
import
|
|
20
|
-
import { pipeline as
|
|
21
|
-
import { randomUUID as
|
|
15
|
+
import { createReadStream as createReadStream2, createWriteStream as createWriteStream2 } from "node:fs";
|
|
16
|
+
import fs13 from "node:fs/promises";
|
|
17
|
+
import http3 from "node:http";
|
|
18
|
+
import os6 from "node:os";
|
|
19
|
+
import path10 from "node:path";
|
|
20
|
+
import { pipeline as pipeline3 } from "node:stream/promises";
|
|
21
|
+
import { randomUUID as randomUUID5, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
|
|
22
22
|
|
|
23
23
|
// ../../packages/protocol/index.js
|
|
24
24
|
var PROTOCOL_VERSION = 4;
|
|
25
|
+
var MIN_CLIENT_PROTOCOL = 4;
|
|
25
26
|
var PAIRING_SCHEME = "vsremote";
|
|
26
27
|
var Routes = Object.freeze({
|
|
27
28
|
info: "GET /api/info",
|
|
@@ -32,6 +33,8 @@ var Routes = Object.freeze({
|
|
|
32
33
|
fsMkdir: "POST /api/fs/mkdir",
|
|
33
34
|
fsRename: "POST /api/fs/rename",
|
|
34
35
|
fsDelete: "DELETE /api/fs",
|
|
36
|
+
// Added within v4: older agents answer 404.
|
|
37
|
+
fsSearch: "GET /api/fs/search",
|
|
35
38
|
termList: "GET /api/term",
|
|
36
39
|
termKill: "DELETE /api/term",
|
|
37
40
|
gitRepos: "GET /api/git/repos",
|
|
@@ -57,12 +60,21 @@ var Routes = Object.freeze({
|
|
|
57
60
|
previewRequestsClear: "DELETE /api/preview/requests",
|
|
58
61
|
tunnelToken: "POST /api/tunnel/token",
|
|
59
62
|
tunnelStatus: "GET /api/tunnel",
|
|
63
|
+
// Added within v4: older agents answer 404.
|
|
64
|
+
tunnelDelete: "DELETE /api/tunnel",
|
|
60
65
|
agentsHook: "POST /api/agents/hook",
|
|
61
66
|
agentsSessions: "GET /api/agents/sessions",
|
|
62
67
|
agentsTimeline: "GET /api/agents/timeline",
|
|
63
68
|
agentsAnswer: "POST /api/agents/answer",
|
|
64
69
|
agentsLaunch: "POST /api/agents/sessions",
|
|
65
70
|
agentsSetup: "GET /api/agents/setup",
|
|
71
|
+
agentsCommands: "GET /api/agents/commands",
|
|
72
|
+
agentsHistory: "GET /api/agents/history",
|
|
73
|
+
buildsSchemes: "GET /api/builds/schemes",
|
|
74
|
+
buildsStart: "POST /api/builds",
|
|
75
|
+
buildsCurrent: "GET /api/builds/current",
|
|
76
|
+
buildsCancel: "POST /api/builds/cancel",
|
|
77
|
+
buildsInstall: "POST /api/builds/install",
|
|
66
78
|
agentsPrompt: "POST /api/agents/prompt",
|
|
67
79
|
agentsKeys: "POST /api/agents/keys",
|
|
68
80
|
agentsStop: "POST /api/agents/stop",
|
|
@@ -192,9 +204,12 @@ var RolloutTail = class {
|
|
|
192
204
|
#partial = "";
|
|
193
205
|
#timer = null;
|
|
194
206
|
#onItem;
|
|
195
|
-
|
|
207
|
+
#onTurnEnd;
|
|
208
|
+
constructor(file, onItem, onTurnEnd = () => {
|
|
209
|
+
}) {
|
|
196
210
|
this.#file = file;
|
|
197
211
|
this.#onItem = onItem;
|
|
212
|
+
this.#onTurnEnd = onTurnEnd;
|
|
198
213
|
this.#timer = setInterval(() => this.poll().catch(() => {
|
|
199
214
|
}), POLL);
|
|
200
215
|
this.#timer.unref?.();
|
|
@@ -220,7 +235,12 @@ var RolloutTail = class {
|
|
|
220
235
|
} catch {
|
|
221
236
|
continue;
|
|
222
237
|
}
|
|
223
|
-
if (record?.type
|
|
238
|
+
if (record?.type !== "event_msg") continue;
|
|
239
|
+
if (record.payload?.type === "item_completed") this.#onItem(record.payload.item);
|
|
240
|
+
else if (record.payload?.type === "task_complete") {
|
|
241
|
+
const message = record.payload.error?.message;
|
|
242
|
+
this.#onTurnEnd(typeof message === "string" && message.trim() ? message.trim() : null);
|
|
243
|
+
}
|
|
224
244
|
}
|
|
225
245
|
} finally {
|
|
226
246
|
await handle.close();
|
|
@@ -284,15 +304,15 @@ async function readTranscript(file, limit = 500) {
|
|
|
284
304
|
} catch {
|
|
285
305
|
return [];
|
|
286
306
|
}
|
|
287
|
-
const
|
|
307
|
+
const entries2 = [];
|
|
288
308
|
for (const line of text.split("\n")) {
|
|
289
309
|
if (!line.trim()) continue;
|
|
290
310
|
try {
|
|
291
|
-
|
|
311
|
+
entries2.push(...entriesOf(JSON.parse(line)));
|
|
292
312
|
} catch {
|
|
293
313
|
}
|
|
294
314
|
}
|
|
295
|
-
return
|
|
315
|
+
return entries2.slice(-limit);
|
|
296
316
|
}
|
|
297
317
|
|
|
298
318
|
// ../../packages/agent-core/src/agents.js
|
|
@@ -302,6 +322,7 @@ var MAX_TITLE = 80;
|
|
|
302
322
|
var ENDED_TTL = 24 * 60 * 60 * 1e3;
|
|
303
323
|
var REQUEST_TTL = 60 * 60 * 1e3;
|
|
304
324
|
var START_TIMEOUT = 15 * 1e3;
|
|
325
|
+
var STOP_GRACE = 3 * 1e3;
|
|
305
326
|
var SEQUENTIAL_REQUEST_TTL = 10 * 60 * 1e3;
|
|
306
327
|
var ADAPTERS = {
|
|
307
328
|
claude: {
|
|
@@ -360,6 +381,7 @@ var AgentSession = class {
|
|
|
360
381
|
this.titled = false;
|
|
361
382
|
this.state = "idle";
|
|
362
383
|
this.terminal = terminal;
|
|
384
|
+
this.app = null;
|
|
363
385
|
this.startedAt = now;
|
|
364
386
|
this.lastActivityAt = now;
|
|
365
387
|
this.endedAt = null;
|
|
@@ -411,7 +433,7 @@ var AgentHub = class extends EventEmitter {
|
|
|
411
433
|
transcript = readTranscript,
|
|
412
434
|
startTimeout = START_TIMEOUT,
|
|
413
435
|
present = async () => false,
|
|
414
|
-
rolloutTail = (file, onItem) => new RolloutTail(file, onItem)
|
|
436
|
+
rolloutTail = (file, onItem, onTurnEnd) => new RolloutTail(file, onItem, onTurnEnd)
|
|
415
437
|
} = {}) {
|
|
416
438
|
super();
|
|
417
439
|
this.#now = now;
|
|
@@ -433,6 +455,17 @@ var AgentHub = class extends EventEmitter {
|
|
|
433
455
|
s.prompts.push({ id, text: comparable(text, paths), at: this.#now() });
|
|
434
456
|
s.prompts = s.prompts.filter((p) => this.#now() - p.at < 6e4).slice(-20);
|
|
435
457
|
}
|
|
458
|
+
/**
|
|
459
|
+
* A client pressed Esc in a running session. Claude Code reports no hook for an interrupted
|
|
460
|
+
* turn (only an idle notification a minute later), so the turn ends here; a hook that shows
|
|
461
|
+
* it still going starts a turn again.
|
|
462
|
+
*/
|
|
463
|
+
interrupted(sessionId) {
|
|
464
|
+
const s = this.#sessions.get(sessionId);
|
|
465
|
+
if (!s || s.state !== "running" || s.starting) return;
|
|
466
|
+
this.#endTurn(s, this.#now());
|
|
467
|
+
this.#emitSession(s);
|
|
468
|
+
}
|
|
436
469
|
/** Whether `resume` may reopen a session: only one no process runs any more. */
|
|
437
470
|
canResume(id) {
|
|
438
471
|
const s = this.#sessions.get(id);
|
|
@@ -477,18 +510,23 @@ var AgentHub = class extends EventEmitter {
|
|
|
477
510
|
session(id) {
|
|
478
511
|
return this.#sessions.get(id)?.toJSON() ?? null;
|
|
479
512
|
}
|
|
513
|
+
/** Where a session runs: one of the agent's terminals, or an app on the computer (or neither known). */
|
|
514
|
+
host(id) {
|
|
515
|
+
const s = this.#sessions.get(id);
|
|
516
|
+
return s ? { terminal: s.terminal, app: s.app } : null;
|
|
517
|
+
}
|
|
480
518
|
timeline(id, after = 0) {
|
|
481
519
|
const s = this.#sessions.get(id);
|
|
482
520
|
if (!s) throw new HttpError(404, "no such session");
|
|
483
|
-
const
|
|
484
|
-
return { entries, next: s.seq, requests: [...s.requests.values()].map((r) => r.request) };
|
|
521
|
+
const entries2 = s.entries.filter((e) => e.seq > after);
|
|
522
|
+
return { entries: entries2, next: s.seq, requests: [...s.requests.values()].map((r) => r.request) };
|
|
485
523
|
}
|
|
486
524
|
/**
|
|
487
525
|
* Takes one hook event. Resolves with the hook's output: for `PermissionRequest` once the
|
|
488
526
|
* request is answered (null when the computer should decide), `{}` for everything else.
|
|
489
527
|
* Aborting `signal` (the hook was killed because the computer answered) withdraws the request.
|
|
490
528
|
*/
|
|
491
|
-
async handle(raw, { agent = "claude", terminal = null, signal } = {}) {
|
|
529
|
+
async handle(raw, { agent = "claude", terminal = null, app = null, signal } = {}) {
|
|
492
530
|
if (!raw || typeof raw.session_id !== "string" || !raw.session_id) {
|
|
493
531
|
throw new HttpError(400, "hook event without session_id");
|
|
494
532
|
}
|
|
@@ -497,8 +535,9 @@ var AgentHub = class extends EventEmitter {
|
|
|
497
535
|
const now = this.#now();
|
|
498
536
|
let replaced = null;
|
|
499
537
|
if (!this.#sessions.has(event.session_id)) {
|
|
538
|
+
const cleared = event.hook_event_name === "SessionStart" && event.source === "clear" && terminal;
|
|
500
539
|
const candidates = [...this.#sessions.values()].filter(
|
|
501
|
-
(p) => p.starting && (terminal ? p.terminal === terminal : p.agent === agent && p.cwd === event.cwd)
|
|
540
|
+
(p) => cleared ? p.terminal === terminal && p.agent === agent : p.starting && (terminal ? p.terminal === terminal : p.agent === agent && p.cwd === event.cwd)
|
|
502
541
|
);
|
|
503
542
|
if (candidates.length === 1) {
|
|
504
543
|
replaced = candidates[0];
|
|
@@ -515,7 +554,7 @@ var AgentHub = class extends EventEmitter {
|
|
|
515
554
|
}
|
|
516
555
|
await s.ready;
|
|
517
556
|
if (s.agent === "codex" && !s.tail && typeof event.transcript_path === "string") {
|
|
518
|
-
s.tail = this.#rolloutTail(event.transcript_path, (item) => this.#fromRollout(s, item));
|
|
557
|
+
s.tail = this.#rolloutTail(event.transcript_path, (item) => this.#fromRollout(s, item), (error) => this.#rolloutTurnEnd(s, error));
|
|
519
558
|
s.tail.poll?.().catch(() => {
|
|
520
559
|
});
|
|
521
560
|
}
|
|
@@ -524,6 +563,7 @@ var AgentHub = class extends EventEmitter {
|
|
|
524
563
|
this.emit("message", { type: "session_removed", session: replaced.id, replacedBy: s.id });
|
|
525
564
|
}
|
|
526
565
|
if (terminal && !s.terminal) s.terminal = terminal;
|
|
566
|
+
if (app) s.app = app;
|
|
527
567
|
s.starting = false;
|
|
528
568
|
s.lastActivityAt = now;
|
|
529
569
|
if (s.state === "ended" && event.hook_event_name !== "SessionEnd") {
|
|
@@ -532,6 +572,7 @@ var AgentHub = class extends EventEmitter {
|
|
|
532
572
|
}
|
|
533
573
|
switch (event.hook_event_name) {
|
|
534
574
|
case "SessionStart":
|
|
575
|
+
if (s.state === "running" && s.turnStartedAt == null) s.state = "idle";
|
|
535
576
|
break;
|
|
536
577
|
case "UserPromptSubmit": {
|
|
537
578
|
const text = typeof event.prompt === "string" ? event.prompt : "";
|
|
@@ -600,11 +641,8 @@ var AgentHub = class extends EventEmitter {
|
|
|
600
641
|
if (last && !turnEntries.some((e) => e.kind === "assistant" && e.text.trim() === last)) {
|
|
601
642
|
this.#entry(s, { kind: "assistant", text: last });
|
|
602
643
|
}
|
|
603
|
-
s.
|
|
604
|
-
|
|
605
|
-
s.turnStartedAt = null;
|
|
606
|
-
s.state = s.requests.size ? "waiting" : "idle";
|
|
607
|
-
this.#entry(s, { kind: "turn_end", duration });
|
|
644
|
+
if (s.turnStartedAt == null && s.entries.at(-1)?.kind === "turn_end") break;
|
|
645
|
+
this.#endTurn(s, now);
|
|
608
646
|
break;
|
|
609
647
|
}
|
|
610
648
|
case "Interrupt":
|
|
@@ -737,13 +775,13 @@ var AgentHub = class extends EventEmitter {
|
|
|
737
775
|
pending.resolve(output);
|
|
738
776
|
}
|
|
739
777
|
async #backfill(s, file) {
|
|
740
|
-
let
|
|
778
|
+
let entries2 = [];
|
|
741
779
|
try {
|
|
742
|
-
|
|
780
|
+
entries2 = await this.#transcript(file, MAX_ENTRIES);
|
|
743
781
|
} catch {
|
|
744
782
|
}
|
|
745
783
|
const tools = /* @__PURE__ */ new Map();
|
|
746
|
-
for (const e of
|
|
784
|
+
for (const e of entries2) {
|
|
747
785
|
if (e.kind === "tool") {
|
|
748
786
|
tools.set(e.toolUseId, e.name);
|
|
749
787
|
e.input = clipInput(e.input);
|
|
@@ -759,6 +797,30 @@ var AgentHub = class extends EventEmitter {
|
|
|
759
797
|
if (s.entries.length > MAX_ENTRIES) s.entries.splice(0, s.entries.length - MAX_ENTRIES);
|
|
760
798
|
}
|
|
761
799
|
/** Something Codex said or a tool's result, from its rollout, unless the hooks told it already. */
|
|
800
|
+
#endTurn(s, now, error = null) {
|
|
801
|
+
s.streaming.clear();
|
|
802
|
+
const duration = s.turnStartedAt ? now - s.turnStartedAt : 0;
|
|
803
|
+
s.turnStartedAt = null;
|
|
804
|
+
s.state = s.requests.size ? "waiting" : "idle";
|
|
805
|
+
this.#entry(s, { kind: "turn_end", duration, ...error ? { error: true, text: error } : {} });
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Codex's rollout says a turn ended. Stop doesn't run when it failed (an unsupported model, a
|
|
809
|
+
* lost connection), so a failed turn ends here, with its error. One that ended normally is
|
|
810
|
+
* left to Stop, which carries the last reply, unless Stop never comes (its hook failed).
|
|
811
|
+
*/
|
|
812
|
+
#rolloutTurnEnd(s, error) {
|
|
813
|
+
if (s.turnStartedAt == null) return;
|
|
814
|
+
const started = s.turnStartedAt;
|
|
815
|
+
const end = () => {
|
|
816
|
+
if (s.turnStartedAt !== started) return;
|
|
817
|
+
this.#endTurn(s, this.#now(), error);
|
|
818
|
+
s.lastActivityAt = this.#now();
|
|
819
|
+
this.#emitSession(s);
|
|
820
|
+
};
|
|
821
|
+
if (error) end();
|
|
822
|
+
else setTimeout(end, STOP_GRACE).unref?.();
|
|
823
|
+
}
|
|
762
824
|
#fromRollout(s, item) {
|
|
763
825
|
const fields = codexItem(item);
|
|
764
826
|
if (!fields) return;
|
|
@@ -789,26 +851,856 @@ var AgentHub = class extends EventEmitter {
|
|
|
789
851
|
s.turnStartedAt = null;
|
|
790
852
|
}
|
|
791
853
|
#entry(s, fields) {
|
|
792
|
-
const
|
|
793
|
-
s.entries.push(
|
|
854
|
+
const entry3 = { seq: ++s.seq, time: this.#now(), ...fields };
|
|
855
|
+
s.entries.push(entry3);
|
|
794
856
|
if (s.entries.length > MAX_ENTRIES) s.entries.shift();
|
|
795
|
-
this.emit("message", { type: "entry", session: s.id, entry:
|
|
857
|
+
this.emit("message", { type: "entry", session: s.id, entry: entry3 });
|
|
796
858
|
}
|
|
797
859
|
#emitSession(s) {
|
|
798
860
|
this.emit("message", { type: "session", session: s.toJSON() });
|
|
799
861
|
}
|
|
800
862
|
};
|
|
801
863
|
|
|
802
|
-
// ../../packages/agent-core/src/
|
|
803
|
-
import { execFile } from "node:child_process";
|
|
864
|
+
// ../../packages/agent-core/src/commands.js
|
|
804
865
|
import fs4 from "node:fs/promises";
|
|
805
866
|
import path3 from "node:path";
|
|
867
|
+
var HEAD_BYTES = 4096;
|
|
868
|
+
var MAX_DESCRIPTION = 300;
|
|
869
|
+
var MAX_DEPTH = 3;
|
|
870
|
+
var CLAUDE = [
|
|
871
|
+
["/add-dir", "Add a working directory", false, "<path>"],
|
|
872
|
+
["/agents", "Manage agent configurations", true],
|
|
873
|
+
["/batch", "Run a task across many files in parallel", false, "<task>"],
|
|
874
|
+
["/claude-api", "Build with the Claude API and Anthropic SDK", false],
|
|
875
|
+
["/clear", "Start a new session with empty context", false],
|
|
876
|
+
["/code-review", "Review the current diff for bugs", false, "[level] [target]"],
|
|
877
|
+
["/color", "Set the prompt bar color for this session", true],
|
|
878
|
+
["/compact", "Clear history but keep a summary in context", false, "[instructions]"],
|
|
879
|
+
["/config", "Open settings", true],
|
|
880
|
+
["/context", "Show context usage", false],
|
|
881
|
+
["/debug", "Debug this session", false, "[issue]"],
|
|
882
|
+
["/doctor", "Check the installation and settings", true],
|
|
883
|
+
["/effort", "Set the reasoning effort", true, "[level]"],
|
|
884
|
+
["/exit", "Exit Claude Code", false],
|
|
885
|
+
["/export", "Export the conversation", true],
|
|
886
|
+
["/fast", "Toggle fast mode", false],
|
|
887
|
+
["/feedback", "Send feedback about Claude Code", true],
|
|
888
|
+
["/fewer-permission-prompts", "Allowlist common read-only commands", false],
|
|
889
|
+
["/goal", "Set or view the goal for this session", false, "[goal]"],
|
|
890
|
+
["/help", "Show help and available commands", true],
|
|
891
|
+
["/hooks", "Manage hook configurations", true],
|
|
892
|
+
["/ide", "Manage IDE integrations", true],
|
|
893
|
+
["/init", "Create a CLAUDE.md with codebase documentation", false],
|
|
894
|
+
["/insights", "Report on your Claude Code sessions", false],
|
|
895
|
+
["/login", "Sign in to your Anthropic account", true],
|
|
896
|
+
["/logout", "Sign out of your Anthropic account", false],
|
|
897
|
+
["/loop", "Run a prompt on a recurring interval", false, "[interval] <prompt>"],
|
|
898
|
+
["/mcp", "Manage MCP servers", true],
|
|
899
|
+
["/memory", "Edit memory files", true],
|
|
900
|
+
["/model", "Set the model", true, "[model]"],
|
|
901
|
+
["/output-style", "Set the output style", true],
|
|
902
|
+
["/permissions", "Manage tool permission rules", true],
|
|
903
|
+
["/plugin", "Manage plugins", true],
|
|
904
|
+
["/recap", "Summarize this session so far", false],
|
|
905
|
+
["/release-notes", "Show release notes", false],
|
|
906
|
+
["/reload-plugins", "Reload plugins", false],
|
|
907
|
+
["/reload-skills", "Reload skills", false],
|
|
908
|
+
["/rename", "Rename this session", false, "<name>"],
|
|
909
|
+
["/resume", "Resume a conversation", true],
|
|
910
|
+
["/rewind", "Restore code or conversation to an earlier point", true],
|
|
911
|
+
["/schedule", "Create or manage scheduled cloud agents", false, "[task]"],
|
|
912
|
+
["/security-review", "Security review of the pending changes", false],
|
|
913
|
+
["/simplify", "Review changed code for cleanups and apply them", false],
|
|
914
|
+
["/status", "Show version, model and account status", true],
|
|
915
|
+
["/statusline", "Set up the status line", false],
|
|
916
|
+
["/terminal-setup", "Set up terminal key bindings", false],
|
|
917
|
+
["/theme", "Change the color theme", true],
|
|
918
|
+
["/ultrareview", "Deep multi-agent review in the cloud", false],
|
|
919
|
+
["/update-config", "Configure Claude Code settings", false, "<change>"],
|
|
920
|
+
["/usage", "Show plan usage limits", true]
|
|
921
|
+
];
|
|
922
|
+
var CODEX = [
|
|
923
|
+
["/agents", "open the agent command center", true],
|
|
924
|
+
["/app", "continue this session in the Desktop app", false],
|
|
925
|
+
["/approve", "approve one retry of a recent auto-review denial", false],
|
|
926
|
+
["/archive", "archive this session", false],
|
|
927
|
+
["/cd", "change the current working directory", false, "<path>"],
|
|
928
|
+
["/clear", "clear the terminal and start a new chat", false],
|
|
929
|
+
["/compact", "summarize conversation to prevent hitting the context limit", false],
|
|
930
|
+
["/config", "configure Codex preferences", true],
|
|
931
|
+
["/copy", "copy the last response or part of it", true],
|
|
932
|
+
["/debug-config", "show config layers and requirement sources for debugging", false],
|
|
933
|
+
["/delete", "permanently delete this session", true],
|
|
934
|
+
["/diff", "show git diff (including untracked files)", false],
|
|
935
|
+
["/exit", "exit Codex", false],
|
|
936
|
+
["/experimental", "toggle experimental features", true],
|
|
937
|
+
["/feedback", "send logs to maintainers", true],
|
|
938
|
+
["/fork", "fork the current chat", false],
|
|
939
|
+
["/goal", "set or view the goal for a long-running task", false, "[goal]"],
|
|
940
|
+
["/hooks", "view and manage lifecycle hooks", true],
|
|
941
|
+
["/ide", "include current selection, open files, and other context from your IDE", false],
|
|
942
|
+
["/import", "import setup, this project, and recent chats from Claude Code", true],
|
|
943
|
+
["/init", "create an AGENTS.md file with instructions for Codex", false],
|
|
944
|
+
["/keymap", "remap TUI shortcuts", true],
|
|
945
|
+
["/logout", "log out of Codex", false],
|
|
946
|
+
["/mcp", "list MCP tools; use /mcp verbose or /mcp login <name>", false, "[verbose | login <name>]"],
|
|
947
|
+
["/mention", "mention a file", true],
|
|
948
|
+
["/model", "choose what model and reasoning effort to use", true],
|
|
949
|
+
["/new", "start a new chat during a conversation", false],
|
|
950
|
+
["/permissions", "choose what Codex is allowed to do", true],
|
|
951
|
+
["/pets", "choose or hide the terminal pet", true],
|
|
952
|
+
["/plan", "switch to Plan mode", false],
|
|
953
|
+
["/plugins", "browse plugins", true],
|
|
954
|
+
["/presets", "run or save reusable prompts and slash commands", true],
|
|
955
|
+
["/ps", "list background terminals", false],
|
|
956
|
+
["/pwd", "show the current working directory", false],
|
|
957
|
+
["/raw", "toggle raw scrollback mode for copy-friendly terminal selection", false],
|
|
958
|
+
["/recap", "summarize the current conversation now", false],
|
|
959
|
+
["/rename", "rename the current thread", false, "<name>"],
|
|
960
|
+
["/resume", "resume a saved chat", true],
|
|
961
|
+
["/review", "review my current changes and find issues", true],
|
|
962
|
+
["/side", "start a side conversation in an ephemeral fork", false],
|
|
963
|
+
["/skills", "use skills to improve how Codex performs specific tasks", true],
|
|
964
|
+
["/status", "show current session configuration and token usage", false],
|
|
965
|
+
["/statusline", "configure which items appear in the status line", true],
|
|
966
|
+
["/stop", "stop all background terminals", false],
|
|
967
|
+
["/subagents", "switch between this session's subagents", true],
|
|
968
|
+
["/theme", "choose a syntax highlighting theme", true],
|
|
969
|
+
["/title", "configure which items appear in the terminal title", true],
|
|
970
|
+
["/tui", "choose the TUI mode for the next launch", true],
|
|
971
|
+
["/usage", "view account usage or use a usage limit reset", true],
|
|
972
|
+
["/vim", "toggle Vim mode for the composer", false],
|
|
973
|
+
["/voice", "start or stop voice; use /voice settings to choose a voice", false],
|
|
974
|
+
["/warnings", "view retained warnings and diagnostic details", true],
|
|
975
|
+
["/worktree", "start or continue a conversation in a new worktree", true]
|
|
976
|
+
];
|
|
977
|
+
var BUILTINS = { claude: CLAUDE, codex: CODEX };
|
|
978
|
+
function builtin([name, description, interactive, argumentHint = ""]) {
|
|
979
|
+
return { name, kind: "command", description, argumentHint, source: "builtin", interactive };
|
|
980
|
+
}
|
|
981
|
+
function unquote(v) {
|
|
982
|
+
const t = v.trim();
|
|
983
|
+
if (/^".*"$/.test(t)) return t.slice(1, -1).replace(/\\(["\\])/g, "$1");
|
|
984
|
+
if (/^'.*'$/.test(t)) return t.slice(1, -1).replace(/''/g, "'");
|
|
985
|
+
return t;
|
|
986
|
+
}
|
|
987
|
+
function frontmatter(text) {
|
|
988
|
+
const lines = text.replace(/^/, "").split(/\r?\n/);
|
|
989
|
+
if (lines[0].trim() !== "---") return null;
|
|
990
|
+
const end = lines.indexOf("---", 1);
|
|
991
|
+
if (end < 0) return null;
|
|
992
|
+
const out = {};
|
|
993
|
+
let block = null;
|
|
994
|
+
for (const line of lines.slice(1, end)) {
|
|
995
|
+
const m = /^([A-Za-z][\w-]*):\s*(.*)$/.exec(line);
|
|
996
|
+
if (m) {
|
|
997
|
+
block = [">", "|", ">-", "|-", ""].includes(m[2].trim()) ? m[1] : null;
|
|
998
|
+
out[m[1]] = block ? "" : unquote(m[2]);
|
|
999
|
+
} else if (block && /^\s+\S/.test(line)) {
|
|
1000
|
+
out[block] = `${out[block]} ${line.trim()}`.trim();
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
return out;
|
|
1004
|
+
}
|
|
1005
|
+
async function readHead(file) {
|
|
1006
|
+
let handle;
|
|
1007
|
+
try {
|
|
1008
|
+
handle = await fs4.open(file, "r");
|
|
1009
|
+
const { buffer, bytesRead } = await handle.read(Buffer.alloc(HEAD_BYTES), 0, HEAD_BYTES, 0);
|
|
1010
|
+
return frontmatter(buffer.subarray(0, bytesRead).toString("utf8")) ?? {};
|
|
1011
|
+
} catch {
|
|
1012
|
+
return {};
|
|
1013
|
+
} finally {
|
|
1014
|
+
await handle?.close();
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
async function entries(dir) {
|
|
1018
|
+
try {
|
|
1019
|
+
return await fs4.readdir(dir, { withFileTypes: true });
|
|
1020
|
+
} catch {
|
|
1021
|
+
return [];
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
async function readJson(file) {
|
|
1025
|
+
try {
|
|
1026
|
+
return JSON.parse(await fs4.readFile(file, "utf8"));
|
|
1027
|
+
} catch {
|
|
1028
|
+
return null;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
function entry(name, kind, head, source) {
|
|
1032
|
+
return {
|
|
1033
|
+
name,
|
|
1034
|
+
kind,
|
|
1035
|
+
description: String(head.description ?? "").slice(0, MAX_DESCRIPTION),
|
|
1036
|
+
argumentHint: kind === "command" ? String(head["argument-hint"] ?? "") : "",
|
|
1037
|
+
source,
|
|
1038
|
+
interactive: false
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
async function commandFiles(dir, prefix, source, depth = 0, parts = []) {
|
|
1042
|
+
const out = [];
|
|
1043
|
+
for (const e of await entries(dir)) {
|
|
1044
|
+
if (e.name.startsWith(".")) continue;
|
|
1045
|
+
const file = path3.join(dir, e.name);
|
|
1046
|
+
if (e.isDirectory() && depth < MAX_DEPTH) {
|
|
1047
|
+
out.push(...await commandFiles(file, prefix, source, depth + 1, [...parts, e.name]));
|
|
1048
|
+
} else if (e.isFile() && e.name.endsWith(".md")) {
|
|
1049
|
+
const name = `/${prefix}${[...parts, e.name.slice(0, -3)].join(":")}`;
|
|
1050
|
+
out.push(entry(name, "command", await readHead(file), source));
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
return out;
|
|
1054
|
+
}
|
|
1055
|
+
async function skillFiles(dir, { sigil = "/", prefix = "", source, kind = "command" }) {
|
|
1056
|
+
const out = [];
|
|
1057
|
+
for (const e of await entries(dir)) {
|
|
1058
|
+
if (!e.isDirectory() || e.name.startsWith(".")) continue;
|
|
1059
|
+
const file = path3.join(dir, e.name, "SKILL.md");
|
|
1060
|
+
try {
|
|
1061
|
+
await fs4.access(file);
|
|
1062
|
+
} catch {
|
|
1063
|
+
continue;
|
|
1064
|
+
}
|
|
1065
|
+
const head = await readHead(file);
|
|
1066
|
+
if (String(head["user-invocable"]).toLowerCase() === "false") continue;
|
|
1067
|
+
const name = /^[\w.:-]+$/.test(head.name ?? "") ? head.name : e.name;
|
|
1068
|
+
out.push(entry(`${sigil}${prefix}${name}`, kind, head, source));
|
|
1069
|
+
}
|
|
1070
|
+
return out;
|
|
1071
|
+
}
|
|
1072
|
+
async function claudePlugins(home, cwd) {
|
|
1073
|
+
const installed = (await readJson(path3.join(home, ".claude", "plugins", "installed_plugins.json")))?.plugins ?? {};
|
|
1074
|
+
const enabled = {};
|
|
1075
|
+
for (const file of [
|
|
1076
|
+
path3.join(home, ".claude", "settings.json"),
|
|
1077
|
+
path3.join(cwd, ".claude", "settings.json"),
|
|
1078
|
+
path3.join(cwd, ".claude", "settings.local.json")
|
|
1079
|
+
]) {
|
|
1080
|
+
Object.assign(enabled, (await readJson(file))?.enabledPlugins ?? {});
|
|
1081
|
+
}
|
|
1082
|
+
const out = [];
|
|
1083
|
+
for (const [key, installs] of Object.entries(installed)) {
|
|
1084
|
+
if (enabled[key] !== true || !Array.isArray(installs)) continue;
|
|
1085
|
+
const install = installs.find((i) => !i.projectPath || path3.resolve(i.projectPath) === path3.resolve(cwd));
|
|
1086
|
+
if (install?.installPath) out.push({ name: key.split("@")[0], root: install.installPath });
|
|
1087
|
+
}
|
|
1088
|
+
return out;
|
|
1089
|
+
}
|
|
1090
|
+
async function claudeCommands(home, cwd) {
|
|
1091
|
+
const user = path3.join(home, ".claude");
|
|
1092
|
+
const project = path3.join(cwd, ".claude");
|
|
1093
|
+
const found = [
|
|
1094
|
+
...await commandFiles(path3.join(user, "commands"), "", "user"),
|
|
1095
|
+
...await skillFiles(path3.join(user, "skills"), { source: "user" })
|
|
1096
|
+
];
|
|
1097
|
+
for (const e of await entries(path3.join(user, "skills", "synced"))) {
|
|
1098
|
+
if (e.isDirectory() && !e.name.startsWith(".")) {
|
|
1099
|
+
found.push(...await skillFiles(path3.join(user, "skills", "synced", e.name), { prefix: "anthropic-skills:", source: "user" }));
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
if (path3.resolve(project) !== path3.resolve(user)) {
|
|
1103
|
+
found.push(
|
|
1104
|
+
...await commandFiles(path3.join(project, "commands"), "", "project"),
|
|
1105
|
+
...await skillFiles(path3.join(project, "skills"), { source: "project" })
|
|
1106
|
+
);
|
|
1107
|
+
}
|
|
1108
|
+
for (const plugin of await claudePlugins(home, cwd)) {
|
|
1109
|
+
found.push(
|
|
1110
|
+
...await commandFiles(path3.join(plugin.root, "commands"), `${plugin.name}:`, "plugin"),
|
|
1111
|
+
...await skillFiles(path3.join(plugin.root, "skills"), { prefix: `${plugin.name}:`, source: "plugin" })
|
|
1112
|
+
);
|
|
1113
|
+
}
|
|
1114
|
+
return found;
|
|
1115
|
+
}
|
|
1116
|
+
async function codexDisabled(codexHome) {
|
|
1117
|
+
const off = /* @__PURE__ */ new Set();
|
|
1118
|
+
let text = "";
|
|
1119
|
+
try {
|
|
1120
|
+
text = await fs4.readFile(path3.join(codexHome, "config.toml"), "utf8");
|
|
1121
|
+
} catch {
|
|
1122
|
+
}
|
|
1123
|
+
let section = null;
|
|
1124
|
+
for (const line of text.split(/\r?\n/)) {
|
|
1125
|
+
const header = /^\s*\[plugins\."([^"]+)"\]\s*$/.exec(line);
|
|
1126
|
+
if (header) section = header[1];
|
|
1127
|
+
else if (/^\s*\[/.test(line)) section = null;
|
|
1128
|
+
else if (section && /^\s*enabled\s*=\s*false\b/.test(line)) off.add(section);
|
|
1129
|
+
}
|
|
1130
|
+
return off;
|
|
1131
|
+
}
|
|
1132
|
+
async function latestVersion(dir) {
|
|
1133
|
+
let best = null;
|
|
1134
|
+
for (const e of await entries(dir)) {
|
|
1135
|
+
if (!e.isDirectory() || e.name.startsWith(".")) continue;
|
|
1136
|
+
const { mtimeMs } = await fs4.stat(path3.join(dir, e.name));
|
|
1137
|
+
if (!best || mtimeMs > best.mtimeMs) best = { dir: path3.join(dir, e.name), mtimeMs };
|
|
1138
|
+
}
|
|
1139
|
+
return best?.dir ?? null;
|
|
1140
|
+
}
|
|
1141
|
+
async function projectDirs(cwd, home) {
|
|
1142
|
+
const dirs = [];
|
|
1143
|
+
for (let dir = path3.resolve(cwd); dir !== path3.resolve(home) && dir !== path3.dirname(dir); dir = path3.dirname(dir)) {
|
|
1144
|
+
dirs.push(dir);
|
|
1145
|
+
try {
|
|
1146
|
+
await fs4.access(path3.join(dir, ".git"));
|
|
1147
|
+
break;
|
|
1148
|
+
} catch {
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
return dirs;
|
|
1152
|
+
}
|
|
1153
|
+
async function codexMentions(home, codexHome, cwd) {
|
|
1154
|
+
const mention = { sigil: "$", kind: "mention" };
|
|
1155
|
+
const skills = path3.join(codexHome, "skills");
|
|
1156
|
+
const found = [
|
|
1157
|
+
...await skillFiles(skills, { ...mention, source: "user" }),
|
|
1158
|
+
...await skillFiles(path3.join(skills, ".system"), { ...mention, source: "user" })
|
|
1159
|
+
];
|
|
1160
|
+
for (const dir of await projectDirs(cwd, home)) {
|
|
1161
|
+
found.push(...await skillFiles(path3.join(dir, ".agents", "skills"), { ...mention, source: "project" }));
|
|
1162
|
+
}
|
|
1163
|
+
const off = await codexDisabled(codexHome);
|
|
1164
|
+
const cache = path3.join(codexHome, "plugins", "cache");
|
|
1165
|
+
for (const market of await entries(cache)) {
|
|
1166
|
+
if (!market.isDirectory()) continue;
|
|
1167
|
+
for (const plugin of await entries(path3.join(cache, market.name))) {
|
|
1168
|
+
if (!plugin.isDirectory() || off.has(`${plugin.name}@${market.name}`)) continue;
|
|
1169
|
+
const root2 = await latestVersion(path3.join(cache, market.name, plugin.name));
|
|
1170
|
+
if (root2) found.push(...await skillFiles(path3.join(root2, "skills"), { ...mention, prefix: `${plugin.name}:`, source: "plugin" }));
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
return found;
|
|
1174
|
+
}
|
|
1175
|
+
async function listCommands({ agent, cwd, home, codexHome = path3.join(home, ".codex") }) {
|
|
1176
|
+
const builtins = (BUILTINS[agent] ?? BUILTINS.claude).map(builtin);
|
|
1177
|
+
const found = agent === "codex" ? await codexMentions(home, codexHome, cwd) : await claudeCommands(home, cwd);
|
|
1178
|
+
const byName = new Map(builtins.map((c) => [c.name, c]));
|
|
1179
|
+
for (const c of found) byName.set(c.name, c);
|
|
1180
|
+
const rest2 = [...byName.values()].filter((c) => c.source !== "builtin").sort((a, b) => a.name.localeCompare(b.name));
|
|
1181
|
+
return [...byName.values()].filter((c) => c.source === "builtin").concat(rest2);
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
// ../../packages/agent-core/src/builds.js
|
|
1185
|
+
import { execFile, spawn } from "node:child_process";
|
|
1186
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
1187
|
+
import { EventEmitter as EventEmitter2 } from "node:events";
|
|
1188
|
+
import fs5 from "node:fs/promises";
|
|
1189
|
+
import os from "node:os";
|
|
1190
|
+
import path4 from "node:path";
|
|
1191
|
+
var MAX_LINES = 5e3;
|
|
1192
|
+
var MAX_LINE = 2048;
|
|
1193
|
+
var MAX_ERRORS = 20;
|
|
1194
|
+
var TAIL_LINES = 15;
|
|
1195
|
+
var KILL_AFTER = 5e3;
|
|
1196
|
+
var SIGNING_HINTS = [
|
|
1197
|
+
[/requires a development team/i, "Set a team in Xcode: select the app target, then Signing & Capabilities \u203A Team."],
|
|
1198
|
+
[/No Account for Team|No accounts? (?:with|for|in)/i, "Sign in to Xcode with an Apple Account of the project's team (Xcode \u203A Settings \u203A Accounts)."],
|
|
1199
|
+
[
|
|
1200
|
+
/No profiles for|requires a provisioning profile|doesn't include (?:the )?(?:currently selected )?device|not registered|no devices/i,
|
|
1201
|
+
"Run the app on this iPhone from Xcode once, so it is registered for development builds."
|
|
1202
|
+
]
|
|
1203
|
+
];
|
|
1204
|
+
async function findContainer(folder) {
|
|
1205
|
+
let names;
|
|
1206
|
+
try {
|
|
1207
|
+
names = await fs5.readdir(folder);
|
|
1208
|
+
} catch {
|
|
1209
|
+
return null;
|
|
1210
|
+
}
|
|
1211
|
+
for (const [ext, kind] of [[".xcworkspace", "workspace"], [".xcodeproj", "project"]]) {
|
|
1212
|
+
const name = names.filter((n) => n.endsWith(ext)).sort()[0];
|
|
1213
|
+
if (name) return { container: path4.join(folder, name), kind };
|
|
1214
|
+
}
|
|
1215
|
+
return null;
|
|
1216
|
+
}
|
|
1217
|
+
function run(command, args, { cwd, timeout = 12e4 } = {}) {
|
|
1218
|
+
return new Promise((resolve, reject) => {
|
|
1219
|
+
execFile(command, args, { cwd, timeout, maxBuffer: 16 * 1024 * 1024 }, (error, stdout, stderr) => {
|
|
1220
|
+
if (error) reject(Object.assign(new HttpError(409, (stderr || error.message).trim().split("\n").slice(-3).join("\n")), { code: "EXCODE" }));
|
|
1221
|
+
else resolve(stdout);
|
|
1222
|
+
});
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
async function listSchemes(folder, { xcodebuild = "xcodebuild" } = {}) {
|
|
1226
|
+
const found = await findContainer(folder);
|
|
1227
|
+
if (!found) throw new HttpError(404, "no Xcode project or workspace in this folder");
|
|
1228
|
+
const out = JSON.parse(await run(xcodebuild, ["-list", "-json", `-${found.kind}`, found.container], { cwd: folder }));
|
|
1229
|
+
const schemes = out[found.kind]?.schemes ?? [];
|
|
1230
|
+
return { container: path4.basename(found.container), kind: found.kind, schemes };
|
|
1231
|
+
}
|
|
1232
|
+
function failureOf(lines) {
|
|
1233
|
+
const compile = [...new Set(lines.filter((l) => /^\S.*?:\d+:(?:\d+:)? error: /.test(l)))];
|
|
1234
|
+
if (compile.length) return compile.slice(0, MAX_ERRORS).join("\n");
|
|
1235
|
+
const errors = [...new Set(lines.filter((l) => /^(?:xcodebuild: )?error: /.test(l.trim())).map((l) => l.trim()))];
|
|
1236
|
+
const text = (errors.length ? errors.slice(0, MAX_ERRORS) : lines.slice(-TAIL_LINES)).join("\n");
|
|
1237
|
+
const hint = SIGNING_HINTS.find(([re]) => re.test(text))?.[1];
|
|
1238
|
+
return hint ? `${hint}
|
|
1239
|
+
|
|
1240
|
+
${text}` : text;
|
|
1241
|
+
}
|
|
1242
|
+
function exportOptions() {
|
|
1243
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
1244
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1245
|
+
<plist version="1.0"><dict>
|
|
1246
|
+
<key>method</key><string>debugging</string>
|
|
1247
|
+
<key>signingStyle</key><string>automatic</string>
|
|
1248
|
+
<key>compileBitcode</key><false/>
|
|
1249
|
+
<key>thinning</key><string><none></string>
|
|
1250
|
+
</dict></plist>
|
|
1251
|
+
`;
|
|
1252
|
+
}
|
|
1253
|
+
async function readInfo(appPath) {
|
|
1254
|
+
const info = JSON.parse(await run("plutil", ["-convert", "json", "-o", "-", path4.join(appPath, "Info.plist")]));
|
|
1255
|
+
return {
|
|
1256
|
+
bundleId: info.CFBundleIdentifier,
|
|
1257
|
+
version: info.CFBundleShortVersionString ?? info.CFBundleVersion ?? "1.0",
|
|
1258
|
+
name: info.CFBundleDisplayName ?? info.CFBundleName ?? path4.basename(appPath, ".app")
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
var BuildRunner = class extends EventEmitter2 {
|
|
1262
|
+
#xcodebuild;
|
|
1263
|
+
#readInfo;
|
|
1264
|
+
#dir;
|
|
1265
|
+
#now;
|
|
1266
|
+
#killAfter;
|
|
1267
|
+
#build = null;
|
|
1268
|
+
#lines = [];
|
|
1269
|
+
#child = null;
|
|
1270
|
+
constructor({ xcodebuild = "xcodebuild", readInfo: info = readInfo, dir = path4.join(os.homedir(), ".coderemote", "builds"), now = Date.now, killAfter = KILL_AFTER } = {}) {
|
|
1271
|
+
super();
|
|
1272
|
+
this.#xcodebuild = xcodebuild;
|
|
1273
|
+
this.#readInfo = info;
|
|
1274
|
+
this.#dir = dir;
|
|
1275
|
+
this.#now = now;
|
|
1276
|
+
this.#killAfter = killAfter;
|
|
1277
|
+
}
|
|
1278
|
+
get running() {
|
|
1279
|
+
return this.#build?.state === "running";
|
|
1280
|
+
}
|
|
1281
|
+
/** The latest build with its log lines after `after` (a line count), or null before any. */
|
|
1282
|
+
current(after = 0) {
|
|
1283
|
+
if (!this.#build) return null;
|
|
1284
|
+
const { first } = this.#build;
|
|
1285
|
+
const from = Math.max(after, first);
|
|
1286
|
+
return { ...this.#public(), log: this.#lines.slice(from - first), next: first + this.#lines.length };
|
|
1287
|
+
}
|
|
1288
|
+
/** The `.ipa` and app fields of the latest build, once it succeeded. */
|
|
1289
|
+
artifact() {
|
|
1290
|
+
return this.#build?.state === "succeeded" ? this.#build.artifact : null;
|
|
1291
|
+
}
|
|
1292
|
+
/** Starts building `scheme` of the project in `folder`; answers the build. */
|
|
1293
|
+
async start({ folder, scheme, configuration = "Debug" }) {
|
|
1294
|
+
if (this.running) throw new HttpError(409, "a build is already running");
|
|
1295
|
+
if (typeof scheme !== "string" || !scheme) throw new HttpError(400, "scheme is required");
|
|
1296
|
+
if (!/^[\w .-]+$/.test(configuration)) throw new HttpError(400, "invalid configuration");
|
|
1297
|
+
const found = await findContainer(folder);
|
|
1298
|
+
if (!found) throw new HttpError(404, "no Xcode project or workspace in this folder");
|
|
1299
|
+
const id = randomUUID2();
|
|
1300
|
+
const out = path4.join(this.#dir, id);
|
|
1301
|
+
await fs5.rm(this.#dir, { recursive: true, force: true });
|
|
1302
|
+
await fs5.mkdir(out, { recursive: true });
|
|
1303
|
+
this.#lines = [];
|
|
1304
|
+
this.#build = { id, path: folder, scheme, configuration, state: "running", step: "archiving", startedAt: this.#now(), endedAt: null, error: null, first: 0, artifact: null };
|
|
1305
|
+
const build = this.#build;
|
|
1306
|
+
this.#run(build, found, out).catch(() => {
|
|
1307
|
+
});
|
|
1308
|
+
return this.#public();
|
|
1309
|
+
}
|
|
1310
|
+
/** Stops the running build. */
|
|
1311
|
+
cancel() {
|
|
1312
|
+
if (!this.running) throw new HttpError(409, "no build is running");
|
|
1313
|
+
this.#build.cancelled = true;
|
|
1314
|
+
this.#stop();
|
|
1315
|
+
}
|
|
1316
|
+
#public() {
|
|
1317
|
+
const { id, path: folder, scheme, configuration, state, step, startedAt, endedAt, error, artifact } = this.#build;
|
|
1318
|
+
return { id, path: folder, scheme, configuration, state, step, startedAt, endedAt, error, app: artifact?.app ?? null };
|
|
1319
|
+
}
|
|
1320
|
+
#log(text) {
|
|
1321
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
1322
|
+
if (!raw) continue;
|
|
1323
|
+
this.#lines.push(raw.length > MAX_LINE ? `${raw.slice(0, MAX_LINE)}\u2026` : raw);
|
|
1324
|
+
}
|
|
1325
|
+
const extra = this.#lines.length - MAX_LINES;
|
|
1326
|
+
if (extra > 0) {
|
|
1327
|
+
this.#lines.splice(0, extra);
|
|
1328
|
+
this.#build.first += extra;
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
async #run(build, { container, kind }, out) {
|
|
1332
|
+
const archive = path4.join(out, "App.xcarchive");
|
|
1333
|
+
const exported = path4.join(out, "export");
|
|
1334
|
+
try {
|
|
1335
|
+
await this.#xcode(build, [
|
|
1336
|
+
`-${kind}`,
|
|
1337
|
+
container,
|
|
1338
|
+
"-scheme",
|
|
1339
|
+
build.scheme,
|
|
1340
|
+
"-configuration",
|
|
1341
|
+
build.configuration,
|
|
1342
|
+
"-destination",
|
|
1343
|
+
"generic/platform=iOS",
|
|
1344
|
+
"-archivePath",
|
|
1345
|
+
archive,
|
|
1346
|
+
"-allowProvisioningUpdates",
|
|
1347
|
+
"-skipPackagePluginValidation",
|
|
1348
|
+
"archive"
|
|
1349
|
+
], build.path);
|
|
1350
|
+
build.step = "exporting";
|
|
1351
|
+
const options = path4.join(out, "ExportOptions.plist");
|
|
1352
|
+
await fs5.writeFile(options, exportOptions());
|
|
1353
|
+
await this.#xcode(build, [
|
|
1354
|
+
"-exportArchive",
|
|
1355
|
+
"-archivePath",
|
|
1356
|
+
archive,
|
|
1357
|
+
"-exportPath",
|
|
1358
|
+
exported,
|
|
1359
|
+
"-exportOptionsPlist",
|
|
1360
|
+
options,
|
|
1361
|
+
"-allowProvisioningUpdates"
|
|
1362
|
+
], build.path);
|
|
1363
|
+
const ipaName = (await fs5.readdir(exported)).find((n) => n.endsWith(".ipa"));
|
|
1364
|
+
if (!ipaName) throw new Error("the export made no .ipa");
|
|
1365
|
+
const apps = path4.join(archive, "Products", "Applications");
|
|
1366
|
+
const appName = (await fs5.readdir(apps)).find((n) => n.endsWith(".app"));
|
|
1367
|
+
if (!appName) throw new Error("the archive holds no app");
|
|
1368
|
+
const ipa = path4.join(exported, ipaName);
|
|
1369
|
+
const { size } = await fs5.stat(ipa);
|
|
1370
|
+
build.artifact = { ipa, app: { ...await this.#readInfo(path4.join(apps, appName)), size } };
|
|
1371
|
+
build.state = "succeeded";
|
|
1372
|
+
} catch (e) {
|
|
1373
|
+
if (build.cancelled) {
|
|
1374
|
+
build.state = "cancelled";
|
|
1375
|
+
} else {
|
|
1376
|
+
build.state = "failed";
|
|
1377
|
+
build.error = e.xcodeFailed ? failureOf(this.#lines) : e.message;
|
|
1378
|
+
}
|
|
1379
|
+
} finally {
|
|
1380
|
+
build.endedAt = this.#now();
|
|
1381
|
+
this.#child = null;
|
|
1382
|
+
if (build === this.#build) this.emit("ended", this.#public());
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
#xcode(build, args, cwd) {
|
|
1386
|
+
return new Promise((resolve, reject) => {
|
|
1387
|
+
if (build.cancelled) return reject(new Error("cancelled"));
|
|
1388
|
+
this.#log(`$ xcodebuild ${args.join(" ")}`);
|
|
1389
|
+
const child = spawn(this.#xcodebuild, args, { cwd, detached: true, stdio: ["ignore", "pipe", "pipe"] });
|
|
1390
|
+
this.#child = child;
|
|
1391
|
+
let partial = "";
|
|
1392
|
+
const take = (chunk) => {
|
|
1393
|
+
const text = partial + chunk.toString("utf8");
|
|
1394
|
+
const cut = text.lastIndexOf("\n");
|
|
1395
|
+
partial = cut < 0 ? text : text.slice(cut + 1);
|
|
1396
|
+
if (cut >= 0) this.#log(text.slice(0, cut));
|
|
1397
|
+
};
|
|
1398
|
+
child.stdout.on("data", take);
|
|
1399
|
+
child.stderr.on("data", take);
|
|
1400
|
+
child.on("error", (e) => reject(e.code === "ENOENT" ? new Error("xcodebuild was not found: install Xcode on the computer") : e));
|
|
1401
|
+
child.on("close", (code) => {
|
|
1402
|
+
if (partial) this.#log(partial);
|
|
1403
|
+
if (code === 0) resolve();
|
|
1404
|
+
else reject(Object.assign(new Error(`xcodebuild exited with ${code}`), { xcodeFailed: true }));
|
|
1405
|
+
});
|
|
1406
|
+
});
|
|
1407
|
+
}
|
|
1408
|
+
#stop() {
|
|
1409
|
+
const child = this.#child;
|
|
1410
|
+
if (!child?.pid) return;
|
|
1411
|
+
const signal = (s) => {
|
|
1412
|
+
try {
|
|
1413
|
+
process.kill(-child.pid, s);
|
|
1414
|
+
} catch {
|
|
1415
|
+
}
|
|
1416
|
+
};
|
|
1417
|
+
signal("SIGTERM");
|
|
1418
|
+
setTimeout(() => {
|
|
1419
|
+
if (child.exitCode === null && child.signalCode === null) signal("SIGKILL");
|
|
1420
|
+
}, this.#killAfter).unref?.();
|
|
1421
|
+
}
|
|
1422
|
+
};
|
|
1423
|
+
|
|
1424
|
+
// ../../packages/agent-core/src/history.js
|
|
1425
|
+
import fs6 from "node:fs/promises";
|
|
1426
|
+
import path5 from "node:path";
|
|
1427
|
+
var LIMIT = 50;
|
|
1428
|
+
var HEAD_BYTES2 = 128 * 1024;
|
|
1429
|
+
var TAIL_BYTES2 = 64 * 1024;
|
|
1430
|
+
var MAX_TITLE2 = 80;
|
|
1431
|
+
async function slice(file, position, length) {
|
|
1432
|
+
let handle;
|
|
1433
|
+
try {
|
|
1434
|
+
handle = await fs6.open(file, "r");
|
|
1435
|
+
const { size } = await handle.stat();
|
|
1436
|
+
const start2 = position < 0 ? Math.max(0, size + position) : position;
|
|
1437
|
+
const { buffer, bytesRead } = await handle.read(Buffer.alloc(Math.min(length, size)), 0, Math.min(length, size), start2);
|
|
1438
|
+
return buffer.subarray(0, bytesRead).toString("utf8");
|
|
1439
|
+
} catch {
|
|
1440
|
+
return "";
|
|
1441
|
+
} finally {
|
|
1442
|
+
await handle?.close();
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
function records(text) {
|
|
1446
|
+
const out = [];
|
|
1447
|
+
for (const line of text.split("\n")) {
|
|
1448
|
+
try {
|
|
1449
|
+
out.push(JSON.parse(line));
|
|
1450
|
+
} catch {
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
return out;
|
|
1454
|
+
}
|
|
1455
|
+
function titleOf2(text) {
|
|
1456
|
+
const line = String(text).trim().split("\n")[0].trim();
|
|
1457
|
+
return line.length > MAX_TITLE2 ? `${line.slice(0, MAX_TITLE2 - 1)}\u2026` : line;
|
|
1458
|
+
}
|
|
1459
|
+
function typedPrompt(record) {
|
|
1460
|
+
if (record.type !== "user" || record.isMeta || record.isSidechain) return null;
|
|
1461
|
+
const content = record.message?.content;
|
|
1462
|
+
const text = typeof content === "string" ? content : Array.isArray(content) ? content.find((c) => c?.type === "text")?.text : null;
|
|
1463
|
+
if (typeof text !== "string") return null;
|
|
1464
|
+
const t = text.trim();
|
|
1465
|
+
return t && !t.startsWith("<") && !t.startsWith("[Request interrupted") ? t : null;
|
|
1466
|
+
}
|
|
1467
|
+
async function claudeHistory(home, cwd) {
|
|
1468
|
+
const dir = path5.join(home, ".claude", "projects", path5.resolve(cwd).replace(/[^a-zA-Z0-9]/g, "-"));
|
|
1469
|
+
let names = [];
|
|
1470
|
+
try {
|
|
1471
|
+
names = (await fs6.readdir(dir)).filter((n) => n.endsWith(".jsonl"));
|
|
1472
|
+
} catch {
|
|
1473
|
+
return [];
|
|
1474
|
+
}
|
|
1475
|
+
const files = [];
|
|
1476
|
+
for (const name of names) {
|
|
1477
|
+
try {
|
|
1478
|
+
const { mtimeMs, size } = await fs6.stat(path5.join(dir, name));
|
|
1479
|
+
files.push({ id: name.slice(0, -6), file: path5.join(dir, name), mtimeMs, size });
|
|
1480
|
+
} catch {
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
files.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1484
|
+
const out = [];
|
|
1485
|
+
for (const f of files) {
|
|
1486
|
+
if (out.length >= LIMIT) break;
|
|
1487
|
+
const tail = records(await slice(f.file, -TAIL_BYTES2, TAIL_BYTES2));
|
|
1488
|
+
const named = tail.findLast((r) => r.type === "custom-title" && r.customTitle)?.customTitle ?? tail.findLast((r) => r.type === "ai-title" && r.aiTitle)?.aiTitle;
|
|
1489
|
+
const head = named ? [] : records(await slice(f.file, 0, HEAD_BYTES2));
|
|
1490
|
+
const prompt = named ? null : head.map(typedPrompt).find(Boolean);
|
|
1491
|
+
if (!named && !prompt) continue;
|
|
1492
|
+
out.push({
|
|
1493
|
+
id: f.id,
|
|
1494
|
+
title: titleOf2(named ?? prompt),
|
|
1495
|
+
updatedAt: Math.round(f.mtimeMs),
|
|
1496
|
+
branch: tail.findLast((r) => typeof r.gitBranch === "string")?.gitBranch ?? null,
|
|
1497
|
+
size: f.size
|
|
1498
|
+
});
|
|
1499
|
+
}
|
|
1500
|
+
return out;
|
|
1501
|
+
}
|
|
1502
|
+
async function codexHistory(codexHome, cwd) {
|
|
1503
|
+
const names = /* @__PURE__ */ new Map();
|
|
1504
|
+
try {
|
|
1505
|
+
for (const r of records(await fs6.readFile(path5.join(codexHome, "session_index.jsonl"), "utf8"))) {
|
|
1506
|
+
if (typeof r.id === "string" && typeof r.thread_name === "string") names.set(r.id, r.thread_name);
|
|
1507
|
+
}
|
|
1508
|
+
} catch {
|
|
1509
|
+
}
|
|
1510
|
+
const files = [];
|
|
1511
|
+
const walk = async (dir, depth) => {
|
|
1512
|
+
let entries2 = [];
|
|
1513
|
+
try {
|
|
1514
|
+
entries2 = await fs6.readdir(dir, { withFileTypes: true });
|
|
1515
|
+
} catch {
|
|
1516
|
+
return;
|
|
1517
|
+
}
|
|
1518
|
+
for (const e of entries2) {
|
|
1519
|
+
const p = path5.join(dir, e.name);
|
|
1520
|
+
if (e.isDirectory() && depth < 3) await walk(p, depth + 1);
|
|
1521
|
+
else if (e.isFile() && /^rollout-.*\.jsonl$/.test(e.name)) {
|
|
1522
|
+
try {
|
|
1523
|
+
const { mtimeMs, size } = await fs6.stat(p);
|
|
1524
|
+
files.push({ file: p, mtimeMs, size });
|
|
1525
|
+
} catch {
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
};
|
|
1530
|
+
await walk(path5.join(codexHome, "sessions"), 0);
|
|
1531
|
+
files.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1532
|
+
const want = path5.resolve(cwd);
|
|
1533
|
+
const out = [];
|
|
1534
|
+
for (const f of files) {
|
|
1535
|
+
if (out.length >= LIMIT) break;
|
|
1536
|
+
const head = await slice(f.file, 0, HEAD_BYTES2);
|
|
1537
|
+
const meta = records(head.split("\n")[0])[0]?.payload;
|
|
1538
|
+
if (!meta || typeof meta.id !== "string" || typeof meta.cwd !== "string" || path5.resolve(meta.cwd) !== want) continue;
|
|
1539
|
+
const prompt = records(head).filter((r) => r.type === "response_item" && r.payload?.type === "message" && r.payload.role === "user").flatMap((r) => Array.isArray(r.payload.content) ? r.payload.content : []).map((c) => c?.type === "input_text" && typeof c.text === "string" ? c.text.trim() : "").find((t) => t && !t.startsWith("<") && !t.startsWith("# AGENTS.md"));
|
|
1540
|
+
const title = names.get(meta.id) ?? prompt;
|
|
1541
|
+
if (!title) continue;
|
|
1542
|
+
out.push({
|
|
1543
|
+
id: meta.id,
|
|
1544
|
+
title: titleOf2(title),
|
|
1545
|
+
updatedAt: Math.round(f.mtimeMs),
|
|
1546
|
+
branch: typeof meta.git?.branch === "string" ? meta.git.branch : null,
|
|
1547
|
+
size: f.size
|
|
1548
|
+
});
|
|
1549
|
+
}
|
|
1550
|
+
return out;
|
|
1551
|
+
}
|
|
1552
|
+
async function listHistory({ agent, cwd, home, codexHome = path5.join(home, ".codex") }) {
|
|
1553
|
+
return agent === "codex" ? codexHistory(codexHome, cwd) : claudeHistory(home, cwd);
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
// ../../packages/agent-core/src/install-links.js
|
|
1557
|
+
import { randomBytes } from "node:crypto";
|
|
1558
|
+
import { createReadStream } from "node:fs";
|
|
1559
|
+
import fs7 from "node:fs/promises";
|
|
1560
|
+
import http from "node:http";
|
|
1561
|
+
import { pipeline } from "node:stream/promises";
|
|
1562
|
+
var TTL = 10 * 60 * 1e3;
|
|
1563
|
+
var SHARE_HOURS = 1;
|
|
1564
|
+
var xml = (s) => String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1565
|
+
function manifest({ ipaUrl, bundleId, version, title }) {
|
|
1566
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
1567
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1568
|
+
<plist version="1.0"><dict>
|
|
1569
|
+
<key>items</key><array><dict>
|
|
1570
|
+
<key>assets</key><array><dict>
|
|
1571
|
+
<key>kind</key><string>software-package</string>
|
|
1572
|
+
<key>url</key><string>${xml(ipaUrl)}</string>
|
|
1573
|
+
</dict></array>
|
|
1574
|
+
<key>metadata</key><dict>
|
|
1575
|
+
<key>bundle-identifier</key><string>${xml(bundleId)}</string>
|
|
1576
|
+
<key>bundle-version</key><string>${xml(version)}</string>
|
|
1577
|
+
<key>kind</key><string>software</string>
|
|
1578
|
+
<key>title</key><string>${xml(title)}</string>
|
|
1579
|
+
</dict>
|
|
1580
|
+
</dict></array>
|
|
1581
|
+
</dict></plist>
|
|
1582
|
+
`;
|
|
1583
|
+
}
|
|
1584
|
+
var InstallLinks = class {
|
|
1585
|
+
#preview;
|
|
1586
|
+
#now;
|
|
1587
|
+
#ttl;
|
|
1588
|
+
#setTimer;
|
|
1589
|
+
#server = null;
|
|
1590
|
+
#port = null;
|
|
1591
|
+
#base = null;
|
|
1592
|
+
#tokens = /* @__PURE__ */ new Map();
|
|
1593
|
+
#serial = Promise.resolve();
|
|
1594
|
+
constructor({ preview: preview2, now = Date.now, ttl = TTL, setTimer = (fn, ms) => setTimeout(fn, ms).unref?.() } = {}) {
|
|
1595
|
+
this.#preview = preview2;
|
|
1596
|
+
this.#now = now;
|
|
1597
|
+
this.#ttl = ttl;
|
|
1598
|
+
this.#setTimer = setTimer;
|
|
1599
|
+
}
|
|
1600
|
+
/** A link installing `ipa` (an app `{ bundleId, version, name }`): `{ url, expiresAt }`. */
|
|
1601
|
+
create({ ipa, app }) {
|
|
1602
|
+
return this.#queue(async () => {
|
|
1603
|
+
await this.#listen();
|
|
1604
|
+
if (!this.#base) {
|
|
1605
|
+
const { url } = await this.#preview.forward(this.#port);
|
|
1606
|
+
await this.#preview.share(this.#port, SHARE_HOURS);
|
|
1607
|
+
this.#base = url.replace(/\/+$/, "");
|
|
1608
|
+
}
|
|
1609
|
+
const token = randomBytes(24).toString("hex");
|
|
1610
|
+
const expiresAt = this.#now() + this.#ttl;
|
|
1611
|
+
const manifestUrl = `${this.#base}/${token}/manifest.plist`;
|
|
1612
|
+
this.#tokens.set(token, {
|
|
1613
|
+
ipa,
|
|
1614
|
+
manifest: manifest({ ipaUrl: `${this.#base}/${token}/app.ipa`, bundleId: app.bundleId, version: app.version, title: app.name })
|
|
1615
|
+
});
|
|
1616
|
+
this.#setTimer(() => this.#expire(token), this.#ttl);
|
|
1617
|
+
return { url: `itms-services://?action=download-manifest&url=${encodeURIComponent(manifestUrl)}`, expiresAt };
|
|
1618
|
+
});
|
|
1619
|
+
}
|
|
1620
|
+
/** Whether links are being served through the preview tunnel right now. */
|
|
1621
|
+
get sharing() {
|
|
1622
|
+
return this.#base !== null;
|
|
1623
|
+
}
|
|
1624
|
+
/** Ends every link now and closes the public window (the Ports tab's Stop). */
|
|
1625
|
+
stop() {
|
|
1626
|
+
const tokens = [...this.#tokens.keys()];
|
|
1627
|
+
if (!tokens.length) return this.#queue(async () => {
|
|
1628
|
+
});
|
|
1629
|
+
for (const token of tokens.slice(0, -1)) this.#tokens.delete(token);
|
|
1630
|
+
this.#expire(tokens.at(-1));
|
|
1631
|
+
return this.#queue(async () => {
|
|
1632
|
+
});
|
|
1633
|
+
}
|
|
1634
|
+
/** Live tokens, for tests. */
|
|
1635
|
+
get size() {
|
|
1636
|
+
return this.#tokens.size;
|
|
1637
|
+
}
|
|
1638
|
+
/** The local port the files are served on (null until first used). */
|
|
1639
|
+
get port() {
|
|
1640
|
+
return this.#port;
|
|
1641
|
+
}
|
|
1642
|
+
close() {
|
|
1643
|
+
this.#tokens.clear();
|
|
1644
|
+
this.#server?.close();
|
|
1645
|
+
}
|
|
1646
|
+
#queue(fn) {
|
|
1647
|
+
const next = this.#serial.then(fn, fn);
|
|
1648
|
+
this.#serial = next.catch(() => {
|
|
1649
|
+
});
|
|
1650
|
+
return next;
|
|
1651
|
+
}
|
|
1652
|
+
#expire(token) {
|
|
1653
|
+
if (!this.#tokens.delete(token) || this.#tokens.size) return;
|
|
1654
|
+
this.#queue(async () => {
|
|
1655
|
+
if (this.#tokens.size || !this.#base) return;
|
|
1656
|
+
this.#base = null;
|
|
1657
|
+
try {
|
|
1658
|
+
await this.#preview.unshare(this.#port);
|
|
1659
|
+
await this.#preview.unforward(this.#port);
|
|
1660
|
+
} catch {
|
|
1661
|
+
}
|
|
1662
|
+
});
|
|
1663
|
+
}
|
|
1664
|
+
async #listen() {
|
|
1665
|
+
if (this.#server) return;
|
|
1666
|
+
this.#server = http.createServer((req, res) => this.#serve(req, res).catch(() => res.destroy()));
|
|
1667
|
+
await new Promise((resolve, reject) => {
|
|
1668
|
+
this.#server.once("error", reject);
|
|
1669
|
+
this.#server.listen(0, "127.0.0.1", resolve);
|
|
1670
|
+
});
|
|
1671
|
+
this.#port = this.#server.address().port;
|
|
1672
|
+
}
|
|
1673
|
+
async #serve(req, res) {
|
|
1674
|
+
const m = /^\/([0-9a-f]{48})\/(manifest\.plist|app\.ipa)$/.exec(req.url.split("?")[0]);
|
|
1675
|
+
const link = m && this.#tokens.get(m[1]);
|
|
1676
|
+
if (!link || req.method !== "GET" && req.method !== "HEAD") {
|
|
1677
|
+
res.writeHead(404).end();
|
|
1678
|
+
return;
|
|
1679
|
+
}
|
|
1680
|
+
if (m[2] === "manifest.plist") {
|
|
1681
|
+
const body = Buffer.from(link.manifest);
|
|
1682
|
+
res.writeHead(200, { "content-type": "text/xml", "content-length": body.length, "cache-control": "no-store" });
|
|
1683
|
+
res.end(req.method === "HEAD" ? void 0 : body);
|
|
1684
|
+
return;
|
|
1685
|
+
}
|
|
1686
|
+
const { size } = await fs7.stat(link.ipa);
|
|
1687
|
+
res.writeHead(200, { "content-type": "application/octet-stream", "content-length": size, "cache-control": "no-store" });
|
|
1688
|
+
if (req.method === "HEAD") return res.end();
|
|
1689
|
+
await pipeline(createReadStream(link.ipa), res);
|
|
1690
|
+
this.#expire(m[1]);
|
|
1691
|
+
}
|
|
1692
|
+
};
|
|
1693
|
+
|
|
1694
|
+
// ../../packages/agent-core/src/git.js
|
|
1695
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
1696
|
+
import fs8 from "node:fs/promises";
|
|
1697
|
+
import path6 from "node:path";
|
|
806
1698
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".build", "build", "dist", "DerivedData", "Pods", "vendor", ".venv", "target"]);
|
|
807
1699
|
var MAX_SCAN_DEPTH = 4;
|
|
808
1700
|
var MAX_DIFF_BYTES = 2 * 1024 * 1024;
|
|
809
1701
|
function git(cwd, args, { timeout = 3e4, okCodes = [0], maxBuffer = 16 * 1024 * 1024 } = {}) {
|
|
810
1702
|
return new Promise((resolve, reject) => {
|
|
811
|
-
|
|
1703
|
+
execFile2(
|
|
812
1704
|
"git",
|
|
813
1705
|
args,
|
|
814
1706
|
{
|
|
@@ -829,30 +1721,36 @@ function git(cwd, args, { timeout = 3e4, okCodes = [0], maxBuffer = 16 * 1024 *
|
|
|
829
1721
|
async function findRepos(root2) {
|
|
830
1722
|
const repos = [];
|
|
831
1723
|
async function walk(dir, depth) {
|
|
832
|
-
let
|
|
1724
|
+
let entries2;
|
|
833
1725
|
try {
|
|
834
|
-
|
|
1726
|
+
entries2 = await fs8.readdir(dir, { withFileTypes: true });
|
|
835
1727
|
} catch {
|
|
836
1728
|
return;
|
|
837
1729
|
}
|
|
838
|
-
if (
|
|
839
|
-
repos.push(dir);
|
|
840
|
-
return;
|
|
841
|
-
}
|
|
1730
|
+
if (entries2.some((e) => e.name === ".git")) repos.push(dir);
|
|
842
1731
|
if (depth >= MAX_SCAN_DEPTH) return;
|
|
843
|
-
const subdirs =
|
|
844
|
-
await Promise.all(subdirs.map((e) => walk(
|
|
1732
|
+
const subdirs = entries2.filter((e) => e.isDirectory() && !e.name.startsWith(".") && !SKIP_DIRS.has(e.name));
|
|
1733
|
+
await Promise.all(subdirs.map((e) => walk(path6.join(dir, e.name), depth + 1)));
|
|
845
1734
|
}
|
|
846
1735
|
await walk(root2, 0);
|
|
847
|
-
repos.sort((a, b) => a.split(
|
|
1736
|
+
repos.sort((a, b) => a.split(path6.sep).length - b.split(path6.sep).length || a.localeCompare(b));
|
|
848
1737
|
return Promise.all(
|
|
849
1738
|
repos.map(async (repo) => ({
|
|
850
1739
|
path: repo,
|
|
851
|
-
name: repo === root2 ?
|
|
852
|
-
branch: await currentBranch(repo)
|
|
1740
|
+
name: repo === root2 ? path6.basename(root2) : path6.relative(root2, repo),
|
|
1741
|
+
branch: await currentBranch(repo),
|
|
1742
|
+
...await summary(repo)
|
|
853
1743
|
}))
|
|
854
1744
|
);
|
|
855
1745
|
}
|
|
1746
|
+
async function summary(repo) {
|
|
1747
|
+
try {
|
|
1748
|
+
const { files, ahead, behind } = await status(repo);
|
|
1749
|
+
return { changes: files.length, ahead, behind };
|
|
1750
|
+
} catch {
|
|
1751
|
+
return { changes: null, ahead: null, behind: null };
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
856
1754
|
async function currentBranch(repo) {
|
|
857
1755
|
try {
|
|
858
1756
|
const { stdout } = await git(repo, ["symbolic-ref", "--short", "-q", "HEAD"], { okCodes: [0, 1] });
|
|
@@ -866,9 +1764,9 @@ async function currentBranch(repo) {
|
|
|
866
1764
|
async function status(repo) {
|
|
867
1765
|
const { stdout } = await git(repo, ["status", "--porcelain=v2", "--branch", "--untracked-files=all", "-z"]);
|
|
868
1766
|
const result = { branch: null, upstream: null, ahead: 0, behind: 0, files: [] };
|
|
869
|
-
const
|
|
870
|
-
for (let i = 0; i <
|
|
871
|
-
const r =
|
|
1767
|
+
const records2 = stdout.split("\0");
|
|
1768
|
+
for (let i = 0; i < records2.length; i++) {
|
|
1769
|
+
const r = records2[i];
|
|
872
1770
|
if (!r) continue;
|
|
873
1771
|
if (r.startsWith("# branch.head ")) {
|
|
874
1772
|
const head = r.slice(14);
|
|
@@ -881,20 +1779,20 @@ async function status(repo) {
|
|
|
881
1779
|
result.behind = Math.abs(parseInt(b, 10));
|
|
882
1780
|
} else if (r.startsWith("1 ")) {
|
|
883
1781
|
const parts = r.split(" ");
|
|
884
|
-
result.files.push(
|
|
1782
|
+
result.files.push(entry2(parts[1], parts.slice(8).join(" ")));
|
|
885
1783
|
} else if (r.startsWith("2 ")) {
|
|
886
1784
|
const parts = r.split(" ");
|
|
887
|
-
result.files.push({ ...
|
|
1785
|
+
result.files.push({ ...entry2(parts[1], parts.slice(9).join(" ")), origPath: records2[++i] });
|
|
888
1786
|
} else if (r.startsWith("u ")) {
|
|
889
1787
|
const parts = r.split(" ");
|
|
890
|
-
result.files.push({ ...
|
|
1788
|
+
result.files.push({ ...entry2(parts[1], parts.slice(10).join(" ")), conflicted: true });
|
|
891
1789
|
} else if (r.startsWith("? ")) {
|
|
892
1790
|
result.files.push({ path: r.slice(2), index: ".", worktree: "?" });
|
|
893
1791
|
}
|
|
894
1792
|
}
|
|
895
1793
|
return result;
|
|
896
1794
|
}
|
|
897
|
-
function
|
|
1795
|
+
function entry2(xy, filePath) {
|
|
898
1796
|
return { path: filePath, index: xy[0], worktree: xy[1] };
|
|
899
1797
|
}
|
|
900
1798
|
async function diff(repo, file, { staged = false, untracked = false } = {}) {
|
|
@@ -1051,32 +1949,33 @@ var LANDING_HTML = `<!doctype html>
|
|
|
1051
1949
|
`;
|
|
1052
1950
|
|
|
1053
1951
|
// ../../packages/agent-core/src/ports.js
|
|
1054
|
-
import { execFile as
|
|
1055
|
-
import
|
|
1056
|
-
import
|
|
1952
|
+
import { execFile as execFile4 } from "node:child_process";
|
|
1953
|
+
import fs10 from "node:fs/promises";
|
|
1954
|
+
import net from "node:net";
|
|
1955
|
+
import os2 from "node:os";
|
|
1057
1956
|
|
|
1058
1957
|
// ../../packages/agent-core/src/platform.js
|
|
1059
|
-
import { execFile as
|
|
1060
|
-
import
|
|
1061
|
-
import
|
|
1958
|
+
import { execFile as execFile3, spawn as spawn2 } from "node:child_process";
|
|
1959
|
+
import fs9 from "node:fs";
|
|
1960
|
+
import path7 from "node:path";
|
|
1062
1961
|
var isWindows = process.platform === "win32";
|
|
1063
1962
|
function shellQuote(arg) {
|
|
1064
1963
|
return `'${String(arg).replace(/'/g, `'\\''`)}'`;
|
|
1065
1964
|
}
|
|
1066
1965
|
function runnable(file) {
|
|
1067
1966
|
try {
|
|
1068
|
-
if (!isWindows)
|
|
1069
|
-
return
|
|
1967
|
+
if (!isWindows) fs9.accessSync(file, fs9.constants.X_OK);
|
|
1968
|
+
return fs9.statSync(file).isFile();
|
|
1070
1969
|
} catch {
|
|
1071
1970
|
return false;
|
|
1072
1971
|
}
|
|
1073
1972
|
}
|
|
1074
1973
|
function findExecutable(name, env = process.env) {
|
|
1075
|
-
const exts = isWindows && !
|
|
1974
|
+
const exts = isWindows && !path7.extname(name) ? (env.PATHEXT || ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean) : [""];
|
|
1076
1975
|
const match = (base2) => exts.map((e) => base2 + e).find(runnable) ?? null;
|
|
1077
|
-
if (name.includes("/") || name.includes(
|
|
1078
|
-
for (const dir of (env.PATH ?? env.Path ?? "").split(
|
|
1079
|
-
const found = dir && match(
|
|
1976
|
+
if (name.includes("/") || name.includes(path7.sep)) return match(path7.resolve(name));
|
|
1977
|
+
for (const dir of (env.PATH ?? env.Path ?? "").split(path7.delimiter)) {
|
|
1978
|
+
const found = dir && match(path7.join(dir.replace(/^"(.*)"$/, "$1"), name));
|
|
1080
1979
|
if (found) return found;
|
|
1081
1980
|
}
|
|
1082
1981
|
return null;
|
|
@@ -1105,12 +2004,12 @@ function terminalCommand(argv2) {
|
|
|
1105
2004
|
}
|
|
1106
2005
|
function spawnCommand(argv2, options = {}) {
|
|
1107
2006
|
const { file, args, verbatim } = resolveCommand(argv2);
|
|
1108
|
-
return
|
|
2007
|
+
return spawn2(file, args, { ...options, windowsVerbatimArguments: verbatim });
|
|
1109
2008
|
}
|
|
1110
2009
|
function execCommand(argv2, options = {}) {
|
|
1111
2010
|
return new Promise((resolve, reject) => {
|
|
1112
2011
|
const { file, args, verbatim } = resolveCommand(argv2);
|
|
1113
|
-
|
|
2012
|
+
execFile3(
|
|
1114
2013
|
file,
|
|
1115
2014
|
args,
|
|
1116
2015
|
{ ...options, windowsVerbatimArguments: verbatim },
|
|
@@ -1120,7 +2019,7 @@ function execCommand(argv2, options = {}) {
|
|
|
1120
2019
|
}
|
|
1121
2020
|
function loginShell(script, timeout) {
|
|
1122
2021
|
return new Promise((resolve) => {
|
|
1123
|
-
|
|
2022
|
+
execFile3(
|
|
1124
2023
|
interactiveShell().file,
|
|
1125
2024
|
["-l", "-c", script],
|
|
1126
2025
|
{ timeout, maxBuffer: 16 * 1024 * 1024 },
|
|
@@ -1142,9 +2041,9 @@ async function commandExists(command) {
|
|
|
1142
2041
|
}
|
|
1143
2042
|
|
|
1144
2043
|
// ../../packages/agent-core/src/ports.js
|
|
1145
|
-
function
|
|
2044
|
+
function run2(cmd2, args) {
|
|
1146
2045
|
return new Promise((resolve, reject) => {
|
|
1147
|
-
|
|
2046
|
+
execFile4(cmd2, args, { maxBuffer: 8 * 1024 * 1024 }, (err, stdout) => {
|
|
1148
2047
|
if (err?.code === "ENOENT") reject(err);
|
|
1149
2048
|
else resolve(stdout || "");
|
|
1150
2049
|
});
|
|
@@ -1158,7 +2057,7 @@ function splitHostPort(name) {
|
|
|
1158
2057
|
}
|
|
1159
2058
|
async function listeningSockets() {
|
|
1160
2059
|
try {
|
|
1161
|
-
const out2 = await
|
|
2060
|
+
const out2 = await run2("lsof", ["-nP", "-a", "-u", String(os2.userInfo().uid), "-iTCP", "-sTCP:LISTEN", "-F", "pcn"]);
|
|
1162
2061
|
const sockets2 = [];
|
|
1163
2062
|
let pid;
|
|
1164
2063
|
let name;
|
|
@@ -1173,9 +2072,9 @@ async function listeningSockets() {
|
|
|
1173
2072
|
}
|
|
1174
2073
|
return sockets2;
|
|
1175
2074
|
} catch (e) {
|
|
1176
|
-
if (e.code !== "ENOENT" ||
|
|
2075
|
+
if (e.code !== "ENOENT" || os2.platform() !== "linux") throw e;
|
|
1177
2076
|
}
|
|
1178
|
-
const out = await
|
|
2077
|
+
const out = await run2("ss", ["-Hltnp"]);
|
|
1179
2078
|
const sockets = [];
|
|
1180
2079
|
for (const line of out.split("\n")) {
|
|
1181
2080
|
const cols = line.trim().split(/\s+/);
|
|
@@ -1186,7 +2085,7 @@ async function listeningSockets() {
|
|
|
1186
2085
|
return sockets;
|
|
1187
2086
|
}
|
|
1188
2087
|
async function processTable() {
|
|
1189
|
-
const out = await
|
|
2088
|
+
const out = await run2("ps", ["-A", "-ww", "-o", "pid=,ppid=,tty=,command="]);
|
|
1190
2089
|
const table = /* @__PURE__ */ new Map();
|
|
1191
2090
|
for (const line of out.split("\n")) {
|
|
1192
2091
|
const m = line.match(/^\s*(\d+)\s+(\d+)\s+(\S+)\s+(.*)$/);
|
|
@@ -1197,11 +2096,11 @@ async function processTable() {
|
|
|
1197
2096
|
async function workingDirs(pids) {
|
|
1198
2097
|
const dirs = /* @__PURE__ */ new Map();
|
|
1199
2098
|
if (!pids.length || isWindows) return dirs;
|
|
1200
|
-
if (
|
|
1201
|
-
await Promise.all(pids.map(async (pid2) => dirs.set(pid2, await
|
|
2099
|
+
if (os2.platform() === "linux") {
|
|
2100
|
+
await Promise.all(pids.map(async (pid2) => dirs.set(pid2, await fs10.readlink(`/proc/${pid2}/cwd`).catch(() => null))));
|
|
1202
2101
|
return dirs;
|
|
1203
2102
|
}
|
|
1204
|
-
const out = await
|
|
2103
|
+
const out = await run2("lsof", ["-a", "-p", pids.join(","), "-d", "cwd", "-F", "pn"]).catch(() => "");
|
|
1205
2104
|
let pid;
|
|
1206
2105
|
for (const line of out.split("\n")) {
|
|
1207
2106
|
if (line[0] === "p") pid = parseInt(line.slice(1), 10);
|
|
@@ -1213,9 +2112,9 @@ var WINDOWS_SHELLS = /* @__PURE__ */ new Set(["cmd.exe", "powershell.exe", "pwsh
|
|
|
1213
2112
|
async function windowsListeners() {
|
|
1214
2113
|
const query = "Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,Name,CommandLine | ConvertTo-Json -Compress";
|
|
1215
2114
|
const [v4, v6, json] = await Promise.all([
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
2115
|
+
run2("netstat", ["-ano", "-p", "TCP"]),
|
|
2116
|
+
run2("netstat", ["-ano", "-p", "TCPv6"]),
|
|
2117
|
+
run2("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", query])
|
|
1219
2118
|
]);
|
|
1220
2119
|
const table = /* @__PURE__ */ new Map();
|
|
1221
2120
|
for (const p of [JSON.parse(json || "[]")].flat()) {
|
|
@@ -1245,9 +2144,9 @@ async function listPorts(shells = /* @__PURE__ */ new Map()) {
|
|
|
1245
2144
|
const byKey = /* @__PURE__ */ new Map();
|
|
1246
2145
|
for (const s of sockets) {
|
|
1247
2146
|
const key = `${s.pid}:${s.port}`;
|
|
1248
|
-
const
|
|
1249
|
-
if (
|
|
1250
|
-
if (!
|
|
2147
|
+
const entry3 = byKey.get(key);
|
|
2148
|
+
if (entry3) {
|
|
2149
|
+
if (!entry3.addresses.includes(s.address)) entry3.addresses.push(s.address);
|
|
1251
2150
|
continue;
|
|
1252
2151
|
}
|
|
1253
2152
|
byKey.set(key, {
|
|
@@ -1264,10 +2163,10 @@ async function listPorts(shells = /* @__PURE__ */ new Map()) {
|
|
|
1264
2163
|
agent: s.pid === process.pid || AGENT_COMMAND.test(table.get(s.pid)?.command ?? "")
|
|
1265
2164
|
});
|
|
1266
2165
|
}
|
|
1267
|
-
const
|
|
1268
|
-
const dirs = await workingDirs([...new Set(
|
|
1269
|
-
for (const e of
|
|
1270
|
-
return
|
|
2166
|
+
const entries2 = [...byKey.values()];
|
|
2167
|
+
const dirs = await workingDirs([...new Set(entries2.map((e) => e.pid))]);
|
|
2168
|
+
for (const e of entries2) e.cwd = dirs.get(e.pid) ?? null;
|
|
2169
|
+
return entries2.sort((a, b) => a.port - b.port || a.pid - b.pid);
|
|
1271
2170
|
}
|
|
1272
2171
|
function sessionOf(pid, table, shells) {
|
|
1273
2172
|
for (let p = pid, depth = 0; p > 1 && depth < 64; p = table.get(p)?.ppid, depth++) {
|
|
@@ -1278,28 +2177,42 @@ function sessionOf(pid, table, shells) {
|
|
|
1278
2177
|
async function killListener(pid, { force = false, shells } = {}) {
|
|
1279
2178
|
if (!Number.isInteger(pid) || pid <= 1) throw new HttpError(400, "pid must be a process id");
|
|
1280
2179
|
if (pid === process.pid) throw new HttpError(403, "the agent cannot stop itself");
|
|
1281
|
-
const ports = await listPorts(shells);
|
|
1282
|
-
if (!ports.
|
|
2180
|
+
const ports = (await listPorts(shells)).filter((p) => p.pid === pid).map((p) => p.port);
|
|
2181
|
+
if (!ports.length) throw new HttpError(404, "no listening process with that pid");
|
|
1283
2182
|
try {
|
|
1284
2183
|
process.kill(pid, force ? "SIGKILL" : "SIGTERM");
|
|
1285
2184
|
} catch (e) {
|
|
1286
2185
|
if (e.code === "ESRCH") throw new HttpError(404, "no listening process with that pid");
|
|
1287
2186
|
throw e;
|
|
1288
2187
|
}
|
|
2188
|
+
return ports;
|
|
2189
|
+
}
|
|
2190
|
+
function isListening(port2, timeout = 2e3) {
|
|
2191
|
+
return new Promise((resolve) => {
|
|
2192
|
+
const socket = net.connect({ host: "localhost", port: port2, timeout });
|
|
2193
|
+
const done = (up) => {
|
|
2194
|
+
socket.destroy();
|
|
2195
|
+
resolve(up);
|
|
2196
|
+
};
|
|
2197
|
+
socket.once("connect", () => done(true));
|
|
2198
|
+
socket.once("timeout", () => done(true));
|
|
2199
|
+
socket.once("error", () => done(false));
|
|
2200
|
+
});
|
|
1289
2201
|
}
|
|
1290
2202
|
|
|
1291
2203
|
// ../../packages/agent-core/src/push.js
|
|
1292
|
-
import { execFile as
|
|
1293
|
-
import { createCipheriv, randomBytes } from "node:crypto";
|
|
1294
|
-
import
|
|
2204
|
+
import { execFile as execFile5 } from "node:child_process";
|
|
2205
|
+
import { createCipheriv, randomBytes as randomBytes2 } from "node:crypto";
|
|
2206
|
+
import os3 from "node:os";
|
|
1295
2207
|
import { promisify } from "node:util";
|
|
1296
|
-
var
|
|
2208
|
+
var run3 = promisify(execFile5);
|
|
1297
2209
|
var FINISHED_AFTER = 60 * 1e3;
|
|
1298
2210
|
var ACTIVE_WITHIN = 2 * 60;
|
|
2211
|
+
var AT_SESSION_WITHIN = 30;
|
|
1299
2212
|
async function macIdleSeconds() {
|
|
1300
|
-
if (
|
|
2213
|
+
if (os3.platform() !== "darwin") return null;
|
|
1301
2214
|
try {
|
|
1302
|
-
const { stdout } = await
|
|
2215
|
+
const { stdout } = await run3("ioreg", ["-c", "IOHIDSystem", "-d", "4", "-k", "HIDIdleTime"], { timeout: 2e3 });
|
|
1303
2216
|
const ns = stdout.match(/"HIDIdleTime"\s*=\s*(\d+)/)?.[1];
|
|
1304
2217
|
return ns ? Number(BigInt(ns) / 1000000000n) : null;
|
|
1305
2218
|
} catch {
|
|
@@ -1310,8 +2223,25 @@ async function userActiveHere() {
|
|
|
1310
2223
|
const idle = await macIdleSeconds();
|
|
1311
2224
|
return idle !== null && idle < ACTIVE_WITHIN;
|
|
1312
2225
|
}
|
|
2226
|
+
async function frontAppHere() {
|
|
2227
|
+
if (os3.platform() !== "darwin") return null;
|
|
2228
|
+
try {
|
|
2229
|
+
const { stdout: asn } = await run3("lsappinfo", ["front"], { timeout: 2e3 });
|
|
2230
|
+
if (!asn.trim()) return null;
|
|
2231
|
+
const { stdout } = await run3("lsappinfo", ["info", asn.trim()], { timeout: 2e3 });
|
|
2232
|
+
return stdout.match(/bundle path="([^"]+)"/)?.[1] ?? null;
|
|
2233
|
+
} catch {
|
|
2234
|
+
return null;
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
async function atSessionHere(session, { idle = macIdleSeconds, front = frontAppHere } = {}) {
|
|
2238
|
+
if (!session || session.terminal) return false;
|
|
2239
|
+
const seconds = await idle();
|
|
2240
|
+
if (seconds === null || seconds >= AT_SESSION_WITHIN) return false;
|
|
2241
|
+
return !session.app || await front() === session.app;
|
|
2242
|
+
}
|
|
1313
2243
|
function seal(key, data) {
|
|
1314
|
-
const nonce =
|
|
2244
|
+
const nonce = randomBytes2(12);
|
|
1315
2245
|
const cipher = createCipheriv("aes-256-gcm", key, nonce);
|
|
1316
2246
|
const body = Buffer.concat([cipher.update(JSON.stringify(data), "utf8"), cipher.final()]);
|
|
1317
2247
|
return Buffer.concat([nonce, body, cipher.getAuthTag()]).toString("base64");
|
|
@@ -1361,8 +2291,8 @@ var PushSender = class {
|
|
|
1361
2291
|
#devices;
|
|
1362
2292
|
#store;
|
|
1363
2293
|
#hub;
|
|
1364
|
-
#
|
|
1365
|
-
#
|
|
2294
|
+
#shows;
|
|
2295
|
+
#atSession;
|
|
1366
2296
|
#fetch;
|
|
1367
2297
|
// Requests a notification went out for, so their resolution can replace it.
|
|
1368
2298
|
#notified = /* @__PURE__ */ new Set();
|
|
@@ -1370,15 +2300,17 @@ var PushSender = class {
|
|
|
1370
2300
|
* @param {object} opts
|
|
1371
2301
|
* @param {import('./agents.js').AgentHub} opts.hub
|
|
1372
2302
|
* @param {{ load: () => object[], save: (devices: object[]) => void }} [opts.store]
|
|
1373
|
-
* @param {() => boolean} [opts.
|
|
1374
|
-
*
|
|
2303
|
+
* @param {(what: { session?: string, builds?: boolean }) => boolean} [opts.shows] whether a
|
|
2304
|
+
* client shows that session (or the builds) right now
|
|
2305
|
+
* @param {(host: { terminal: string | null, app: string | null } | null) => Promise<boolean>} [opts.atSession]
|
|
2306
|
+
* whether someone at the computer is working in the session running there
|
|
1375
2307
|
*/
|
|
1376
|
-
constructor({ hub, store = null,
|
|
2308
|
+
constructor({ hub, store = null, shows = () => false, atSession = atSessionHere, fetch: fetch2 = globalThis.fetch }) {
|
|
1377
2309
|
this.#hub = hub;
|
|
1378
2310
|
this.#store = store;
|
|
1379
2311
|
this.#devices = store?.load() ?? [];
|
|
1380
|
-
this.#
|
|
1381
|
-
this.#
|
|
2312
|
+
this.#shows = shows;
|
|
2313
|
+
this.#atSession = atSession;
|
|
1382
2314
|
this.#fetch = fetch2;
|
|
1383
2315
|
hub.on("message", (msg) => this.#follow(msg).catch(() => {
|
|
1384
2316
|
}));
|
|
@@ -1411,10 +2343,12 @@ var PushSender = class {
|
|
|
1411
2343
|
#save() {
|
|
1412
2344
|
this.#store?.save(this.#devices);
|
|
1413
2345
|
}
|
|
1414
|
-
|
|
1415
|
-
|
|
2346
|
+
/** No push about a session someone is looking at, on a client or at the computer. */
|
|
2347
|
+
async #quiet(what) {
|
|
2348
|
+
if (this.#shows(what)) return true;
|
|
2349
|
+
if (!what.session) return false;
|
|
1416
2350
|
try {
|
|
1417
|
-
return await this.#
|
|
2351
|
+
return await this.#atSession(this.#hub.host(what.session));
|
|
1418
2352
|
} catch {
|
|
1419
2353
|
return false;
|
|
1420
2354
|
}
|
|
@@ -1423,7 +2357,7 @@ var PushSender = class {
|
|
|
1423
2357
|
if (!this.#devices.length) return;
|
|
1424
2358
|
if (msg.type === "request") {
|
|
1425
2359
|
const { request } = msg;
|
|
1426
|
-
if (await this.#quiet()) return;
|
|
2360
|
+
if (await this.#quiet({ session: request.session })) return;
|
|
1427
2361
|
if (!this.#hub.requests().some((r) => r.id === request.id)) return;
|
|
1428
2362
|
this.#notified.add(request.id);
|
|
1429
2363
|
const question = request.kind === "question";
|
|
@@ -1455,7 +2389,7 @@ var PushSender = class {
|
|
|
1455
2389
|
fallback: "Answered on your computer"
|
|
1456
2390
|
});
|
|
1457
2391
|
} else if (msg.type === "entry" && msg.entry.kind === "turn_end" && msg.entry.duration >= FINISHED_AFTER) {
|
|
1458
|
-
if (await this.#quiet()) return;
|
|
2392
|
+
if (await this.#quiet({ session: msg.session })) return;
|
|
1459
2393
|
const session = this.#hub.session(msg.session);
|
|
1460
2394
|
await this.#broadcast("finished", {
|
|
1461
2395
|
kind: "finished",
|
|
@@ -1467,11 +2401,27 @@ var PushSender = class {
|
|
|
1467
2401
|
});
|
|
1468
2402
|
}
|
|
1469
2403
|
}
|
|
2404
|
+
/** A build ended (see builds.js): ready to install, or failed. Cancelled builds say nothing. */
|
|
2405
|
+
async buildEnded(build) {
|
|
2406
|
+
if (!this.#devices.length || build.state === "cancelled") return;
|
|
2407
|
+
if (await this.#quiet({ builds: true })) return;
|
|
2408
|
+
const ok = build.state === "succeeded";
|
|
2409
|
+
const firstError = String(build.error ?? "").split("\n").find((l) => l.trim()) ?? "";
|
|
2410
|
+
await this.#broadcast("finished", {
|
|
2411
|
+
kind: "build",
|
|
2412
|
+
title: ok ? "Build ready to install" : "Build failed",
|
|
2413
|
+
body: ok ? `${build.app?.name ?? build.scheme} ${build.app?.version ?? ""}`.trim() : `${build.scheme}: ${firstError}`,
|
|
2414
|
+
build: build.id,
|
|
2415
|
+
category: "BUILD",
|
|
2416
|
+
collapseId: `build-${build.id}`,
|
|
2417
|
+
fallback: ok ? "A build is ready to install" : "A build failed"
|
|
2418
|
+
});
|
|
2419
|
+
}
|
|
1470
2420
|
async #broadcast(kind, note) {
|
|
1471
2421
|
await Promise.all(this.#devices.filter((d) => d.kinds[kind]).map((d) => this.#send(d, note)));
|
|
1472
2422
|
}
|
|
1473
|
-
async #send(device, { kind, title, body, session, request, question, options, category, collapseId, priority, silent, fallback }) {
|
|
1474
|
-
const secret = { kind, title, body, session, request, question, options };
|
|
2423
|
+
async #send(device, { kind, title, body, session, request, build, question, options, category, collapseId, priority, silent, fallback }) {
|
|
2424
|
+
const secret = { kind, title, body, session, request, build, question, options };
|
|
1475
2425
|
const aps = { alert: { title: "Code Remote", body: fallback }, "mutable-content": 1 };
|
|
1476
2426
|
if (category) aps.category = category;
|
|
1477
2427
|
if (session) aps["thread-id"] = session;
|
|
@@ -1506,8 +2456,8 @@ var PushSender = class {
|
|
|
1506
2456
|
};
|
|
1507
2457
|
|
|
1508
2458
|
// ../../packages/agent-core/src/inspector.js
|
|
1509
|
-
import
|
|
1510
|
-
import
|
|
2459
|
+
import http2 from "node:http";
|
|
2460
|
+
import net2 from "node:net";
|
|
1511
2461
|
import zlib from "node:zlib";
|
|
1512
2462
|
var MAX_ENTRIES2 = 200;
|
|
1513
2463
|
var MAX_BODY = 64 * 1024;
|
|
@@ -1549,7 +2499,7 @@ var InspectingProxy = class {
|
|
|
1549
2499
|
/** @param {number} target the dev server's port on this machine */
|
|
1550
2500
|
constructor(target) {
|
|
1551
2501
|
this.target = target;
|
|
1552
|
-
this.server =
|
|
2502
|
+
this.server = http2.createServer((req, res) => this.#forward(req, res));
|
|
1553
2503
|
this.server.on("upgrade", (req, socket, head) => this.#tunnelUpgrade(req, socket, head));
|
|
1554
2504
|
}
|
|
1555
2505
|
/** Listens on a free loopback port; resolves to it. */
|
|
@@ -1567,7 +2517,7 @@ var InspectingProxy = class {
|
|
|
1567
2517
|
}
|
|
1568
2518
|
/** Summaries, oldest first; `after` skips ids up to and including it. */
|
|
1569
2519
|
list(after = 0) {
|
|
1570
|
-
return this.#entries.filter((e) => e.id > after).map(
|
|
2520
|
+
return this.#entries.filter((e) => e.id > after).map(summary2);
|
|
1571
2521
|
}
|
|
1572
2522
|
get(id) {
|
|
1573
2523
|
return this.#entries.find((e) => e.id === id);
|
|
@@ -1576,7 +2526,7 @@ var InspectingProxy = class {
|
|
|
1576
2526
|
this.#entries = [];
|
|
1577
2527
|
}
|
|
1578
2528
|
#record(req) {
|
|
1579
|
-
const
|
|
2529
|
+
const entry3 = {
|
|
1580
2530
|
id: nextId++,
|
|
1581
2531
|
time: Date.now(),
|
|
1582
2532
|
method: req.method,
|
|
@@ -1587,21 +2537,21 @@ var InspectingProxy = class {
|
|
|
1587
2537
|
request: { headers: req.headers, tap: new BodyTap() },
|
|
1588
2538
|
response: { headers: {}, tap: new BodyTap() }
|
|
1589
2539
|
};
|
|
1590
|
-
this.#entries.push(
|
|
2540
|
+
this.#entries.push(entry3);
|
|
1591
2541
|
if (this.#entries.length > MAX_ENTRIES2) this.#entries.shift();
|
|
1592
|
-
return
|
|
2542
|
+
return entry3;
|
|
1593
2543
|
}
|
|
1594
2544
|
/** The dev server sees itself addressed as localhost:<its port>, as without the proxy. */
|
|
1595
2545
|
#headers(req) {
|
|
1596
2546
|
return { ...req.headers, host: `localhost:${this.target}` };
|
|
1597
2547
|
}
|
|
1598
2548
|
#forward(req, res) {
|
|
1599
|
-
const
|
|
2549
|
+
const entry3 = this.#record(req);
|
|
1600
2550
|
const started = performance.now();
|
|
1601
2551
|
const finish = () => {
|
|
1602
|
-
|
|
2552
|
+
entry3.duration = Math.round(performance.now() - started);
|
|
1603
2553
|
};
|
|
1604
|
-
const upstream =
|
|
2554
|
+
const upstream = http2.request({
|
|
1605
2555
|
agent: false,
|
|
1606
2556
|
host: "localhost",
|
|
1607
2557
|
port: this.target,
|
|
@@ -1610,11 +2560,11 @@ var InspectingProxy = class {
|
|
|
1610
2560
|
headers: this.#headers(req)
|
|
1611
2561
|
});
|
|
1612
2562
|
upstream.on("response", (up) => {
|
|
1613
|
-
|
|
1614
|
-
|
|
2563
|
+
entry3.status = up.statusCode;
|
|
2564
|
+
entry3.response.headers = up.headers;
|
|
1615
2565
|
res.writeHead(up.statusCode, up.statusMessage, up.rawHeaders);
|
|
1616
2566
|
up.on("data", (chunk) => {
|
|
1617
|
-
|
|
2567
|
+
entry3.response.tap.push(chunk);
|
|
1618
2568
|
res.write(chunk);
|
|
1619
2569
|
});
|
|
1620
2570
|
up.on("end", () => {
|
|
@@ -1625,14 +2575,14 @@ var InspectingProxy = class {
|
|
|
1625
2575
|
});
|
|
1626
2576
|
upstream.on("error", (e) => {
|
|
1627
2577
|
finish();
|
|
1628
|
-
|
|
2578
|
+
entry3.error = e.code === "ECONNREFUSED" ? `nothing is listening on port ${this.target}` : e.message;
|
|
1629
2579
|
if (res.headersSent) return res.destroy();
|
|
1630
|
-
|
|
2580
|
+
entry3.status = 502;
|
|
1631
2581
|
res.writeHead(502, { "content-type": "text/plain; charset=utf-8" });
|
|
1632
|
-
res.end(`coderemote preview: ${
|
|
2582
|
+
res.end(`coderemote preview: ${entry3.error}
|
|
1633
2583
|
`);
|
|
1634
2584
|
});
|
|
1635
|
-
req.on("data", (chunk) =>
|
|
2585
|
+
req.on("data", (chunk) => entry3.request.tap.push(chunk));
|
|
1636
2586
|
req.pipe(upstream);
|
|
1637
2587
|
res.on("close", () => {
|
|
1638
2588
|
if (!res.writableFinished) upstream.destroy();
|
|
@@ -1640,8 +2590,8 @@ var InspectingProxy = class {
|
|
|
1640
2590
|
}
|
|
1641
2591
|
/** WebSockets (hot reload) pass through as raw bytes; only the handshake is recorded. */
|
|
1642
2592
|
#tunnelUpgrade(req, socket, head) {
|
|
1643
|
-
const
|
|
1644
|
-
const upstream =
|
|
2593
|
+
const entry3 = this.#record(req);
|
|
2594
|
+
const upstream = net2.connect({ host: "localhost", port: this.target }, () => {
|
|
1645
2595
|
const headers = this.#headers(req);
|
|
1646
2596
|
const lines = [`${req.method} ${req.url} HTTP/1.1`];
|
|
1647
2597
|
for (const [k, v] of Object.entries(headers)) {
|
|
@@ -1651,18 +2601,18 @@ var InspectingProxy = class {
|
|
|
1651
2601
|
\r
|
|
1652
2602
|
`);
|
|
1653
2603
|
if (head?.length) upstream.write(head);
|
|
1654
|
-
|
|
2604
|
+
entry3.status = 101;
|
|
1655
2605
|
upstream.pipe(socket);
|
|
1656
2606
|
socket.pipe(upstream);
|
|
1657
2607
|
});
|
|
1658
2608
|
upstream.on("error", (e) => {
|
|
1659
|
-
|
|
2609
|
+
entry3.error = e.message;
|
|
1660
2610
|
socket.destroy();
|
|
1661
2611
|
});
|
|
1662
2612
|
socket.on("error", () => upstream.destroy());
|
|
1663
2613
|
}
|
|
1664
2614
|
};
|
|
1665
|
-
function
|
|
2615
|
+
function summary2(e) {
|
|
1666
2616
|
return {
|
|
1667
2617
|
id: e.id,
|
|
1668
2618
|
time: e.time,
|
|
@@ -1678,16 +2628,369 @@ function summary(e) {
|
|
|
1678
2628
|
}
|
|
1679
2629
|
function detail(e) {
|
|
1680
2630
|
return {
|
|
1681
|
-
...
|
|
2631
|
+
...summary2(e),
|
|
1682
2632
|
request: { headers: e.request.headers, body: e.request.tap.describe(e.request.headers) },
|
|
1683
2633
|
response: { headers: e.response.headers, body: e.response.tap.describe(e.response.headers) }
|
|
1684
2634
|
};
|
|
1685
2635
|
}
|
|
1686
2636
|
|
|
2637
|
+
// ../../packages/agent-core/src/search.js
|
|
2638
|
+
import { execFile as execFile6, spawn as spawn3 } from "node:child_process";
|
|
2639
|
+
import fs11 from "node:fs/promises";
|
|
2640
|
+
import path8 from "node:path";
|
|
2641
|
+
import readline from "node:readline";
|
|
2642
|
+
var ALWAYS_SKIPPED = /* @__PURE__ */ new Set([".git", "node_modules"]);
|
|
2643
|
+
var MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
2644
|
+
var MAX_QUERY = 1e3;
|
|
2645
|
+
var MAX_LINE2 = 240;
|
|
2646
|
+
var LEAD = 40;
|
|
2647
|
+
var MAX_RANGES = 20;
|
|
2648
|
+
var NAME_LIMIT = 100;
|
|
2649
|
+
var DEFAULT_LIMIT = 500;
|
|
2650
|
+
var MAX_LIMIT = 2e3;
|
|
2651
|
+
var PER_FILE = 100;
|
|
2652
|
+
var TIMEOUT_MS = 15e3;
|
|
2653
|
+
var rgPath;
|
|
2654
|
+
function ripgrep() {
|
|
2655
|
+
if (rgPath === void 0) rgPath = findExecutable("rg");
|
|
2656
|
+
return rgPath;
|
|
2657
|
+
}
|
|
2658
|
+
function queryRegex(query, { caseSensitive = false, wholeWord = false, regex = false } = {}, global = false) {
|
|
2659
|
+
let source = regex ? query : query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2660
|
+
if (wholeWord) source = `\\b(?:${source})\\b`;
|
|
2661
|
+
try {
|
|
2662
|
+
return new RegExp(source, `${global ? "g" : ""}${caseSensitive ? "" : "i"}`);
|
|
2663
|
+
} catch (e) {
|
|
2664
|
+
throw new HttpError(400, `invalid regular expression: ${e.message}`);
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
function globSource(glob) {
|
|
2668
|
+
let out = "";
|
|
2669
|
+
for (let i = 0; i < glob.length; i++) {
|
|
2670
|
+
const c = glob[i];
|
|
2671
|
+
if (c === "*" && glob[i + 1] === "*") {
|
|
2672
|
+
if (glob[i + 2] === "/") {
|
|
2673
|
+
out += "(?:.*/)?";
|
|
2674
|
+
i += 2;
|
|
2675
|
+
} else {
|
|
2676
|
+
out += ".*";
|
|
2677
|
+
i += 1;
|
|
2678
|
+
}
|
|
2679
|
+
} else if (c === "*") out += "[^/]*";
|
|
2680
|
+
else if (c === "?") out += "[^/]";
|
|
2681
|
+
else if (c === "{") out += "(?:";
|
|
2682
|
+
else if (c === "}") out += ")";
|
|
2683
|
+
else if (c === ",") out += "|";
|
|
2684
|
+
else out += c.replace(/[.+^$()|[\]\\]/g, "\\$&");
|
|
2685
|
+
}
|
|
2686
|
+
return out;
|
|
2687
|
+
}
|
|
2688
|
+
function pathFilter(include = "", exclude = "") {
|
|
2689
|
+
const matchers = (list) => splitPatterns(list).map((raw) => {
|
|
2690
|
+
let p = raw.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
2691
|
+
if (!/[*?{]/.test(p)) {
|
|
2692
|
+
if (p.startsWith("/")) {
|
|
2693
|
+
const head = p.slice(1).replace(/\/$/, "").toLowerCase();
|
|
2694
|
+
return (rel) => {
|
|
2695
|
+
const r = rel.toLowerCase();
|
|
2696
|
+
return r === head || r.startsWith(`${head}/`);
|
|
2697
|
+
};
|
|
2698
|
+
}
|
|
2699
|
+
const needle = p.toLowerCase();
|
|
2700
|
+
return (rel) => rel.toLowerCase().includes(needle);
|
|
2701
|
+
}
|
|
2702
|
+
if (p.startsWith("/")) p = p.slice(1);
|
|
2703
|
+
else if (!p.startsWith("**/")) p = `**/${p}`;
|
|
2704
|
+
const re = new RegExp(`^${globSource(p.replace(/\/$/, ""))}(?:/.*)?$`, "i");
|
|
2705
|
+
return (rel) => re.test(rel);
|
|
2706
|
+
});
|
|
2707
|
+
const inc = matchers(include);
|
|
2708
|
+
const exc = matchers(exclude);
|
|
2709
|
+
return (rel) => (!inc.length || inc.some((m) => m(rel))) && !exc.some((m) => m(rel));
|
|
2710
|
+
}
|
|
2711
|
+
function splitPatterns(list) {
|
|
2712
|
+
const out = [];
|
|
2713
|
+
let depth = 0;
|
|
2714
|
+
let cur = "";
|
|
2715
|
+
for (const c of list) {
|
|
2716
|
+
if (c === "{") depth++;
|
|
2717
|
+
if (c === "}") depth = Math.max(0, depth - 1);
|
|
2718
|
+
if (c === "," && depth === 0) {
|
|
2719
|
+
out.push(cur);
|
|
2720
|
+
cur = "";
|
|
2721
|
+
} else cur += c;
|
|
2722
|
+
}
|
|
2723
|
+
out.push(cur);
|
|
2724
|
+
return out.map((s) => s.trim()).filter(Boolean);
|
|
2725
|
+
}
|
|
2726
|
+
function preview(line, re) {
|
|
2727
|
+
const all = [];
|
|
2728
|
+
re.lastIndex = 0;
|
|
2729
|
+
for (let m; all.length < MAX_RANGES && (m = re.exec(line)); ) {
|
|
2730
|
+
if (m[0].length === 0) {
|
|
2731
|
+
re.lastIndex++;
|
|
2732
|
+
continue;
|
|
2733
|
+
}
|
|
2734
|
+
all.push([m.index, m.index + m[0].length]);
|
|
2735
|
+
}
|
|
2736
|
+
const indent2 = line.length - line.trimStart().length;
|
|
2737
|
+
let start2 = indent2;
|
|
2738
|
+
if (all.length && line.length - start2 > MAX_LINE2) start2 = Math.max(indent2, all[0][0] - LEAD);
|
|
2739
|
+
const end = Math.min(line.length, start2 + MAX_LINE2);
|
|
2740
|
+
const ranges = all.filter(([s, e]) => e > start2 && s < end).map(([s, e]) => [Math.max(s, start2) - start2, Math.min(e, end) - start2]);
|
|
2741
|
+
return { text: line.slice(start2, end).trimEnd(), ranges, clipped: start2 > indent2 || end < line.length };
|
|
2742
|
+
}
|
|
2743
|
+
function runGit(cwd, args) {
|
|
2744
|
+
return new Promise((resolve) => {
|
|
2745
|
+
execFile6(
|
|
2746
|
+
"git",
|
|
2747
|
+
args,
|
|
2748
|
+
{ cwd, maxBuffer: 256 * 1024 * 1024, env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" } },
|
|
2749
|
+
(err, out) => resolve(err ? null : out)
|
|
2750
|
+
);
|
|
2751
|
+
});
|
|
2752
|
+
}
|
|
2753
|
+
async function exists(p) {
|
|
2754
|
+
try {
|
|
2755
|
+
await fs11.lstat(p);
|
|
2756
|
+
return true;
|
|
2757
|
+
} catch {
|
|
2758
|
+
return false;
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
async function listFiles(root2, signal) {
|
|
2762
|
+
const files = [];
|
|
2763
|
+
const skipped = (rel) => rel.split("/").some((part) => ALWAYS_SKIPPED.has(part));
|
|
2764
|
+
const join = (prefix, name) => prefix ? `${prefix}/${name}` : name;
|
|
2765
|
+
async function fromGit(dir, prefix) {
|
|
2766
|
+
const [tracked, untracked] = await Promise.all([
|
|
2767
|
+
runGit(dir, ["ls-files", "-z", "-s"]),
|
|
2768
|
+
runGit(dir, ["ls-files", "-z", "-o", "--exclude-standard"])
|
|
2769
|
+
]);
|
|
2770
|
+
if (tracked === null || untracked === null) return false;
|
|
2771
|
+
const subdirs = [];
|
|
2772
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2773
|
+
for (const entry3 of tracked.split("\0")) {
|
|
2774
|
+
if (!entry3) continue;
|
|
2775
|
+
const tab = entry3.indexOf(" ");
|
|
2776
|
+
const mode = entry3.slice(0, entry3.indexOf(" "));
|
|
2777
|
+
const rel = entry3.slice(tab + 1);
|
|
2778
|
+
if (seen.has(rel)) continue;
|
|
2779
|
+
seen.add(rel);
|
|
2780
|
+
if (skipped(rel)) continue;
|
|
2781
|
+
if (mode === "160000") subdirs.push(rel);
|
|
2782
|
+
else if (mode !== "120000") files.push(join(prefix, rel));
|
|
2783
|
+
}
|
|
2784
|
+
for (const rel of untracked.split("\0")) {
|
|
2785
|
+
if (!rel || skipped(rel)) continue;
|
|
2786
|
+
if (rel.endsWith("/")) subdirs.push(rel.slice(0, -1));
|
|
2787
|
+
else files.push(join(prefix, rel));
|
|
2788
|
+
}
|
|
2789
|
+
for (const rel of subdirs) await visit(path8.join(dir, rel), join(prefix, rel));
|
|
2790
|
+
return true;
|
|
2791
|
+
}
|
|
2792
|
+
async function visit(dir, prefix) {
|
|
2793
|
+
if (signal?.aborted) return;
|
|
2794
|
+
if (await exists(path8.join(dir, ".git")) && await fromGit(dir, prefix)) return;
|
|
2795
|
+
let entries2;
|
|
2796
|
+
try {
|
|
2797
|
+
entries2 = await fs11.readdir(dir, { withFileTypes: true });
|
|
2798
|
+
} catch {
|
|
2799
|
+
return;
|
|
2800
|
+
}
|
|
2801
|
+
for (const e of entries2) {
|
|
2802
|
+
if (ALWAYS_SKIPPED.has(e.name)) continue;
|
|
2803
|
+
if (e.isDirectory()) await visit(path8.join(dir, e.name), join(prefix, e.name));
|
|
2804
|
+
else if (e.isFile()) files.push(join(prefix, e.name));
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
await visit(root2, "");
|
|
2808
|
+
return files;
|
|
2809
|
+
}
|
|
2810
|
+
var Results = class {
|
|
2811
|
+
constructor(limit) {
|
|
2812
|
+
this.limit = limit;
|
|
2813
|
+
this.count = 0;
|
|
2814
|
+
this.byFile = /* @__PURE__ */ new Map();
|
|
2815
|
+
this.truncated = false;
|
|
2816
|
+
}
|
|
2817
|
+
get full() {
|
|
2818
|
+
return this.count >= this.limit;
|
|
2819
|
+
}
|
|
2820
|
+
/** Whether a match came past the limit: there are more than it, so searching can stop. */
|
|
2821
|
+
get done() {
|
|
2822
|
+
return this.full && this.truncated;
|
|
2823
|
+
}
|
|
2824
|
+
add(rel, line, match) {
|
|
2825
|
+
if (this.full) {
|
|
2826
|
+
this.truncated = true;
|
|
2827
|
+
return;
|
|
2828
|
+
}
|
|
2829
|
+
let file = this.byFile.get(rel);
|
|
2830
|
+
if (!file) this.byFile.set(rel, file = []);
|
|
2831
|
+
if (file.length >= PER_FILE) {
|
|
2832
|
+
this.truncated = true;
|
|
2833
|
+
return;
|
|
2834
|
+
}
|
|
2835
|
+
file.push({ line, ...match });
|
|
2836
|
+
this.count++;
|
|
2837
|
+
}
|
|
2838
|
+
};
|
|
2839
|
+
async function searchFile(root2, rel, test, re, results) {
|
|
2840
|
+
const abs = path8.join(root2, ...rel.split("/"));
|
|
2841
|
+
let stat;
|
|
2842
|
+
try {
|
|
2843
|
+
stat = await fs11.lstat(abs);
|
|
2844
|
+
} catch {
|
|
2845
|
+
return;
|
|
2846
|
+
}
|
|
2847
|
+
if (!stat.isFile() || stat.size > MAX_FILE_BYTES || stat.size === 0) return;
|
|
2848
|
+
let buf;
|
|
2849
|
+
try {
|
|
2850
|
+
buf = await fs11.readFile(abs);
|
|
2851
|
+
} catch {
|
|
2852
|
+
return;
|
|
2853
|
+
}
|
|
2854
|
+
if (buf.subarray(0, 8e3).includes(0)) return;
|
|
2855
|
+
const text = buf.toString("utf8");
|
|
2856
|
+
if (!new RegExp(test.source, `${test.flags}m`).test(text)) return;
|
|
2857
|
+
const lines = text.split("\n");
|
|
2858
|
+
for (let i = 0; i < lines.length && !results.done; i++) {
|
|
2859
|
+
const line = lines[i].endsWith("\r") ? lines[i].slice(0, -1) : lines[i];
|
|
2860
|
+
if (test.test(line)) results.add(rel, i + 1, preview(line, re));
|
|
2861
|
+
}
|
|
2862
|
+
}
|
|
2863
|
+
async function searchHere(root2, files, test, re, results, signal) {
|
|
2864
|
+
let next = 0;
|
|
2865
|
+
const worker = async () => {
|
|
2866
|
+
while (next < files.length && !results.done && !signal?.aborted) {
|
|
2867
|
+
await searchFile(root2, files[next++], test, re, results);
|
|
2868
|
+
}
|
|
2869
|
+
};
|
|
2870
|
+
await Promise.all(Array.from({ length: 16 }, worker));
|
|
2871
|
+
}
|
|
2872
|
+
function runRipgrep(rg, root2, args, onLine, signal) {
|
|
2873
|
+
return new Promise((resolve, reject) => {
|
|
2874
|
+
const child = spawn3(rg, ["--no-config", "--no-messages", "--hidden", "--glob", "!.git", "--glob", "!node_modules", ...args], {
|
|
2875
|
+
cwd: root2,
|
|
2876
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2877
|
+
});
|
|
2878
|
+
let stderr = "";
|
|
2879
|
+
let stopped = false;
|
|
2880
|
+
const stop = () => {
|
|
2881
|
+
stopped = true;
|
|
2882
|
+
child.kill();
|
|
2883
|
+
};
|
|
2884
|
+
signal?.addEventListener("abort", stop, { once: true });
|
|
2885
|
+
child.stderr.on("data", (d) => stderr += d);
|
|
2886
|
+
const lines = readline.createInterface({ input: child.stdout, crlfDelay: Infinity });
|
|
2887
|
+
lines.on("line", (line) => {
|
|
2888
|
+
if (!stopped && onLine(line) === false) stop();
|
|
2889
|
+
});
|
|
2890
|
+
child.on("error", reject);
|
|
2891
|
+
child.on("close", (code) => {
|
|
2892
|
+
signal?.removeEventListener("abort", stop);
|
|
2893
|
+
if (code === 2 && /regex/i.test(stderr)) return reject(new HttpError(400, stderr.trim().split("\n").pop()));
|
|
2894
|
+
resolve();
|
|
2895
|
+
});
|
|
2896
|
+
});
|
|
2897
|
+
}
|
|
2898
|
+
var relOf = (p) => p.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
2899
|
+
async function search({
|
|
2900
|
+
root: root2,
|
|
2901
|
+
query = "",
|
|
2902
|
+
caseSensitive = false,
|
|
2903
|
+
wholeWord = false,
|
|
2904
|
+
regex = false,
|
|
2905
|
+
include = "",
|
|
2906
|
+
exclude = "",
|
|
2907
|
+
limit = DEFAULT_LIMIT,
|
|
2908
|
+
engine = "auto",
|
|
2909
|
+
signal
|
|
2910
|
+
}) {
|
|
2911
|
+
if (query.length > MAX_QUERY) throw new HttpError(400, "query is too long");
|
|
2912
|
+
if (!query && !include.trim()) throw new HttpError(400, "missing query");
|
|
2913
|
+
limit = Math.min(Math.max(1, Math.floor(limit) || DEFAULT_LIMIT), MAX_LIMIT);
|
|
2914
|
+
const options = { caseSensitive, wholeWord, regex };
|
|
2915
|
+
const re = query ? queryRegex(query, options, true) : null;
|
|
2916
|
+
const test = query ? queryRegex(query, options) : null;
|
|
2917
|
+
const keep = pathFilter(include, exclude);
|
|
2918
|
+
const rg = engine === "node" ? null : ripgrep();
|
|
2919
|
+
if (engine === "rg" && !rg) throw new HttpError(500, "ripgrep is not installed");
|
|
2920
|
+
const timeout = new AbortController();
|
|
2921
|
+
const timer = setTimeout(() => timeout.abort(), TIMEOUT_MS);
|
|
2922
|
+
const stop = signal ? AbortSignal.any([signal, timeout.signal]) : timeout.signal;
|
|
2923
|
+
const results = new Results(limit);
|
|
2924
|
+
const names = [];
|
|
2925
|
+
let namesTruncated = false;
|
|
2926
|
+
const nameLimit = query ? NAME_LIMIT : limit;
|
|
2927
|
+
const addName = (rel) => {
|
|
2928
|
+
if (!keep(rel)) return true;
|
|
2929
|
+
const base2 = rel.slice(rel.lastIndexOf("/") + 1);
|
|
2930
|
+
if (test && !test.test(base2)) return true;
|
|
2931
|
+
if (names.length >= nameLimit) {
|
|
2932
|
+
namesTruncated = true;
|
|
2933
|
+
return false;
|
|
2934
|
+
}
|
|
2935
|
+
names.push(rel);
|
|
2936
|
+
return true;
|
|
2937
|
+
};
|
|
2938
|
+
try {
|
|
2939
|
+
if (rg) {
|
|
2940
|
+
const listing = runRipgrep(rg, root2, ["--files", "./"], (line) => addName(relOf(line)), stop);
|
|
2941
|
+
const flags = [
|
|
2942
|
+
"--json",
|
|
2943
|
+
"--max-filesize",
|
|
2944
|
+
String(MAX_FILE_BYTES),
|
|
2945
|
+
"--max-count",
|
|
2946
|
+
String(PER_FILE),
|
|
2947
|
+
...regex ? [] : ["--fixed-strings"],
|
|
2948
|
+
...caseSensitive ? ["--case-sensitive"] : ["--ignore-case"],
|
|
2949
|
+
...wholeWord ? ["--word-regexp"] : []
|
|
2950
|
+
];
|
|
2951
|
+
const content = query ? runRipgrep(
|
|
2952
|
+
rg,
|
|
2953
|
+
root2,
|
|
2954
|
+
[...flags, "--regexp", query, "--", "./"],
|
|
2955
|
+
(line) => {
|
|
2956
|
+
let msg;
|
|
2957
|
+
try {
|
|
2958
|
+
msg = JSON.parse(line);
|
|
2959
|
+
} catch {
|
|
2960
|
+
return true;
|
|
2961
|
+
}
|
|
2962
|
+
if (msg.type !== "match" || msg.data.path.text === void 0 || msg.data.lines.text === void 0) return true;
|
|
2963
|
+
const rel = relOf(msg.data.path.text);
|
|
2964
|
+
if (!keep(rel)) return true;
|
|
2965
|
+
const text = msg.data.lines.text.replace(/\r?\n$/, "");
|
|
2966
|
+
results.add(rel, msg.data.line_number, preview(text, re));
|
|
2967
|
+
return !results.done;
|
|
2968
|
+
},
|
|
2969
|
+
stop
|
|
2970
|
+
) : null;
|
|
2971
|
+
await Promise.all([listing, content]);
|
|
2972
|
+
if (stop.aborted && !signal?.aborted) results.truncated = true;
|
|
2973
|
+
} else {
|
|
2974
|
+
const files = (await listFiles(root2, stop)).filter(keep);
|
|
2975
|
+
for (const rel of files) if (!addName(rel)) break;
|
|
2976
|
+
if (query) await searchHere(root2, files, test, re, results, stop);
|
|
2977
|
+
}
|
|
2978
|
+
} finally {
|
|
2979
|
+
clearTimeout(timer);
|
|
2980
|
+
}
|
|
2981
|
+
if (timeout.signal.aborted) results.truncated = true;
|
|
2982
|
+
const byPath = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
2983
|
+
return {
|
|
2984
|
+
names: names.sort(byPath).map((p) => ({ path: p })),
|
|
2985
|
+
files: [...results.byFile.entries()].sort(([a], [b]) => byPath(a, b)).map(([p, matches]) => ({ path: p, matches })),
|
|
2986
|
+
truncated: results.truncated || namesTruncated
|
|
2987
|
+
};
|
|
2988
|
+
}
|
|
2989
|
+
|
|
1687
2990
|
// ../../packages/agent-core/src/sessions.js
|
|
1688
|
-
import { execFile as
|
|
1689
|
-
import { randomUUID as
|
|
1690
|
-
import
|
|
2991
|
+
import { execFile as execFile7 } from "node:child_process";
|
|
2992
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
2993
|
+
import os4 from "node:os";
|
|
1691
2994
|
import pty from "node-pty";
|
|
1692
2995
|
var SCROLLBACK_LIMIT = 256 * 1024;
|
|
1693
2996
|
function guardConpty(term) {
|
|
@@ -1706,7 +3009,7 @@ var TerminalSession = class {
|
|
|
1706
3009
|
* terminalCommand says; the session ends when it exits.
|
|
1707
3010
|
*/
|
|
1708
3011
|
constructor({ cols, rows, cwd, env = {}, command = null }) {
|
|
1709
|
-
this.id =
|
|
3012
|
+
this.id = randomUUID3();
|
|
1710
3013
|
this.createdAt = Date.now();
|
|
1711
3014
|
this.chunks = [];
|
|
1712
3015
|
this.bufferedBytes = 0;
|
|
@@ -1719,7 +3022,7 @@ var TerminalSession = class {
|
|
|
1719
3022
|
name: "xterm-256color",
|
|
1720
3023
|
cols,
|
|
1721
3024
|
rows,
|
|
1722
|
-
cwd: cwd ||
|
|
3025
|
+
cwd: cwd || os4.homedir(),
|
|
1723
3026
|
// CODEREMOTE_TERM_ID lets `coderemote hook` tell which terminal a coding agent runs in.
|
|
1724
3027
|
env: { ...process.env, ...env, CODEREMOTE_TERM_ID: this.id, TERM: "xterm-256color", COLORTERM: "truecolor" }
|
|
1725
3028
|
});
|
|
@@ -1772,7 +3075,7 @@ var TerminalSession = class {
|
|
|
1772
3075
|
/** Ends the session; on Windows, which has no signals, always forcibly (see releaseConpty). */
|
|
1773
3076
|
kill(signal) {
|
|
1774
3077
|
if (!isWindows) this.pty.kill(signal);
|
|
1775
|
-
else if (this.exitCode === null)
|
|
3078
|
+
else if (this.exitCode === null) execFile7("taskkill", ["/T", "/F", "/PID", String(this.pty.pid)], () => {
|
|
1776
3079
|
});
|
|
1777
3080
|
}
|
|
1778
3081
|
/** Types into the session, as a client's keyboard would; nothing once it has exited. */
|
|
@@ -1831,21 +3134,21 @@ var SessionManager = class {
|
|
|
1831
3134
|
};
|
|
1832
3135
|
|
|
1833
3136
|
// ../../packages/agent-core/src/uploads.js
|
|
1834
|
-
import { execFile as
|
|
1835
|
-
import { randomUUID as
|
|
3137
|
+
import { execFile as execFile8 } from "node:child_process";
|
|
3138
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
1836
3139
|
import { createWriteStream } from "node:fs";
|
|
1837
|
-
import
|
|
1838
|
-
import
|
|
1839
|
-
import
|
|
3140
|
+
import fs12 from "node:fs/promises";
|
|
3141
|
+
import os5 from "node:os";
|
|
3142
|
+
import path9 from "node:path";
|
|
1840
3143
|
import { Transform } from "node:stream";
|
|
1841
|
-
import { pipeline } from "node:stream/promises";
|
|
3144
|
+
import { pipeline as pipeline2 } from "node:stream/promises";
|
|
1842
3145
|
import { promisify as promisify2 } from "node:util";
|
|
1843
|
-
var
|
|
3146
|
+
var run4 = promisify2(execFile8);
|
|
1844
3147
|
var UPLOAD_LIMIT = 100 * 1024 * 1024;
|
|
1845
3148
|
var KEEP = 7 * 24 * 60 * 60 * 1e3;
|
|
1846
3149
|
var ID = /^[0-9a-f-]{36}$/;
|
|
1847
3150
|
function safeName(name) {
|
|
1848
|
-
const base2 =
|
|
3151
|
+
const base2 = path9.basename(String(name ?? "").replace(/\\/g, "/"));
|
|
1849
3152
|
const clean = base2.replace(/[^\p{L}\p{N}._ -]/gu, "_").replace(/^[.\s]+/, "").slice(-80).trim();
|
|
1850
3153
|
return clean || "file";
|
|
1851
3154
|
}
|
|
@@ -1857,10 +3160,10 @@ var UploadStore = class {
|
|
|
1857
3160
|
/** Streams `body` to disk. 413 past `limit`; HEIC photos become JPEG on macOS. */
|
|
1858
3161
|
async save(body, { name, type, length, limit = UPLOAD_LIMIT }) {
|
|
1859
3162
|
if (Number(length) > limit) throw new HttpError(413, `files are limited to ${limit / 1024 / 1024} MB`);
|
|
1860
|
-
const id =
|
|
3163
|
+
const id = randomUUID4();
|
|
1861
3164
|
const day = new Date(this.now()).toISOString().slice(0, 10);
|
|
1862
|
-
await
|
|
1863
|
-
let file =
|
|
3165
|
+
await fs12.mkdir(path9.join(this.dir, day), { recursive: true, mode: 448 });
|
|
3166
|
+
let file = path9.join(this.dir, day, `${id}-${safeName(name)}`);
|
|
1864
3167
|
let size = 0;
|
|
1865
3168
|
const counter = new Transform({
|
|
1866
3169
|
transform(chunk, _enc, done) {
|
|
@@ -1869,41 +3172,41 @@ var UploadStore = class {
|
|
|
1869
3172
|
}
|
|
1870
3173
|
});
|
|
1871
3174
|
try {
|
|
1872
|
-
await
|
|
3175
|
+
await pipeline2(body, counter, createWriteStream(file, { mode: 384 }));
|
|
1873
3176
|
} catch (e) {
|
|
1874
|
-
await
|
|
3177
|
+
await fs12.rm(file, { force: true });
|
|
1875
3178
|
throw e;
|
|
1876
3179
|
}
|
|
1877
3180
|
let kind = type || "application/octet-stream";
|
|
1878
|
-
if ((kind === "image/heic" || /\.heic$/i.test(file)) &&
|
|
3181
|
+
if ((kind === "image/heic" || /\.heic$/i.test(file)) && os5.platform() === "darwin") {
|
|
1879
3182
|
const jpeg = file.replace(/(\.heic)?$/i, ".jpg");
|
|
1880
3183
|
try {
|
|
1881
|
-
await
|
|
1882
|
-
await
|
|
3184
|
+
await run4("sips", ["-s", "format", "jpeg", file, "--out", jpeg], { timeout: 3e4 });
|
|
3185
|
+
await fs12.rm(file);
|
|
1883
3186
|
file = jpeg;
|
|
1884
3187
|
kind = "image/jpeg";
|
|
1885
|
-
size = (await
|
|
3188
|
+
size = (await fs12.stat(file)).size;
|
|
1886
3189
|
} catch {
|
|
1887
3190
|
}
|
|
1888
3191
|
}
|
|
1889
|
-
return { id, name:
|
|
3192
|
+
return { id, name: path9.basename(file).slice(id.length + 1), type: kind, size, path: file };
|
|
1890
3193
|
}
|
|
1891
3194
|
/** The file of an upload; 404 once it's gone (swept, or never existed). */
|
|
1892
3195
|
async path(id) {
|
|
1893
3196
|
if (typeof id !== "string" || !ID.test(id)) throw new HttpError(400, "invalid upload id");
|
|
1894
3197
|
let days = [];
|
|
1895
3198
|
try {
|
|
1896
|
-
days = (await
|
|
3199
|
+
days = (await fs12.readdir(this.dir)).sort().reverse();
|
|
1897
3200
|
} catch {
|
|
1898
3201
|
}
|
|
1899
3202
|
for (const day of days) {
|
|
1900
3203
|
let names = [];
|
|
1901
3204
|
try {
|
|
1902
|
-
names = await
|
|
3205
|
+
names = await fs12.readdir(path9.join(this.dir, day));
|
|
1903
3206
|
} catch {
|
|
1904
3207
|
}
|
|
1905
3208
|
const hit = names.find((n) => n.startsWith(`${id}-`));
|
|
1906
|
-
if (hit) return
|
|
3209
|
+
if (hit) return path9.join(this.dir, day, hit);
|
|
1907
3210
|
}
|
|
1908
3211
|
throw new HttpError(404, "no such upload (they are kept for 7 days)");
|
|
1909
3212
|
}
|
|
@@ -1911,25 +3214,25 @@ var UploadStore = class {
|
|
|
1911
3214
|
async sweep() {
|
|
1912
3215
|
let days = [];
|
|
1913
3216
|
try {
|
|
1914
|
-
days = await
|
|
3217
|
+
days = await fs12.readdir(this.dir);
|
|
1915
3218
|
} catch {
|
|
1916
3219
|
return;
|
|
1917
3220
|
}
|
|
1918
3221
|
const cutoff = this.now() - KEEP;
|
|
1919
3222
|
for (const day of days) {
|
|
1920
|
-
const dir =
|
|
3223
|
+
const dir = path9.join(this.dir, day);
|
|
1921
3224
|
let names = [];
|
|
1922
3225
|
try {
|
|
1923
|
-
names = await
|
|
3226
|
+
names = await fs12.readdir(dir);
|
|
1924
3227
|
} catch {
|
|
1925
3228
|
continue;
|
|
1926
3229
|
}
|
|
1927
3230
|
for (const n of names) {
|
|
1928
|
-
const file =
|
|
1929
|
-
const stat = await
|
|
1930
|
-
if (stat && stat.mtimeMs < cutoff) await
|
|
3231
|
+
const file = path9.join(dir, n);
|
|
3232
|
+
const stat = await fs12.stat(file).catch(() => null);
|
|
3233
|
+
if (stat && stat.mtimeMs < cutoff) await fs12.rm(file, { force: true });
|
|
1931
3234
|
}
|
|
1932
|
-
if (!(await
|
|
3235
|
+
if (!(await fs12.readdir(dir).catch(() => [])).length) await fs12.rm(dir, { recursive: true, force: true });
|
|
1933
3236
|
}
|
|
1934
3237
|
}
|
|
1935
3238
|
};
|
|
@@ -1942,21 +3245,48 @@ function kindOf(stat) {
|
|
|
1942
3245
|
return "other";
|
|
1943
3246
|
}
|
|
1944
3247
|
async function describe(fullPath, name) {
|
|
1945
|
-
const lst = await
|
|
1946
|
-
const
|
|
3248
|
+
const lst = await fs13.lstat(fullPath);
|
|
3249
|
+
const entry3 = { name, type: kindOf(lst), size: lst.size, mtime: lst.mtimeMs };
|
|
1947
3250
|
if (lst.isSymbolicLink()) {
|
|
1948
3251
|
try {
|
|
1949
|
-
|
|
3252
|
+
entry3.target = kindOf(await fs13.stat(fullPath));
|
|
1950
3253
|
} catch {
|
|
1951
|
-
|
|
3254
|
+
entry3.target = "broken";
|
|
1952
3255
|
}
|
|
1953
3256
|
}
|
|
1954
|
-
return
|
|
3257
|
+
return entry3;
|
|
1955
3258
|
}
|
|
1956
3259
|
var PASTE_START = "\x1B[200~";
|
|
1957
3260
|
var PASTE_END = "\x1B[201~";
|
|
1958
3261
|
var KEYS = { interrupt: "\x1B", cycle_mode: "\x1B[Z" };
|
|
1959
|
-
var
|
|
3262
|
+
var MEDIA_TYPES = {
|
|
3263
|
+
".mp4": "video/mp4",
|
|
3264
|
+
".m4v": "video/x-m4v",
|
|
3265
|
+
".mov": "video/quicktime",
|
|
3266
|
+
".webm": "video/webm",
|
|
3267
|
+
".mp3": "audio/mpeg",
|
|
3268
|
+
".m4a": "audio/mp4",
|
|
3269
|
+
".aac": "audio/aac",
|
|
3270
|
+
".wav": "audio/wav",
|
|
3271
|
+
".caf": "audio/x-caf"
|
|
3272
|
+
};
|
|
3273
|
+
function byteRange(header, size) {
|
|
3274
|
+
const m = /^bytes=(\d*)-(\d*)$/.exec(String(header ?? "").trim());
|
|
3275
|
+
if (!m || m[1] === "" && m[2] === "") return null;
|
|
3276
|
+
let start2;
|
|
3277
|
+
let end;
|
|
3278
|
+
if (m[1] === "") {
|
|
3279
|
+
const n = Number(m[2]);
|
|
3280
|
+
if (n === 0) return "unsatisfiable";
|
|
3281
|
+
start2 = Math.max(0, size - n);
|
|
3282
|
+
end = size - 1;
|
|
3283
|
+
} else {
|
|
3284
|
+
start2 = Number(m[1]);
|
|
3285
|
+
end = m[2] === "" ? size - 1 : Math.min(Number(m[2]), size - 1);
|
|
3286
|
+
}
|
|
3287
|
+
if (start2 >= size || start2 > end) return "unsatisfiable";
|
|
3288
|
+
return { start: start2, end };
|
|
3289
|
+
}
|
|
1960
3290
|
function typeable(text) {
|
|
1961
3291
|
return text.replace(/\r\n?/g, "\n").replace(/[\x00-\x08\x0b-\x1f\x7f]/g, "");
|
|
1962
3292
|
}
|
|
@@ -1966,7 +3296,7 @@ function sendJson(res, status2, body) {
|
|
|
1966
3296
|
res.writeHead(status2, { "content-type": "application/json", "content-length": Buffer.byteLength(json) });
|
|
1967
3297
|
res.end(json);
|
|
1968
3298
|
}
|
|
1969
|
-
async function
|
|
3299
|
+
async function readJson2(req) {
|
|
1970
3300
|
const chunks = [];
|
|
1971
3301
|
for await (const c of req) chunks.push(c);
|
|
1972
3302
|
try {
|
|
@@ -1979,18 +3309,24 @@ function createAgentServer({
|
|
|
1979
3309
|
token,
|
|
1980
3310
|
root: root2,
|
|
1981
3311
|
tunnelToken = async () => null,
|
|
1982
|
-
preview = null,
|
|
3312
|
+
preview: preview2 = null,
|
|
1983
3313
|
tunnelStatus: tunnelStatus2 = null,
|
|
3314
|
+
tunnelDelete: tunnelDelete2 = null,
|
|
1984
3315
|
hookSecret = null,
|
|
1985
3316
|
hookBin = null,
|
|
1986
3317
|
uploadsDir = null,
|
|
1987
3318
|
claudeCommand = "claude",
|
|
1988
3319
|
codexCommand = "codex",
|
|
1989
|
-
codexHome = process.env.CODEX_HOME ||
|
|
3320
|
+
codexHome = process.env.CODEX_HOME || path10.join(os6.homedir(), ".codex"),
|
|
3321
|
+
home = os6.homedir(),
|
|
3322
|
+
xcodebuild = "xcodebuild",
|
|
3323
|
+
builds = new BuildRunner({ xcodebuild }),
|
|
1990
3324
|
pluginInstalled = null,
|
|
1991
3325
|
agents = new AgentHub(),
|
|
1992
3326
|
pushStore: pushStore2 = null,
|
|
1993
|
-
pushOptions = {}
|
|
3327
|
+
pushOptions = {},
|
|
3328
|
+
previewSweep = {},
|
|
3329
|
+
version = null
|
|
1994
3330
|
}) {
|
|
1995
3331
|
const handoff = new TunnelHandoff({ token, mint: tunnelToken });
|
|
1996
3332
|
const sandbox = new Sandbox(root2);
|
|
@@ -2003,9 +3339,13 @@ function createAgentServer({
|
|
|
2003
3339
|
});
|
|
2004
3340
|
}, 10 * 60 * 1e3);
|
|
2005
3341
|
sweep.unref();
|
|
2006
|
-
|
|
2007
|
-
const
|
|
2008
|
-
const
|
|
3342
|
+
const presences = /* @__PURE__ */ new Map();
|
|
3343
|
+
const freshPresences = () => [...presences.values()].filter((p) => Date.now() - p.at < 3e4);
|
|
3344
|
+
const clientInForeground = () => freshPresences().length > 0;
|
|
3345
|
+
const clientShows = ({ session = null, builds: builds2 = false }) => freshPresences().some((p) => p.all || (session ? p.session === session : builds2 && p.builds));
|
|
3346
|
+
const push2 = new PushSender({ hub: agents, store: pushStore2, shows: clientShows, ...pushOptions });
|
|
3347
|
+
builds.on("ended", (build) => push2.buildEnded(build).catch(() => {
|
|
3348
|
+
}));
|
|
2009
3349
|
if (!agents.presenceSet) {
|
|
2010
3350
|
agents.present = async () => !clientInForeground() && await (pushOptions.userActive ?? userActiveHere)();
|
|
2011
3351
|
}
|
|
@@ -2096,12 +3436,16 @@ function createAgentServer({
|
|
|
2096
3436
|
}
|
|
2097
3437
|
async function codexModels() {
|
|
2098
3438
|
try {
|
|
2099
|
-
const { models } = JSON.parse(await
|
|
3439
|
+
const { models } = JSON.parse(await fs13.readFile(path10.join(codexHome, "models_cache.json"), "utf8"));
|
|
2100
3440
|
return models.filter((m) => m.visibility === "list" && typeof m.slug === "string").sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0)).map((m) => ({ id: m.slug, name: m.display_name || m.slug, description: m.description ?? "" }));
|
|
2101
3441
|
} catch {
|
|
2102
3442
|
return [];
|
|
2103
3443
|
}
|
|
2104
3444
|
}
|
|
3445
|
+
const installs = preview2 ? new InstallLinks({ preview: preview2 }) : null;
|
|
3446
|
+
function sessionCommands(session) {
|
|
3447
|
+
return listCommands({ agent: session.agent, cwd: session.cwd || sandbox.root, home, codexHome });
|
|
3448
|
+
}
|
|
2105
3449
|
function hookAuthorized(req) {
|
|
2106
3450
|
if (!hookSecret) return false;
|
|
2107
3451
|
const got = Buffer.from(String(req.headers[HOOK_SECRET_HEADER] || ""));
|
|
@@ -2109,7 +3453,7 @@ function createAgentServer({
|
|
|
2109
3453
|
return got.length === want.length && timingSafeEqual2(got, want);
|
|
2110
3454
|
}
|
|
2111
3455
|
function previewPort(value, ownPort) {
|
|
2112
|
-
if (!
|
|
3456
|
+
if (!preview2) throw new HttpError(404, "previews need a tunnel");
|
|
2113
3457
|
const port2 = Number(value);
|
|
2114
3458
|
if (!Number.isInteger(port2) || port2 < 1 || port2 > 65535) throw new HttpError(400, "port must be 1-65535");
|
|
2115
3459
|
if (port2 === ownPort) throw new HttpError(400, "the agent's own port is already on its tunnel");
|
|
@@ -2119,7 +3463,7 @@ function createAgentServer({
|
|
|
2119
3463
|
const proxies = /* @__PURE__ */ new Map();
|
|
2120
3464
|
function previewed(port2) {
|
|
2121
3465
|
const proxy = proxies.get(port2);
|
|
2122
|
-
if (!proxy || !
|
|
3466
|
+
if (!proxy || !preview2.list().some((p) => p.port === proxy.port)) {
|
|
2123
3467
|
throw new HttpError(404, "that port is not being previewed");
|
|
2124
3468
|
}
|
|
2125
3469
|
return proxy;
|
|
@@ -2130,36 +3474,68 @@ function createAgentServer({
|
|
|
2130
3474
|
return proxy;
|
|
2131
3475
|
}
|
|
2132
3476
|
async function previews() {
|
|
2133
|
-
const issued = await
|
|
2134
|
-
const forwarded = new Map(
|
|
3477
|
+
const issued = await preview2.token();
|
|
3478
|
+
const forwarded = new Map(preview2.list().map((p) => [p.port, p]));
|
|
2135
3479
|
const ports = [];
|
|
2136
3480
|
for (const [port2, proxy] of proxies) {
|
|
2137
3481
|
const p = forwarded.get(proxy.port);
|
|
2138
3482
|
if (p) ports.push({ ...p, port: port2 });
|
|
2139
3483
|
}
|
|
2140
3484
|
ports.sort((a, b) => a.port - b.port);
|
|
3485
|
+
const install = installs?.sharing ? forwarded.get(installs.port) : null;
|
|
3486
|
+
if (install) ports.push({ ...install, install: true });
|
|
2141
3487
|
return { ports, token: issued?.token ?? null, expiresAt: issued?.expiresAt ?? null };
|
|
2142
3488
|
}
|
|
3489
|
+
const { checkMs = 5e3, graceMs = 3e4, killedMs = 1500 } = previewSweep;
|
|
3490
|
+
const silentSince = /* @__PURE__ */ new Map();
|
|
3491
|
+
let sweeping = false;
|
|
3492
|
+
async function sweepPreviews(ports = [...proxies.keys()], now = false) {
|
|
3493
|
+
const forwarded = new Set(preview2.list().map((p) => p.port));
|
|
3494
|
+
for (const port2 of ports) {
|
|
3495
|
+
const proxy = proxies.get(port2);
|
|
3496
|
+
if (!proxy || !forwarded.has(proxy.port) || await isListening(port2)) {
|
|
3497
|
+
silentSince.delete(port2);
|
|
3498
|
+
continue;
|
|
3499
|
+
}
|
|
3500
|
+
if (!silentSince.has(port2)) silentSince.set(port2, Date.now());
|
|
3501
|
+
if (!now && Date.now() - silentSince.get(port2) < graceMs) continue;
|
|
3502
|
+
silentSince.delete(port2);
|
|
3503
|
+
try {
|
|
3504
|
+
await preview2.unforward(proxy.port);
|
|
3505
|
+
} catch {
|
|
3506
|
+
}
|
|
3507
|
+
}
|
|
3508
|
+
}
|
|
3509
|
+
const previewTimer = preview2 && setInterval(async () => {
|
|
3510
|
+
if (sweeping || !proxies.size) return;
|
|
3511
|
+
sweeping = true;
|
|
3512
|
+
try {
|
|
3513
|
+
await sweepPreviews();
|
|
3514
|
+
} finally {
|
|
3515
|
+
sweeping = false;
|
|
3516
|
+
}
|
|
3517
|
+
}, checkMs);
|
|
3518
|
+
previewTimer?.unref();
|
|
2143
3519
|
function shellPids() {
|
|
2144
3520
|
return new Map(sessions.list().map((s) => [s.pid, s.id]));
|
|
2145
3521
|
}
|
|
2146
3522
|
async function repoOf(p) {
|
|
2147
3523
|
const repo = await sandbox.resolve(p);
|
|
2148
3524
|
try {
|
|
2149
|
-
await
|
|
3525
|
+
await fs13.access(path10.join(repo, ".git"));
|
|
2150
3526
|
} catch {
|
|
2151
3527
|
throw new HttpError(400, "not a git repository");
|
|
2152
3528
|
}
|
|
2153
3529
|
return repo;
|
|
2154
3530
|
}
|
|
2155
3531
|
async function filesIn(repo, files) {
|
|
2156
|
-
if (!Array.isArray(files) || !files.length || files.some((f) => typeof f !== "string" || !f ||
|
|
3532
|
+
if (!Array.isArray(files) || !files.length || files.some((f) => typeof f !== "string" || !f || path10.isAbsolute(f))) {
|
|
2157
3533
|
throw new HttpError(400, "files must be a non-empty list of paths relative to the repository");
|
|
2158
3534
|
}
|
|
2159
3535
|
return Promise.all(
|
|
2160
3536
|
files.map(async (f) => {
|
|
2161
|
-
const abs = await sandbox.resolve(
|
|
2162
|
-
const rel =
|
|
3537
|
+
const abs = await sandbox.resolve(path10.join(repo, f), { allowMissing: true });
|
|
3538
|
+
const rel = path10.relative(repo, abs);
|
|
2163
3539
|
if (!rel || rel.startsWith("..")) throw new HttpError(403, "path is outside the repository");
|
|
2164
3540
|
return rel;
|
|
2165
3541
|
})
|
|
@@ -2168,55 +3544,95 @@ function createAgentServer({
|
|
|
2168
3544
|
const routes = {
|
|
2169
3545
|
[Routes.info]: async () => ({
|
|
2170
3546
|
protocol: PROTOCOL_VERSION,
|
|
2171
|
-
|
|
2172
|
-
|
|
3547
|
+
minClientProtocol: MIN_CLIENT_PROTOCOL,
|
|
3548
|
+
version,
|
|
3549
|
+
hostname: os6.hostname(),
|
|
3550
|
+
platform: os6.platform(),
|
|
2173
3551
|
root: sandbox.root,
|
|
2174
|
-
user:
|
|
3552
|
+
user: os6.userInfo().username,
|
|
2175
3553
|
// On Windows, terminals run PowerShell whatever $SHELL (Git Bash sets one) says.
|
|
2176
3554
|
shell: isWindows ? interactiveShell().file : process.env.SHELL || null
|
|
2177
3555
|
}),
|
|
2178
3556
|
[Routes.fsList]: async (url) => {
|
|
2179
3557
|
const dir = await sandbox.resolve(url.searchParams.get("path"));
|
|
2180
|
-
const names = await
|
|
2181
|
-
const
|
|
2182
|
-
return { path: dir, entries };
|
|
3558
|
+
const names = await fs13.readdir(dir);
|
|
3559
|
+
const entries2 = (await Promise.all(names.map((n) => describe(path10.join(dir, n), n).catch(() => null)))).filter(Boolean);
|
|
3560
|
+
return { path: dir, entries: entries2 };
|
|
2183
3561
|
},
|
|
2184
3562
|
[Routes.fsStat]: async (url) => {
|
|
2185
3563
|
const p = await sandbox.resolve(url.searchParams.get("path"));
|
|
2186
|
-
return { path: p, ...await describe(p,
|
|
3564
|
+
return { path: p, ...await describe(p, path10.basename(p)) };
|
|
2187
3565
|
},
|
|
2188
3566
|
[Routes.fsRead]: async (url, req, res) => {
|
|
2189
3567
|
const p = await sandbox.resolve(url.searchParams.get("path"));
|
|
2190
|
-
const stat = await
|
|
3568
|
+
const stat = await fs13.stat(p);
|
|
2191
3569
|
if (!stat.isFile()) throw new HttpError(400, "not a file");
|
|
2192
|
-
|
|
2193
|
-
|
|
3570
|
+
const type = MEDIA_TYPES[path10.extname(p).toLowerCase()] ?? "application/octet-stream";
|
|
3571
|
+
const range = byteRange(req.headers.range, stat.size);
|
|
3572
|
+
if (range === "unsatisfiable") {
|
|
3573
|
+
res.writeHead(416, { "content-range": `bytes */${stat.size}`, "accept-ranges": "bytes" });
|
|
3574
|
+
res.end();
|
|
3575
|
+
return;
|
|
3576
|
+
}
|
|
3577
|
+
if (range) {
|
|
3578
|
+
res.writeHead(206, {
|
|
3579
|
+
"content-type": type,
|
|
3580
|
+
"content-length": range.end - range.start + 1,
|
|
3581
|
+
"content-range": `bytes ${range.start}-${range.end}/${stat.size}`,
|
|
3582
|
+
"accept-ranges": "bytes"
|
|
3583
|
+
});
|
|
3584
|
+
await pipeline3(createReadStream2(p, range), res);
|
|
3585
|
+
return;
|
|
3586
|
+
}
|
|
3587
|
+
res.writeHead(200, { "content-type": type, "content-length": stat.size, "accept-ranges": "bytes" });
|
|
3588
|
+
await pipeline3(createReadStream2(p), res);
|
|
2194
3589
|
},
|
|
2195
3590
|
[Routes.fsWrite]: async (url, req) => {
|
|
2196
3591
|
const p = await sandbox.resolveChild(url.searchParams.get("path"), { allowMissing: true });
|
|
2197
3592
|
const tmp = `${p}.vsremote-${process.pid}-${Date.now()}.tmp`;
|
|
2198
3593
|
try {
|
|
2199
|
-
await
|
|
2200
|
-
await
|
|
3594
|
+
await pipeline3(req, createWriteStream2(tmp));
|
|
3595
|
+
await fs13.rename(tmp, p);
|
|
2201
3596
|
} catch (e) {
|
|
2202
|
-
await
|
|
3597
|
+
await fs13.rm(tmp, { force: true });
|
|
2203
3598
|
throw e;
|
|
2204
3599
|
}
|
|
2205
|
-
return describe(p,
|
|
3600
|
+
return describe(p, path10.basename(p));
|
|
2206
3601
|
},
|
|
2207
3602
|
[Routes.fsMkdir]: async (url) => {
|
|
2208
|
-
await
|
|
3603
|
+
await fs13.mkdir(await sandbox.resolve(url.searchParams.get("path"), { allowMissing: true }), { recursive: true });
|
|
2209
3604
|
return {};
|
|
2210
3605
|
},
|
|
2211
3606
|
[Routes.fsRename]: async (url, req) => {
|
|
2212
|
-
const { from, to } = await
|
|
2213
|
-
await
|
|
3607
|
+
const { from, to } = await readJson2(req);
|
|
3608
|
+
await fs13.rename(await sandbox.resolveChild(from), await sandbox.resolveChild(to, { allowMissing: true }));
|
|
2214
3609
|
return {};
|
|
2215
3610
|
},
|
|
2216
3611
|
[Routes.fsDelete]: async (url) => {
|
|
2217
|
-
await
|
|
3612
|
+
await fs13.rm(await sandbox.resolveChild(url.searchParams.get("path")), { recursive: true });
|
|
2218
3613
|
return {};
|
|
2219
3614
|
},
|
|
3615
|
+
[Routes.fsSearch]: async (url, req, res) => {
|
|
3616
|
+
const q = url.searchParams;
|
|
3617
|
+
const flag2 = (name) => q.get(name) === "1";
|
|
3618
|
+
const gone = new AbortController();
|
|
3619
|
+
res.on("close", () => {
|
|
3620
|
+
if (!res.writableFinished) gone.abort();
|
|
3621
|
+
});
|
|
3622
|
+
const found = await search({
|
|
3623
|
+
root: sandbox.root,
|
|
3624
|
+
query: q.get("q") ?? "",
|
|
3625
|
+
caseSensitive: flag2("case"),
|
|
3626
|
+
wholeWord: flag2("word"),
|
|
3627
|
+
regex: flag2("regex"),
|
|
3628
|
+
include: q.get("include") ?? "",
|
|
3629
|
+
exclude: q.get("exclude") ?? "",
|
|
3630
|
+
limit: Number(q.get("limit")) || void 0,
|
|
3631
|
+
signal: gone.signal
|
|
3632
|
+
});
|
|
3633
|
+
if (gone.signal.aborted) return void 0;
|
|
3634
|
+
return { root: sandbox.root, ...found };
|
|
3635
|
+
},
|
|
2220
3636
|
[Routes.gitRepos]: async () => ({ repos: await findRepos(sandbox.root) }),
|
|
2221
3637
|
[Routes.gitStatus]: async (url) => status(await repoOf(url.searchParams.get("repo"))),
|
|
2222
3638
|
[Routes.gitDiff]: async (url) => {
|
|
@@ -2228,46 +3644,48 @@ function createAgentServer({
|
|
|
2228
3644
|
});
|
|
2229
3645
|
},
|
|
2230
3646
|
[Routes.gitStage]: async (url, req) => {
|
|
2231
|
-
const { repo: r, files } = await
|
|
3647
|
+
const { repo: r, files } = await readJson2(req);
|
|
2232
3648
|
const repo = await repoOf(r);
|
|
2233
3649
|
await stage(repo, await filesIn(repo, files));
|
|
2234
3650
|
return status(repo);
|
|
2235
3651
|
},
|
|
2236
3652
|
[Routes.gitUnstage]: async (url, req) => {
|
|
2237
|
-
const { repo: r, files } = await
|
|
3653
|
+
const { repo: r, files } = await readJson2(req);
|
|
2238
3654
|
const repo = await repoOf(r);
|
|
2239
3655
|
await unstage(repo, await filesIn(repo, files));
|
|
2240
3656
|
return status(repo);
|
|
2241
3657
|
},
|
|
2242
3658
|
[Routes.gitDiscard]: async (url, req) => {
|
|
2243
|
-
const { repo: r, files } = await
|
|
3659
|
+
const { repo: r, files } = await readJson2(req);
|
|
2244
3660
|
const repo = await repoOf(r);
|
|
2245
3661
|
await discard(repo, await filesIn(repo, files));
|
|
2246
3662
|
return status(repo);
|
|
2247
3663
|
},
|
|
2248
3664
|
[Routes.gitCommit]: async (url, req) => {
|
|
2249
|
-
const { repo: r, message } = await
|
|
3665
|
+
const { repo: r, message } = await readJson2(req);
|
|
2250
3666
|
return commit(await repoOf(r), message);
|
|
2251
3667
|
},
|
|
2252
3668
|
[Routes.gitLog]: async (url) => ({
|
|
2253
3669
|
commits: await log(await repoOf(url.searchParams.get("repo")), parseInt(url.searchParams.get("limit"), 10) || 50)
|
|
2254
3670
|
}),
|
|
2255
3671
|
[Routes.gitShow]: async (url) => show(await repoOf(url.searchParams.get("repo")), url.searchParams.get("commit")),
|
|
2256
|
-
[Routes.gitPull]: async (url, req) => ({ output: await pull(await repoOf((await
|
|
2257
|
-
[Routes.gitPush]: async (url, req) => ({ output: await push(await repoOf((await
|
|
3672
|
+
[Routes.gitPull]: async (url, req) => ({ output: await pull(await repoOf((await readJson2(req)).repo)) }),
|
|
3673
|
+
[Routes.gitPush]: async (url, req) => ({ output: await push(await repoOf((await readJson2(req)).repo)) }),
|
|
2258
3674
|
[Routes.termList]: async () => ({ sessions: sessions.list() }),
|
|
2259
3675
|
[Routes.portsList]: async () => ({ ports: await listPorts(shellPids()) }),
|
|
2260
3676
|
[Routes.portsKill]: async (url, req) => {
|
|
2261
|
-
const { pid, force } = await
|
|
2262
|
-
await killListener(pid, { force: force === true, shells: shellPids() });
|
|
3677
|
+
const { pid, force } = await readJson2(req);
|
|
3678
|
+
const ports = await killListener(pid, { force: force === true, shells: shellPids() });
|
|
3679
|
+
if (preview2) setTimeout(() => sweepPreviews(ports, true).catch(() => {
|
|
3680
|
+
}), killedMs).unref();
|
|
2263
3681
|
return {};
|
|
2264
3682
|
},
|
|
2265
3683
|
[Routes.previewList]: async () => {
|
|
2266
|
-
if (!
|
|
3684
|
+
if (!preview2) throw new HttpError(404, "previews need a tunnel");
|
|
2267
3685
|
return previews();
|
|
2268
3686
|
},
|
|
2269
3687
|
[Routes.previewOpen]: async (url, req) => {
|
|
2270
|
-
const port2 = previewPort((await
|
|
3688
|
+
const port2 = previewPort((await readJson2(req)).port, server.address()?.port);
|
|
2271
3689
|
let proxy = proxies.get(port2);
|
|
2272
3690
|
if (!proxy) {
|
|
2273
3691
|
proxy = new InspectingProxy(port2);
|
|
@@ -2275,7 +3693,7 @@ function createAgentServer({
|
|
|
2275
3693
|
proxies.set(port2, proxy);
|
|
2276
3694
|
}
|
|
2277
3695
|
try {
|
|
2278
|
-
await
|
|
3696
|
+
await preview2.forward(proxy.port);
|
|
2279
3697
|
} catch (e) {
|
|
2280
3698
|
throw new HttpError(502, e.message);
|
|
2281
3699
|
}
|
|
@@ -2283,25 +3701,34 @@ function createAgentServer({
|
|
|
2283
3701
|
},
|
|
2284
3702
|
[Routes.previewClose]: async (url) => {
|
|
2285
3703
|
const port2 = previewPort(url.searchParams.get("port"), server.address()?.port);
|
|
3704
|
+
if (installs && port2 === installs.port) {
|
|
3705
|
+
await installs.stop();
|
|
3706
|
+
return previews();
|
|
3707
|
+
}
|
|
2286
3708
|
const proxy = proxies.get(port2);
|
|
2287
|
-
if (proxy) await
|
|
3709
|
+
if (proxy) await preview2.unforward(proxy.port);
|
|
2288
3710
|
return previews();
|
|
2289
3711
|
},
|
|
2290
3712
|
[Routes.previewShare]: async (url, req) => {
|
|
2291
|
-
const { port: value, hours } = await
|
|
3713
|
+
const { port: value, hours } = await readJson2(req);
|
|
2292
3714
|
const proxy = previewed(previewPort(value, server.address()?.port));
|
|
2293
3715
|
if (!Number.isInteger(hours) || hours < 1 || hours > 720) throw new HttpError(400, "hours must be 1-720");
|
|
2294
3716
|
try {
|
|
2295
|
-
await
|
|
3717
|
+
await preview2.share(proxy.port, hours);
|
|
2296
3718
|
} catch (e) {
|
|
2297
3719
|
throw new HttpError(502, e.message);
|
|
2298
3720
|
}
|
|
2299
3721
|
return previews();
|
|
2300
3722
|
},
|
|
2301
3723
|
[Routes.previewUnshare]: async (url) => {
|
|
3724
|
+
const asked = previewPort(url.searchParams.get("port"), server.address()?.port);
|
|
3725
|
+
if (installs && asked === installs.port) {
|
|
3726
|
+
await installs.stop();
|
|
3727
|
+
return previews();
|
|
3728
|
+
}
|
|
2302
3729
|
const proxy = proxies.get(previewPort(url.searchParams.get("port"), server.address()?.port));
|
|
2303
3730
|
try {
|
|
2304
|
-
if (proxy) await
|
|
3731
|
+
if (proxy) await preview2.unshare(proxy.port);
|
|
2305
3732
|
} catch (e) {
|
|
2306
3733
|
throw new HttpError(502, e.message);
|
|
2307
3734
|
}
|
|
@@ -2311,9 +3738,9 @@ function createAgentServer({
|
|
|
2311
3738
|
requests: inspected(url.searchParams.get("port")).list(parseInt(url.searchParams.get("after"), 10) || 0)
|
|
2312
3739
|
}),
|
|
2313
3740
|
[Routes.previewRequest]: async (url) => {
|
|
2314
|
-
const
|
|
2315
|
-
if (!
|
|
2316
|
-
return detail(
|
|
3741
|
+
const entry3 = inspected(url.searchParams.get("port")).get(parseInt(url.searchParams.get("id"), 10));
|
|
3742
|
+
if (!entry3) throw new HttpError(404, "no such request (only the latest 200 are kept)");
|
|
3743
|
+
return detail(entry3);
|
|
2317
3744
|
},
|
|
2318
3745
|
[Routes.previewRequestsClear]: async (url) => {
|
|
2319
3746
|
inspected(url.searchParams.get("port")).clear();
|
|
@@ -2324,6 +3751,11 @@ function createAgentServer({
|
|
|
2324
3751
|
if (!status2) throw new HttpError(404, "this agent runs without a tunnel");
|
|
2325
3752
|
return status2;
|
|
2326
3753
|
},
|
|
3754
|
+
[Routes.tunnelDelete]: async (url, req) => {
|
|
3755
|
+
if (!tunnelDelete2) throw new HttpError(404, "this agent runs without a tunnel");
|
|
3756
|
+
const { id, account, force } = await readJson2(req);
|
|
3757
|
+
return tunnelDelete2({ id, account, force: force === true });
|
|
3758
|
+
},
|
|
2327
3759
|
[Routes.tunnelToken]: async () => {
|
|
2328
3760
|
const issued = await handoff.issue();
|
|
2329
3761
|
if (!issued) throw new HttpError(404, "this tunnel is anonymous");
|
|
@@ -2336,8 +3768,43 @@ function createAgentServer({
|
|
|
2336
3768
|
const [plugin, codexPlugin] = await Promise.all([claude && hasPlugin("claude"), codex && hasPlugin("codex")]);
|
|
2337
3769
|
return { claude, plugin: Boolean(plugin), codex, codexPlugin: Boolean(codexPlugin), codexModels: codex ? await codexModels() : [] };
|
|
2338
3770
|
},
|
|
3771
|
+
[Routes.agentsCommands]: async (url) => {
|
|
3772
|
+
const session = agents.session(url.searchParams.get("session") ?? "");
|
|
3773
|
+
if (!session) throw new HttpError(404, "no such session");
|
|
3774
|
+
return { commands: await sessionCommands(session) };
|
|
3775
|
+
},
|
|
3776
|
+
[Routes.agentsHistory]: async (url) => {
|
|
3777
|
+
const session = agents.session(url.searchParams.get("session") ?? "");
|
|
3778
|
+
if (!session) throw new HttpError(404, "no such session");
|
|
3779
|
+
const found = await listHistory({ agent: session.agent, cwd: session.cwd || sandbox.root, home, codexHome });
|
|
3780
|
+
return { sessions: found.filter((h) => h.id !== session.id) };
|
|
3781
|
+
},
|
|
3782
|
+
[Routes.buildsSchemes]: async (url) => listSchemes(await sandbox.resolve(url.searchParams.get("path")), { xcodebuild }),
|
|
3783
|
+
[Routes.buildsStart]: async (url, req) => {
|
|
3784
|
+
const { path: folder, scheme, configuration } = await readJson2(req);
|
|
3785
|
+
return builds.start({ folder: await sandbox.resolve(folder), scheme, configuration });
|
|
3786
|
+
},
|
|
3787
|
+
[Routes.buildsCurrent]: async (url) => {
|
|
3788
|
+
const build = builds.current(parseInt(url.searchParams.get("after"), 10) || 0);
|
|
3789
|
+
if (!build) throw new HttpError(404, "no build yet");
|
|
3790
|
+
return build;
|
|
3791
|
+
},
|
|
3792
|
+
[Routes.buildsCancel]: async () => {
|
|
3793
|
+
builds.cancel();
|
|
3794
|
+
return {};
|
|
3795
|
+
},
|
|
3796
|
+
[Routes.buildsInstall]: async () => {
|
|
3797
|
+
if (!installs) throw new HttpError(409, "installing needs the tunnel: start the agent with `coderemote up`");
|
|
3798
|
+
const artifact = builds.artifact();
|
|
3799
|
+
if (!artifact) throw new HttpError(409, "there is no successful build to install");
|
|
3800
|
+
try {
|
|
3801
|
+
return await installs.create(artifact);
|
|
3802
|
+
} catch (e) {
|
|
3803
|
+
throw e instanceof HttpError ? e : new HttpError(502, e.message);
|
|
3804
|
+
}
|
|
3805
|
+
},
|
|
2339
3806
|
[Routes.agentsLaunch]: async (url, req) => {
|
|
2340
|
-
const { agent = "claude", cwd, model, permissionMode, approvalPolicy, resume, cols, rows } = await
|
|
3807
|
+
const { agent = "claude", cwd, model, permissionMode, approvalPolicy, resume, cols, rows } = await readJson2(req);
|
|
2341
3808
|
if (approvalPolicy !== void 0 && !["on-request", "never"].includes(approvalPolicy)) {
|
|
2342
3809
|
throw new HttpError(400, "approvalPolicy must be on-request or never");
|
|
2343
3810
|
}
|
|
@@ -2349,7 +3816,7 @@ function createAgentServer({
|
|
|
2349
3816
|
} catch (e) {
|
|
2350
3817
|
throw new HttpError(400, e.message);
|
|
2351
3818
|
}
|
|
2352
|
-
if (!(await
|
|
3819
|
+
if (!(await fs13.stat(dir)).isDirectory()) throw new HttpError(400, "cwd must be a folder");
|
|
2353
3820
|
if (model !== void 0 && (typeof model !== "string" || !/^[A-Za-z0-9._[\]-]{1,80}$/.test(model))) {
|
|
2354
3821
|
throw new HttpError(400, "invalid model");
|
|
2355
3822
|
}
|
|
@@ -2370,8 +3837,8 @@ function createAgentServer({
|
|
|
2370
3837
|
if (!await hasPlugin(agent)) {
|
|
2371
3838
|
throw Object.assign(new HttpError(400, launcher.missingPlugin), { code: "ENOPLUGIN" });
|
|
2372
3839
|
}
|
|
2373
|
-
const id = resume ??
|
|
2374
|
-
if (uploadsDir) await
|
|
3840
|
+
const id = resume ?? randomUUID5();
|
|
3841
|
+
if (uploadsDir) await fs13.mkdir(uploadsDir, { recursive: true, mode: 448 });
|
|
2375
3842
|
const argv2 = [
|
|
2376
3843
|
launcher.command,
|
|
2377
3844
|
...launcher.args({
|
|
@@ -2392,14 +3859,14 @@ function createAgentServer({
|
|
|
2392
3859
|
return { terminal: term.id, session: agents.launched({ id, terminal: term.id, cwd: dir, agent }) };
|
|
2393
3860
|
},
|
|
2394
3861
|
[Routes.agentsPrompt]: async (url, req) => {
|
|
2395
|
-
const { session: id, text, attachments } = await
|
|
3862
|
+
const { session: id, text, attachments } = await readJson2(req);
|
|
2396
3863
|
if (typeof text !== "string" || !text.trim() || text.length > 1e5) {
|
|
2397
3864
|
throw new HttpError(400, "text must be a non-empty string");
|
|
2398
3865
|
}
|
|
2399
3866
|
const { term } = typingTarget(id, { whileWaiting: false });
|
|
2400
3867
|
const paths = await attachmentPaths(attachments);
|
|
2401
3868
|
const typed = typeable([text.trim(), ...paths].join("\n"));
|
|
2402
|
-
const prompt =
|
|
3869
|
+
const prompt = randomUUID5();
|
|
2403
3870
|
agents.prompted(id, { id: prompt, text: typed, paths });
|
|
2404
3871
|
await typeInto(term, typed);
|
|
2405
3872
|
return { prompt };
|
|
@@ -2419,23 +3886,30 @@ function createAgentServer({
|
|
|
2419
3886
|
return { id, name: saved, type, size };
|
|
2420
3887
|
},
|
|
2421
3888
|
[Routes.agentsKeys]: async (url, req) => {
|
|
2422
|
-
const { session: id, action, command } = await
|
|
3889
|
+
const { session: id, action, command, args } = await readJson2(req);
|
|
2423
3890
|
const allowed = agents.session(typeof id === "string" ? id : "")?.capabilities?.keys;
|
|
2424
|
-
if (
|
|
3891
|
+
if (action !== "command" && allowed && !allowed.includes(action)) {
|
|
2425
3892
|
throw new HttpError(400, `this session takes ${allowed.join(", ")}`);
|
|
2426
3893
|
}
|
|
2427
3894
|
if (action === "command") {
|
|
2428
|
-
if (
|
|
2429
|
-
|
|
3895
|
+
if (typeof command !== "string") throw new HttpError(400, "command must be a string");
|
|
3896
|
+
if (args !== void 0 && (typeof args !== "string" || args.length > 1e5)) throw new HttpError(400, "args must be a string");
|
|
3897
|
+
const { session, term } = typingTarget(id, { whileWaiting: false });
|
|
3898
|
+
const listed = (await sessionCommands(session)).some((c) => c.kind === "command" && c.name === command);
|
|
3899
|
+
if (!listed && !session.capabilities?.keys?.includes(command)) {
|
|
3900
|
+
throw new HttpError(400, `${command} is not a command this session takes`);
|
|
3901
|
+
}
|
|
3902
|
+
await typeInto(term, typeable(args?.trim() ? `${command} ${args.trim()}` : command));
|
|
2430
3903
|
} else if (Object.hasOwn(KEYS, action)) {
|
|
2431
3904
|
typingTarget(id, { whileWaiting: true }).term.write(KEYS[action]);
|
|
3905
|
+
if (action === "interrupt") agents.interrupted(id);
|
|
2432
3906
|
} else {
|
|
2433
3907
|
throw new HttpError(400, `action must be one of ${[...Object.keys(KEYS), "command"].join(", ")}`);
|
|
2434
3908
|
}
|
|
2435
3909
|
return {};
|
|
2436
3910
|
},
|
|
2437
3911
|
[Routes.agentsStop]: async (url, req) => {
|
|
2438
|
-
const { session: id } = await
|
|
3912
|
+
const { session: id } = await readJson2(req);
|
|
2439
3913
|
const { term } = typingTarget(id, { whileWaiting: true });
|
|
2440
3914
|
term.kill();
|
|
2441
3915
|
setTimeout(() => {
|
|
@@ -2444,7 +3918,7 @@ function createAgentServer({
|
|
|
2444
3918
|
return {};
|
|
2445
3919
|
},
|
|
2446
3920
|
[Routes.agentsForget]: async (url, req) => {
|
|
2447
|
-
const { session: id } = await
|
|
3921
|
+
const { session: id } = await readJson2(req);
|
|
2448
3922
|
const session = typeof id === "string" ? agents.session(id) : null;
|
|
2449
3923
|
if (!session) throw new HttpError(404, "no such session");
|
|
2450
3924
|
if (session.state !== "ended" && session.terminal && sessions.get(session.terminal)) {
|
|
@@ -2454,11 +3928,11 @@ function createAgentServer({
|
|
|
2454
3928
|
return {};
|
|
2455
3929
|
},
|
|
2456
3930
|
[Routes.agentsAnswer]: async (url, req) => {
|
|
2457
|
-
agents.answer(await
|
|
3931
|
+
agents.answer(await readJson2(req));
|
|
2458
3932
|
return {};
|
|
2459
3933
|
},
|
|
2460
3934
|
[Routes.pushDeviceSet]: async (url, req) => {
|
|
2461
|
-
push2.register(await
|
|
3935
|
+
push2.register(await readJson2(req));
|
|
2462
3936
|
return {};
|
|
2463
3937
|
},
|
|
2464
3938
|
[Routes.pushDeviceDelete]: async (url) => {
|
|
@@ -2466,7 +3940,7 @@ function createAgentServer({
|
|
|
2466
3940
|
return {};
|
|
2467
3941
|
},
|
|
2468
3942
|
[Routes.pushTest]: async (url, req) => {
|
|
2469
|
-
await push2.test((await
|
|
3943
|
+
await push2.test((await readJson2(req)).token);
|
|
2470
3944
|
return {};
|
|
2471
3945
|
},
|
|
2472
3946
|
[Routes.termKill]: async (url) => {
|
|
@@ -2476,7 +3950,7 @@ function createAgentServer({
|
|
|
2476
3950
|
return {};
|
|
2477
3951
|
}
|
|
2478
3952
|
};
|
|
2479
|
-
const server =
|
|
3953
|
+
const server = http3.createServer(async (req, res) => {
|
|
2480
3954
|
const url = new URL(req.url, "http://localhost");
|
|
2481
3955
|
if (req.method === "GET" && url.pathname === "/") {
|
|
2482
3956
|
res.writeHead(200, {
|
|
@@ -2504,14 +3978,16 @@ function createAgentServer({
|
|
|
2504
3978
|
if (!hookAuthorized(req)) return sendJson(res, 401, { error: "unauthorized" });
|
|
2505
3979
|
plugins[url.searchParams.get("agent") || "claude"] = { installed: true, checkedAt: Date.now() };
|
|
2506
3980
|
const terminal = url.searchParams.get("terminal");
|
|
3981
|
+
const app = url.searchParams.get("app");
|
|
2507
3982
|
const closed = new AbortController();
|
|
2508
3983
|
res.on("close", () => {
|
|
2509
3984
|
if (!res.writableFinished) closed.abort();
|
|
2510
3985
|
});
|
|
2511
3986
|
try {
|
|
2512
|
-
const output = await agents.handle(await
|
|
3987
|
+
const output = await agents.handle(await readJson2(req), {
|
|
2513
3988
|
agent: url.searchParams.get("agent") || "claude",
|
|
2514
3989
|
terminal: terminal && sessions.get(terminal) ? terminal : null,
|
|
3990
|
+
app: app && app.length <= 1024 && /\.app$/.test(app) ? app : null,
|
|
2515
3991
|
signal: closed.signal
|
|
2516
3992
|
});
|
|
2517
3993
|
if (res.destroyed) return;
|
|
@@ -2590,30 +4066,43 @@ function createAgentServer({
|
|
|
2590
4066
|
} catch {
|
|
2591
4067
|
return;
|
|
2592
4068
|
}
|
|
2593
|
-
if (msg.type
|
|
4069
|
+
if (msg.type !== AgentsClientMessage.presence) return;
|
|
4070
|
+
if (msg.foreground !== true) return presences.delete(ws);
|
|
4071
|
+
presences.set(ws, {
|
|
4072
|
+
at: Date.now(),
|
|
4073
|
+
all: msg.session === void 0 && msg.builds === void 0,
|
|
4074
|
+
session: typeof msg.session === "string" ? msg.session : null,
|
|
4075
|
+
builds: msg.builds === true
|
|
4076
|
+
});
|
|
4077
|
+
});
|
|
4078
|
+
ws.on("close", () => {
|
|
4079
|
+
agents.off("message", send);
|
|
4080
|
+
presences.delete(ws);
|
|
2594
4081
|
});
|
|
2595
|
-
ws.on("close", () => agents.off("message", send));
|
|
2596
4082
|
}
|
|
2597
4083
|
server.on("close", () => {
|
|
4084
|
+
installs?.close();
|
|
2598
4085
|
clearInterval(sweep);
|
|
4086
|
+
clearInterval(previewTimer);
|
|
2599
4087
|
sessions.killAll();
|
|
2600
4088
|
for (const proxy of proxies.values()) proxy.close();
|
|
2601
4089
|
});
|
|
2602
4090
|
server.killAllSessions = () => sessions.killAll();
|
|
2603
4091
|
server.agents = agents;
|
|
2604
4092
|
server.clientInForeground = clientInForeground;
|
|
4093
|
+
server.clientShows = clientShows;
|
|
2605
4094
|
server.push = push2;
|
|
2606
4095
|
server.agentsUploadPath = (id) => uploadPath(id);
|
|
2607
4096
|
return server;
|
|
2608
4097
|
}
|
|
2609
4098
|
|
|
2610
4099
|
// src/devtunnel.js
|
|
2611
|
-
import { execFile as
|
|
4100
|
+
import { execFile as execFile9 } from "node:child_process";
|
|
2612
4101
|
import { createHash } from "node:crypto";
|
|
2613
|
-
import
|
|
2614
|
-
import
|
|
4102
|
+
import fs14 from "node:fs";
|
|
4103
|
+
import path11 from "node:path";
|
|
2615
4104
|
import { promisify as promisify3 } from "node:util";
|
|
2616
|
-
var
|
|
4105
|
+
var run5 = promisify3(execFile9);
|
|
2617
4106
|
var DOWNLOAD_BASE = "https://tunnelsassetsprod.blob.core.windows.net/cli";
|
|
2618
4107
|
var BUILDS = {
|
|
2619
4108
|
"darwin-arm64": "osx-arm64-devtunnel",
|
|
@@ -2642,11 +4131,11 @@ async function ensureDevtunnel({ dir, progress }) {
|
|
|
2642
4131
|
`No devtunnel build for ${process.platform}-${process.arch}. Install it yourself: https://learn.microsoft.com/azure/developer/dev-tunnels/get-started`
|
|
2643
4132
|
);
|
|
2644
4133
|
}
|
|
2645
|
-
const bin =
|
|
2646
|
-
const metaFile =
|
|
4134
|
+
const bin = path11.join(dir, isWindows ? "devtunnel.exe" : "devtunnel");
|
|
4135
|
+
const metaFile = path11.join(dir, "devtunnel.json");
|
|
2647
4136
|
let meta = {};
|
|
2648
4137
|
try {
|
|
2649
|
-
meta = JSON.parse(
|
|
4138
|
+
meta = JSON.parse(fs14.readFileSync(metaFile, "utf8"));
|
|
2650
4139
|
} catch {
|
|
2651
4140
|
}
|
|
2652
4141
|
const installed = findExecutable(bin) !== null;
|
|
@@ -2657,7 +4146,7 @@ async function ensureDevtunnel({ dir, progress }) {
|
|
|
2657
4146
|
const head = await fetch(url, { method: "HEAD", signal: AbortSignal.timeout(3e3) });
|
|
2658
4147
|
const lastModified = head.headers.get("last-modified");
|
|
2659
4148
|
if (head.ok && lastModified && lastModified === meta.lastModified) {
|
|
2660
|
-
|
|
4149
|
+
fs14.writeFileSync(metaFile, JSON.stringify({ ...meta, checkedAt: Date.now() }));
|
|
2661
4150
|
return bin;
|
|
2662
4151
|
}
|
|
2663
4152
|
} catch {
|
|
@@ -2672,12 +4161,12 @@ async function ensureDevtunnel({ dir, progress }) {
|
|
|
2672
4161
|
return bin;
|
|
2673
4162
|
}
|
|
2674
4163
|
async function download(url, bin, metaFile, progress) {
|
|
2675
|
-
|
|
4164
|
+
fs14.mkdirSync(path11.dirname(bin), { recursive: true, mode: 448 });
|
|
2676
4165
|
const res = await fetch(url, { signal: AbortSignal.timeout(12e4) });
|
|
2677
4166
|
if (!res.ok || !res.body) throw new Error(`Downloading devtunnel failed: HTTP ${res.status} from ${url}`);
|
|
2678
4167
|
const total = Number(res.headers.get("content-length")) || 0;
|
|
2679
4168
|
const tmp = `${bin}.${process.pid}.download`;
|
|
2680
|
-
const out =
|
|
4169
|
+
const out = fs14.createWriteStream(tmp, { mode: 493 });
|
|
2681
4170
|
const md5 = createHash("md5");
|
|
2682
4171
|
let received = 0;
|
|
2683
4172
|
try {
|
|
@@ -2690,22 +4179,22 @@ async function download(url, bin, metaFile, progress) {
|
|
|
2690
4179
|
await new Promise((resolve, reject) => out.end((e) => e ? reject(e) : resolve()));
|
|
2691
4180
|
if (total && received !== total) throw new Error(`devtunnel download was cut short (${received} of ${total} bytes)`);
|
|
2692
4181
|
await verify(tmp, md5.digest("base64"), res.headers.get("content-md5"));
|
|
2693
|
-
|
|
2694
|
-
|
|
4182
|
+
fs14.chmodSync(tmp, 493);
|
|
4183
|
+
fs14.renameSync(tmp, bin);
|
|
2695
4184
|
} catch (e) {
|
|
2696
4185
|
out.destroy();
|
|
2697
|
-
|
|
4186
|
+
fs14.rmSync(tmp, { force: true });
|
|
2698
4187
|
throw e;
|
|
2699
4188
|
}
|
|
2700
|
-
|
|
4189
|
+
fs14.writeFileSync(metaFile, JSON.stringify({ lastModified: res.headers.get("last-modified"), checkedAt: Date.now() }));
|
|
2701
4190
|
}
|
|
2702
4191
|
async function verify(file, md5, expectedMd5) {
|
|
2703
4192
|
if (expectedMd5 && md5 !== expectedMd5) throw new Error("devtunnel download is corrupt (MD5 mismatch)");
|
|
2704
4193
|
if (isWindows) return verifyAuthenticode(file);
|
|
2705
4194
|
if (process.platform !== "darwin") return;
|
|
2706
4195
|
try {
|
|
2707
|
-
await
|
|
2708
|
-
const { stderr } = await
|
|
4196
|
+
await run5("codesign", ["--verify", "--strict", file]);
|
|
4197
|
+
const { stderr } = await run5("codesign", ["-dv", file]);
|
|
2709
4198
|
if (!stderr.includes(`TeamIdentifier=${MICROSOFT_TEAM_ID}`)) throw new Error("not signed by Microsoft");
|
|
2710
4199
|
} catch (e) {
|
|
2711
4200
|
throw new Error(`devtunnel download failed its signature check: ${e.message}`);
|
|
@@ -2716,7 +4205,7 @@ async function verifyAuthenticode(file) {
|
|
|
2716
4205
|
let status2;
|
|
2717
4206
|
let signer;
|
|
2718
4207
|
try {
|
|
2719
|
-
const { stdout } = await
|
|
4208
|
+
const { stdout } = await run5("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], {
|
|
2720
4209
|
env: { ...process.env, CODEREMOTE_VERIFY: file }
|
|
2721
4210
|
});
|
|
2722
4211
|
[status2, signer] = stdout.trim().split("|");
|
|
@@ -2729,51 +4218,52 @@ async function verifyAuthenticode(file) {
|
|
|
2729
4218
|
}
|
|
2730
4219
|
|
|
2731
4220
|
// src/hook.js
|
|
2732
|
-
import {
|
|
2733
|
-
import
|
|
2734
|
-
import
|
|
2735
|
-
import
|
|
4221
|
+
import { execFile as execFile10 } from "node:child_process";
|
|
4222
|
+
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
4223
|
+
import fs15 from "node:fs";
|
|
4224
|
+
import http4 from "node:http";
|
|
4225
|
+
import path12 from "node:path";
|
|
2736
4226
|
var CONNECT_TIMEOUT = 1e3;
|
|
2737
4227
|
var EVENT_TIMEOUT = 5e3;
|
|
2738
4228
|
function ensureHookSecret(dir) {
|
|
2739
|
-
const file =
|
|
4229
|
+
const file = path12.join(dir, "hook-secret");
|
|
2740
4230
|
try {
|
|
2741
|
-
const secret2 =
|
|
4231
|
+
const secret2 = fs15.readFileSync(file, "utf8").trim();
|
|
2742
4232
|
if (secret2) return secret2;
|
|
2743
4233
|
} catch {
|
|
2744
4234
|
}
|
|
2745
|
-
|
|
2746
|
-
const secret =
|
|
2747
|
-
|
|
4235
|
+
fs15.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
4236
|
+
const secret = randomBytes3(32).toString("base64url");
|
|
4237
|
+
fs15.writeFileSync(file, secret, { mode: 384 });
|
|
2748
4238
|
return secret;
|
|
2749
4239
|
}
|
|
2750
4240
|
function writeHookLauncher(dir, { node = process.execPath, script }) {
|
|
2751
|
-
const bin =
|
|
2752
|
-
|
|
4241
|
+
const bin = path12.join(dir, "bin");
|
|
4242
|
+
fs15.mkdirSync(bin, { recursive: true, mode: 448 });
|
|
2753
4243
|
const quote = (s) => `'${String(s).replace(/'/g, `'\\''`)}'`;
|
|
2754
|
-
const file =
|
|
2755
|
-
|
|
4244
|
+
const file = path12.join(bin, "hook");
|
|
4245
|
+
fs15.writeFileSync(file, `#!/bin/sh
|
|
2756
4246
|
exec ${quote(node)} ${quote(script)} hook "$@"
|
|
2757
4247
|
`, { mode: 493 });
|
|
2758
|
-
|
|
2759
|
-
|
|
4248
|
+
fs15.chmodSync(file, 493);
|
|
4249
|
+
fs15.writeFileSync(path12.join(bin, "hook.json"), JSON.stringify({ node, script }), { mode: 384 });
|
|
2760
4250
|
return file;
|
|
2761
4251
|
}
|
|
2762
4252
|
function readHookSecret(dir) {
|
|
2763
4253
|
try {
|
|
2764
|
-
return
|
|
4254
|
+
return fs15.readFileSync(path12.join(dir, "hook-secret"), "utf8").trim() || null;
|
|
2765
4255
|
} catch {
|
|
2766
4256
|
return null;
|
|
2767
4257
|
}
|
|
2768
4258
|
}
|
|
2769
4259
|
function real(p) {
|
|
2770
4260
|
try {
|
|
2771
|
-
return
|
|
4261
|
+
return fs15.realpathSync.native(p);
|
|
2772
4262
|
} catch {
|
|
2773
|
-
return
|
|
4263
|
+
return path12.resolve(p);
|
|
2774
4264
|
}
|
|
2775
4265
|
}
|
|
2776
|
-
var within = (dir, root2) => dir === root2 || dir.startsWith(root2.endsWith(
|
|
4266
|
+
var within = (dir, root2) => dir === root2 || dir.startsWith(root2.endsWith(path12.sep) ? root2 : root2 + path12.sep);
|
|
2777
4267
|
function hookTarget(agents, cwd, env = process.env) {
|
|
2778
4268
|
const port2 = parseInt(env.CODEREMOTE_AGENT_PORT, 10);
|
|
2779
4269
|
if (port2 && agents.some((a) => a.port === port2)) return { port: port2, terminal: env.CODEREMOTE_TERM_ID || null };
|
|
@@ -2782,23 +4272,46 @@ function hookTarget(agents, cwd, env = process.env) {
|
|
|
2782
4272
|
const match = agents.map((a) => ({ ...a, real: real(a.root) })).filter((a) => within(dir, a.real)).sort((a, b) => b.real.length - a.real.length)[0];
|
|
2783
4273
|
return match ? { port: match.port, terminal: null } : null;
|
|
2784
4274
|
}
|
|
2785
|
-
|
|
4275
|
+
var HOST_EVENTS = /* @__PURE__ */ new Set(["SessionStart", "UserPromptSubmit", "PermissionRequest"]);
|
|
4276
|
+
function hostAppFrom(psOutput, pid) {
|
|
4277
|
+
const parents = /* @__PURE__ */ new Map();
|
|
4278
|
+
for (const line of psOutput.split("\n")) {
|
|
4279
|
+
const m = line.match(/^\s*(\d+)\s+(\d+)\s+(.+)$/);
|
|
4280
|
+
if (m) parents.set(Number(m[1]), { ppid: Number(m[2]), comm: m[3] });
|
|
4281
|
+
}
|
|
4282
|
+
for (let seen = 0, p = parents.get(pid); p && seen < 64; seen++, p = parents.get(p.ppid)) {
|
|
4283
|
+
const at = p.comm.indexOf(".app/");
|
|
4284
|
+
if (at > 0) return p.comm.slice(0, at + 4);
|
|
4285
|
+
if (p.ppid <= 1) break;
|
|
4286
|
+
}
|
|
4287
|
+
return null;
|
|
4288
|
+
}
|
|
4289
|
+
function hostApp() {
|
|
4290
|
+
if (process.platform !== "darwin") return Promise.resolve(null);
|
|
4291
|
+
return new Promise((resolve) => {
|
|
4292
|
+
execFile10("ps", ["-A", "-o", "pid=,ppid=,comm="], { timeout: 1e3, maxBuffer: 8 * 1024 * 1024 }, (err, stdout) => {
|
|
4293
|
+
resolve(err ? null : hostAppFrom(stdout, process.ppid));
|
|
4294
|
+
});
|
|
4295
|
+
});
|
|
4296
|
+
}
|
|
4297
|
+
function deliverHook({ port: port2, terminal, app = null, agent, secret, body, wait }) {
|
|
2786
4298
|
return new Promise((resolve) => {
|
|
2787
4299
|
const query = new URLSearchParams({ agent });
|
|
2788
4300
|
if (terminal) query.set("terminal", terminal);
|
|
2789
|
-
|
|
4301
|
+
if (app) query.set("app", app);
|
|
4302
|
+
const req = http4.request({
|
|
2790
4303
|
host: "127.0.0.1",
|
|
2791
4304
|
port: port2,
|
|
2792
4305
|
method: "POST",
|
|
2793
4306
|
path: `/api/agents/hook?${query}`,
|
|
2794
4307
|
headers: { "content-type": "application/json", "content-length": Buffer.byteLength(body), [HOOK_SECRET_HEADER]: secret }
|
|
2795
4308
|
});
|
|
2796
|
-
const
|
|
4309
|
+
const fail2 = () => {
|
|
2797
4310
|
req.destroy();
|
|
2798
4311
|
resolve("");
|
|
2799
4312
|
};
|
|
2800
|
-
const connect = setTimeout(
|
|
2801
|
-
const overall = wait ? null : setTimeout(
|
|
4313
|
+
const connect = setTimeout(fail2, CONNECT_TIMEOUT);
|
|
4314
|
+
const overall = wait ? null : setTimeout(fail2, EVENT_TIMEOUT);
|
|
2802
4315
|
req.on("socket", (socket) => socket.once("connect", () => clearTimeout(connect)));
|
|
2803
4316
|
req.on("error", () => resolve(""));
|
|
2804
4317
|
req.on("response", (res) => {
|
|
@@ -2813,7 +4326,7 @@ function deliverHook({ port: port2, terminal, agent, secret, body, wait }) {
|
|
|
2813
4326
|
req.end(body);
|
|
2814
4327
|
});
|
|
2815
4328
|
}
|
|
2816
|
-
async function runHook({ input, agents, secret, agent = "claude", env = process.env }) {
|
|
4329
|
+
async function runHook({ input, agents, secret, agent = "claude", env = process.env, findApp = hostApp }) {
|
|
2817
4330
|
if (!secret) return "";
|
|
2818
4331
|
let event;
|
|
2819
4332
|
try {
|
|
@@ -2824,16 +4337,23 @@ async function runHook({ input, agents, secret, agent = "claude", env = process.
|
|
|
2824
4337
|
const target = hookTarget(agents, event?.cwd, env);
|
|
2825
4338
|
if (!target) return "";
|
|
2826
4339
|
const wait = event.hook_event_name === "PermissionRequest";
|
|
2827
|
-
const
|
|
4340
|
+
const app = !target.terminal && HOST_EVENTS.has(event.hook_event_name) ? await findApp() : null;
|
|
4341
|
+
const out = await deliverHook({ ...target, app, agent, secret, body: input, wait });
|
|
2828
4342
|
return wait ? out.trim() : "";
|
|
2829
4343
|
}
|
|
2830
4344
|
|
|
2831
4345
|
// src/preview.js
|
|
2832
|
-
import { spawn as
|
|
4346
|
+
import { spawn as spawn4 } from "node:child_process";
|
|
2833
4347
|
|
|
2834
4348
|
// src/tunnels.js
|
|
2835
4349
|
var TUNNEL_LIMIT = 10;
|
|
2836
4350
|
var PREVIEW_LABEL = "coderemote-preview";
|
|
4351
|
+
var LOGIN_NEEDED = /not logged in|login token expired|login required|please log ?in/i;
|
|
4352
|
+
function signedInUser(res) {
|
|
4353
|
+
if (!res.ok || LOGIN_NEEDED.test(res.out)) return null;
|
|
4354
|
+
const m = res.out.match(/Logged in as (\S+) using (\S+?)\.?\s*$/m);
|
|
4355
|
+
return m ? `${m[1]} (${m[2]})` : res.out.trim() || null;
|
|
4356
|
+
}
|
|
2837
4357
|
var base = (id) => String(id).split(".")[0];
|
|
2838
4358
|
async function listTunnels(devtunnel2) {
|
|
2839
4359
|
const res = await devtunnel2("list", "--limit", "100", "--json");
|
|
@@ -2965,8 +4485,8 @@ var PreviewTunnel = class {
|
|
|
2965
4485
|
"--json"
|
|
2966
4486
|
);
|
|
2967
4487
|
if (!res.ok) throw new Error(`devtunnel access create failed: ${res.out.trim()}`);
|
|
2968
|
-
const
|
|
2969
|
-
const until = Date.parse(
|
|
4488
|
+
const entries2 = JSON.parse(res.out.slice(res.out.indexOf("{"))).accessControlEntries ?? [];
|
|
4489
|
+
const until = Date.parse(entries2.find((e) => e.type === "Anonymous")?.expiration);
|
|
2970
4490
|
this.#shares.set(port2, Number.isFinite(until) ? until : Date.now() + hours * 36e5);
|
|
2971
4491
|
});
|
|
2972
4492
|
}
|
|
@@ -3076,7 +4596,7 @@ ${created.out}`);
|
|
|
3076
4596
|
}
|
|
3077
4597
|
#startHost() {
|
|
3078
4598
|
return new Promise((resolve, reject) => {
|
|
3079
|
-
const child =
|
|
4599
|
+
const child = spawn4(this.command(), ["host", this.#tunnelId], { stdio: ["ignore", "pipe", "pipe"] });
|
|
3080
4600
|
const urls = /* @__PURE__ */ new Map();
|
|
3081
4601
|
let log2 = "";
|
|
3082
4602
|
const timer = setTimeout(() => {
|
|
@@ -3111,40 +4631,40 @@ ${log2}`));
|
|
|
3111
4631
|
};
|
|
3112
4632
|
|
|
3113
4633
|
// src/push-store.js
|
|
3114
|
-
import
|
|
3115
|
-
import
|
|
4634
|
+
import fs16 from "node:fs";
|
|
4635
|
+
import path13 from "node:path";
|
|
3116
4636
|
function pushStore(dir, port2) {
|
|
3117
|
-
const file =
|
|
4637
|
+
const file = path13.join(dir, `${port2}.json`);
|
|
3118
4638
|
return {
|
|
3119
4639
|
load() {
|
|
3120
4640
|
try {
|
|
3121
|
-
const devices = JSON.parse(
|
|
4641
|
+
const devices = JSON.parse(fs16.readFileSync(file, "utf8"));
|
|
3122
4642
|
return Array.isArray(devices) ? devices : [];
|
|
3123
4643
|
} catch {
|
|
3124
4644
|
return [];
|
|
3125
4645
|
}
|
|
3126
4646
|
},
|
|
3127
4647
|
save(devices) {
|
|
3128
|
-
|
|
3129
|
-
|
|
4648
|
+
fs16.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
4649
|
+
fs16.writeFileSync(file, JSON.stringify(devices, null, 2), { mode: 384 });
|
|
3130
4650
|
}
|
|
3131
4651
|
};
|
|
3132
4652
|
}
|
|
3133
4653
|
|
|
3134
4654
|
// src/registry.js
|
|
3135
|
-
import
|
|
3136
|
-
import
|
|
4655
|
+
import fs17 from "node:fs";
|
|
4656
|
+
import path14 from "node:path";
|
|
3137
4657
|
var AgentRegistry = class {
|
|
3138
4658
|
constructor(dir) {
|
|
3139
4659
|
this.dir = dir;
|
|
3140
4660
|
}
|
|
3141
4661
|
register({ port: port2, root: root2, url }) {
|
|
3142
|
-
|
|
4662
|
+
fs17.mkdirSync(this.dir, { recursive: true, mode: 448 });
|
|
3143
4663
|
const file = this.#file(port2);
|
|
3144
|
-
|
|
4664
|
+
fs17.writeFileSync(file, JSON.stringify({ pid: process.pid, port: port2, root: root2, url, startedAt: Date.now() }), { mode: 384 });
|
|
3145
4665
|
const cleanup = () => {
|
|
3146
4666
|
try {
|
|
3147
|
-
if (JSON.parse(
|
|
4667
|
+
if (JSON.parse(fs17.readFileSync(file, "utf8")).pid === process.pid) fs17.rmSync(file);
|
|
3148
4668
|
} catch {
|
|
3149
4669
|
}
|
|
3150
4670
|
};
|
|
@@ -3155,30 +4675,31 @@ var AgentRegistry = class {
|
|
|
3155
4675
|
list() {
|
|
3156
4676
|
let names = [];
|
|
3157
4677
|
try {
|
|
3158
|
-
names =
|
|
4678
|
+
names = fs17.readdirSync(this.dir).filter((n) => n.endsWith(".json"));
|
|
3159
4679
|
} catch {
|
|
3160
4680
|
return [];
|
|
3161
4681
|
}
|
|
3162
4682
|
const agents = [];
|
|
3163
4683
|
for (const name of names) {
|
|
3164
|
-
const file =
|
|
4684
|
+
const file = path14.join(this.dir, name);
|
|
3165
4685
|
try {
|
|
3166
|
-
const agent = JSON.parse(
|
|
4686
|
+
const agent = JSON.parse(fs17.readFileSync(file, "utf8"));
|
|
3167
4687
|
process.kill(agent.pid, 0);
|
|
3168
4688
|
agents.push(agent);
|
|
3169
4689
|
} catch {
|
|
3170
|
-
|
|
4690
|
+
fs17.rmSync(file, { force: true });
|
|
3171
4691
|
}
|
|
3172
4692
|
}
|
|
3173
4693
|
return agents.sort((a, b) => a.port - b.port);
|
|
3174
4694
|
}
|
|
3175
4695
|
#file(port2) {
|
|
3176
|
-
return
|
|
4696
|
+
return path14.join(this.dir, `${port2}.json`);
|
|
3177
4697
|
}
|
|
3178
4698
|
};
|
|
3179
4699
|
|
|
3180
4700
|
// src/tunnel-status.js
|
|
3181
4701
|
var MACHINE_LABEL = "coderemote";
|
|
4702
|
+
var PROVIDER = "devtunnels";
|
|
3182
4703
|
var CACHE_MS = 15e3;
|
|
3183
4704
|
function parse(out) {
|
|
3184
4705
|
return JSON.parse(out.slice(out.indexOf("{")));
|
|
@@ -3186,6 +4707,13 @@ function parse(out) {
|
|
|
3186
4707
|
function machineName(description) {
|
|
3187
4708
|
return (description || "").replace(/^(coderemote|vsremote)\s+/, "").replace(/\.local\b/, "") || null;
|
|
3188
4709
|
}
|
|
4710
|
+
var VSCODE_LABEL = "vscode-server-launcher";
|
|
4711
|
+
function otherTunnel(tunnel) {
|
|
4712
|
+
const labels = tunnel.labels ?? [];
|
|
4713
|
+
const kind = labels.includes(PREVIEW_LABEL) ? "preview" : labels.includes(VSCODE_LABEL) ? "vscode" : "other";
|
|
4714
|
+
const name = kind === "preview" ? machineName((tunnel.description || "").replace(/^coderemote preview\s+/, "")) : tunnel.description || null;
|
|
4715
|
+
return { id: tunnel.tunnelId, kind, name, online: tunnel.hostConnections > 0 };
|
|
4716
|
+
}
|
|
3189
4717
|
function machine(tunnel, current) {
|
|
3190
4718
|
return {
|
|
3191
4719
|
id: tunnel.tunnelId,
|
|
@@ -3197,19 +4725,21 @@ function machine(tunnel, current) {
|
|
|
3197
4725
|
current
|
|
3198
4726
|
};
|
|
3199
4727
|
}
|
|
3200
|
-
function tunnelStatus({ devtunnel: devtunnel2, tunnelId, account, anonymous: anonymous2, preview }) {
|
|
4728
|
+
function tunnelStatus({ devtunnel: devtunnel2, tunnelId, account, anonymous: anonymous2, preview: preview2 }) {
|
|
3201
4729
|
let cached = null;
|
|
3202
|
-
|
|
4730
|
+
const status2 = async () => {
|
|
3203
4731
|
const id = tunnelId();
|
|
3204
4732
|
if (!id) return null;
|
|
3205
|
-
if (cached && Date.now() - cached.at < CACHE_MS) return { ...cached.value, previewPorts:
|
|
4733
|
+
if (cached && Date.now() - cached.at < CACHE_MS) return { ...cached.value, previewPorts: preview2?.list().length ?? 0 };
|
|
3206
4734
|
const [own, listed] = await Promise.all([
|
|
3207
4735
|
devtunnel2("show", id, "--json"),
|
|
3208
|
-
|
|
4736
|
+
// Every tunnel, previews' too: they all count against the account's limit.
|
|
4737
|
+
devtunnel2("list", "--limit", "100", "--json")
|
|
3209
4738
|
]);
|
|
3210
4739
|
if (!own.ok) throw new Error(`devtunnel show failed: ${own.out.trim()}`);
|
|
3211
4740
|
const tunnel = parse(own.out).tunnel;
|
|
3212
|
-
const
|
|
4741
|
+
const all = listed.ok ? parse(listed.out).tunnels ?? [] : null;
|
|
4742
|
+
const others = (all ?? []).filter((t) => t.tunnelId !== id && t.labels?.includes(MACHINE_LABEL));
|
|
3213
4743
|
const shown = await Promise.all(
|
|
3214
4744
|
others.map(async (t) => {
|
|
3215
4745
|
const res = await devtunnel2("show", t.tunnelId, "--json");
|
|
@@ -3218,6 +4748,7 @@ function tunnelStatus({ devtunnel: devtunnel2, tunnelId, account, anonymous: ano
|
|
|
3218
4748
|
);
|
|
3219
4749
|
const machines = [machine(tunnel, true), ...shown.filter(Boolean).map((t) => machine(t, false))];
|
|
3220
4750
|
const value = {
|
|
4751
|
+
provider: PROVIDER,
|
|
3221
4752
|
account: account() ?? null,
|
|
3222
4753
|
tunnel: {
|
|
3223
4754
|
id,
|
|
@@ -3229,16 +4760,54 @@ function tunnelStatus({ devtunnel: devtunnel2, tunnelId, account, anonymous: ano
|
|
|
3229
4760
|
uploaded: tunnel.uploadTotal ?? null,
|
|
3230
4761
|
downloaded: tunnel.downloadTotal ?? null
|
|
3231
4762
|
},
|
|
3232
|
-
machines
|
|
4763
|
+
machines,
|
|
4764
|
+
// The account's tunnels that aren't machines; empty when it couldn't be listed.
|
|
4765
|
+
others: (all ?? []).filter((t) => t.tunnelId !== id && !t.labels?.includes(MACHINE_LABEL)).map(otherTunnel),
|
|
4766
|
+
// null when the account couldn't be listed
|
|
4767
|
+
quota: all && { used: all.length, limit: TUNNEL_LIMIT }
|
|
3233
4768
|
};
|
|
3234
4769
|
cached = { at: Date.now(), value };
|
|
3235
|
-
return { ...value, previewPorts:
|
|
4770
|
+
return { ...value, previewPorts: preview2?.list().length ?? 0 };
|
|
4771
|
+
};
|
|
4772
|
+
status2.invalidate = () => {
|
|
4773
|
+
cached = null;
|
|
4774
|
+
};
|
|
4775
|
+
return status2;
|
|
4776
|
+
}
|
|
4777
|
+
var fail = (status2, code, message) => Object.assign(new Error(message), { status: status2, code });
|
|
4778
|
+
function tunnelDelete({ devtunnel: devtunnel2, tunnelId, deleted = () => {
|
|
4779
|
+
} }) {
|
|
4780
|
+
return async ({ id, account, force = false }) => {
|
|
4781
|
+
if (typeof id !== "string" || !id) throw fail(400, "EINVAL", "id is required");
|
|
4782
|
+
if (typeof account !== "string" || !account) throw fail(400, "EINVAL", "account is required");
|
|
4783
|
+
const user = signedInUser(await devtunnel2("user", "show"));
|
|
4784
|
+
if (!user) {
|
|
4785
|
+
throw fail(403, "ELOGIN", "The computer\u2019s dev tunnels sign-in has expired. Run `coderemote login` on it, then try again.");
|
|
4786
|
+
}
|
|
4787
|
+
if (user !== account) {
|
|
4788
|
+
throw fail(409, "EACCOUNT", `The computer is now signed in as ${user}, not ${account}. Refresh, then try again.`);
|
|
4789
|
+
}
|
|
4790
|
+
const { tunnels, error } = await listTunnels(devtunnel2);
|
|
4791
|
+
if (error) throw fail(502, "EDEVTUNNEL", error);
|
|
4792
|
+
const tunnel = findTunnel(tunnels, id);
|
|
4793
|
+
if (!tunnel) throw fail(404, "ENOTUNNEL", `No tunnel ${id} on ${account}.`);
|
|
4794
|
+
const own = tunnelId();
|
|
4795
|
+
if (own && findTunnel([tunnel], own)) {
|
|
4796
|
+
throw fail(409, "EOWN", "This is the tunnel this connection runs through. Delete it on the computer: `coderemote tunnels delete <id> --force`.");
|
|
4797
|
+
}
|
|
4798
|
+
if (tunnel.hostConnections > 0 && !force) {
|
|
4799
|
+
throw fail(409, "EONLINE", "An agent is serving this tunnel right now. Deleting it unpairs its phones; send force to delete it anyway.");
|
|
4800
|
+
}
|
|
4801
|
+
const res = await devtunnel2("delete", tunnel.tunnelId, "-f");
|
|
4802
|
+
if (!res.ok) throw fail(502, "EDEVTUNNEL", res.out.trim());
|
|
4803
|
+
deleted(tunnel.tunnelId);
|
|
4804
|
+
return { deleted: tunnel.tunnelId };
|
|
3236
4805
|
};
|
|
3237
4806
|
}
|
|
3238
4807
|
|
|
3239
4808
|
// src/update.js
|
|
3240
|
-
import
|
|
3241
|
-
import
|
|
4809
|
+
import fs18 from "node:fs";
|
|
4810
|
+
import path15 from "node:path";
|
|
3242
4811
|
var LATEST_URL = "https://registry.npmjs.org/coderemote/latest";
|
|
3243
4812
|
var CHECK_INTERVAL_MS = 24 * 36e5;
|
|
3244
4813
|
var SEMVER = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/;
|
|
@@ -3262,10 +4831,10 @@ async function fetchLatest({ fetch: fetch2 = globalThis.fetch, timeoutMs = 3e3 }
|
|
|
3262
4831
|
}
|
|
3263
4832
|
}
|
|
3264
4833
|
async function availableUpdate({ current, dir, fetch: fetch2, now = Date.now() }) {
|
|
3265
|
-
const file =
|
|
4834
|
+
const file = path15.join(dir, "update.json");
|
|
3266
4835
|
let cache = {};
|
|
3267
4836
|
try {
|
|
3268
|
-
cache = JSON.parse(
|
|
4837
|
+
cache = JSON.parse(fs18.readFileSync(file, "utf8"));
|
|
3269
4838
|
} catch {
|
|
3270
4839
|
}
|
|
3271
4840
|
let latest = cache.latest;
|
|
@@ -3274,8 +4843,8 @@ async function availableUpdate({ current, dir, fetch: fetch2, now = Date.now() }
|
|
|
3274
4843
|
if (fetched) {
|
|
3275
4844
|
latest = fetched;
|
|
3276
4845
|
try {
|
|
3277
|
-
|
|
3278
|
-
|
|
4846
|
+
fs18.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
4847
|
+
fs18.writeFileSync(file, JSON.stringify({ latest, checkedAt: now }));
|
|
3279
4848
|
} catch {
|
|
3280
4849
|
}
|
|
3281
4850
|
}
|
|
@@ -3283,12 +4852,12 @@ async function availableUpdate({ current, dir, fetch: fetch2, now = Date.now() }
|
|
|
3283
4852
|
return isNewer(latest, current) ? latest : null;
|
|
3284
4853
|
}
|
|
3285
4854
|
function installCommand(installedAt) {
|
|
3286
|
-
const p = installedAt.split(
|
|
4855
|
+
const p = installedAt.split(path15.sep).join("/");
|
|
3287
4856
|
const runtime = p.match(/^(.*\/runtime\/node)\/(lib\/)?node_modules\/coderemote\//);
|
|
3288
4857
|
if (runtime) {
|
|
3289
4858
|
const prefix = installedAt.slice(0, runtime[1].length);
|
|
3290
|
-
const [node, modules] = runtime[2] ? [
|
|
3291
|
-
return [node, [
|
|
4859
|
+
const [node, modules] = runtime[2] ? [path15.join(prefix, "bin", "node"), path15.join(prefix, "lib", "node_modules")] : [path15.join(prefix, "node.exe"), path15.join(prefix, "node_modules")];
|
|
4860
|
+
return [node, [path15.join(modules, "npm", "bin", "npm-cli.js"), "install", "-g", "--prefix", prefix, "coderemote@latest"]];
|
|
3292
4861
|
}
|
|
3293
4862
|
if (p.includes("/Cellar/coderemote/")) return ["brew", ["upgrade", "coderemote"]];
|
|
3294
4863
|
if (p.includes("/pnpm/") || p.includes("/.pnpm/")) return ["pnpm", ["add", "-g", "coderemote@latest"]];
|
|
@@ -3300,7 +4869,7 @@ function installCommand(installedAt) {
|
|
|
3300
4869
|
// package.json
|
|
3301
4870
|
var package_default = {
|
|
3302
4871
|
name: "coderemote",
|
|
3303
|
-
version: "0.3.
|
|
4872
|
+
version: "0.3.9",
|
|
3304
4873
|
description: "Use your computer from your iPhone: files, git and a real terminal over a secure Microsoft Dev Tunnel. No SSH, no port forwarding, no VPN.",
|
|
3305
4874
|
keywords: [
|
|
3306
4875
|
"remote",
|
|
@@ -3623,28 +5192,28 @@ ${log2}`));
|
|
|
3623
5192
|
}
|
|
3624
5193
|
|
|
3625
5194
|
// bin/coderemote.js
|
|
3626
|
-
var
|
|
3627
|
-
var CONFIG_DIR =
|
|
3628
|
-
var LEGACY_CONFIG_DIR =
|
|
3629
|
-
if (!
|
|
3630
|
-
var CONFIG_FILE =
|
|
3631
|
-
var PUSH_DIR =
|
|
3632
|
-
var UPLOADS_DIR =
|
|
5195
|
+
var run6 = promisify4(execFile11);
|
|
5196
|
+
var CONFIG_DIR = path16.join(os7.homedir(), ".coderemote");
|
|
5197
|
+
var LEGACY_CONFIG_DIR = path16.join(os7.homedir(), ".vsremote");
|
|
5198
|
+
if (!fs19.existsSync(CONFIG_DIR) && fs19.existsSync(LEGACY_CONFIG_DIR)) fs19.renameSync(LEGACY_CONFIG_DIR, CONFIG_DIR);
|
|
5199
|
+
var CONFIG_FILE = path16.join(CONFIG_DIR, "config.json");
|
|
5200
|
+
var PUSH_DIR = path16.join(CONFIG_DIR, "push");
|
|
5201
|
+
var UPLOADS_DIR = path16.join(CONFIG_DIR, "uploads");
|
|
3633
5202
|
var DEFAULT_PORT = 31580;
|
|
3634
5203
|
var VERSION = package_default.version;
|
|
3635
|
-
var PACKAGED =
|
|
5204
|
+
var PACKAGED = path16.basename(path16.dirname(fileURLToPath(import.meta.url))) === "dist";
|
|
3636
5205
|
var spinner = new Spinner();
|
|
3637
5206
|
function loadConfig() {
|
|
3638
5207
|
let cfg2 = {};
|
|
3639
5208
|
try {
|
|
3640
|
-
cfg2 = JSON.parse(
|
|
5209
|
+
cfg2 = JSON.parse(fs19.readFileSync(CONFIG_FILE, "utf8"));
|
|
3641
5210
|
} catch {
|
|
3642
5211
|
}
|
|
3643
5212
|
if (!cfg2.token) {
|
|
3644
|
-
cfg2.token =
|
|
5213
|
+
cfg2.token = randomBytes4(32).toString("base64url");
|
|
3645
5214
|
saveConfig(cfg2);
|
|
3646
|
-
|
|
3647
|
-
|
|
5215
|
+
fs19.rmSync(PUSH_DIR, { recursive: true, force: true });
|
|
5216
|
+
fs19.rmSync(UPLOADS_DIR, { recursive: true, force: true });
|
|
3648
5217
|
}
|
|
3649
5218
|
if (cfg2.tunnelId && !cfg2.tunnels) {
|
|
3650
5219
|
cfg2.tunnels = { [DEFAULT_PORT]: cfg2.tunnelId };
|
|
@@ -3660,30 +5229,32 @@ function stopAgent(pid) {
|
|
|
3660
5229
|
}
|
|
3661
5230
|
}
|
|
3662
5231
|
function saveConfig(cfg2) {
|
|
3663
|
-
|
|
3664
|
-
|
|
5232
|
+
fs19.mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
5233
|
+
fs19.writeFileSync(CONFIG_FILE, JSON.stringify(cfg2, null, 2), { mode: 384 });
|
|
3665
5234
|
}
|
|
3666
5235
|
function portFree(port2) {
|
|
3667
5236
|
return new Promise((resolve) => {
|
|
3668
|
-
const probe =
|
|
5237
|
+
const probe = net3.createServer();
|
|
3669
5238
|
probe.once("error", () => resolve(false));
|
|
3670
5239
|
probe.listen(port2, "127.0.0.1", () => probe.close(() => resolve(true)));
|
|
3671
5240
|
});
|
|
3672
5241
|
}
|
|
3673
|
-
function listen(token, port2, root2, tunnelToken,
|
|
5242
|
+
function listen(token, port2, root2, tunnelToken, preview2, status2, remover) {
|
|
3674
5243
|
const hookSecret = ensureHookSecret(CONFIG_DIR);
|
|
3675
|
-
writeHookLauncher(CONFIG_DIR, { script:
|
|
5244
|
+
writeHookLauncher(CONFIG_DIR, { script: fs19.realpathSync(process.argv[1]) });
|
|
3676
5245
|
const server = createAgentServer({
|
|
3677
5246
|
token,
|
|
3678
5247
|
root: root2,
|
|
3679
5248
|
tunnelToken,
|
|
3680
|
-
preview,
|
|
5249
|
+
preview: preview2,
|
|
3681
5250
|
tunnelStatus: status2,
|
|
5251
|
+
tunnelDelete: remover,
|
|
3682
5252
|
hookSecret,
|
|
3683
5253
|
// This very CLI, so coding agents in the app's terminals report through the same version.
|
|
3684
|
-
hookBin:
|
|
5254
|
+
hookBin: fs19.realpathSync(process.argv[1]),
|
|
3685
5255
|
pushStore: pushStore(PUSH_DIR, port2),
|
|
3686
|
-
uploadsDir: UPLOADS_DIR
|
|
5256
|
+
uploadsDir: UPLOADS_DIR,
|
|
5257
|
+
version: VERSION
|
|
3687
5258
|
});
|
|
3688
5259
|
return new Promise((resolve, reject) => {
|
|
3689
5260
|
server.once("error", reject);
|
|
@@ -3694,7 +5265,7 @@ var DEVTUNNEL = "devtunnel";
|
|
|
3694
5265
|
async function resolveDevtunnel() {
|
|
3695
5266
|
let installing = false;
|
|
3696
5267
|
DEVTUNNEL = await ensureDevtunnel({
|
|
3697
|
-
dir:
|
|
5268
|
+
dir: path16.join(CONFIG_DIR, "bin"),
|
|
3698
5269
|
progress: (detail2) => {
|
|
3699
5270
|
if (!installing) {
|
|
3700
5271
|
installing = true;
|
|
@@ -3707,7 +5278,7 @@ async function resolveDevtunnel() {
|
|
|
3707
5278
|
}
|
|
3708
5279
|
async function devtunnel(...args) {
|
|
3709
5280
|
try {
|
|
3710
|
-
const { stdout } = await
|
|
5281
|
+
const { stdout } = await run6(DEVTUNNEL, args);
|
|
3711
5282
|
return { ok: true, out: stdout };
|
|
3712
5283
|
} catch (e) {
|
|
3713
5284
|
return { ok: false, out: `${e.stdout || ""}${e.stderr || ""}` || e.message };
|
|
@@ -3721,10 +5292,9 @@ function loginArgs(opts = rest) {
|
|
|
3721
5292
|
}
|
|
3722
5293
|
function interactiveLogin(args) {
|
|
3723
5294
|
return new Promise((resolve) => {
|
|
3724
|
-
|
|
5295
|
+
spawn5(DEVTUNNEL, args, { stdio: "inherit" }).on("exit", resolve);
|
|
3725
5296
|
});
|
|
3726
5297
|
}
|
|
3727
|
-
var LOGIN_NEEDED = /not logged in|login token expired|login required|please log ?in/i;
|
|
3728
5298
|
async function login(reason) {
|
|
3729
5299
|
spinner.stop();
|
|
3730
5300
|
console.error(`Dev tunnels: ${reason.trim()}`);
|
|
@@ -3740,12 +5310,11 @@ async function login(reason) {
|
|
|
3740
5310
|
}
|
|
3741
5311
|
async function tunnelUser() {
|
|
3742
5312
|
let who = await devtunnel("user", "show");
|
|
3743
|
-
if (!
|
|
5313
|
+
if (!signedInUser(who)) {
|
|
3744
5314
|
await login(who.out || "not logged in");
|
|
3745
5315
|
who = await devtunnel("user", "show");
|
|
3746
5316
|
}
|
|
3747
|
-
|
|
3748
|
-
return m ? `${m[1]} (${m[2]})` : who.out.trim();
|
|
5317
|
+
return signedInUser(who) ?? who.out.trim();
|
|
3749
5318
|
}
|
|
3750
5319
|
function applyAccess(tunnelId, anonymous2) {
|
|
3751
5320
|
return anonymous2 ? devtunnel("access", "create", tunnelId, "--anonymous") : devtunnel("access", "reset", tunnelId);
|
|
@@ -3765,7 +5334,7 @@ ${access.out}`);
|
|
|
3765
5334
|
if (!/not found/i.test(updated.out)) throw new Error(`devtunnel update failed:
|
|
3766
5335
|
${updated.out}`);
|
|
3767
5336
|
}
|
|
3768
|
-
const name = port2 === DEFAULT_PORT ?
|
|
5337
|
+
const name = port2 === DEFAULT_PORT ? os7.hostname() : `${os7.hostname()} :${port2}`;
|
|
3769
5338
|
const createArgs = ["create", "--expiration", "30d", "-d", `coderemote ${name}`, "--labels", MACHINE_LABEL, "--json"];
|
|
3770
5339
|
if (anonymous2) createArgs.push("--allow-anonymous");
|
|
3771
5340
|
let created = await devtunnel(...createArgs);
|
|
@@ -3807,7 +5376,7 @@ ${res.out}`);
|
|
|
3807
5376
|
}
|
|
3808
5377
|
function hostTunnel(tunnelId, port2) {
|
|
3809
5378
|
return new Promise((resolve, reject) => {
|
|
3810
|
-
const child =
|
|
5379
|
+
const child = spawn5(DEVTUNNEL, ["host", tunnelId], { stdio: ["ignore", "pipe", "pipe"] });
|
|
3811
5380
|
let log2 = "";
|
|
3812
5381
|
const onData = (d) => {
|
|
3813
5382
|
log2 += d;
|
|
@@ -3911,9 +5480,9 @@ var flag = (name) => {
|
|
|
3911
5480
|
};
|
|
3912
5481
|
var anonymous = rest.includes("--allow-anonymous") || rest.includes("--no-auth");
|
|
3913
5482
|
var port = flag("--port") ? parseInt(flag("--port"), 10) : DEFAULT_PORT;
|
|
3914
|
-
var root =
|
|
5483
|
+
var root = path16.resolve(flag("--root") ?? process.cwd());
|
|
3915
5484
|
var cfg = loadConfig();
|
|
3916
|
-
var registry = new AgentRegistry(
|
|
5485
|
+
var registry = new AgentRegistry(path16.join(CONFIG_DIR, "agents"));
|
|
3917
5486
|
async function start({ tunnel }) {
|
|
3918
5487
|
const agents = registry.list();
|
|
3919
5488
|
if (flag("--port")) {
|
|
@@ -3945,10 +5514,10 @@ async function start({ tunnel }) {
|
|
|
3945
5514
|
let server;
|
|
3946
5515
|
let tunnelId;
|
|
3947
5516
|
const tunnelToken = async () => tunnel && !anonymous && tunnelId ? connectToken(tunnelId) : null;
|
|
3948
|
-
const
|
|
5517
|
+
const preview2 = tunnel ? new PreviewTunnel({
|
|
3949
5518
|
devtunnel,
|
|
3950
5519
|
command: () => DEVTUNNEL,
|
|
3951
|
-
description: `coderemote preview ${
|
|
5520
|
+
description: `coderemote preview ${os7.hostname()}`,
|
|
3952
5521
|
leftover: cfg.previewTunnels?.[port],
|
|
3953
5522
|
remember: (id) => {
|
|
3954
5523
|
cfg.previewTunnels = { ...cfg.previewTunnels, [port]: id };
|
|
@@ -3958,10 +5527,19 @@ async function start({ tunnel }) {
|
|
|
3958
5527
|
mintToken: connectToken
|
|
3959
5528
|
}) : null;
|
|
3960
5529
|
let account;
|
|
3961
|
-
const status2 = tunnel ? tunnelStatus({ devtunnel, tunnelId: () => tunnelId, account: () => account, anonymous, preview }) : null;
|
|
5530
|
+
const status2 = tunnel ? tunnelStatus({ devtunnel, tunnelId: () => tunnelId, account: () => account, anonymous, preview: preview2 }) : null;
|
|
5531
|
+
const remover = tunnel ? tunnelDelete({
|
|
5532
|
+
devtunnel,
|
|
5533
|
+
tunnelId: () => tunnelId,
|
|
5534
|
+
deleted: (id) => {
|
|
5535
|
+
status2.invalidate();
|
|
5536
|
+
const saved = loadConfig();
|
|
5537
|
+
if (forgetTunnel(saved, id)) saveConfig(saved);
|
|
5538
|
+
}
|
|
5539
|
+
}) : null;
|
|
3962
5540
|
spinner.start("Starting agent");
|
|
3963
5541
|
try {
|
|
3964
|
-
server = await listen(cfg.token, port, root, tunnelToken,
|
|
5542
|
+
server = await listen(cfg.token, port, root, tunnelToken, preview2, status2, remover);
|
|
3965
5543
|
} catch (e) {
|
|
3966
5544
|
spinner.fail();
|
|
3967
5545
|
if (e.code !== "EADDRINUSE") throw e;
|
|
@@ -3998,7 +5576,7 @@ async function start({ tunnel }) {
|
|
|
3998
5576
|
registry.register({ port, root, url });
|
|
3999
5577
|
const shutdown = async () => {
|
|
4000
5578
|
child?.kill();
|
|
4001
|
-
await
|
|
5579
|
+
await preview2?.close();
|
|
4002
5580
|
await server.killAllSessions();
|
|
4003
5581
|
server.close();
|
|
4004
5582
|
process.exit(0);
|
|
@@ -4008,7 +5586,7 @@ async function start({ tunnel }) {
|
|
|
4008
5586
|
process.on("SIGHUP", shutdown);
|
|
4009
5587
|
process.on("exit", () => {
|
|
4010
5588
|
child?.kill();
|
|
4011
|
-
|
|
5589
|
+
preview2?.stop();
|
|
4012
5590
|
});
|
|
4013
5591
|
return { url, access };
|
|
4014
5592
|
}
|
|
@@ -4131,7 +5709,7 @@ function currentAgent() {
|
|
|
4131
5709
|
const agents = registry.list();
|
|
4132
5710
|
if (flag("--port")) return agents.find((a) => a.port === port);
|
|
4133
5711
|
const here = process.cwd();
|
|
4134
|
-
const containing = agents.filter((a) => here === a.root || here.startsWith(a.root +
|
|
5712
|
+
const containing = agents.filter((a) => here === a.root || here.startsWith(a.root + path16.sep)).sort((a, b) => b.root.length - a.root.length);
|
|
4135
5713
|
return containing[0] ?? (agents.length === 1 ? agents[0] : void 0);
|
|
4136
5714
|
}
|
|
4137
5715
|
async function previewPorts(agent, given) {
|
|
@@ -4267,7 +5845,7 @@ Token: ${cfg.token}`);
|
|
|
4267
5845
|
break;
|
|
4268
5846
|
}
|
|
4269
5847
|
case "rotate-token": {
|
|
4270
|
-
cfg.token =
|
|
5848
|
+
cfg.token = randomBytes4(32).toString("base64url");
|
|
4271
5849
|
saveConfig(cfg);
|
|
4272
5850
|
const agents = registry.list();
|
|
4273
5851
|
for (const agent of agents) {
|
|
@@ -4340,7 +5918,7 @@ Token: ${cfg.token}`);
|
|
|
4340
5918
|
console.log(`coderemote ${VERSION} is the latest version.`);
|
|
4341
5919
|
break;
|
|
4342
5920
|
}
|
|
4343
|
-
const [command, args] = installCommand(
|
|
5921
|
+
const [command, args] = installCommand(fs19.realpathSync(process.argv[1]));
|
|
4344
5922
|
console.log(`Updating coderemote ${VERSION} \u2192 ${latest}: ${command} ${args.join(" ")}
|
|
4345
5923
|
`);
|
|
4346
5924
|
const code = await new Promise((resolve) => {
|