coderemote 0.3.8 → 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/dist/coderemote.js +1884 -361
- 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
|
|
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
7
|
import net3 from "node:net";
|
|
8
|
-
import
|
|
9
|
-
import
|
|
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") {
|
|
@@ -601,11 +641,8 @@ var AgentHub = class extends EventEmitter {
|
|
|
601
641
|
if (last && !turnEntries.some((e) => e.kind === "assistant" && e.text.trim() === last)) {
|
|
602
642
|
this.#entry(s, { kind: "assistant", text: last });
|
|
603
643
|
}
|
|
604
|
-
s.
|
|
605
|
-
|
|
606
|
-
s.turnStartedAt = null;
|
|
607
|
-
s.state = s.requests.size ? "waiting" : "idle";
|
|
608
|
-
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);
|
|
609
646
|
break;
|
|
610
647
|
}
|
|
611
648
|
case "Interrupt":
|
|
@@ -738,13 +775,13 @@ var AgentHub = class extends EventEmitter {
|
|
|
738
775
|
pending.resolve(output);
|
|
739
776
|
}
|
|
740
777
|
async #backfill(s, file) {
|
|
741
|
-
let
|
|
778
|
+
let entries2 = [];
|
|
742
779
|
try {
|
|
743
|
-
|
|
780
|
+
entries2 = await this.#transcript(file, MAX_ENTRIES);
|
|
744
781
|
} catch {
|
|
745
782
|
}
|
|
746
783
|
const tools = /* @__PURE__ */ new Map();
|
|
747
|
-
for (const e of
|
|
784
|
+
for (const e of entries2) {
|
|
748
785
|
if (e.kind === "tool") {
|
|
749
786
|
tools.set(e.toolUseId, e.name);
|
|
750
787
|
e.input = clipInput(e.input);
|
|
@@ -760,6 +797,30 @@ var AgentHub = class extends EventEmitter {
|
|
|
760
797
|
if (s.entries.length > MAX_ENTRIES) s.entries.splice(0, s.entries.length - MAX_ENTRIES);
|
|
761
798
|
}
|
|
762
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
|
+
}
|
|
763
824
|
#fromRollout(s, item) {
|
|
764
825
|
const fields = codexItem(item);
|
|
765
826
|
if (!fields) return;
|
|
@@ -790,26 +851,856 @@ var AgentHub = class extends EventEmitter {
|
|
|
790
851
|
s.turnStartedAt = null;
|
|
791
852
|
}
|
|
792
853
|
#entry(s, fields) {
|
|
793
|
-
const
|
|
794
|
-
s.entries.push(
|
|
854
|
+
const entry3 = { seq: ++s.seq, time: this.#now(), ...fields };
|
|
855
|
+
s.entries.push(entry3);
|
|
795
856
|
if (s.entries.length > MAX_ENTRIES) s.entries.shift();
|
|
796
|
-
this.emit("message", { type: "entry", session: s.id, entry:
|
|
857
|
+
this.emit("message", { type: "entry", session: s.id, entry: entry3 });
|
|
797
858
|
}
|
|
798
859
|
#emitSession(s) {
|
|
799
860
|
this.emit("message", { type: "session", session: s.toJSON() });
|
|
800
861
|
}
|
|
801
862
|
};
|
|
802
863
|
|
|
803
|
-
// ../../packages/agent-core/src/
|
|
804
|
-
import { execFile } from "node:child_process";
|
|
864
|
+
// ../../packages/agent-core/src/commands.js
|
|
805
865
|
import fs4 from "node:fs/promises";
|
|
806
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";
|
|
807
1698
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".build", "build", "dist", "DerivedData", "Pods", "vendor", ".venv", "target"]);
|
|
808
1699
|
var MAX_SCAN_DEPTH = 4;
|
|
809
1700
|
var MAX_DIFF_BYTES = 2 * 1024 * 1024;
|
|
810
1701
|
function git(cwd, args, { timeout = 3e4, okCodes = [0], maxBuffer = 16 * 1024 * 1024 } = {}) {
|
|
811
1702
|
return new Promise((resolve, reject) => {
|
|
812
|
-
|
|
1703
|
+
execFile2(
|
|
813
1704
|
"git",
|
|
814
1705
|
args,
|
|
815
1706
|
{
|
|
@@ -830,30 +1721,36 @@ function git(cwd, args, { timeout = 3e4, okCodes = [0], maxBuffer = 16 * 1024 *
|
|
|
830
1721
|
async function findRepos(root2) {
|
|
831
1722
|
const repos = [];
|
|
832
1723
|
async function walk(dir, depth) {
|
|
833
|
-
let
|
|
1724
|
+
let entries2;
|
|
834
1725
|
try {
|
|
835
|
-
|
|
1726
|
+
entries2 = await fs8.readdir(dir, { withFileTypes: true });
|
|
836
1727
|
} catch {
|
|
837
1728
|
return;
|
|
838
1729
|
}
|
|
839
|
-
if (
|
|
840
|
-
repos.push(dir);
|
|
841
|
-
return;
|
|
842
|
-
}
|
|
1730
|
+
if (entries2.some((e) => e.name === ".git")) repos.push(dir);
|
|
843
1731
|
if (depth >= MAX_SCAN_DEPTH) return;
|
|
844
|
-
const subdirs =
|
|
845
|
-
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)));
|
|
846
1734
|
}
|
|
847
1735
|
await walk(root2, 0);
|
|
848
|
-
repos.sort((a, b) => a.split(
|
|
1736
|
+
repos.sort((a, b) => a.split(path6.sep).length - b.split(path6.sep).length || a.localeCompare(b));
|
|
849
1737
|
return Promise.all(
|
|
850
1738
|
repos.map(async (repo) => ({
|
|
851
1739
|
path: repo,
|
|
852
|
-
name: repo === root2 ?
|
|
853
|
-
branch: await currentBranch(repo)
|
|
1740
|
+
name: repo === root2 ? path6.basename(root2) : path6.relative(root2, repo),
|
|
1741
|
+
branch: await currentBranch(repo),
|
|
1742
|
+
...await summary(repo)
|
|
854
1743
|
}))
|
|
855
1744
|
);
|
|
856
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
|
+
}
|
|
857
1754
|
async function currentBranch(repo) {
|
|
858
1755
|
try {
|
|
859
1756
|
const { stdout } = await git(repo, ["symbolic-ref", "--short", "-q", "HEAD"], { okCodes: [0, 1] });
|
|
@@ -867,9 +1764,9 @@ async function currentBranch(repo) {
|
|
|
867
1764
|
async function status(repo) {
|
|
868
1765
|
const { stdout } = await git(repo, ["status", "--porcelain=v2", "--branch", "--untracked-files=all", "-z"]);
|
|
869
1766
|
const result = { branch: null, upstream: null, ahead: 0, behind: 0, files: [] };
|
|
870
|
-
const
|
|
871
|
-
for (let i = 0; i <
|
|
872
|
-
const r =
|
|
1767
|
+
const records2 = stdout.split("\0");
|
|
1768
|
+
for (let i = 0; i < records2.length; i++) {
|
|
1769
|
+
const r = records2[i];
|
|
873
1770
|
if (!r) continue;
|
|
874
1771
|
if (r.startsWith("# branch.head ")) {
|
|
875
1772
|
const head = r.slice(14);
|
|
@@ -882,20 +1779,20 @@ async function status(repo) {
|
|
|
882
1779
|
result.behind = Math.abs(parseInt(b, 10));
|
|
883
1780
|
} else if (r.startsWith("1 ")) {
|
|
884
1781
|
const parts = r.split(" ");
|
|
885
|
-
result.files.push(
|
|
1782
|
+
result.files.push(entry2(parts[1], parts.slice(8).join(" ")));
|
|
886
1783
|
} else if (r.startsWith("2 ")) {
|
|
887
1784
|
const parts = r.split(" ");
|
|
888
|
-
result.files.push({ ...
|
|
1785
|
+
result.files.push({ ...entry2(parts[1], parts.slice(9).join(" ")), origPath: records2[++i] });
|
|
889
1786
|
} else if (r.startsWith("u ")) {
|
|
890
1787
|
const parts = r.split(" ");
|
|
891
|
-
result.files.push({ ...
|
|
1788
|
+
result.files.push({ ...entry2(parts[1], parts.slice(10).join(" ")), conflicted: true });
|
|
892
1789
|
} else if (r.startsWith("? ")) {
|
|
893
1790
|
result.files.push({ path: r.slice(2), index: ".", worktree: "?" });
|
|
894
1791
|
}
|
|
895
1792
|
}
|
|
896
1793
|
return result;
|
|
897
1794
|
}
|
|
898
|
-
function
|
|
1795
|
+
function entry2(xy, filePath) {
|
|
899
1796
|
return { path: filePath, index: xy[0], worktree: xy[1] };
|
|
900
1797
|
}
|
|
901
1798
|
async function diff(repo, file, { staged = false, untracked = false } = {}) {
|
|
@@ -1052,33 +1949,33 @@ var LANDING_HTML = `<!doctype html>
|
|
|
1052
1949
|
`;
|
|
1053
1950
|
|
|
1054
1951
|
// ../../packages/agent-core/src/ports.js
|
|
1055
|
-
import { execFile as
|
|
1056
|
-
import
|
|
1952
|
+
import { execFile as execFile4 } from "node:child_process";
|
|
1953
|
+
import fs10 from "node:fs/promises";
|
|
1057
1954
|
import net from "node:net";
|
|
1058
|
-
import
|
|
1955
|
+
import os2 from "node:os";
|
|
1059
1956
|
|
|
1060
1957
|
// ../../packages/agent-core/src/platform.js
|
|
1061
|
-
import { execFile as
|
|
1062
|
-
import
|
|
1063
|
-
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";
|
|
1064
1961
|
var isWindows = process.platform === "win32";
|
|
1065
1962
|
function shellQuote(arg) {
|
|
1066
1963
|
return `'${String(arg).replace(/'/g, `'\\''`)}'`;
|
|
1067
1964
|
}
|
|
1068
1965
|
function runnable(file) {
|
|
1069
1966
|
try {
|
|
1070
|
-
if (!isWindows)
|
|
1071
|
-
return
|
|
1967
|
+
if (!isWindows) fs9.accessSync(file, fs9.constants.X_OK);
|
|
1968
|
+
return fs9.statSync(file).isFile();
|
|
1072
1969
|
} catch {
|
|
1073
1970
|
return false;
|
|
1074
1971
|
}
|
|
1075
1972
|
}
|
|
1076
1973
|
function findExecutable(name, env = process.env) {
|
|
1077
|
-
const exts = isWindows && !
|
|
1974
|
+
const exts = isWindows && !path7.extname(name) ? (env.PATHEXT || ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean) : [""];
|
|
1078
1975
|
const match = (base2) => exts.map((e) => base2 + e).find(runnable) ?? null;
|
|
1079
|
-
if (name.includes("/") || name.includes(
|
|
1080
|
-
for (const dir of (env.PATH ?? env.Path ?? "").split(
|
|
1081
|
-
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));
|
|
1082
1979
|
if (found) return found;
|
|
1083
1980
|
}
|
|
1084
1981
|
return null;
|
|
@@ -1107,12 +2004,12 @@ function terminalCommand(argv2) {
|
|
|
1107
2004
|
}
|
|
1108
2005
|
function spawnCommand(argv2, options = {}) {
|
|
1109
2006
|
const { file, args, verbatim } = resolveCommand(argv2);
|
|
1110
|
-
return
|
|
2007
|
+
return spawn2(file, args, { ...options, windowsVerbatimArguments: verbatim });
|
|
1111
2008
|
}
|
|
1112
2009
|
function execCommand(argv2, options = {}) {
|
|
1113
2010
|
return new Promise((resolve, reject) => {
|
|
1114
2011
|
const { file, args, verbatim } = resolveCommand(argv2);
|
|
1115
|
-
|
|
2012
|
+
execFile3(
|
|
1116
2013
|
file,
|
|
1117
2014
|
args,
|
|
1118
2015
|
{ ...options, windowsVerbatimArguments: verbatim },
|
|
@@ -1122,7 +2019,7 @@ function execCommand(argv2, options = {}) {
|
|
|
1122
2019
|
}
|
|
1123
2020
|
function loginShell(script, timeout) {
|
|
1124
2021
|
return new Promise((resolve) => {
|
|
1125
|
-
|
|
2022
|
+
execFile3(
|
|
1126
2023
|
interactiveShell().file,
|
|
1127
2024
|
["-l", "-c", script],
|
|
1128
2025
|
{ timeout, maxBuffer: 16 * 1024 * 1024 },
|
|
@@ -1144,9 +2041,9 @@ async function commandExists(command) {
|
|
|
1144
2041
|
}
|
|
1145
2042
|
|
|
1146
2043
|
// ../../packages/agent-core/src/ports.js
|
|
1147
|
-
function
|
|
2044
|
+
function run2(cmd2, args) {
|
|
1148
2045
|
return new Promise((resolve, reject) => {
|
|
1149
|
-
|
|
2046
|
+
execFile4(cmd2, args, { maxBuffer: 8 * 1024 * 1024 }, (err, stdout) => {
|
|
1150
2047
|
if (err?.code === "ENOENT") reject(err);
|
|
1151
2048
|
else resolve(stdout || "");
|
|
1152
2049
|
});
|
|
@@ -1160,7 +2057,7 @@ function splitHostPort(name) {
|
|
|
1160
2057
|
}
|
|
1161
2058
|
async function listeningSockets() {
|
|
1162
2059
|
try {
|
|
1163
|
-
const out2 = await
|
|
2060
|
+
const out2 = await run2("lsof", ["-nP", "-a", "-u", String(os2.userInfo().uid), "-iTCP", "-sTCP:LISTEN", "-F", "pcn"]);
|
|
1164
2061
|
const sockets2 = [];
|
|
1165
2062
|
let pid;
|
|
1166
2063
|
let name;
|
|
@@ -1175,9 +2072,9 @@ async function listeningSockets() {
|
|
|
1175
2072
|
}
|
|
1176
2073
|
return sockets2;
|
|
1177
2074
|
} catch (e) {
|
|
1178
|
-
if (e.code !== "ENOENT" ||
|
|
2075
|
+
if (e.code !== "ENOENT" || os2.platform() !== "linux") throw e;
|
|
1179
2076
|
}
|
|
1180
|
-
const out = await
|
|
2077
|
+
const out = await run2("ss", ["-Hltnp"]);
|
|
1181
2078
|
const sockets = [];
|
|
1182
2079
|
for (const line of out.split("\n")) {
|
|
1183
2080
|
const cols = line.trim().split(/\s+/);
|
|
@@ -1188,7 +2085,7 @@ async function listeningSockets() {
|
|
|
1188
2085
|
return sockets;
|
|
1189
2086
|
}
|
|
1190
2087
|
async function processTable() {
|
|
1191
|
-
const out = await
|
|
2088
|
+
const out = await run2("ps", ["-A", "-ww", "-o", "pid=,ppid=,tty=,command="]);
|
|
1192
2089
|
const table = /* @__PURE__ */ new Map();
|
|
1193
2090
|
for (const line of out.split("\n")) {
|
|
1194
2091
|
const m = line.match(/^\s*(\d+)\s+(\d+)\s+(\S+)\s+(.*)$/);
|
|
@@ -1199,11 +2096,11 @@ async function processTable() {
|
|
|
1199
2096
|
async function workingDirs(pids) {
|
|
1200
2097
|
const dirs = /* @__PURE__ */ new Map();
|
|
1201
2098
|
if (!pids.length || isWindows) return dirs;
|
|
1202
|
-
if (
|
|
1203
|
-
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))));
|
|
1204
2101
|
return dirs;
|
|
1205
2102
|
}
|
|
1206
|
-
const out = await
|
|
2103
|
+
const out = await run2("lsof", ["-a", "-p", pids.join(","), "-d", "cwd", "-F", "pn"]).catch(() => "");
|
|
1207
2104
|
let pid;
|
|
1208
2105
|
for (const line of out.split("\n")) {
|
|
1209
2106
|
if (line[0] === "p") pid = parseInt(line.slice(1), 10);
|
|
@@ -1215,9 +2112,9 @@ var WINDOWS_SHELLS = /* @__PURE__ */ new Set(["cmd.exe", "powershell.exe", "pwsh
|
|
|
1215
2112
|
async function windowsListeners() {
|
|
1216
2113
|
const query = "Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,Name,CommandLine | ConvertTo-Json -Compress";
|
|
1217
2114
|
const [v4, v6, json] = await Promise.all([
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
2115
|
+
run2("netstat", ["-ano", "-p", "TCP"]),
|
|
2116
|
+
run2("netstat", ["-ano", "-p", "TCPv6"]),
|
|
2117
|
+
run2("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", query])
|
|
1221
2118
|
]);
|
|
1222
2119
|
const table = /* @__PURE__ */ new Map();
|
|
1223
2120
|
for (const p of [JSON.parse(json || "[]")].flat()) {
|
|
@@ -1247,9 +2144,9 @@ async function listPorts(shells = /* @__PURE__ */ new Map()) {
|
|
|
1247
2144
|
const byKey = /* @__PURE__ */ new Map();
|
|
1248
2145
|
for (const s of sockets) {
|
|
1249
2146
|
const key = `${s.pid}:${s.port}`;
|
|
1250
|
-
const
|
|
1251
|
-
if (
|
|
1252
|
-
if (!
|
|
2147
|
+
const entry3 = byKey.get(key);
|
|
2148
|
+
if (entry3) {
|
|
2149
|
+
if (!entry3.addresses.includes(s.address)) entry3.addresses.push(s.address);
|
|
1253
2150
|
continue;
|
|
1254
2151
|
}
|
|
1255
2152
|
byKey.set(key, {
|
|
@@ -1266,10 +2163,10 @@ async function listPorts(shells = /* @__PURE__ */ new Map()) {
|
|
|
1266
2163
|
agent: s.pid === process.pid || AGENT_COMMAND.test(table.get(s.pid)?.command ?? "")
|
|
1267
2164
|
});
|
|
1268
2165
|
}
|
|
1269
|
-
const
|
|
1270
|
-
const dirs = await workingDirs([...new Set(
|
|
1271
|
-
for (const e of
|
|
1272
|
-
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);
|
|
1273
2170
|
}
|
|
1274
2171
|
function sessionOf(pid, table, shells) {
|
|
1275
2172
|
for (let p = pid, depth = 0; p > 1 && depth < 64; p = table.get(p)?.ppid, depth++) {
|
|
@@ -1304,17 +2201,18 @@ function isListening(port2, timeout = 2e3) {
|
|
|
1304
2201
|
}
|
|
1305
2202
|
|
|
1306
2203
|
// ../../packages/agent-core/src/push.js
|
|
1307
|
-
import { execFile as
|
|
1308
|
-
import { createCipheriv, randomBytes } from "node:crypto";
|
|
1309
|
-
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";
|
|
1310
2207
|
import { promisify } from "node:util";
|
|
1311
|
-
var
|
|
2208
|
+
var run3 = promisify(execFile5);
|
|
1312
2209
|
var FINISHED_AFTER = 60 * 1e3;
|
|
1313
2210
|
var ACTIVE_WITHIN = 2 * 60;
|
|
2211
|
+
var AT_SESSION_WITHIN = 30;
|
|
1314
2212
|
async function macIdleSeconds() {
|
|
1315
|
-
if (
|
|
2213
|
+
if (os3.platform() !== "darwin") return null;
|
|
1316
2214
|
try {
|
|
1317
|
-
const { stdout } = await
|
|
2215
|
+
const { stdout } = await run3("ioreg", ["-c", "IOHIDSystem", "-d", "4", "-k", "HIDIdleTime"], { timeout: 2e3 });
|
|
1318
2216
|
const ns = stdout.match(/"HIDIdleTime"\s*=\s*(\d+)/)?.[1];
|
|
1319
2217
|
return ns ? Number(BigInt(ns) / 1000000000n) : null;
|
|
1320
2218
|
} catch {
|
|
@@ -1325,8 +2223,25 @@ async function userActiveHere() {
|
|
|
1325
2223
|
const idle = await macIdleSeconds();
|
|
1326
2224
|
return idle !== null && idle < ACTIVE_WITHIN;
|
|
1327
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
|
+
}
|
|
1328
2243
|
function seal(key, data) {
|
|
1329
|
-
const nonce =
|
|
2244
|
+
const nonce = randomBytes2(12);
|
|
1330
2245
|
const cipher = createCipheriv("aes-256-gcm", key, nonce);
|
|
1331
2246
|
const body = Buffer.concat([cipher.update(JSON.stringify(data), "utf8"), cipher.final()]);
|
|
1332
2247
|
return Buffer.concat([nonce, body, cipher.getAuthTag()]).toString("base64");
|
|
@@ -1376,8 +2291,8 @@ var PushSender = class {
|
|
|
1376
2291
|
#devices;
|
|
1377
2292
|
#store;
|
|
1378
2293
|
#hub;
|
|
1379
|
-
#
|
|
1380
|
-
#
|
|
2294
|
+
#shows;
|
|
2295
|
+
#atSession;
|
|
1381
2296
|
#fetch;
|
|
1382
2297
|
// Requests a notification went out for, so their resolution can replace it.
|
|
1383
2298
|
#notified = /* @__PURE__ */ new Set();
|
|
@@ -1385,15 +2300,17 @@ var PushSender = class {
|
|
|
1385
2300
|
* @param {object} opts
|
|
1386
2301
|
* @param {import('./agents.js').AgentHub} opts.hub
|
|
1387
2302
|
* @param {{ load: () => object[], save: (devices: object[]) => void }} [opts.store]
|
|
1388
|
-
* @param {() => boolean} [opts.
|
|
1389
|
-
*
|
|
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
|
|
1390
2307
|
*/
|
|
1391
|
-
constructor({ hub, store = null,
|
|
2308
|
+
constructor({ hub, store = null, shows = () => false, atSession = atSessionHere, fetch: fetch2 = globalThis.fetch }) {
|
|
1392
2309
|
this.#hub = hub;
|
|
1393
2310
|
this.#store = store;
|
|
1394
2311
|
this.#devices = store?.load() ?? [];
|
|
1395
|
-
this.#
|
|
1396
|
-
this.#
|
|
2312
|
+
this.#shows = shows;
|
|
2313
|
+
this.#atSession = atSession;
|
|
1397
2314
|
this.#fetch = fetch2;
|
|
1398
2315
|
hub.on("message", (msg) => this.#follow(msg).catch(() => {
|
|
1399
2316
|
}));
|
|
@@ -1426,10 +2343,12 @@ var PushSender = class {
|
|
|
1426
2343
|
#save() {
|
|
1427
2344
|
this.#store?.save(this.#devices);
|
|
1428
2345
|
}
|
|
1429
|
-
|
|
1430
|
-
|
|
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;
|
|
1431
2350
|
try {
|
|
1432
|
-
return await this.#
|
|
2351
|
+
return await this.#atSession(this.#hub.host(what.session));
|
|
1433
2352
|
} catch {
|
|
1434
2353
|
return false;
|
|
1435
2354
|
}
|
|
@@ -1438,7 +2357,7 @@ var PushSender = class {
|
|
|
1438
2357
|
if (!this.#devices.length) return;
|
|
1439
2358
|
if (msg.type === "request") {
|
|
1440
2359
|
const { request } = msg;
|
|
1441
|
-
if (await this.#quiet()) return;
|
|
2360
|
+
if (await this.#quiet({ session: request.session })) return;
|
|
1442
2361
|
if (!this.#hub.requests().some((r) => r.id === request.id)) return;
|
|
1443
2362
|
this.#notified.add(request.id);
|
|
1444
2363
|
const question = request.kind === "question";
|
|
@@ -1470,7 +2389,7 @@ var PushSender = class {
|
|
|
1470
2389
|
fallback: "Answered on your computer"
|
|
1471
2390
|
});
|
|
1472
2391
|
} else if (msg.type === "entry" && msg.entry.kind === "turn_end" && msg.entry.duration >= FINISHED_AFTER) {
|
|
1473
|
-
if (await this.#quiet()) return;
|
|
2392
|
+
if (await this.#quiet({ session: msg.session })) return;
|
|
1474
2393
|
const session = this.#hub.session(msg.session);
|
|
1475
2394
|
await this.#broadcast("finished", {
|
|
1476
2395
|
kind: "finished",
|
|
@@ -1482,11 +2401,27 @@ var PushSender = class {
|
|
|
1482
2401
|
});
|
|
1483
2402
|
}
|
|
1484
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
|
+
}
|
|
1485
2420
|
async #broadcast(kind, note) {
|
|
1486
2421
|
await Promise.all(this.#devices.filter((d) => d.kinds[kind]).map((d) => this.#send(d, note)));
|
|
1487
2422
|
}
|
|
1488
|
-
async #send(device, { kind, title, body, session, request, question, options, category, collapseId, priority, silent, fallback }) {
|
|
1489
|
-
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 };
|
|
1490
2425
|
const aps = { alert: { title: "Code Remote", body: fallback }, "mutable-content": 1 };
|
|
1491
2426
|
if (category) aps.category = category;
|
|
1492
2427
|
if (session) aps["thread-id"] = session;
|
|
@@ -1521,7 +2456,7 @@ var PushSender = class {
|
|
|
1521
2456
|
};
|
|
1522
2457
|
|
|
1523
2458
|
// ../../packages/agent-core/src/inspector.js
|
|
1524
|
-
import
|
|
2459
|
+
import http2 from "node:http";
|
|
1525
2460
|
import net2 from "node:net";
|
|
1526
2461
|
import zlib from "node:zlib";
|
|
1527
2462
|
var MAX_ENTRIES2 = 200;
|
|
@@ -1564,7 +2499,7 @@ var InspectingProxy = class {
|
|
|
1564
2499
|
/** @param {number} target the dev server's port on this machine */
|
|
1565
2500
|
constructor(target) {
|
|
1566
2501
|
this.target = target;
|
|
1567
|
-
this.server =
|
|
2502
|
+
this.server = http2.createServer((req, res) => this.#forward(req, res));
|
|
1568
2503
|
this.server.on("upgrade", (req, socket, head) => this.#tunnelUpgrade(req, socket, head));
|
|
1569
2504
|
}
|
|
1570
2505
|
/** Listens on a free loopback port; resolves to it. */
|
|
@@ -1582,7 +2517,7 @@ var InspectingProxy = class {
|
|
|
1582
2517
|
}
|
|
1583
2518
|
/** Summaries, oldest first; `after` skips ids up to and including it. */
|
|
1584
2519
|
list(after = 0) {
|
|
1585
|
-
return this.#entries.filter((e) => e.id > after).map(
|
|
2520
|
+
return this.#entries.filter((e) => e.id > after).map(summary2);
|
|
1586
2521
|
}
|
|
1587
2522
|
get(id) {
|
|
1588
2523
|
return this.#entries.find((e) => e.id === id);
|
|
@@ -1591,7 +2526,7 @@ var InspectingProxy = class {
|
|
|
1591
2526
|
this.#entries = [];
|
|
1592
2527
|
}
|
|
1593
2528
|
#record(req) {
|
|
1594
|
-
const
|
|
2529
|
+
const entry3 = {
|
|
1595
2530
|
id: nextId++,
|
|
1596
2531
|
time: Date.now(),
|
|
1597
2532
|
method: req.method,
|
|
@@ -1602,21 +2537,21 @@ var InspectingProxy = class {
|
|
|
1602
2537
|
request: { headers: req.headers, tap: new BodyTap() },
|
|
1603
2538
|
response: { headers: {}, tap: new BodyTap() }
|
|
1604
2539
|
};
|
|
1605
|
-
this.#entries.push(
|
|
2540
|
+
this.#entries.push(entry3);
|
|
1606
2541
|
if (this.#entries.length > MAX_ENTRIES2) this.#entries.shift();
|
|
1607
|
-
return
|
|
2542
|
+
return entry3;
|
|
1608
2543
|
}
|
|
1609
2544
|
/** The dev server sees itself addressed as localhost:<its port>, as without the proxy. */
|
|
1610
2545
|
#headers(req) {
|
|
1611
2546
|
return { ...req.headers, host: `localhost:${this.target}` };
|
|
1612
2547
|
}
|
|
1613
2548
|
#forward(req, res) {
|
|
1614
|
-
const
|
|
2549
|
+
const entry3 = this.#record(req);
|
|
1615
2550
|
const started = performance.now();
|
|
1616
2551
|
const finish = () => {
|
|
1617
|
-
|
|
2552
|
+
entry3.duration = Math.round(performance.now() - started);
|
|
1618
2553
|
};
|
|
1619
|
-
const upstream =
|
|
2554
|
+
const upstream = http2.request({
|
|
1620
2555
|
agent: false,
|
|
1621
2556
|
host: "localhost",
|
|
1622
2557
|
port: this.target,
|
|
@@ -1625,11 +2560,11 @@ var InspectingProxy = class {
|
|
|
1625
2560
|
headers: this.#headers(req)
|
|
1626
2561
|
});
|
|
1627
2562
|
upstream.on("response", (up) => {
|
|
1628
|
-
|
|
1629
|
-
|
|
2563
|
+
entry3.status = up.statusCode;
|
|
2564
|
+
entry3.response.headers = up.headers;
|
|
1630
2565
|
res.writeHead(up.statusCode, up.statusMessage, up.rawHeaders);
|
|
1631
2566
|
up.on("data", (chunk) => {
|
|
1632
|
-
|
|
2567
|
+
entry3.response.tap.push(chunk);
|
|
1633
2568
|
res.write(chunk);
|
|
1634
2569
|
});
|
|
1635
2570
|
up.on("end", () => {
|
|
@@ -1640,14 +2575,14 @@ var InspectingProxy = class {
|
|
|
1640
2575
|
});
|
|
1641
2576
|
upstream.on("error", (e) => {
|
|
1642
2577
|
finish();
|
|
1643
|
-
|
|
2578
|
+
entry3.error = e.code === "ECONNREFUSED" ? `nothing is listening on port ${this.target}` : e.message;
|
|
1644
2579
|
if (res.headersSent) return res.destroy();
|
|
1645
|
-
|
|
2580
|
+
entry3.status = 502;
|
|
1646
2581
|
res.writeHead(502, { "content-type": "text/plain; charset=utf-8" });
|
|
1647
|
-
res.end(`coderemote preview: ${
|
|
2582
|
+
res.end(`coderemote preview: ${entry3.error}
|
|
1648
2583
|
`);
|
|
1649
2584
|
});
|
|
1650
|
-
req.on("data", (chunk) =>
|
|
2585
|
+
req.on("data", (chunk) => entry3.request.tap.push(chunk));
|
|
1651
2586
|
req.pipe(upstream);
|
|
1652
2587
|
res.on("close", () => {
|
|
1653
2588
|
if (!res.writableFinished) upstream.destroy();
|
|
@@ -1655,7 +2590,7 @@ var InspectingProxy = class {
|
|
|
1655
2590
|
}
|
|
1656
2591
|
/** WebSockets (hot reload) pass through as raw bytes; only the handshake is recorded. */
|
|
1657
2592
|
#tunnelUpgrade(req, socket, head) {
|
|
1658
|
-
const
|
|
2593
|
+
const entry3 = this.#record(req);
|
|
1659
2594
|
const upstream = net2.connect({ host: "localhost", port: this.target }, () => {
|
|
1660
2595
|
const headers = this.#headers(req);
|
|
1661
2596
|
const lines = [`${req.method} ${req.url} HTTP/1.1`];
|
|
@@ -1666,18 +2601,18 @@ var InspectingProxy = class {
|
|
|
1666
2601
|
\r
|
|
1667
2602
|
`);
|
|
1668
2603
|
if (head?.length) upstream.write(head);
|
|
1669
|
-
|
|
2604
|
+
entry3.status = 101;
|
|
1670
2605
|
upstream.pipe(socket);
|
|
1671
2606
|
socket.pipe(upstream);
|
|
1672
2607
|
});
|
|
1673
2608
|
upstream.on("error", (e) => {
|
|
1674
|
-
|
|
2609
|
+
entry3.error = e.message;
|
|
1675
2610
|
socket.destroy();
|
|
1676
2611
|
});
|
|
1677
2612
|
socket.on("error", () => upstream.destroy());
|
|
1678
2613
|
}
|
|
1679
2614
|
};
|
|
1680
|
-
function
|
|
2615
|
+
function summary2(e) {
|
|
1681
2616
|
return {
|
|
1682
2617
|
id: e.id,
|
|
1683
2618
|
time: e.time,
|
|
@@ -1693,16 +2628,369 @@ function summary(e) {
|
|
|
1693
2628
|
}
|
|
1694
2629
|
function detail(e) {
|
|
1695
2630
|
return {
|
|
1696
|
-
...
|
|
2631
|
+
...summary2(e),
|
|
1697
2632
|
request: { headers: e.request.headers, body: e.request.tap.describe(e.request.headers) },
|
|
1698
2633
|
response: { headers: e.response.headers, body: e.response.tap.describe(e.response.headers) }
|
|
1699
2634
|
};
|
|
1700
2635
|
}
|
|
1701
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
|
+
|
|
1702
2990
|
// ../../packages/agent-core/src/sessions.js
|
|
1703
|
-
import { execFile as
|
|
1704
|
-
import { randomUUID as
|
|
1705
|
-
import
|
|
2991
|
+
import { execFile as execFile7 } from "node:child_process";
|
|
2992
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
2993
|
+
import os4 from "node:os";
|
|
1706
2994
|
import pty from "node-pty";
|
|
1707
2995
|
var SCROLLBACK_LIMIT = 256 * 1024;
|
|
1708
2996
|
function guardConpty(term) {
|
|
@@ -1721,7 +3009,7 @@ var TerminalSession = class {
|
|
|
1721
3009
|
* terminalCommand says; the session ends when it exits.
|
|
1722
3010
|
*/
|
|
1723
3011
|
constructor({ cols, rows, cwd, env = {}, command = null }) {
|
|
1724
|
-
this.id =
|
|
3012
|
+
this.id = randomUUID3();
|
|
1725
3013
|
this.createdAt = Date.now();
|
|
1726
3014
|
this.chunks = [];
|
|
1727
3015
|
this.bufferedBytes = 0;
|
|
@@ -1734,7 +3022,7 @@ var TerminalSession = class {
|
|
|
1734
3022
|
name: "xterm-256color",
|
|
1735
3023
|
cols,
|
|
1736
3024
|
rows,
|
|
1737
|
-
cwd: cwd ||
|
|
3025
|
+
cwd: cwd || os4.homedir(),
|
|
1738
3026
|
// CODEREMOTE_TERM_ID lets `coderemote hook` tell which terminal a coding agent runs in.
|
|
1739
3027
|
env: { ...process.env, ...env, CODEREMOTE_TERM_ID: this.id, TERM: "xterm-256color", COLORTERM: "truecolor" }
|
|
1740
3028
|
});
|
|
@@ -1787,7 +3075,7 @@ var TerminalSession = class {
|
|
|
1787
3075
|
/** Ends the session; on Windows, which has no signals, always forcibly (see releaseConpty). */
|
|
1788
3076
|
kill(signal) {
|
|
1789
3077
|
if (!isWindows) this.pty.kill(signal);
|
|
1790
|
-
else if (this.exitCode === null)
|
|
3078
|
+
else if (this.exitCode === null) execFile7("taskkill", ["/T", "/F", "/PID", String(this.pty.pid)], () => {
|
|
1791
3079
|
});
|
|
1792
3080
|
}
|
|
1793
3081
|
/** Types into the session, as a client's keyboard would; nothing once it has exited. */
|
|
@@ -1846,21 +3134,21 @@ var SessionManager = class {
|
|
|
1846
3134
|
};
|
|
1847
3135
|
|
|
1848
3136
|
// ../../packages/agent-core/src/uploads.js
|
|
1849
|
-
import { execFile as
|
|
1850
|
-
import { randomUUID as
|
|
3137
|
+
import { execFile as execFile8 } from "node:child_process";
|
|
3138
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
1851
3139
|
import { createWriteStream } from "node:fs";
|
|
1852
|
-
import
|
|
1853
|
-
import
|
|
1854
|
-
import
|
|
3140
|
+
import fs12 from "node:fs/promises";
|
|
3141
|
+
import os5 from "node:os";
|
|
3142
|
+
import path9 from "node:path";
|
|
1855
3143
|
import { Transform } from "node:stream";
|
|
1856
|
-
import { pipeline } from "node:stream/promises";
|
|
3144
|
+
import { pipeline as pipeline2 } from "node:stream/promises";
|
|
1857
3145
|
import { promisify as promisify2 } from "node:util";
|
|
1858
|
-
var
|
|
3146
|
+
var run4 = promisify2(execFile8);
|
|
1859
3147
|
var UPLOAD_LIMIT = 100 * 1024 * 1024;
|
|
1860
3148
|
var KEEP = 7 * 24 * 60 * 60 * 1e3;
|
|
1861
3149
|
var ID = /^[0-9a-f-]{36}$/;
|
|
1862
3150
|
function safeName(name) {
|
|
1863
|
-
const base2 =
|
|
3151
|
+
const base2 = path9.basename(String(name ?? "").replace(/\\/g, "/"));
|
|
1864
3152
|
const clean = base2.replace(/[^\p{L}\p{N}._ -]/gu, "_").replace(/^[.\s]+/, "").slice(-80).trim();
|
|
1865
3153
|
return clean || "file";
|
|
1866
3154
|
}
|
|
@@ -1872,10 +3160,10 @@ var UploadStore = class {
|
|
|
1872
3160
|
/** Streams `body` to disk. 413 past `limit`; HEIC photos become JPEG on macOS. */
|
|
1873
3161
|
async save(body, { name, type, length, limit = UPLOAD_LIMIT }) {
|
|
1874
3162
|
if (Number(length) > limit) throw new HttpError(413, `files are limited to ${limit / 1024 / 1024} MB`);
|
|
1875
|
-
const id =
|
|
3163
|
+
const id = randomUUID4();
|
|
1876
3164
|
const day = new Date(this.now()).toISOString().slice(0, 10);
|
|
1877
|
-
await
|
|
1878
|
-
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)}`);
|
|
1879
3167
|
let size = 0;
|
|
1880
3168
|
const counter = new Transform({
|
|
1881
3169
|
transform(chunk, _enc, done) {
|
|
@@ -1884,41 +3172,41 @@ var UploadStore = class {
|
|
|
1884
3172
|
}
|
|
1885
3173
|
});
|
|
1886
3174
|
try {
|
|
1887
|
-
await
|
|
3175
|
+
await pipeline2(body, counter, createWriteStream(file, { mode: 384 }));
|
|
1888
3176
|
} catch (e) {
|
|
1889
|
-
await
|
|
3177
|
+
await fs12.rm(file, { force: true });
|
|
1890
3178
|
throw e;
|
|
1891
3179
|
}
|
|
1892
3180
|
let kind = type || "application/octet-stream";
|
|
1893
|
-
if ((kind === "image/heic" || /\.heic$/i.test(file)) &&
|
|
3181
|
+
if ((kind === "image/heic" || /\.heic$/i.test(file)) && os5.platform() === "darwin") {
|
|
1894
3182
|
const jpeg = file.replace(/(\.heic)?$/i, ".jpg");
|
|
1895
3183
|
try {
|
|
1896
|
-
await
|
|
1897
|
-
await
|
|
3184
|
+
await run4("sips", ["-s", "format", "jpeg", file, "--out", jpeg], { timeout: 3e4 });
|
|
3185
|
+
await fs12.rm(file);
|
|
1898
3186
|
file = jpeg;
|
|
1899
3187
|
kind = "image/jpeg";
|
|
1900
|
-
size = (await
|
|
3188
|
+
size = (await fs12.stat(file)).size;
|
|
1901
3189
|
} catch {
|
|
1902
3190
|
}
|
|
1903
3191
|
}
|
|
1904
|
-
return { id, name:
|
|
3192
|
+
return { id, name: path9.basename(file).slice(id.length + 1), type: kind, size, path: file };
|
|
1905
3193
|
}
|
|
1906
3194
|
/** The file of an upload; 404 once it's gone (swept, or never existed). */
|
|
1907
3195
|
async path(id) {
|
|
1908
3196
|
if (typeof id !== "string" || !ID.test(id)) throw new HttpError(400, "invalid upload id");
|
|
1909
3197
|
let days = [];
|
|
1910
3198
|
try {
|
|
1911
|
-
days = (await
|
|
3199
|
+
days = (await fs12.readdir(this.dir)).sort().reverse();
|
|
1912
3200
|
} catch {
|
|
1913
3201
|
}
|
|
1914
3202
|
for (const day of days) {
|
|
1915
3203
|
let names = [];
|
|
1916
3204
|
try {
|
|
1917
|
-
names = await
|
|
3205
|
+
names = await fs12.readdir(path9.join(this.dir, day));
|
|
1918
3206
|
} catch {
|
|
1919
3207
|
}
|
|
1920
3208
|
const hit = names.find((n) => n.startsWith(`${id}-`));
|
|
1921
|
-
if (hit) return
|
|
3209
|
+
if (hit) return path9.join(this.dir, day, hit);
|
|
1922
3210
|
}
|
|
1923
3211
|
throw new HttpError(404, "no such upload (they are kept for 7 days)");
|
|
1924
3212
|
}
|
|
@@ -1926,25 +3214,25 @@ var UploadStore = class {
|
|
|
1926
3214
|
async sweep() {
|
|
1927
3215
|
let days = [];
|
|
1928
3216
|
try {
|
|
1929
|
-
days = await
|
|
3217
|
+
days = await fs12.readdir(this.dir);
|
|
1930
3218
|
} catch {
|
|
1931
3219
|
return;
|
|
1932
3220
|
}
|
|
1933
3221
|
const cutoff = this.now() - KEEP;
|
|
1934
3222
|
for (const day of days) {
|
|
1935
|
-
const dir =
|
|
3223
|
+
const dir = path9.join(this.dir, day);
|
|
1936
3224
|
let names = [];
|
|
1937
3225
|
try {
|
|
1938
|
-
names = await
|
|
3226
|
+
names = await fs12.readdir(dir);
|
|
1939
3227
|
} catch {
|
|
1940
3228
|
continue;
|
|
1941
3229
|
}
|
|
1942
3230
|
for (const n of names) {
|
|
1943
|
-
const file =
|
|
1944
|
-
const stat = await
|
|
1945
|
-
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 });
|
|
1946
3234
|
}
|
|
1947
|
-
if (!(await
|
|
3235
|
+
if (!(await fs12.readdir(dir).catch(() => [])).length) await fs12.rm(dir, { recursive: true, force: true });
|
|
1948
3236
|
}
|
|
1949
3237
|
}
|
|
1950
3238
|
};
|
|
@@ -1957,21 +3245,48 @@ function kindOf(stat) {
|
|
|
1957
3245
|
return "other";
|
|
1958
3246
|
}
|
|
1959
3247
|
async function describe(fullPath, name) {
|
|
1960
|
-
const lst = await
|
|
1961
|
-
const
|
|
3248
|
+
const lst = await fs13.lstat(fullPath);
|
|
3249
|
+
const entry3 = { name, type: kindOf(lst), size: lst.size, mtime: lst.mtimeMs };
|
|
1962
3250
|
if (lst.isSymbolicLink()) {
|
|
1963
3251
|
try {
|
|
1964
|
-
|
|
3252
|
+
entry3.target = kindOf(await fs13.stat(fullPath));
|
|
1965
3253
|
} catch {
|
|
1966
|
-
|
|
3254
|
+
entry3.target = "broken";
|
|
1967
3255
|
}
|
|
1968
3256
|
}
|
|
1969
|
-
return
|
|
3257
|
+
return entry3;
|
|
1970
3258
|
}
|
|
1971
3259
|
var PASTE_START = "\x1B[200~";
|
|
1972
3260
|
var PASTE_END = "\x1B[201~";
|
|
1973
3261
|
var KEYS = { interrupt: "\x1B", cycle_mode: "\x1B[Z" };
|
|
1974
|
-
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
|
+
}
|
|
1975
3290
|
function typeable(text) {
|
|
1976
3291
|
return text.replace(/\r\n?/g, "\n").replace(/[\x00-\x08\x0b-\x1f\x7f]/g, "");
|
|
1977
3292
|
}
|
|
@@ -1981,7 +3296,7 @@ function sendJson(res, status2, body) {
|
|
|
1981
3296
|
res.writeHead(status2, { "content-type": "application/json", "content-length": Buffer.byteLength(json) });
|
|
1982
3297
|
res.end(json);
|
|
1983
3298
|
}
|
|
1984
|
-
async function
|
|
3299
|
+
async function readJson2(req) {
|
|
1985
3300
|
const chunks = [];
|
|
1986
3301
|
for await (const c of req) chunks.push(c);
|
|
1987
3302
|
try {
|
|
@@ -1994,19 +3309,24 @@ function createAgentServer({
|
|
|
1994
3309
|
token,
|
|
1995
3310
|
root: root2,
|
|
1996
3311
|
tunnelToken = async () => null,
|
|
1997
|
-
preview = null,
|
|
3312
|
+
preview: preview2 = null,
|
|
1998
3313
|
tunnelStatus: tunnelStatus2 = null,
|
|
3314
|
+
tunnelDelete: tunnelDelete2 = null,
|
|
1999
3315
|
hookSecret = null,
|
|
2000
3316
|
hookBin = null,
|
|
2001
3317
|
uploadsDir = null,
|
|
2002
3318
|
claudeCommand = "claude",
|
|
2003
3319
|
codexCommand = "codex",
|
|
2004
|
-
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 }),
|
|
2005
3324
|
pluginInstalled = null,
|
|
2006
3325
|
agents = new AgentHub(),
|
|
2007
3326
|
pushStore: pushStore2 = null,
|
|
2008
3327
|
pushOptions = {},
|
|
2009
|
-
previewSweep = {}
|
|
3328
|
+
previewSweep = {},
|
|
3329
|
+
version = null
|
|
2010
3330
|
}) {
|
|
2011
3331
|
const handoff = new TunnelHandoff({ token, mint: tunnelToken });
|
|
2012
3332
|
const sandbox = new Sandbox(root2);
|
|
@@ -2019,9 +3339,13 @@ function createAgentServer({
|
|
|
2019
3339
|
});
|
|
2020
3340
|
}, 10 * 60 * 1e3);
|
|
2021
3341
|
sweep.unref();
|
|
2022
|
-
|
|
2023
|
-
const
|
|
2024
|
-
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
|
+
}));
|
|
2025
3349
|
if (!agents.presenceSet) {
|
|
2026
3350
|
agents.present = async () => !clientInForeground() && await (pushOptions.userActive ?? userActiveHere)();
|
|
2027
3351
|
}
|
|
@@ -2112,12 +3436,16 @@ function createAgentServer({
|
|
|
2112
3436
|
}
|
|
2113
3437
|
async function codexModels() {
|
|
2114
3438
|
try {
|
|
2115
|
-
const { models } = JSON.parse(await
|
|
3439
|
+
const { models } = JSON.parse(await fs13.readFile(path10.join(codexHome, "models_cache.json"), "utf8"));
|
|
2116
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 ?? "" }));
|
|
2117
3441
|
} catch {
|
|
2118
3442
|
return [];
|
|
2119
3443
|
}
|
|
2120
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
|
+
}
|
|
2121
3449
|
function hookAuthorized(req) {
|
|
2122
3450
|
if (!hookSecret) return false;
|
|
2123
3451
|
const got = Buffer.from(String(req.headers[HOOK_SECRET_HEADER] || ""));
|
|
@@ -2125,7 +3453,7 @@ function createAgentServer({
|
|
|
2125
3453
|
return got.length === want.length && timingSafeEqual2(got, want);
|
|
2126
3454
|
}
|
|
2127
3455
|
function previewPort(value, ownPort) {
|
|
2128
|
-
if (!
|
|
3456
|
+
if (!preview2) throw new HttpError(404, "previews need a tunnel");
|
|
2129
3457
|
const port2 = Number(value);
|
|
2130
3458
|
if (!Number.isInteger(port2) || port2 < 1 || port2 > 65535) throw new HttpError(400, "port must be 1-65535");
|
|
2131
3459
|
if (port2 === ownPort) throw new HttpError(400, "the agent's own port is already on its tunnel");
|
|
@@ -2135,7 +3463,7 @@ function createAgentServer({
|
|
|
2135
3463
|
const proxies = /* @__PURE__ */ new Map();
|
|
2136
3464
|
function previewed(port2) {
|
|
2137
3465
|
const proxy = proxies.get(port2);
|
|
2138
|
-
if (!proxy || !
|
|
3466
|
+
if (!proxy || !preview2.list().some((p) => p.port === proxy.port)) {
|
|
2139
3467
|
throw new HttpError(404, "that port is not being previewed");
|
|
2140
3468
|
}
|
|
2141
3469
|
return proxy;
|
|
@@ -2146,21 +3474,23 @@ function createAgentServer({
|
|
|
2146
3474
|
return proxy;
|
|
2147
3475
|
}
|
|
2148
3476
|
async function previews() {
|
|
2149
|
-
const issued = await
|
|
2150
|
-
const forwarded = new Map(
|
|
3477
|
+
const issued = await preview2.token();
|
|
3478
|
+
const forwarded = new Map(preview2.list().map((p) => [p.port, p]));
|
|
2151
3479
|
const ports = [];
|
|
2152
3480
|
for (const [port2, proxy] of proxies) {
|
|
2153
3481
|
const p = forwarded.get(proxy.port);
|
|
2154
3482
|
if (p) ports.push({ ...p, port: port2 });
|
|
2155
3483
|
}
|
|
2156
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 });
|
|
2157
3487
|
return { ports, token: issued?.token ?? null, expiresAt: issued?.expiresAt ?? null };
|
|
2158
3488
|
}
|
|
2159
3489
|
const { checkMs = 5e3, graceMs = 3e4, killedMs = 1500 } = previewSweep;
|
|
2160
3490
|
const silentSince = /* @__PURE__ */ new Map();
|
|
2161
3491
|
let sweeping = false;
|
|
2162
3492
|
async function sweepPreviews(ports = [...proxies.keys()], now = false) {
|
|
2163
|
-
const forwarded = new Set(
|
|
3493
|
+
const forwarded = new Set(preview2.list().map((p) => p.port));
|
|
2164
3494
|
for (const port2 of ports) {
|
|
2165
3495
|
const proxy = proxies.get(port2);
|
|
2166
3496
|
if (!proxy || !forwarded.has(proxy.port) || await isListening(port2)) {
|
|
@@ -2171,12 +3501,12 @@ function createAgentServer({
|
|
|
2171
3501
|
if (!now && Date.now() - silentSince.get(port2) < graceMs) continue;
|
|
2172
3502
|
silentSince.delete(port2);
|
|
2173
3503
|
try {
|
|
2174
|
-
await
|
|
3504
|
+
await preview2.unforward(proxy.port);
|
|
2175
3505
|
} catch {
|
|
2176
3506
|
}
|
|
2177
3507
|
}
|
|
2178
3508
|
}
|
|
2179
|
-
const previewTimer =
|
|
3509
|
+
const previewTimer = preview2 && setInterval(async () => {
|
|
2180
3510
|
if (sweeping || !proxies.size) return;
|
|
2181
3511
|
sweeping = true;
|
|
2182
3512
|
try {
|
|
@@ -2192,20 +3522,20 @@ function createAgentServer({
|
|
|
2192
3522
|
async function repoOf(p) {
|
|
2193
3523
|
const repo = await sandbox.resolve(p);
|
|
2194
3524
|
try {
|
|
2195
|
-
await
|
|
3525
|
+
await fs13.access(path10.join(repo, ".git"));
|
|
2196
3526
|
} catch {
|
|
2197
3527
|
throw new HttpError(400, "not a git repository");
|
|
2198
3528
|
}
|
|
2199
3529
|
return repo;
|
|
2200
3530
|
}
|
|
2201
3531
|
async function filesIn(repo, files) {
|
|
2202
|
-
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))) {
|
|
2203
3533
|
throw new HttpError(400, "files must be a non-empty list of paths relative to the repository");
|
|
2204
3534
|
}
|
|
2205
3535
|
return Promise.all(
|
|
2206
3536
|
files.map(async (f) => {
|
|
2207
|
-
const abs = await sandbox.resolve(
|
|
2208
|
-
const rel =
|
|
3537
|
+
const abs = await sandbox.resolve(path10.join(repo, f), { allowMissing: true });
|
|
3538
|
+
const rel = path10.relative(repo, abs);
|
|
2209
3539
|
if (!rel || rel.startsWith("..")) throw new HttpError(403, "path is outside the repository");
|
|
2210
3540
|
return rel;
|
|
2211
3541
|
})
|
|
@@ -2214,55 +3544,95 @@ function createAgentServer({
|
|
|
2214
3544
|
const routes = {
|
|
2215
3545
|
[Routes.info]: async () => ({
|
|
2216
3546
|
protocol: PROTOCOL_VERSION,
|
|
2217
|
-
|
|
2218
|
-
|
|
3547
|
+
minClientProtocol: MIN_CLIENT_PROTOCOL,
|
|
3548
|
+
version,
|
|
3549
|
+
hostname: os6.hostname(),
|
|
3550
|
+
platform: os6.platform(),
|
|
2219
3551
|
root: sandbox.root,
|
|
2220
|
-
user:
|
|
3552
|
+
user: os6.userInfo().username,
|
|
2221
3553
|
// On Windows, terminals run PowerShell whatever $SHELL (Git Bash sets one) says.
|
|
2222
3554
|
shell: isWindows ? interactiveShell().file : process.env.SHELL || null
|
|
2223
3555
|
}),
|
|
2224
3556
|
[Routes.fsList]: async (url) => {
|
|
2225
3557
|
const dir = await sandbox.resolve(url.searchParams.get("path"));
|
|
2226
|
-
const names = await
|
|
2227
|
-
const
|
|
2228
|
-
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 };
|
|
2229
3561
|
},
|
|
2230
3562
|
[Routes.fsStat]: async (url) => {
|
|
2231
3563
|
const p = await sandbox.resolve(url.searchParams.get("path"));
|
|
2232
|
-
return { path: p, ...await describe(p,
|
|
3564
|
+
return { path: p, ...await describe(p, path10.basename(p)) };
|
|
2233
3565
|
},
|
|
2234
3566
|
[Routes.fsRead]: async (url, req, res) => {
|
|
2235
3567
|
const p = await sandbox.resolve(url.searchParams.get("path"));
|
|
2236
|
-
const stat = await
|
|
3568
|
+
const stat = await fs13.stat(p);
|
|
2237
3569
|
if (!stat.isFile()) throw new HttpError(400, "not a file");
|
|
2238
|
-
|
|
2239
|
-
|
|
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);
|
|
2240
3589
|
},
|
|
2241
3590
|
[Routes.fsWrite]: async (url, req) => {
|
|
2242
3591
|
const p = await sandbox.resolveChild(url.searchParams.get("path"), { allowMissing: true });
|
|
2243
3592
|
const tmp = `${p}.vsremote-${process.pid}-${Date.now()}.tmp`;
|
|
2244
3593
|
try {
|
|
2245
|
-
await
|
|
2246
|
-
await
|
|
3594
|
+
await pipeline3(req, createWriteStream2(tmp));
|
|
3595
|
+
await fs13.rename(tmp, p);
|
|
2247
3596
|
} catch (e) {
|
|
2248
|
-
await
|
|
3597
|
+
await fs13.rm(tmp, { force: true });
|
|
2249
3598
|
throw e;
|
|
2250
3599
|
}
|
|
2251
|
-
return describe(p,
|
|
3600
|
+
return describe(p, path10.basename(p));
|
|
2252
3601
|
},
|
|
2253
3602
|
[Routes.fsMkdir]: async (url) => {
|
|
2254
|
-
await
|
|
3603
|
+
await fs13.mkdir(await sandbox.resolve(url.searchParams.get("path"), { allowMissing: true }), { recursive: true });
|
|
2255
3604
|
return {};
|
|
2256
3605
|
},
|
|
2257
3606
|
[Routes.fsRename]: async (url, req) => {
|
|
2258
|
-
const { from, to } = await
|
|
2259
|
-
await
|
|
3607
|
+
const { from, to } = await readJson2(req);
|
|
3608
|
+
await fs13.rename(await sandbox.resolveChild(from), await sandbox.resolveChild(to, { allowMissing: true }));
|
|
2260
3609
|
return {};
|
|
2261
3610
|
},
|
|
2262
3611
|
[Routes.fsDelete]: async (url) => {
|
|
2263
|
-
await
|
|
3612
|
+
await fs13.rm(await sandbox.resolveChild(url.searchParams.get("path")), { recursive: true });
|
|
2264
3613
|
return {};
|
|
2265
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
|
+
},
|
|
2266
3636
|
[Routes.gitRepos]: async () => ({ repos: await findRepos(sandbox.root) }),
|
|
2267
3637
|
[Routes.gitStatus]: async (url) => status(await repoOf(url.searchParams.get("repo"))),
|
|
2268
3638
|
[Routes.gitDiff]: async (url) => {
|
|
@@ -2274,48 +3644,48 @@ function createAgentServer({
|
|
|
2274
3644
|
});
|
|
2275
3645
|
},
|
|
2276
3646
|
[Routes.gitStage]: async (url, req) => {
|
|
2277
|
-
const { repo: r, files } = await
|
|
3647
|
+
const { repo: r, files } = await readJson2(req);
|
|
2278
3648
|
const repo = await repoOf(r);
|
|
2279
3649
|
await stage(repo, await filesIn(repo, files));
|
|
2280
3650
|
return status(repo);
|
|
2281
3651
|
},
|
|
2282
3652
|
[Routes.gitUnstage]: async (url, req) => {
|
|
2283
|
-
const { repo: r, files } = await
|
|
3653
|
+
const { repo: r, files } = await readJson2(req);
|
|
2284
3654
|
const repo = await repoOf(r);
|
|
2285
3655
|
await unstage(repo, await filesIn(repo, files));
|
|
2286
3656
|
return status(repo);
|
|
2287
3657
|
},
|
|
2288
3658
|
[Routes.gitDiscard]: async (url, req) => {
|
|
2289
|
-
const { repo: r, files } = await
|
|
3659
|
+
const { repo: r, files } = await readJson2(req);
|
|
2290
3660
|
const repo = await repoOf(r);
|
|
2291
3661
|
await discard(repo, await filesIn(repo, files));
|
|
2292
3662
|
return status(repo);
|
|
2293
3663
|
},
|
|
2294
3664
|
[Routes.gitCommit]: async (url, req) => {
|
|
2295
|
-
const { repo: r, message } = await
|
|
3665
|
+
const { repo: r, message } = await readJson2(req);
|
|
2296
3666
|
return commit(await repoOf(r), message);
|
|
2297
3667
|
},
|
|
2298
3668
|
[Routes.gitLog]: async (url) => ({
|
|
2299
3669
|
commits: await log(await repoOf(url.searchParams.get("repo")), parseInt(url.searchParams.get("limit"), 10) || 50)
|
|
2300
3670
|
}),
|
|
2301
3671
|
[Routes.gitShow]: async (url) => show(await repoOf(url.searchParams.get("repo")), url.searchParams.get("commit")),
|
|
2302
|
-
[Routes.gitPull]: async (url, req) => ({ output: await pull(await repoOf((await
|
|
2303
|
-
[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)) }),
|
|
2304
3674
|
[Routes.termList]: async () => ({ sessions: sessions.list() }),
|
|
2305
3675
|
[Routes.portsList]: async () => ({ ports: await listPorts(shellPids()) }),
|
|
2306
3676
|
[Routes.portsKill]: async (url, req) => {
|
|
2307
|
-
const { pid, force } = await
|
|
3677
|
+
const { pid, force } = await readJson2(req);
|
|
2308
3678
|
const ports = await killListener(pid, { force: force === true, shells: shellPids() });
|
|
2309
|
-
if (
|
|
3679
|
+
if (preview2) setTimeout(() => sweepPreviews(ports, true).catch(() => {
|
|
2310
3680
|
}), killedMs).unref();
|
|
2311
3681
|
return {};
|
|
2312
3682
|
},
|
|
2313
3683
|
[Routes.previewList]: async () => {
|
|
2314
|
-
if (!
|
|
3684
|
+
if (!preview2) throw new HttpError(404, "previews need a tunnel");
|
|
2315
3685
|
return previews();
|
|
2316
3686
|
},
|
|
2317
3687
|
[Routes.previewOpen]: async (url, req) => {
|
|
2318
|
-
const port2 = previewPort((await
|
|
3688
|
+
const port2 = previewPort((await readJson2(req)).port, server.address()?.port);
|
|
2319
3689
|
let proxy = proxies.get(port2);
|
|
2320
3690
|
if (!proxy) {
|
|
2321
3691
|
proxy = new InspectingProxy(port2);
|
|
@@ -2323,7 +3693,7 @@ function createAgentServer({
|
|
|
2323
3693
|
proxies.set(port2, proxy);
|
|
2324
3694
|
}
|
|
2325
3695
|
try {
|
|
2326
|
-
await
|
|
3696
|
+
await preview2.forward(proxy.port);
|
|
2327
3697
|
} catch (e) {
|
|
2328
3698
|
throw new HttpError(502, e.message);
|
|
2329
3699
|
}
|
|
@@ -2331,25 +3701,34 @@ function createAgentServer({
|
|
|
2331
3701
|
},
|
|
2332
3702
|
[Routes.previewClose]: async (url) => {
|
|
2333
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
|
+
}
|
|
2334
3708
|
const proxy = proxies.get(port2);
|
|
2335
|
-
if (proxy) await
|
|
3709
|
+
if (proxy) await preview2.unforward(proxy.port);
|
|
2336
3710
|
return previews();
|
|
2337
3711
|
},
|
|
2338
3712
|
[Routes.previewShare]: async (url, req) => {
|
|
2339
|
-
const { port: value, hours } = await
|
|
3713
|
+
const { port: value, hours } = await readJson2(req);
|
|
2340
3714
|
const proxy = previewed(previewPort(value, server.address()?.port));
|
|
2341
3715
|
if (!Number.isInteger(hours) || hours < 1 || hours > 720) throw new HttpError(400, "hours must be 1-720");
|
|
2342
3716
|
try {
|
|
2343
|
-
await
|
|
3717
|
+
await preview2.share(proxy.port, hours);
|
|
2344
3718
|
} catch (e) {
|
|
2345
3719
|
throw new HttpError(502, e.message);
|
|
2346
3720
|
}
|
|
2347
3721
|
return previews();
|
|
2348
3722
|
},
|
|
2349
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
|
+
}
|
|
2350
3729
|
const proxy = proxies.get(previewPort(url.searchParams.get("port"), server.address()?.port));
|
|
2351
3730
|
try {
|
|
2352
|
-
if (proxy) await
|
|
3731
|
+
if (proxy) await preview2.unshare(proxy.port);
|
|
2353
3732
|
} catch (e) {
|
|
2354
3733
|
throw new HttpError(502, e.message);
|
|
2355
3734
|
}
|
|
@@ -2359,9 +3738,9 @@ function createAgentServer({
|
|
|
2359
3738
|
requests: inspected(url.searchParams.get("port")).list(parseInt(url.searchParams.get("after"), 10) || 0)
|
|
2360
3739
|
}),
|
|
2361
3740
|
[Routes.previewRequest]: async (url) => {
|
|
2362
|
-
const
|
|
2363
|
-
if (!
|
|
2364
|
-
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);
|
|
2365
3744
|
},
|
|
2366
3745
|
[Routes.previewRequestsClear]: async (url) => {
|
|
2367
3746
|
inspected(url.searchParams.get("port")).clear();
|
|
@@ -2372,6 +3751,11 @@ function createAgentServer({
|
|
|
2372
3751
|
if (!status2) throw new HttpError(404, "this agent runs without a tunnel");
|
|
2373
3752
|
return status2;
|
|
2374
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
|
+
},
|
|
2375
3759
|
[Routes.tunnelToken]: async () => {
|
|
2376
3760
|
const issued = await handoff.issue();
|
|
2377
3761
|
if (!issued) throw new HttpError(404, "this tunnel is anonymous");
|
|
@@ -2384,8 +3768,43 @@ function createAgentServer({
|
|
|
2384
3768
|
const [plugin, codexPlugin] = await Promise.all([claude && hasPlugin("claude"), codex && hasPlugin("codex")]);
|
|
2385
3769
|
return { claude, plugin: Boolean(plugin), codex, codexPlugin: Boolean(codexPlugin), codexModels: codex ? await codexModels() : [] };
|
|
2386
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
|
+
},
|
|
2387
3806
|
[Routes.agentsLaunch]: async (url, req) => {
|
|
2388
|
-
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);
|
|
2389
3808
|
if (approvalPolicy !== void 0 && !["on-request", "never"].includes(approvalPolicy)) {
|
|
2390
3809
|
throw new HttpError(400, "approvalPolicy must be on-request or never");
|
|
2391
3810
|
}
|
|
@@ -2397,7 +3816,7 @@ function createAgentServer({
|
|
|
2397
3816
|
} catch (e) {
|
|
2398
3817
|
throw new HttpError(400, e.message);
|
|
2399
3818
|
}
|
|
2400
|
-
if (!(await
|
|
3819
|
+
if (!(await fs13.stat(dir)).isDirectory()) throw new HttpError(400, "cwd must be a folder");
|
|
2401
3820
|
if (model !== void 0 && (typeof model !== "string" || !/^[A-Za-z0-9._[\]-]{1,80}$/.test(model))) {
|
|
2402
3821
|
throw new HttpError(400, "invalid model");
|
|
2403
3822
|
}
|
|
@@ -2418,8 +3837,8 @@ function createAgentServer({
|
|
|
2418
3837
|
if (!await hasPlugin(agent)) {
|
|
2419
3838
|
throw Object.assign(new HttpError(400, launcher.missingPlugin), { code: "ENOPLUGIN" });
|
|
2420
3839
|
}
|
|
2421
|
-
const id = resume ??
|
|
2422
|
-
if (uploadsDir) await
|
|
3840
|
+
const id = resume ?? randomUUID5();
|
|
3841
|
+
if (uploadsDir) await fs13.mkdir(uploadsDir, { recursive: true, mode: 448 });
|
|
2423
3842
|
const argv2 = [
|
|
2424
3843
|
launcher.command,
|
|
2425
3844
|
...launcher.args({
|
|
@@ -2440,14 +3859,14 @@ function createAgentServer({
|
|
|
2440
3859
|
return { terminal: term.id, session: agents.launched({ id, terminal: term.id, cwd: dir, agent }) };
|
|
2441
3860
|
},
|
|
2442
3861
|
[Routes.agentsPrompt]: async (url, req) => {
|
|
2443
|
-
const { session: id, text, attachments } = await
|
|
3862
|
+
const { session: id, text, attachments } = await readJson2(req);
|
|
2444
3863
|
if (typeof text !== "string" || !text.trim() || text.length > 1e5) {
|
|
2445
3864
|
throw new HttpError(400, "text must be a non-empty string");
|
|
2446
3865
|
}
|
|
2447
3866
|
const { term } = typingTarget(id, { whileWaiting: false });
|
|
2448
3867
|
const paths = await attachmentPaths(attachments);
|
|
2449
3868
|
const typed = typeable([text.trim(), ...paths].join("\n"));
|
|
2450
|
-
const prompt =
|
|
3869
|
+
const prompt = randomUUID5();
|
|
2451
3870
|
agents.prompted(id, { id: prompt, text: typed, paths });
|
|
2452
3871
|
await typeInto(term, typed);
|
|
2453
3872
|
return { prompt };
|
|
@@ -2467,23 +3886,30 @@ function createAgentServer({
|
|
|
2467
3886
|
return { id, name: saved, type, size };
|
|
2468
3887
|
},
|
|
2469
3888
|
[Routes.agentsKeys]: async (url, req) => {
|
|
2470
|
-
const { session: id, action, command } = await
|
|
3889
|
+
const { session: id, action, command, args } = await readJson2(req);
|
|
2471
3890
|
const allowed = agents.session(typeof id === "string" ? id : "")?.capabilities?.keys;
|
|
2472
|
-
if (
|
|
3891
|
+
if (action !== "command" && allowed && !allowed.includes(action)) {
|
|
2473
3892
|
throw new HttpError(400, `this session takes ${allowed.join(", ")}`);
|
|
2474
3893
|
}
|
|
2475
3894
|
if (action === "command") {
|
|
2476
|
-
if (
|
|
2477
|
-
|
|
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));
|
|
2478
3903
|
} else if (Object.hasOwn(KEYS, action)) {
|
|
2479
3904
|
typingTarget(id, { whileWaiting: true }).term.write(KEYS[action]);
|
|
3905
|
+
if (action === "interrupt") agents.interrupted(id);
|
|
2480
3906
|
} else {
|
|
2481
3907
|
throw new HttpError(400, `action must be one of ${[...Object.keys(KEYS), "command"].join(", ")}`);
|
|
2482
3908
|
}
|
|
2483
3909
|
return {};
|
|
2484
3910
|
},
|
|
2485
3911
|
[Routes.agentsStop]: async (url, req) => {
|
|
2486
|
-
const { session: id } = await
|
|
3912
|
+
const { session: id } = await readJson2(req);
|
|
2487
3913
|
const { term } = typingTarget(id, { whileWaiting: true });
|
|
2488
3914
|
term.kill();
|
|
2489
3915
|
setTimeout(() => {
|
|
@@ -2492,7 +3918,7 @@ function createAgentServer({
|
|
|
2492
3918
|
return {};
|
|
2493
3919
|
},
|
|
2494
3920
|
[Routes.agentsForget]: async (url, req) => {
|
|
2495
|
-
const { session: id } = await
|
|
3921
|
+
const { session: id } = await readJson2(req);
|
|
2496
3922
|
const session = typeof id === "string" ? agents.session(id) : null;
|
|
2497
3923
|
if (!session) throw new HttpError(404, "no such session");
|
|
2498
3924
|
if (session.state !== "ended" && session.terminal && sessions.get(session.terminal)) {
|
|
@@ -2502,11 +3928,11 @@ function createAgentServer({
|
|
|
2502
3928
|
return {};
|
|
2503
3929
|
},
|
|
2504
3930
|
[Routes.agentsAnswer]: async (url, req) => {
|
|
2505
|
-
agents.answer(await
|
|
3931
|
+
agents.answer(await readJson2(req));
|
|
2506
3932
|
return {};
|
|
2507
3933
|
},
|
|
2508
3934
|
[Routes.pushDeviceSet]: async (url, req) => {
|
|
2509
|
-
push2.register(await
|
|
3935
|
+
push2.register(await readJson2(req));
|
|
2510
3936
|
return {};
|
|
2511
3937
|
},
|
|
2512
3938
|
[Routes.pushDeviceDelete]: async (url) => {
|
|
@@ -2514,7 +3940,7 @@ function createAgentServer({
|
|
|
2514
3940
|
return {};
|
|
2515
3941
|
},
|
|
2516
3942
|
[Routes.pushTest]: async (url, req) => {
|
|
2517
|
-
await push2.test((await
|
|
3943
|
+
await push2.test((await readJson2(req)).token);
|
|
2518
3944
|
return {};
|
|
2519
3945
|
},
|
|
2520
3946
|
[Routes.termKill]: async (url) => {
|
|
@@ -2524,7 +3950,7 @@ function createAgentServer({
|
|
|
2524
3950
|
return {};
|
|
2525
3951
|
}
|
|
2526
3952
|
};
|
|
2527
|
-
const server =
|
|
3953
|
+
const server = http3.createServer(async (req, res) => {
|
|
2528
3954
|
const url = new URL(req.url, "http://localhost");
|
|
2529
3955
|
if (req.method === "GET" && url.pathname === "/") {
|
|
2530
3956
|
res.writeHead(200, {
|
|
@@ -2552,14 +3978,16 @@ function createAgentServer({
|
|
|
2552
3978
|
if (!hookAuthorized(req)) return sendJson(res, 401, { error: "unauthorized" });
|
|
2553
3979
|
plugins[url.searchParams.get("agent") || "claude"] = { installed: true, checkedAt: Date.now() };
|
|
2554
3980
|
const terminal = url.searchParams.get("terminal");
|
|
3981
|
+
const app = url.searchParams.get("app");
|
|
2555
3982
|
const closed = new AbortController();
|
|
2556
3983
|
res.on("close", () => {
|
|
2557
3984
|
if (!res.writableFinished) closed.abort();
|
|
2558
3985
|
});
|
|
2559
3986
|
try {
|
|
2560
|
-
const output = await agents.handle(await
|
|
3987
|
+
const output = await agents.handle(await readJson2(req), {
|
|
2561
3988
|
agent: url.searchParams.get("agent") || "claude",
|
|
2562
3989
|
terminal: terminal && sessions.get(terminal) ? terminal : null,
|
|
3990
|
+
app: app && app.length <= 1024 && /\.app$/.test(app) ? app : null,
|
|
2563
3991
|
signal: closed.signal
|
|
2564
3992
|
});
|
|
2565
3993
|
if (res.destroyed) return;
|
|
@@ -2638,11 +4066,22 @@ function createAgentServer({
|
|
|
2638
4066
|
} catch {
|
|
2639
4067
|
return;
|
|
2640
4068
|
}
|
|
2641
|
-
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);
|
|
2642
4081
|
});
|
|
2643
|
-
ws.on("close", () => agents.off("message", send));
|
|
2644
4082
|
}
|
|
2645
4083
|
server.on("close", () => {
|
|
4084
|
+
installs?.close();
|
|
2646
4085
|
clearInterval(sweep);
|
|
2647
4086
|
clearInterval(previewTimer);
|
|
2648
4087
|
sessions.killAll();
|
|
@@ -2651,18 +4090,19 @@ function createAgentServer({
|
|
|
2651
4090
|
server.killAllSessions = () => sessions.killAll();
|
|
2652
4091
|
server.agents = agents;
|
|
2653
4092
|
server.clientInForeground = clientInForeground;
|
|
4093
|
+
server.clientShows = clientShows;
|
|
2654
4094
|
server.push = push2;
|
|
2655
4095
|
server.agentsUploadPath = (id) => uploadPath(id);
|
|
2656
4096
|
return server;
|
|
2657
4097
|
}
|
|
2658
4098
|
|
|
2659
4099
|
// src/devtunnel.js
|
|
2660
|
-
import { execFile as
|
|
4100
|
+
import { execFile as execFile9 } from "node:child_process";
|
|
2661
4101
|
import { createHash } from "node:crypto";
|
|
2662
|
-
import
|
|
2663
|
-
import
|
|
4102
|
+
import fs14 from "node:fs";
|
|
4103
|
+
import path11 from "node:path";
|
|
2664
4104
|
import { promisify as promisify3 } from "node:util";
|
|
2665
|
-
var
|
|
4105
|
+
var run5 = promisify3(execFile9);
|
|
2666
4106
|
var DOWNLOAD_BASE = "https://tunnelsassetsprod.blob.core.windows.net/cli";
|
|
2667
4107
|
var BUILDS = {
|
|
2668
4108
|
"darwin-arm64": "osx-arm64-devtunnel",
|
|
@@ -2691,11 +4131,11 @@ async function ensureDevtunnel({ dir, progress }) {
|
|
|
2691
4131
|
`No devtunnel build for ${process.platform}-${process.arch}. Install it yourself: https://learn.microsoft.com/azure/developer/dev-tunnels/get-started`
|
|
2692
4132
|
);
|
|
2693
4133
|
}
|
|
2694
|
-
const bin =
|
|
2695
|
-
const metaFile =
|
|
4134
|
+
const bin = path11.join(dir, isWindows ? "devtunnel.exe" : "devtunnel");
|
|
4135
|
+
const metaFile = path11.join(dir, "devtunnel.json");
|
|
2696
4136
|
let meta = {};
|
|
2697
4137
|
try {
|
|
2698
|
-
meta = JSON.parse(
|
|
4138
|
+
meta = JSON.parse(fs14.readFileSync(metaFile, "utf8"));
|
|
2699
4139
|
} catch {
|
|
2700
4140
|
}
|
|
2701
4141
|
const installed = findExecutable(bin) !== null;
|
|
@@ -2706,7 +4146,7 @@ async function ensureDevtunnel({ dir, progress }) {
|
|
|
2706
4146
|
const head = await fetch(url, { method: "HEAD", signal: AbortSignal.timeout(3e3) });
|
|
2707
4147
|
const lastModified = head.headers.get("last-modified");
|
|
2708
4148
|
if (head.ok && lastModified && lastModified === meta.lastModified) {
|
|
2709
|
-
|
|
4149
|
+
fs14.writeFileSync(metaFile, JSON.stringify({ ...meta, checkedAt: Date.now() }));
|
|
2710
4150
|
return bin;
|
|
2711
4151
|
}
|
|
2712
4152
|
} catch {
|
|
@@ -2721,12 +4161,12 @@ async function ensureDevtunnel({ dir, progress }) {
|
|
|
2721
4161
|
return bin;
|
|
2722
4162
|
}
|
|
2723
4163
|
async function download(url, bin, metaFile, progress) {
|
|
2724
|
-
|
|
4164
|
+
fs14.mkdirSync(path11.dirname(bin), { recursive: true, mode: 448 });
|
|
2725
4165
|
const res = await fetch(url, { signal: AbortSignal.timeout(12e4) });
|
|
2726
4166
|
if (!res.ok || !res.body) throw new Error(`Downloading devtunnel failed: HTTP ${res.status} from ${url}`);
|
|
2727
4167
|
const total = Number(res.headers.get("content-length")) || 0;
|
|
2728
4168
|
const tmp = `${bin}.${process.pid}.download`;
|
|
2729
|
-
const out =
|
|
4169
|
+
const out = fs14.createWriteStream(tmp, { mode: 493 });
|
|
2730
4170
|
const md5 = createHash("md5");
|
|
2731
4171
|
let received = 0;
|
|
2732
4172
|
try {
|
|
@@ -2739,22 +4179,22 @@ async function download(url, bin, metaFile, progress) {
|
|
|
2739
4179
|
await new Promise((resolve, reject) => out.end((e) => e ? reject(e) : resolve()));
|
|
2740
4180
|
if (total && received !== total) throw new Error(`devtunnel download was cut short (${received} of ${total} bytes)`);
|
|
2741
4181
|
await verify(tmp, md5.digest("base64"), res.headers.get("content-md5"));
|
|
2742
|
-
|
|
2743
|
-
|
|
4182
|
+
fs14.chmodSync(tmp, 493);
|
|
4183
|
+
fs14.renameSync(tmp, bin);
|
|
2744
4184
|
} catch (e) {
|
|
2745
4185
|
out.destroy();
|
|
2746
|
-
|
|
4186
|
+
fs14.rmSync(tmp, { force: true });
|
|
2747
4187
|
throw e;
|
|
2748
4188
|
}
|
|
2749
|
-
|
|
4189
|
+
fs14.writeFileSync(metaFile, JSON.stringify({ lastModified: res.headers.get("last-modified"), checkedAt: Date.now() }));
|
|
2750
4190
|
}
|
|
2751
4191
|
async function verify(file, md5, expectedMd5) {
|
|
2752
4192
|
if (expectedMd5 && md5 !== expectedMd5) throw new Error("devtunnel download is corrupt (MD5 mismatch)");
|
|
2753
4193
|
if (isWindows) return verifyAuthenticode(file);
|
|
2754
4194
|
if (process.platform !== "darwin") return;
|
|
2755
4195
|
try {
|
|
2756
|
-
await
|
|
2757
|
-
const { stderr } = await
|
|
4196
|
+
await run5("codesign", ["--verify", "--strict", file]);
|
|
4197
|
+
const { stderr } = await run5("codesign", ["-dv", file]);
|
|
2758
4198
|
if (!stderr.includes(`TeamIdentifier=${MICROSOFT_TEAM_ID}`)) throw new Error("not signed by Microsoft");
|
|
2759
4199
|
} catch (e) {
|
|
2760
4200
|
throw new Error(`devtunnel download failed its signature check: ${e.message}`);
|
|
@@ -2765,7 +4205,7 @@ async function verifyAuthenticode(file) {
|
|
|
2765
4205
|
let status2;
|
|
2766
4206
|
let signer;
|
|
2767
4207
|
try {
|
|
2768
|
-
const { stdout } = await
|
|
4208
|
+
const { stdout } = await run5("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], {
|
|
2769
4209
|
env: { ...process.env, CODEREMOTE_VERIFY: file }
|
|
2770
4210
|
});
|
|
2771
4211
|
[status2, signer] = stdout.trim().split("|");
|
|
@@ -2778,51 +4218,52 @@ async function verifyAuthenticode(file) {
|
|
|
2778
4218
|
}
|
|
2779
4219
|
|
|
2780
4220
|
// src/hook.js
|
|
2781
|
-
import {
|
|
2782
|
-
import
|
|
2783
|
-
import
|
|
2784
|
-
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";
|
|
2785
4226
|
var CONNECT_TIMEOUT = 1e3;
|
|
2786
4227
|
var EVENT_TIMEOUT = 5e3;
|
|
2787
4228
|
function ensureHookSecret(dir) {
|
|
2788
|
-
const file =
|
|
4229
|
+
const file = path12.join(dir, "hook-secret");
|
|
2789
4230
|
try {
|
|
2790
|
-
const secret2 =
|
|
4231
|
+
const secret2 = fs15.readFileSync(file, "utf8").trim();
|
|
2791
4232
|
if (secret2) return secret2;
|
|
2792
4233
|
} catch {
|
|
2793
4234
|
}
|
|
2794
|
-
|
|
2795
|
-
const secret =
|
|
2796
|
-
|
|
4235
|
+
fs15.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
4236
|
+
const secret = randomBytes3(32).toString("base64url");
|
|
4237
|
+
fs15.writeFileSync(file, secret, { mode: 384 });
|
|
2797
4238
|
return secret;
|
|
2798
4239
|
}
|
|
2799
4240
|
function writeHookLauncher(dir, { node = process.execPath, script }) {
|
|
2800
|
-
const bin =
|
|
2801
|
-
|
|
4241
|
+
const bin = path12.join(dir, "bin");
|
|
4242
|
+
fs15.mkdirSync(bin, { recursive: true, mode: 448 });
|
|
2802
4243
|
const quote = (s) => `'${String(s).replace(/'/g, `'\\''`)}'`;
|
|
2803
|
-
const file =
|
|
2804
|
-
|
|
4244
|
+
const file = path12.join(bin, "hook");
|
|
4245
|
+
fs15.writeFileSync(file, `#!/bin/sh
|
|
2805
4246
|
exec ${quote(node)} ${quote(script)} hook "$@"
|
|
2806
4247
|
`, { mode: 493 });
|
|
2807
|
-
|
|
2808
|
-
|
|
4248
|
+
fs15.chmodSync(file, 493);
|
|
4249
|
+
fs15.writeFileSync(path12.join(bin, "hook.json"), JSON.stringify({ node, script }), { mode: 384 });
|
|
2809
4250
|
return file;
|
|
2810
4251
|
}
|
|
2811
4252
|
function readHookSecret(dir) {
|
|
2812
4253
|
try {
|
|
2813
|
-
return
|
|
4254
|
+
return fs15.readFileSync(path12.join(dir, "hook-secret"), "utf8").trim() || null;
|
|
2814
4255
|
} catch {
|
|
2815
4256
|
return null;
|
|
2816
4257
|
}
|
|
2817
4258
|
}
|
|
2818
4259
|
function real(p) {
|
|
2819
4260
|
try {
|
|
2820
|
-
return
|
|
4261
|
+
return fs15.realpathSync.native(p);
|
|
2821
4262
|
} catch {
|
|
2822
|
-
return
|
|
4263
|
+
return path12.resolve(p);
|
|
2823
4264
|
}
|
|
2824
4265
|
}
|
|
2825
|
-
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);
|
|
2826
4267
|
function hookTarget(agents, cwd, env = process.env) {
|
|
2827
4268
|
const port2 = parseInt(env.CODEREMOTE_AGENT_PORT, 10);
|
|
2828
4269
|
if (port2 && agents.some((a) => a.port === port2)) return { port: port2, terminal: env.CODEREMOTE_TERM_ID || null };
|
|
@@ -2831,23 +4272,46 @@ function hookTarget(agents, cwd, env = process.env) {
|
|
|
2831
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];
|
|
2832
4273
|
return match ? { port: match.port, terminal: null } : null;
|
|
2833
4274
|
}
|
|
2834
|
-
|
|
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 }) {
|
|
2835
4298
|
return new Promise((resolve) => {
|
|
2836
4299
|
const query = new URLSearchParams({ agent });
|
|
2837
4300
|
if (terminal) query.set("terminal", terminal);
|
|
2838
|
-
|
|
4301
|
+
if (app) query.set("app", app);
|
|
4302
|
+
const req = http4.request({
|
|
2839
4303
|
host: "127.0.0.1",
|
|
2840
4304
|
port: port2,
|
|
2841
4305
|
method: "POST",
|
|
2842
4306
|
path: `/api/agents/hook?${query}`,
|
|
2843
4307
|
headers: { "content-type": "application/json", "content-length": Buffer.byteLength(body), [HOOK_SECRET_HEADER]: secret }
|
|
2844
4308
|
});
|
|
2845
|
-
const
|
|
4309
|
+
const fail2 = () => {
|
|
2846
4310
|
req.destroy();
|
|
2847
4311
|
resolve("");
|
|
2848
4312
|
};
|
|
2849
|
-
const connect = setTimeout(
|
|
2850
|
-
const overall = wait ? null : setTimeout(
|
|
4313
|
+
const connect = setTimeout(fail2, CONNECT_TIMEOUT);
|
|
4314
|
+
const overall = wait ? null : setTimeout(fail2, EVENT_TIMEOUT);
|
|
2851
4315
|
req.on("socket", (socket) => socket.once("connect", () => clearTimeout(connect)));
|
|
2852
4316
|
req.on("error", () => resolve(""));
|
|
2853
4317
|
req.on("response", (res) => {
|
|
@@ -2862,7 +4326,7 @@ function deliverHook({ port: port2, terminal, agent, secret, body, wait }) {
|
|
|
2862
4326
|
req.end(body);
|
|
2863
4327
|
});
|
|
2864
4328
|
}
|
|
2865
|
-
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 }) {
|
|
2866
4330
|
if (!secret) return "";
|
|
2867
4331
|
let event;
|
|
2868
4332
|
try {
|
|
@@ -2873,16 +4337,23 @@ async function runHook({ input, agents, secret, agent = "claude", env = process.
|
|
|
2873
4337
|
const target = hookTarget(agents, event?.cwd, env);
|
|
2874
4338
|
if (!target) return "";
|
|
2875
4339
|
const wait = event.hook_event_name === "PermissionRequest";
|
|
2876
|
-
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 });
|
|
2877
4342
|
return wait ? out.trim() : "";
|
|
2878
4343
|
}
|
|
2879
4344
|
|
|
2880
4345
|
// src/preview.js
|
|
2881
|
-
import { spawn as
|
|
4346
|
+
import { spawn as spawn4 } from "node:child_process";
|
|
2882
4347
|
|
|
2883
4348
|
// src/tunnels.js
|
|
2884
4349
|
var TUNNEL_LIMIT = 10;
|
|
2885
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
|
+
}
|
|
2886
4357
|
var base = (id) => String(id).split(".")[0];
|
|
2887
4358
|
async function listTunnels(devtunnel2) {
|
|
2888
4359
|
const res = await devtunnel2("list", "--limit", "100", "--json");
|
|
@@ -3014,8 +4485,8 @@ var PreviewTunnel = class {
|
|
|
3014
4485
|
"--json"
|
|
3015
4486
|
);
|
|
3016
4487
|
if (!res.ok) throw new Error(`devtunnel access create failed: ${res.out.trim()}`);
|
|
3017
|
-
const
|
|
3018
|
-
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);
|
|
3019
4490
|
this.#shares.set(port2, Number.isFinite(until) ? until : Date.now() + hours * 36e5);
|
|
3020
4491
|
});
|
|
3021
4492
|
}
|
|
@@ -3125,7 +4596,7 @@ ${created.out}`);
|
|
|
3125
4596
|
}
|
|
3126
4597
|
#startHost() {
|
|
3127
4598
|
return new Promise((resolve, reject) => {
|
|
3128
|
-
const child =
|
|
4599
|
+
const child = spawn4(this.command(), ["host", this.#tunnelId], { stdio: ["ignore", "pipe", "pipe"] });
|
|
3129
4600
|
const urls = /* @__PURE__ */ new Map();
|
|
3130
4601
|
let log2 = "";
|
|
3131
4602
|
const timer = setTimeout(() => {
|
|
@@ -3160,40 +4631,40 @@ ${log2}`));
|
|
|
3160
4631
|
};
|
|
3161
4632
|
|
|
3162
4633
|
// src/push-store.js
|
|
3163
|
-
import
|
|
3164
|
-
import
|
|
4634
|
+
import fs16 from "node:fs";
|
|
4635
|
+
import path13 from "node:path";
|
|
3165
4636
|
function pushStore(dir, port2) {
|
|
3166
|
-
const file =
|
|
4637
|
+
const file = path13.join(dir, `${port2}.json`);
|
|
3167
4638
|
return {
|
|
3168
4639
|
load() {
|
|
3169
4640
|
try {
|
|
3170
|
-
const devices = JSON.parse(
|
|
4641
|
+
const devices = JSON.parse(fs16.readFileSync(file, "utf8"));
|
|
3171
4642
|
return Array.isArray(devices) ? devices : [];
|
|
3172
4643
|
} catch {
|
|
3173
4644
|
return [];
|
|
3174
4645
|
}
|
|
3175
4646
|
},
|
|
3176
4647
|
save(devices) {
|
|
3177
|
-
|
|
3178
|
-
|
|
4648
|
+
fs16.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
4649
|
+
fs16.writeFileSync(file, JSON.stringify(devices, null, 2), { mode: 384 });
|
|
3179
4650
|
}
|
|
3180
4651
|
};
|
|
3181
4652
|
}
|
|
3182
4653
|
|
|
3183
4654
|
// src/registry.js
|
|
3184
|
-
import
|
|
3185
|
-
import
|
|
4655
|
+
import fs17 from "node:fs";
|
|
4656
|
+
import path14 from "node:path";
|
|
3186
4657
|
var AgentRegistry = class {
|
|
3187
4658
|
constructor(dir) {
|
|
3188
4659
|
this.dir = dir;
|
|
3189
4660
|
}
|
|
3190
4661
|
register({ port: port2, root: root2, url }) {
|
|
3191
|
-
|
|
4662
|
+
fs17.mkdirSync(this.dir, { recursive: true, mode: 448 });
|
|
3192
4663
|
const file = this.#file(port2);
|
|
3193
|
-
|
|
4664
|
+
fs17.writeFileSync(file, JSON.stringify({ pid: process.pid, port: port2, root: root2, url, startedAt: Date.now() }), { mode: 384 });
|
|
3194
4665
|
const cleanup = () => {
|
|
3195
4666
|
try {
|
|
3196
|
-
if (JSON.parse(
|
|
4667
|
+
if (JSON.parse(fs17.readFileSync(file, "utf8")).pid === process.pid) fs17.rmSync(file);
|
|
3197
4668
|
} catch {
|
|
3198
4669
|
}
|
|
3199
4670
|
};
|
|
@@ -3204,25 +4675,25 @@ var AgentRegistry = class {
|
|
|
3204
4675
|
list() {
|
|
3205
4676
|
let names = [];
|
|
3206
4677
|
try {
|
|
3207
|
-
names =
|
|
4678
|
+
names = fs17.readdirSync(this.dir).filter((n) => n.endsWith(".json"));
|
|
3208
4679
|
} catch {
|
|
3209
4680
|
return [];
|
|
3210
4681
|
}
|
|
3211
4682
|
const agents = [];
|
|
3212
4683
|
for (const name of names) {
|
|
3213
|
-
const file =
|
|
4684
|
+
const file = path14.join(this.dir, name);
|
|
3214
4685
|
try {
|
|
3215
|
-
const agent = JSON.parse(
|
|
4686
|
+
const agent = JSON.parse(fs17.readFileSync(file, "utf8"));
|
|
3216
4687
|
process.kill(agent.pid, 0);
|
|
3217
4688
|
agents.push(agent);
|
|
3218
4689
|
} catch {
|
|
3219
|
-
|
|
4690
|
+
fs17.rmSync(file, { force: true });
|
|
3220
4691
|
}
|
|
3221
4692
|
}
|
|
3222
4693
|
return agents.sort((a, b) => a.port - b.port);
|
|
3223
4694
|
}
|
|
3224
4695
|
#file(port2) {
|
|
3225
|
-
return
|
|
4696
|
+
return path14.join(this.dir, `${port2}.json`);
|
|
3226
4697
|
}
|
|
3227
4698
|
};
|
|
3228
4699
|
|
|
@@ -3236,6 +4707,13 @@ function parse(out) {
|
|
|
3236
4707
|
function machineName(description) {
|
|
3237
4708
|
return (description || "").replace(/^(coderemote|vsremote)\s+/, "").replace(/\.local\b/, "") || null;
|
|
3238
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
|
+
}
|
|
3239
4717
|
function machine(tunnel, current) {
|
|
3240
4718
|
return {
|
|
3241
4719
|
id: tunnel.tunnelId,
|
|
@@ -3247,12 +4725,12 @@ function machine(tunnel, current) {
|
|
|
3247
4725
|
current
|
|
3248
4726
|
};
|
|
3249
4727
|
}
|
|
3250
|
-
function tunnelStatus({ devtunnel: devtunnel2, tunnelId, account, anonymous: anonymous2, preview }) {
|
|
4728
|
+
function tunnelStatus({ devtunnel: devtunnel2, tunnelId, account, anonymous: anonymous2, preview: preview2 }) {
|
|
3251
4729
|
let cached = null;
|
|
3252
|
-
|
|
4730
|
+
const status2 = async () => {
|
|
3253
4731
|
const id = tunnelId();
|
|
3254
4732
|
if (!id) return null;
|
|
3255
|
-
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 };
|
|
3256
4734
|
const [own, listed] = await Promise.all([
|
|
3257
4735
|
devtunnel2("show", id, "--json"),
|
|
3258
4736
|
// Every tunnel, previews' too: they all count against the account's limit.
|
|
@@ -3283,17 +4761,53 @@ function tunnelStatus({ devtunnel: devtunnel2, tunnelId, account, anonymous: ano
|
|
|
3283
4761
|
downloaded: tunnel.downloadTotal ?? null
|
|
3284
4762
|
},
|
|
3285
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),
|
|
3286
4766
|
// null when the account couldn't be listed
|
|
3287
4767
|
quota: all && { used: all.length, limit: TUNNEL_LIMIT }
|
|
3288
4768
|
};
|
|
3289
4769
|
cached = { at: Date.now(), value };
|
|
3290
|
-
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 };
|
|
3291
4805
|
};
|
|
3292
4806
|
}
|
|
3293
4807
|
|
|
3294
4808
|
// src/update.js
|
|
3295
|
-
import
|
|
3296
|
-
import
|
|
4809
|
+
import fs18 from "node:fs";
|
|
4810
|
+
import path15 from "node:path";
|
|
3297
4811
|
var LATEST_URL = "https://registry.npmjs.org/coderemote/latest";
|
|
3298
4812
|
var CHECK_INTERVAL_MS = 24 * 36e5;
|
|
3299
4813
|
var SEMVER = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/;
|
|
@@ -3317,10 +4831,10 @@ async function fetchLatest({ fetch: fetch2 = globalThis.fetch, timeoutMs = 3e3 }
|
|
|
3317
4831
|
}
|
|
3318
4832
|
}
|
|
3319
4833
|
async function availableUpdate({ current, dir, fetch: fetch2, now = Date.now() }) {
|
|
3320
|
-
const file =
|
|
4834
|
+
const file = path15.join(dir, "update.json");
|
|
3321
4835
|
let cache = {};
|
|
3322
4836
|
try {
|
|
3323
|
-
cache = JSON.parse(
|
|
4837
|
+
cache = JSON.parse(fs18.readFileSync(file, "utf8"));
|
|
3324
4838
|
} catch {
|
|
3325
4839
|
}
|
|
3326
4840
|
let latest = cache.latest;
|
|
@@ -3329,8 +4843,8 @@ async function availableUpdate({ current, dir, fetch: fetch2, now = Date.now() }
|
|
|
3329
4843
|
if (fetched) {
|
|
3330
4844
|
latest = fetched;
|
|
3331
4845
|
try {
|
|
3332
|
-
|
|
3333
|
-
|
|
4846
|
+
fs18.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
4847
|
+
fs18.writeFileSync(file, JSON.stringify({ latest, checkedAt: now }));
|
|
3334
4848
|
} catch {
|
|
3335
4849
|
}
|
|
3336
4850
|
}
|
|
@@ -3338,12 +4852,12 @@ async function availableUpdate({ current, dir, fetch: fetch2, now = Date.now() }
|
|
|
3338
4852
|
return isNewer(latest, current) ? latest : null;
|
|
3339
4853
|
}
|
|
3340
4854
|
function installCommand(installedAt) {
|
|
3341
|
-
const p = installedAt.split(
|
|
4855
|
+
const p = installedAt.split(path15.sep).join("/");
|
|
3342
4856
|
const runtime = p.match(/^(.*\/runtime\/node)\/(lib\/)?node_modules\/coderemote\//);
|
|
3343
4857
|
if (runtime) {
|
|
3344
4858
|
const prefix = installedAt.slice(0, runtime[1].length);
|
|
3345
|
-
const [node, modules] = runtime[2] ? [
|
|
3346
|
-
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"]];
|
|
3347
4861
|
}
|
|
3348
4862
|
if (p.includes("/Cellar/coderemote/")) return ["brew", ["upgrade", "coderemote"]];
|
|
3349
4863
|
if (p.includes("/pnpm/") || p.includes("/.pnpm/")) return ["pnpm", ["add", "-g", "coderemote@latest"]];
|
|
@@ -3355,7 +4869,7 @@ function installCommand(installedAt) {
|
|
|
3355
4869
|
// package.json
|
|
3356
4870
|
var package_default = {
|
|
3357
4871
|
name: "coderemote",
|
|
3358
|
-
version: "0.3.
|
|
4872
|
+
version: "0.3.9",
|
|
3359
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.",
|
|
3360
4874
|
keywords: [
|
|
3361
4875
|
"remote",
|
|
@@ -3678,28 +5192,28 @@ ${log2}`));
|
|
|
3678
5192
|
}
|
|
3679
5193
|
|
|
3680
5194
|
// bin/coderemote.js
|
|
3681
|
-
var
|
|
3682
|
-
var CONFIG_DIR =
|
|
3683
|
-
var LEGACY_CONFIG_DIR =
|
|
3684
|
-
if (!
|
|
3685
|
-
var CONFIG_FILE =
|
|
3686
|
-
var PUSH_DIR =
|
|
3687
|
-
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");
|
|
3688
5202
|
var DEFAULT_PORT = 31580;
|
|
3689
5203
|
var VERSION = package_default.version;
|
|
3690
|
-
var PACKAGED =
|
|
5204
|
+
var PACKAGED = path16.basename(path16.dirname(fileURLToPath(import.meta.url))) === "dist";
|
|
3691
5205
|
var spinner = new Spinner();
|
|
3692
5206
|
function loadConfig() {
|
|
3693
5207
|
let cfg2 = {};
|
|
3694
5208
|
try {
|
|
3695
|
-
cfg2 = JSON.parse(
|
|
5209
|
+
cfg2 = JSON.parse(fs19.readFileSync(CONFIG_FILE, "utf8"));
|
|
3696
5210
|
} catch {
|
|
3697
5211
|
}
|
|
3698
5212
|
if (!cfg2.token) {
|
|
3699
|
-
cfg2.token =
|
|
5213
|
+
cfg2.token = randomBytes4(32).toString("base64url");
|
|
3700
5214
|
saveConfig(cfg2);
|
|
3701
|
-
|
|
3702
|
-
|
|
5215
|
+
fs19.rmSync(PUSH_DIR, { recursive: true, force: true });
|
|
5216
|
+
fs19.rmSync(UPLOADS_DIR, { recursive: true, force: true });
|
|
3703
5217
|
}
|
|
3704
5218
|
if (cfg2.tunnelId && !cfg2.tunnels) {
|
|
3705
5219
|
cfg2.tunnels = { [DEFAULT_PORT]: cfg2.tunnelId };
|
|
@@ -3715,8 +5229,8 @@ function stopAgent(pid) {
|
|
|
3715
5229
|
}
|
|
3716
5230
|
}
|
|
3717
5231
|
function saveConfig(cfg2) {
|
|
3718
|
-
|
|
3719
|
-
|
|
5232
|
+
fs19.mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
5233
|
+
fs19.writeFileSync(CONFIG_FILE, JSON.stringify(cfg2, null, 2), { mode: 384 });
|
|
3720
5234
|
}
|
|
3721
5235
|
function portFree(port2) {
|
|
3722
5236
|
return new Promise((resolve) => {
|
|
@@ -3725,20 +5239,22 @@ function portFree(port2) {
|
|
|
3725
5239
|
probe.listen(port2, "127.0.0.1", () => probe.close(() => resolve(true)));
|
|
3726
5240
|
});
|
|
3727
5241
|
}
|
|
3728
|
-
function listen(token, port2, root2, tunnelToken,
|
|
5242
|
+
function listen(token, port2, root2, tunnelToken, preview2, status2, remover) {
|
|
3729
5243
|
const hookSecret = ensureHookSecret(CONFIG_DIR);
|
|
3730
|
-
writeHookLauncher(CONFIG_DIR, { script:
|
|
5244
|
+
writeHookLauncher(CONFIG_DIR, { script: fs19.realpathSync(process.argv[1]) });
|
|
3731
5245
|
const server = createAgentServer({
|
|
3732
5246
|
token,
|
|
3733
5247
|
root: root2,
|
|
3734
5248
|
tunnelToken,
|
|
3735
|
-
preview,
|
|
5249
|
+
preview: preview2,
|
|
3736
5250
|
tunnelStatus: status2,
|
|
5251
|
+
tunnelDelete: remover,
|
|
3737
5252
|
hookSecret,
|
|
3738
5253
|
// This very CLI, so coding agents in the app's terminals report through the same version.
|
|
3739
|
-
hookBin:
|
|
5254
|
+
hookBin: fs19.realpathSync(process.argv[1]),
|
|
3740
5255
|
pushStore: pushStore(PUSH_DIR, port2),
|
|
3741
|
-
uploadsDir: UPLOADS_DIR
|
|
5256
|
+
uploadsDir: UPLOADS_DIR,
|
|
5257
|
+
version: VERSION
|
|
3742
5258
|
});
|
|
3743
5259
|
return new Promise((resolve, reject) => {
|
|
3744
5260
|
server.once("error", reject);
|
|
@@ -3749,7 +5265,7 @@ var DEVTUNNEL = "devtunnel";
|
|
|
3749
5265
|
async function resolveDevtunnel() {
|
|
3750
5266
|
let installing = false;
|
|
3751
5267
|
DEVTUNNEL = await ensureDevtunnel({
|
|
3752
|
-
dir:
|
|
5268
|
+
dir: path16.join(CONFIG_DIR, "bin"),
|
|
3753
5269
|
progress: (detail2) => {
|
|
3754
5270
|
if (!installing) {
|
|
3755
5271
|
installing = true;
|
|
@@ -3762,7 +5278,7 @@ async function resolveDevtunnel() {
|
|
|
3762
5278
|
}
|
|
3763
5279
|
async function devtunnel(...args) {
|
|
3764
5280
|
try {
|
|
3765
|
-
const { stdout } = await
|
|
5281
|
+
const { stdout } = await run6(DEVTUNNEL, args);
|
|
3766
5282
|
return { ok: true, out: stdout };
|
|
3767
5283
|
} catch (e) {
|
|
3768
5284
|
return { ok: false, out: `${e.stdout || ""}${e.stderr || ""}` || e.message };
|
|
@@ -3776,10 +5292,9 @@ function loginArgs(opts = rest) {
|
|
|
3776
5292
|
}
|
|
3777
5293
|
function interactiveLogin(args) {
|
|
3778
5294
|
return new Promise((resolve) => {
|
|
3779
|
-
|
|
5295
|
+
spawn5(DEVTUNNEL, args, { stdio: "inherit" }).on("exit", resolve);
|
|
3780
5296
|
});
|
|
3781
5297
|
}
|
|
3782
|
-
var LOGIN_NEEDED = /not logged in|login token expired|login required|please log ?in/i;
|
|
3783
5298
|
async function login(reason) {
|
|
3784
5299
|
spinner.stop();
|
|
3785
5300
|
console.error(`Dev tunnels: ${reason.trim()}`);
|
|
@@ -3795,12 +5310,11 @@ async function login(reason) {
|
|
|
3795
5310
|
}
|
|
3796
5311
|
async function tunnelUser() {
|
|
3797
5312
|
let who = await devtunnel("user", "show");
|
|
3798
|
-
if (!
|
|
5313
|
+
if (!signedInUser(who)) {
|
|
3799
5314
|
await login(who.out || "not logged in");
|
|
3800
5315
|
who = await devtunnel("user", "show");
|
|
3801
5316
|
}
|
|
3802
|
-
|
|
3803
|
-
return m ? `${m[1]} (${m[2]})` : who.out.trim();
|
|
5317
|
+
return signedInUser(who) ?? who.out.trim();
|
|
3804
5318
|
}
|
|
3805
5319
|
function applyAccess(tunnelId, anonymous2) {
|
|
3806
5320
|
return anonymous2 ? devtunnel("access", "create", tunnelId, "--anonymous") : devtunnel("access", "reset", tunnelId);
|
|
@@ -3820,7 +5334,7 @@ ${access.out}`);
|
|
|
3820
5334
|
if (!/not found/i.test(updated.out)) throw new Error(`devtunnel update failed:
|
|
3821
5335
|
${updated.out}`);
|
|
3822
5336
|
}
|
|
3823
|
-
const name = port2 === DEFAULT_PORT ?
|
|
5337
|
+
const name = port2 === DEFAULT_PORT ? os7.hostname() : `${os7.hostname()} :${port2}`;
|
|
3824
5338
|
const createArgs = ["create", "--expiration", "30d", "-d", `coderemote ${name}`, "--labels", MACHINE_LABEL, "--json"];
|
|
3825
5339
|
if (anonymous2) createArgs.push("--allow-anonymous");
|
|
3826
5340
|
let created = await devtunnel(...createArgs);
|
|
@@ -3862,7 +5376,7 @@ ${res.out}`);
|
|
|
3862
5376
|
}
|
|
3863
5377
|
function hostTunnel(tunnelId, port2) {
|
|
3864
5378
|
return new Promise((resolve, reject) => {
|
|
3865
|
-
const child =
|
|
5379
|
+
const child = spawn5(DEVTUNNEL, ["host", tunnelId], { stdio: ["ignore", "pipe", "pipe"] });
|
|
3866
5380
|
let log2 = "";
|
|
3867
5381
|
const onData = (d) => {
|
|
3868
5382
|
log2 += d;
|
|
@@ -3966,9 +5480,9 @@ var flag = (name) => {
|
|
|
3966
5480
|
};
|
|
3967
5481
|
var anonymous = rest.includes("--allow-anonymous") || rest.includes("--no-auth");
|
|
3968
5482
|
var port = flag("--port") ? parseInt(flag("--port"), 10) : DEFAULT_PORT;
|
|
3969
|
-
var root =
|
|
5483
|
+
var root = path16.resolve(flag("--root") ?? process.cwd());
|
|
3970
5484
|
var cfg = loadConfig();
|
|
3971
|
-
var registry = new AgentRegistry(
|
|
5485
|
+
var registry = new AgentRegistry(path16.join(CONFIG_DIR, "agents"));
|
|
3972
5486
|
async function start({ tunnel }) {
|
|
3973
5487
|
const agents = registry.list();
|
|
3974
5488
|
if (flag("--port")) {
|
|
@@ -4000,10 +5514,10 @@ async function start({ tunnel }) {
|
|
|
4000
5514
|
let server;
|
|
4001
5515
|
let tunnelId;
|
|
4002
5516
|
const tunnelToken = async () => tunnel && !anonymous && tunnelId ? connectToken(tunnelId) : null;
|
|
4003
|
-
const
|
|
5517
|
+
const preview2 = tunnel ? new PreviewTunnel({
|
|
4004
5518
|
devtunnel,
|
|
4005
5519
|
command: () => DEVTUNNEL,
|
|
4006
|
-
description: `coderemote preview ${
|
|
5520
|
+
description: `coderemote preview ${os7.hostname()}`,
|
|
4007
5521
|
leftover: cfg.previewTunnels?.[port],
|
|
4008
5522
|
remember: (id) => {
|
|
4009
5523
|
cfg.previewTunnels = { ...cfg.previewTunnels, [port]: id };
|
|
@@ -4013,10 +5527,19 @@ async function start({ tunnel }) {
|
|
|
4013
5527
|
mintToken: connectToken
|
|
4014
5528
|
}) : null;
|
|
4015
5529
|
let account;
|
|
4016
|
-
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;
|
|
4017
5540
|
spinner.start("Starting agent");
|
|
4018
5541
|
try {
|
|
4019
|
-
server = await listen(cfg.token, port, root, tunnelToken,
|
|
5542
|
+
server = await listen(cfg.token, port, root, tunnelToken, preview2, status2, remover);
|
|
4020
5543
|
} catch (e) {
|
|
4021
5544
|
spinner.fail();
|
|
4022
5545
|
if (e.code !== "EADDRINUSE") throw e;
|
|
@@ -4053,7 +5576,7 @@ async function start({ tunnel }) {
|
|
|
4053
5576
|
registry.register({ port, root, url });
|
|
4054
5577
|
const shutdown = async () => {
|
|
4055
5578
|
child?.kill();
|
|
4056
|
-
await
|
|
5579
|
+
await preview2?.close();
|
|
4057
5580
|
await server.killAllSessions();
|
|
4058
5581
|
server.close();
|
|
4059
5582
|
process.exit(0);
|
|
@@ -4063,7 +5586,7 @@ async function start({ tunnel }) {
|
|
|
4063
5586
|
process.on("SIGHUP", shutdown);
|
|
4064
5587
|
process.on("exit", () => {
|
|
4065
5588
|
child?.kill();
|
|
4066
|
-
|
|
5589
|
+
preview2?.stop();
|
|
4067
5590
|
});
|
|
4068
5591
|
return { url, access };
|
|
4069
5592
|
}
|
|
@@ -4186,7 +5709,7 @@ function currentAgent() {
|
|
|
4186
5709
|
const agents = registry.list();
|
|
4187
5710
|
if (flag("--port")) return agents.find((a) => a.port === port);
|
|
4188
5711
|
const here = process.cwd();
|
|
4189
|
-
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);
|
|
4190
5713
|
return containing[0] ?? (agents.length === 1 ? agents[0] : void 0);
|
|
4191
5714
|
}
|
|
4192
5715
|
async function previewPorts(agent, given) {
|
|
@@ -4322,7 +5845,7 @@ Token: ${cfg.token}`);
|
|
|
4322
5845
|
break;
|
|
4323
5846
|
}
|
|
4324
5847
|
case "rotate-token": {
|
|
4325
|
-
cfg.token =
|
|
5848
|
+
cfg.token = randomBytes4(32).toString("base64url");
|
|
4326
5849
|
saveConfig(cfg);
|
|
4327
5850
|
const agents = registry.list();
|
|
4328
5851
|
for (const agent of agents) {
|
|
@@ -4395,7 +5918,7 @@ Token: ${cfg.token}`);
|
|
|
4395
5918
|
console.log(`coderemote ${VERSION} is the latest version.`);
|
|
4396
5919
|
break;
|
|
4397
5920
|
}
|
|
4398
|
-
const [command, args] = installCommand(
|
|
5921
|
+
const [command, args] = installCommand(fs19.realpathSync(process.argv[1]));
|
|
4399
5922
|
console.log(`Updating coderemote ${VERSION} \u2192 ${latest}: ${command} ${args.join(" ")}
|
|
4400
5923
|
`);
|
|
4401
5924
|
const code = await new Promise((resolve) => {
|