harnery 0.41.0 → 0.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commander.js +3 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +9 -1
- package/dist/commands/artifacts.d.ts.map +1 -1
- package/dist/commands/artifacts.js +113 -12
- package/dist/commands/browse.d.ts +164 -0
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +263 -51
- package/dist/commands/claude-desktop.d.ts.map +1 -1
- package/dist/commands/claude-desktop.js +52 -0
- package/dist/commands/disk.d.ts +6 -0
- package/dist/commands/disk.d.ts.map +1 -0
- package/dist/commands/disk.js +80 -0
- package/dist/commands/doctor.d.ts +8 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +48 -1
- package/dist/commands/files.d.ts +16 -0
- package/dist/commands/files.d.ts.map +1 -1
- package/dist/commands/files.js +54 -2
- package/dist/commands/init.d.ts +2 -2
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +43 -9
- package/dist/commands/rm.d.ts +4 -0
- package/dist/commands/rm.d.ts.map +1 -0
- package/dist/commands/rm.js +87 -0
- package/dist/commands/servers.d.ts +7 -0
- package/dist/commands/servers.d.ts.map +1 -0
- package/dist/commands/servers.js +394 -0
- package/dist/commands/tunnel.d.ts.map +1 -1
- package/dist/commands/tunnel.js +82 -2
- package/dist/core/adapters/profiles.d.ts +9 -0
- package/dist/core/adapters/profiles.d.ts.map +1 -1
- package/dist/core/adapters/profiles.js +22 -1
- package/dist/core/adapters/types.d.ts +9 -0
- package/dist/core/adapters/types.d.ts.map +1 -1
- package/dist/core/agents/cli.js +13 -6
- package/dist/core/agents/render/session-context.d.ts.map +1 -1
- package/dist/core/agents/render/session-context.js +2 -0
- package/dist/core/agents/state/live-coordination-view.d.ts +20 -0
- package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-view.js +60 -0
- package/dist/core/artifacts/delivery-card.js +9 -2
- package/dist/core/artifacts/index.d.ts +140 -4
- package/dist/core/artifacts/index.d.ts.map +1 -1
- package/dist/core/artifacts/index.js +522 -49
- package/dist/core/config.d.ts +68 -11
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +128 -3
- package/dist/core/diagnostics/bundle.d.ts.map +1 -1
- package/dist/core/diagnostics/bundle.js +2 -2
- package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/recorder.js +16 -0
- package/dist/core/events/v3/projection.js +9 -1
- package/dist/core/hooks/adapter/events.d.ts +17 -0
- package/dist/core/hooks/adapter/events.d.ts.map +1 -1
- package/dist/core/hooks/adapter/events.js +5 -0
- package/dist/core/hooks/adapter/runtime-telemetry.js +3 -1
- package/dist/core/hooks/adapter/wiring.d.ts +37 -7
- package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
- package/dist/core/hooks/adapter/wiring.js +62 -4
- package/dist/core/hooks/cli.js +45 -6
- package/dist/core/hooks/run-markers.d.ts +37 -0
- package/dist/core/hooks/run-markers.d.ts.map +1 -0
- package/dist/core/hooks/run-markers.js +109 -0
- package/dist/core/servers/index.d.ts +211 -0
- package/dist/core/servers/index.d.ts.map +1 -0
- package/dist/core/servers/index.js +560 -0
- package/dist/core/servers/net.d.ts +58 -0
- package/dist/core/servers/net.d.ts.map +1 -0
- package/dist/core/servers/net.js +270 -0
- package/dist/core/servers/tunnels.d.ts +14 -0
- package/dist/core/servers/tunnels.d.ts.map +1 -0
- package/dist/core/servers/tunnels.js +33 -0
- package/dist/core/storage/builtins.js +1 -0
- package/dist/core/workflow/engine.d.ts.map +1 -1
- package/dist/core/workflow/engine.js +30 -1
- package/dist/core/workflow/sandbox-projection.d.ts +13 -2
- package/dist/core/workflow/sandbox-projection.d.ts.map +1 -1
- package/dist/core/workflow/sandbox-projection.js +23 -0
- package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
- package/dist/core/workflow/spawn-claude.js +6 -1
- package/dist/core/workflow/spawn-codex.d.ts +2 -0
- package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
- package/dist/core/workflow/spawn-codex.js +20 -1
- package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
- package/dist/core/workflow/spawn-cursor.js +6 -1
- package/dist/core/workflow/spawn-opencode.d.ts.map +1 -1
- package/dist/core/workflow/spawn-opencode.js +4 -1
- package/dist/core/workflow/types.d.ts +47 -2
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/core/workflow/worker-access.d.ts +92 -0
- package/dist/core/workflow/worker-access.d.ts.map +1 -0
- package/dist/core/workflow/worker-access.js +450 -0
- package/dist/lib/browser/batch-steps.d.ts +18 -0
- package/dist/lib/browser/batch-steps.d.ts.map +1 -0
- package/dist/lib/browser/batch-steps.js +82 -0
- package/dist/lib/browser/collect/collector.d.ts +99 -0
- package/dist/lib/browser/collect/collector.d.ts.map +1 -0
- package/dist/lib/browser/collect/collector.js +202 -0
- package/dist/lib/browser/collect/fields.d.ts +31 -0
- package/dist/lib/browser/collect/fields.d.ts.map +1 -0
- package/dist/lib/browser/collect/fields.js +75 -0
- package/dist/lib/browser/collect/index.d.ts +6 -0
- package/dist/lib/browser/collect/index.d.ts.map +1 -0
- package/dist/lib/browser/collect/index.js +5 -0
- package/dist/lib/browser/collect/page-driver.d.ts +14 -0
- package/dist/lib/browser/collect/page-driver.d.ts.map +1 -0
- package/dist/lib/browser/collect/page-driver.js +306 -0
- package/dist/lib/browser/collect/presets.d.ts +52 -0
- package/dist/lib/browser/collect/presets.d.ts.map +1 -0
- package/dist/lib/browser/collect/presets.js +176 -0
- package/dist/lib/browser/collect/scroll-plan.d.ts +85 -0
- package/dist/lib/browser/collect/scroll-plan.d.ts.map +1 -0
- package/dist/lib/browser/collect/scroll-plan.js +118 -0
- package/dist/lib/browser/index.d.ts +1 -1
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/page-review-judge.d.ts +2 -0
- package/dist/lib/browser/page-review-judge.d.ts.map +1 -1
- package/dist/lib/browser/page-review-judge.js +13 -0
- package/dist/lib/browser/qa-run-contracts.d.ts +10 -0
- package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
- package/dist/lib/browser/qa-run.d.ts.map +1 -1
- package/dist/lib/browser/qa-run.js +1 -0
- package/dist/lib/browser/windows-chrome.d.ts +59 -0
- package/dist/lib/browser/windows-chrome.d.ts.map +1 -0
- package/dist/lib/browser/windows-chrome.js +179 -0
- package/dist/lib/claude-desktop-share.d.ts +136 -0
- package/dist/lib/claude-desktop-share.d.ts.map +1 -0
- package/dist/lib/claude-desktop-share.js +394 -0
- package/dist/lib/disk-usage.d.ts +50 -0
- package/dist/lib/disk-usage.d.ts.map +1 -0
- package/dist/lib/disk-usage.js +303 -0
- package/dist/lib/docs-lint.d.ts +6 -0
- package/dist/lib/docs-lint.d.ts.map +1 -1
- package/dist/lib/docs-lint.js +8 -3
- package/dist/lib/guarded-remove.d.ts +34 -0
- package/dist/lib/guarded-remove.d.ts.map +1 -0
- package/dist/lib/guarded-remove.js +310 -0
- package/dist/lib/instructions/templates.js +8 -8
- package/dist/lib/tunnel/error-page.d.ts +1 -1
- package/dist/lib/tunnel/error-page.d.ts.map +1 -1
- package/dist/lib/tunnel/error-page.js +19 -8
- package/dist/lib/tunnel/gate.js +20 -1
- package/dist/lib/tunnel/path-scope.d.ts +22 -0
- package/dist/lib/tunnel/path-scope.d.ts.map +1 -0
- package/dist/lib/tunnel/path-scope.js +62 -0
- package/dist/lib/tunnel/state.d.ts +5 -0
- package/dist/lib/tunnel/state.d.ts.map +1 -1
- package/dist/lib/tunnel/state.js +17 -3
- package/package.json +6 -1
- package/schemas/config.schema.json +91 -0
- package/src/commander.ts +19 -0
- package/src/commands/agents.ts +14 -0
- package/src/commands/artifacts.ts +171 -19
- package/src/commands/browse.ts +400 -57
- package/src/commands/claude-desktop.ts +61 -0
- package/src/commands/disk.ts +114 -0
- package/src/commands/doctor.ts +52 -1
- package/src/commands/files.ts +93 -2
- package/src/commands/init.ts +55 -5
- package/src/commands/rm.ts +115 -0
- package/src/commands/servers.ts +506 -0
- package/src/commands/tunnel.ts +102 -2
- package/src/core/adapters/profiles.ts +22 -1
- package/src/core/adapters/types.ts +10 -0
- package/src/core/agents/cli.ts +14 -6
- package/src/core/agents/render/session-context.ts +1 -0
- package/src/core/agents/state/live-coordination-view.ts +73 -0
- package/src/core/artifacts/delivery-card.ts +10 -2
- package/src/core/artifacts/index.ts +697 -44
- package/src/core/config.ts +183 -4
- package/src/core/diagnostics/bundle.ts +2 -1
- package/src/core/events/v3/producers/recorder.ts +16 -0
- package/src/core/events/v3/projection.ts +11 -1
- package/src/core/hooks/adapter/events.ts +19 -0
- package/src/core/hooks/adapter/runtime-telemetry.ts +3 -1
- package/src/core/hooks/adapter/wiring.ts +116 -8
- package/src/core/hooks/cli.ts +47 -6
- package/src/core/hooks/run-markers.ts +132 -0
- package/src/core/servers/index.ts +712 -0
- package/src/core/servers/net.ts +283 -0
- package/src/core/servers/tunnels.ts +39 -0
- package/src/core/storage/builtins.ts +1 -0
- package/src/core/workflow/engine.ts +38 -1
- package/src/core/workflow/sandbox-projection.ts +38 -2
- package/src/core/workflow/spawn-claude.ts +5 -1
- package/src/core/workflow/spawn-codex.ts +18 -1
- package/src/core/workflow/spawn-cursor.ts +5 -1
- package/src/core/workflow/spawn-opencode.ts +4 -1
- package/src/core/workflow/types.ts +62 -2
- package/src/core/workflow/worker-access.ts +525 -0
- package/src/lib/browser/batch-steps.ts +79 -0
- package/src/lib/browser/collect/collector.ts +306 -0
- package/src/lib/browser/collect/fields.ts +87 -0
- package/src/lib/browser/collect/index.ts +47 -0
- package/src/lib/browser/collect/page-driver.ts +327 -0
- package/src/lib/browser/collect/presets.ts +237 -0
- package/src/lib/browser/collect/scroll-plan.ts +179 -0
- package/src/lib/browser/index.ts +1 -0
- package/src/lib/browser/page-review-judge.ts +16 -0
- package/src/lib/browser/qa-run-contracts.ts +11 -0
- package/src/lib/browser/qa-run.ts +1 -0
- package/src/lib/browser/windows-chrome.ts +227 -0
- package/src/lib/claude-desktop-share.ts +511 -0
- package/src/lib/disk-usage.ts +347 -0
- package/src/lib/docs-lint.ts +8 -3
- package/src/lib/guarded-remove.ts +376 -0
- package/src/lib/instructions/templates.ts +8 -8
- package/src/lib/tunnel/error-page.ts +25 -9
- package/src/lib/tunnel/gate.ts +22 -1
- package/src/lib/tunnel/path-scope.ts +73 -0
- package/src/lib/tunnel/state.ts +20 -3
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { hostname } from "node:os";
|
|
3
|
+
import { resolveCoordRoot } from "../core/agents/coord-client.js";
|
|
4
|
+
import { resolveBinName, serverGcConfig } from "../core/config.js";
|
|
5
|
+
import { adoptServer, allocateServerPort, isValidServerId, listServers, planServerGc, readServer, registerServer, SERVER_DEFAULT_IDLE_HOURS, SERVER_DEFAULT_OWNER_STALE_HOURS, stopServer, touchServer, unregisterServer, } from "../core/servers/index.js";
|
|
6
|
+
export function registerServersCommand(program, emit, context) {
|
|
7
|
+
const root = () => {
|
|
8
|
+
const resolved = context?.resolveCoordRoot?.() ?? context?.repoRoot ?? resolveCoordRoot();
|
|
9
|
+
if (!resolved)
|
|
10
|
+
throw new Error("not inside a Harnery project");
|
|
11
|
+
return resolved;
|
|
12
|
+
};
|
|
13
|
+
const fail = (error, code = "servers_error") => {
|
|
14
|
+
emit.error({ code, message: error instanceof Error ? error.message : String(error) });
|
|
15
|
+
emit.setExitCode(1);
|
|
16
|
+
};
|
|
17
|
+
const cmd = program
|
|
18
|
+
.command("servers")
|
|
19
|
+
.description("Every local server started in this project: list, stop, clean up idle session servers, read logs");
|
|
20
|
+
cmd
|
|
21
|
+
.command("list", { isDefault: true })
|
|
22
|
+
.alias("status")
|
|
23
|
+
.description("List registered servers and any unregistered listener running inside the project")
|
|
24
|
+
.option("--no-scan", "Skip the scan for unregistered listeners")
|
|
25
|
+
.option("--json", "Emit the versioned report")
|
|
26
|
+
.action((options) => {
|
|
27
|
+
try {
|
|
28
|
+
const report = listServers({ coordRoot: root(), scan: options.scan !== false });
|
|
29
|
+
if (options.json) {
|
|
30
|
+
emit.config({ format: "json" });
|
|
31
|
+
emit.data(report);
|
|
32
|
+
}
|
|
33
|
+
else
|
|
34
|
+
emit.text(renderReport(report, resolveBinName(report.coord_root)));
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
fail(error);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
cmd
|
|
41
|
+
.command("stop [ids...]")
|
|
42
|
+
.description("Stop named servers now; --type selects several and previews unless --yes")
|
|
43
|
+
.option("--type <type>", "Select every running server of this type")
|
|
44
|
+
.option("--pid <pid>", "Stop an unregistered listener found by the list scan (repeatable)", (value, previous = []) => [...previous, value])
|
|
45
|
+
.option("--kind <kind>", "With --type, limit to session or service")
|
|
46
|
+
.option("--yes", "Apply a --type selection")
|
|
47
|
+
.option("--json", "Emit JSON")
|
|
48
|
+
.action(async (ids, options) => {
|
|
49
|
+
try {
|
|
50
|
+
const coordRoot = root();
|
|
51
|
+
const pids = (options.pid ?? []).map((value) => integer(value, "--pid"));
|
|
52
|
+
if (!ids.length && !options.type && !pids.length)
|
|
53
|
+
throw new Error("name server ids, or pass --type or --pid");
|
|
54
|
+
const report = listServers({ coordRoot, scan: pids.length > 0 });
|
|
55
|
+
const strays = pids.map((pid) => {
|
|
56
|
+
const found = report.unregistered.find((item) => item.pid === pid);
|
|
57
|
+
if (!found)
|
|
58
|
+
throw new Error(`pid ${pid} is not an unregistered listener inside this project; only pids shown by "servers list" can be stopped this way`);
|
|
59
|
+
return found;
|
|
60
|
+
});
|
|
61
|
+
const byId = new Map(report.servers.map((view) => [view.record.id, view]));
|
|
62
|
+
const missing = ids.filter((id) => !byId.has(id));
|
|
63
|
+
if (missing.length)
|
|
64
|
+
throw new Error(`no running server named: ${missing.join(", ")}`);
|
|
65
|
+
const selected = [
|
|
66
|
+
...ids.map((id) => byId.get(id)),
|
|
67
|
+
...(options.type
|
|
68
|
+
? report.servers.filter((view) => view.record.type === options.type &&
|
|
69
|
+
view.state === "running" &&
|
|
70
|
+
(!options.kind || view.record.kind === options.kind) &&
|
|
71
|
+
!ids.includes(view.record.id))
|
|
72
|
+
: []),
|
|
73
|
+
];
|
|
74
|
+
const preview = Boolean(options.type) && !options.yes;
|
|
75
|
+
const results = [];
|
|
76
|
+
for (const stray of strays) {
|
|
77
|
+
results.push(await stopServer({
|
|
78
|
+
schema_version: 1,
|
|
79
|
+
id: `pid-${stray.pid}`,
|
|
80
|
+
kind: "session",
|
|
81
|
+
type: "unregistered",
|
|
82
|
+
label: stray.command,
|
|
83
|
+
pid: stray.pid,
|
|
84
|
+
host: hostname(),
|
|
85
|
+
started_at: new Date().toISOString(),
|
|
86
|
+
registered_at: new Date().toISOString(),
|
|
87
|
+
}, { coordRoot }));
|
|
88
|
+
}
|
|
89
|
+
for (const view of selected) {
|
|
90
|
+
if (preview && !ids.includes(view.record.id))
|
|
91
|
+
results.push({ id: view.record.id, would_stop: true });
|
|
92
|
+
else
|
|
93
|
+
results.push(await stopServer(view.record, { coordRoot }));
|
|
94
|
+
}
|
|
95
|
+
if (options.json) {
|
|
96
|
+
emit.config({ format: "json" });
|
|
97
|
+
emit.data({ applied: !preview, results });
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
emit.text(results.length
|
|
101
|
+
? results
|
|
102
|
+
.map((r) => "would_stop" in r
|
|
103
|
+
? `would stop ${r.id}`
|
|
104
|
+
: `${r.stopped ? "stopped" : r.method === "not-running" ? "already stopped" : "could not stop"} ${r.id}${r.detail ? ` (${r.detail})` : ""}`)
|
|
105
|
+
.join("\n") +
|
|
106
|
+
(preview ? `\nPreview only. Re-run with --yes to stop these.` : "")
|
|
107
|
+
: "No matching running servers.");
|
|
108
|
+
}
|
|
109
|
+
if (results.some((r) => "stopped" in r && !r.stopped && r.method === "refused"))
|
|
110
|
+
emit.setExitCode(1);
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
fail(error);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
cmd
|
|
117
|
+
.command("gc")
|
|
118
|
+
.description(`Stop idle session servers whose starting agent ended (preview unless --yes); services are never touched`)
|
|
119
|
+
.option("--idle-hours <hours>", `Idle window before a server may stop (default ${SERVER_DEFAULT_IDLE_HOURS})`)
|
|
120
|
+
.option("--owner-stale-hours <hours>", `Treat a starting agent unobserved this long as gone (default ${SERVER_DEFAULT_OWNER_STALE_HOURS})`)
|
|
121
|
+
.option("--yes", "Stop the selected servers")
|
|
122
|
+
.option("--json", "Emit JSON")
|
|
123
|
+
.action(async (options) => {
|
|
124
|
+
try {
|
|
125
|
+
const coordRoot = root();
|
|
126
|
+
const configured = serverGcConfig(coordRoot);
|
|
127
|
+
const idleHours = positive(options.idleHours, configured.idle_hours ?? SERVER_DEFAULT_IDLE_HOURS, "--idle-hours");
|
|
128
|
+
const ownerStaleHours = positive(options.ownerStaleHours, configured.owner_stale_hours ?? SERVER_DEFAULT_OWNER_STALE_HOURS, "--owner-stale-hours");
|
|
129
|
+
const report = listServers({ coordRoot, scan: false, ownerStaleHours });
|
|
130
|
+
const decisions = planServerGc(report.servers, { idleHours, ownerStaleHours });
|
|
131
|
+
const stops = decisions.filter((d) => d.action === "stop");
|
|
132
|
+
const results = [];
|
|
133
|
+
if (options.yes) {
|
|
134
|
+
for (const decision of stops) {
|
|
135
|
+
const view = report.servers.find((v) => v.record.id === decision.id);
|
|
136
|
+
results.push(await stopServer(view.record, { coordRoot }));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (options.json) {
|
|
140
|
+
emit.config({ format: "json" });
|
|
141
|
+
emit.data({ applied: Boolean(options.yes), pruned: report.pruned, decisions, results });
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
const lines = decisions.map((d) => `${d.action === "stop" ? (options.yes ? "stopped" : "would stop") : "keep"} ${d.id} ${d.reason}`);
|
|
145
|
+
if (report.pruned.length)
|
|
146
|
+
lines.push(`Removed records of exited servers: ${report.pruned.join(", ")}`);
|
|
147
|
+
if (!decisions.length)
|
|
148
|
+
lines.push("No registered servers.");
|
|
149
|
+
if (stops.length && !options.yes)
|
|
150
|
+
lines.push("Preview only. Re-run with --yes to stop.");
|
|
151
|
+
emit.text(lines.join("\n"));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
fail(error);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
cmd
|
|
159
|
+
.command("adopt")
|
|
160
|
+
.description("Register running servers that never registered (only pids the list reports as unregistered)")
|
|
161
|
+
.option("--pid <pid>", "Adopt this unregistered listener (repeatable)", (value, previous = []) => [...previous, value])
|
|
162
|
+
.option("--all", "Adopt every unregistered listener the scan reports")
|
|
163
|
+
.option("--type <type>", "Server family to record", "adopted")
|
|
164
|
+
.option("--kind <kind>", "session (cleaned up when idle) or service", "session")
|
|
165
|
+
.option("--label <label>", "Human-readable name (single --pid only)")
|
|
166
|
+
.option("--scope <path>", "What the server serves (default: its working directory)")
|
|
167
|
+
.option("--no-owner", "Record no owner, so gc never stops it")
|
|
168
|
+
.option("--json", "Emit JSON")
|
|
169
|
+
.action((options) => {
|
|
170
|
+
try {
|
|
171
|
+
const coordRoot = root();
|
|
172
|
+
if (options.kind !== "session" && options.kind !== "service")
|
|
173
|
+
throw new Error("--kind must be session or service");
|
|
174
|
+
const pids = options.all
|
|
175
|
+
? [...new Set(listServers({ coordRoot, sample: false }).unregistered.map((u) => u.pid))]
|
|
176
|
+
: (options.pid ?? []).map((value) => integer(value, "--pid"));
|
|
177
|
+
if (!pids.length)
|
|
178
|
+
throw new Error(options.all ? "no unregistered listeners" : "pass --pid or --all");
|
|
179
|
+
if (options.label && pids.length > 1)
|
|
180
|
+
throw new Error("--label needs a single --pid");
|
|
181
|
+
const records = pids.map((pid) => adoptServer(pid, {
|
|
182
|
+
coordRoot,
|
|
183
|
+
type: options.type,
|
|
184
|
+
kind: options.kind,
|
|
185
|
+
...(options.label ? { label: options.label } : {}),
|
|
186
|
+
...(options.scope ? { scope: options.scope } : {}),
|
|
187
|
+
owner: options.owner === false ? "none" : "self",
|
|
188
|
+
}));
|
|
189
|
+
if (options.json) {
|
|
190
|
+
emit.config({ format: "json" });
|
|
191
|
+
emit.data({ adopted: records });
|
|
192
|
+
}
|
|
193
|
+
else
|
|
194
|
+
emit.text(records.map((r) => `adopted ${r.id} ${r.url ?? ""}`).join("\n"));
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
fail(error);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
cmd
|
|
201
|
+
.command("port <type>")
|
|
202
|
+
.description("Print a free port from the type's configured range (.harnery/config.jsonc servers.port_ranges)")
|
|
203
|
+
.action((type) => {
|
|
204
|
+
try {
|
|
205
|
+
const port = allocateServerPort(type, { coordRoot: root() });
|
|
206
|
+
if (port === null)
|
|
207
|
+
throw new Error(`no port range configured for ${type}`);
|
|
208
|
+
emit.text(String(port));
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
fail(error);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
cmd
|
|
215
|
+
.command("logs <id>")
|
|
216
|
+
.description("Print the end of a server's log file")
|
|
217
|
+
.option("-n, --lines <n>", "Lines to print", "60")
|
|
218
|
+
.action((id, options) => {
|
|
219
|
+
try {
|
|
220
|
+
const record = readServer(id, { coordRoot: root() });
|
|
221
|
+
if (!record)
|
|
222
|
+
throw new Error(`no server named ${id}`);
|
|
223
|
+
if (!record.log || !existsSync(record.log))
|
|
224
|
+
throw new Error(`${id} did not record a log file`);
|
|
225
|
+
const lines = positive(options.lines, 60, "--lines");
|
|
226
|
+
const size = statSync(record.log).size;
|
|
227
|
+
const content = readFileSync(record.log, "utf8").slice(Math.max(0, size - 512 * 1024));
|
|
228
|
+
emit.text(content
|
|
229
|
+
.split("\n")
|
|
230
|
+
.slice(-Math.floor(lines) - 1)
|
|
231
|
+
.join("\n"));
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
fail(error);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
cmd
|
|
238
|
+
.command("open <id>")
|
|
239
|
+
.description("Print a server's URL")
|
|
240
|
+
.action((id) => {
|
|
241
|
+
try {
|
|
242
|
+
const record = readServer(id, { coordRoot: root() });
|
|
243
|
+
if (!record)
|
|
244
|
+
throw new Error(`no server named ${id}`);
|
|
245
|
+
if (!record.url)
|
|
246
|
+
throw new Error(`${id} did not record a URL`);
|
|
247
|
+
emit.text(record.url);
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
fail(error);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
cmd
|
|
254
|
+
.command("register")
|
|
255
|
+
.description("Record a server started by a script or another language (prints the record; the calling agent is recorded as owner)")
|
|
256
|
+
.requiredOption("--type <type>", "Server family, for example preview or review")
|
|
257
|
+
.option("--kind <kind>", "session (per piece of work) or service (long-running)", "session")
|
|
258
|
+
.option("--pid <pid>", "Process that owns the server's lifetime (default: this command's parent)")
|
|
259
|
+
.option("--port <port>", "Listening port")
|
|
260
|
+
.option("--id <id>", "Stable id (default derived from type and scope)")
|
|
261
|
+
.option("--url <url>", "Local URL")
|
|
262
|
+
.option("--scope <path>", "What the server serves, usually a directory")
|
|
263
|
+
.option("--label <label>", "Human-readable name")
|
|
264
|
+
.option("--log <path>", "Log file")
|
|
265
|
+
.option("--cwd <path>", "Working directory")
|
|
266
|
+
.action((options) => {
|
|
267
|
+
try {
|
|
268
|
+
if (options.kind !== "session" && options.kind !== "service")
|
|
269
|
+
throw new Error("--kind must be session or service");
|
|
270
|
+
if (options.id && !isValidServerId(options.id))
|
|
271
|
+
throw new Error(`invalid id: ${options.id}`);
|
|
272
|
+
const record = registerServer({
|
|
273
|
+
kind: options.kind,
|
|
274
|
+
type: options.type,
|
|
275
|
+
pid: options.pid ? integer(options.pid, "--pid") : process.ppid,
|
|
276
|
+
...(options.port ? { port: integer(options.port, "--port") } : {}),
|
|
277
|
+
...(options.id ? { id: options.id } : {}),
|
|
278
|
+
...(options.url ? { url: options.url } : {}),
|
|
279
|
+
...(options.scope ? { scope: options.scope } : {}),
|
|
280
|
+
...(options.label ? { label: options.label } : {}),
|
|
281
|
+
...(options.log ? { log: options.log } : {}),
|
|
282
|
+
...(options.cwd ? { cwd: options.cwd } : {}),
|
|
283
|
+
}, { coordRoot: root() });
|
|
284
|
+
emit.config({ format: "json" });
|
|
285
|
+
emit.data(record);
|
|
286
|
+
}
|
|
287
|
+
catch (error) {
|
|
288
|
+
fail(error);
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
cmd
|
|
292
|
+
.command("unregister <id>")
|
|
293
|
+
.description("Remove a server's record (with --pid, only if the record still names that pid)")
|
|
294
|
+
.option("--pid <pid>", "Only remove when the record names this pid")
|
|
295
|
+
.action((id, options) => {
|
|
296
|
+
try {
|
|
297
|
+
const removed = unregisterServer(id, {
|
|
298
|
+
coordRoot: root(),
|
|
299
|
+
...(options.pid ? { pid: integer(options.pid, "--pid") } : {}),
|
|
300
|
+
});
|
|
301
|
+
emit.config({ format: "json" });
|
|
302
|
+
emit.data({ id, removed });
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
fail(error);
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
cmd
|
|
309
|
+
.command("touch <id>")
|
|
310
|
+
.description("Mark a server as used now, postponing idle cleanup")
|
|
311
|
+
.action((id) => {
|
|
312
|
+
try {
|
|
313
|
+
touchServer(id, { coordRoot: root() });
|
|
314
|
+
}
|
|
315
|
+
catch (error) {
|
|
316
|
+
fail(error);
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
function integer(value, flag) {
|
|
321
|
+
const parsed = Number(value);
|
|
322
|
+
if (!Number.isSafeInteger(parsed) || parsed < 1)
|
|
323
|
+
throw new Error(`${flag} must be a positive integer`);
|
|
324
|
+
return parsed;
|
|
325
|
+
}
|
|
326
|
+
function positive(value, fallback, flag) {
|
|
327
|
+
if (value === undefined)
|
|
328
|
+
return fallback;
|
|
329
|
+
const parsed = Number(value);
|
|
330
|
+
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
331
|
+
throw new Error(`${flag} must be a positive number`);
|
|
332
|
+
return parsed;
|
|
333
|
+
}
|
|
334
|
+
function duration(ms) {
|
|
335
|
+
const minutes = Math.round(ms / 60_000);
|
|
336
|
+
if (minutes < 60)
|
|
337
|
+
return `${minutes}m`;
|
|
338
|
+
const hours = Math.floor(minutes / 60);
|
|
339
|
+
if (hours < 48)
|
|
340
|
+
return `${hours}h${String(minutes % 60).padStart(2, "0")}m`;
|
|
341
|
+
return `${Math.floor(hours / 24)}d`;
|
|
342
|
+
}
|
|
343
|
+
function ownerText(view) {
|
|
344
|
+
if (!view.record.owner)
|
|
345
|
+
return "no agent";
|
|
346
|
+
const name = view.record.owner.name ?? view.record.owner.instance_id.slice(0, 8);
|
|
347
|
+
return view.owner_state === "live" ? name : `${name} (${view.owner_state})`;
|
|
348
|
+
}
|
|
349
|
+
/** Plain-text table grouped by kind; exported for tests. */
|
|
350
|
+
export function renderReport(report, bin) {
|
|
351
|
+
const lines = [];
|
|
352
|
+
for (const kind of ["service", "session"]) {
|
|
353
|
+
const views = report.servers.filter((view) => view.record.kind === kind);
|
|
354
|
+
lines.push(kind === "service"
|
|
355
|
+
? "Services (stopped only by hand)"
|
|
356
|
+
: "Session servers (cleaned up when idle and their agent is gone)");
|
|
357
|
+
if (!views.length) {
|
|
358
|
+
lines.push(" none");
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
for (const view of views) {
|
|
362
|
+
const { record } = view;
|
|
363
|
+
const state = view.state === "running" ? "" : ` [${view.state}]`;
|
|
364
|
+
const started = Date.parse(record.started_at);
|
|
365
|
+
const use = kind === "service"
|
|
366
|
+
? `up ${Number.isFinite(started) ? duration(Date.now() - started) : "?"}`
|
|
367
|
+
: view.connections === null
|
|
368
|
+
? `idle ${duration(view.idle_ms)}`
|
|
369
|
+
: view.connections > 0
|
|
370
|
+
? `${view.connections} open connection(s)`
|
|
371
|
+
: `idle ${duration(view.idle_ms)}`;
|
|
372
|
+
lines.push(` ${record.id}${state} ${record.url ?? (record.port ? `port ${record.port}` : "")}`, ` ${record.label} · ${ownerText(view)} · ${use}`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
if (report.scan === "unsupported") {
|
|
376
|
+
lines.push("Unregistered listeners: not checked on this platform");
|
|
377
|
+
}
|
|
378
|
+
else if (report.unregistered.length) {
|
|
379
|
+
const byPid = new Map();
|
|
380
|
+
for (const item of report.unregistered) {
|
|
381
|
+
const entry = byPid.get(item.pid) ?? { ports: [], command: item.command };
|
|
382
|
+
entry.ports.push(item.port);
|
|
383
|
+
byPid.set(item.pid, entry);
|
|
384
|
+
}
|
|
385
|
+
lines.push(`Unregistered servers inside this project (${byPid.size} processes):`);
|
|
386
|
+
for (const [pid, entry] of byPid)
|
|
387
|
+
lines.push(` pid ${pid} port ${entry.ports.join(", ")} ${entry.command.slice(0, 100)}`);
|
|
388
|
+
lines.push(` These started without registering. Stop one with: ${bin} servers stop --pid <pid>`);
|
|
389
|
+
}
|
|
390
|
+
if (report.pruned.length)
|
|
391
|
+
lines.push(`Removed records of exited servers: ${report.pruned.join(", ")}`);
|
|
392
|
+
lines.push(`Control: ${bin} servers stop <id> · ${bin} servers gc · ${bin} servers logs <id>`);
|
|
393
|
+
return lines.join("\n");
|
|
394
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tunnel.d.ts","sourceRoot":"","sources":["../../src/commands/tunnel.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"tunnel.d.ts","sourceRoot":"","sources":["../../src/commands/tunnel.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAgB1E,OAAO,EAYL,KAAK,cAAc,EACnB,KAAK,WAAW,EAGjB,MAAM,wBAAwB,CAAC;AAuDhC,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,cAAc,EACxB,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,IAAI,SAAgB,GACnB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAcpC;AA2sBD;;;;;;;;;;GAUG;AACH,wBAAsB,SAAS,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAiF7F;AAkND,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,WAAW,EACtB,YAAY,CAAC,EAAE,qBAAqB,GACnC,IAAI,CAkFN"}
|
package/dist/commands/tunnel.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
|
+
import { resolveCoordRoot } from "../core/agents/coord-client.js";
|
|
4
5
|
import { resolveBinName } from "../core/config.js";
|
|
6
|
+
import { tryRegisterServer, tunnelServerId, tunnelServerInput, unregisterServer, } from "../core/servers/index.js";
|
|
5
7
|
import { processLogDestination, runRotatingProcessSync, spawnRotatingProcess, } from "../core/storage/process-log.js";
|
|
8
|
+
import { ALLOW_PATHS_ENV, normalizeAllowPaths } from "../lib/tunnel/path-scope.js";
|
|
6
9
|
import { clearState, DEFAULT_INSTANCE, ensureCloudflared, gateLogFile, isProcessAlive, isTunnelStateLive, listStates, providerLogFile, readConfig, readState, writeConfig, writeState, } from "../lib/tunnel/state.js";
|
|
7
10
|
/**
|
|
8
11
|
* `tunnel`: provider-backed tunnel in front of a local upstream.
|
|
@@ -11,6 +14,8 @@ import { clearState, DEFAULT_INSTANCE, ensureCloudflared, gateLogFile, isProcess
|
|
|
11
14
|
* upstream. Cloudflare quick tunnels add an IP allowlist at the gate via the
|
|
12
15
|
* Cloudflare-set `CF-Connecting-IP` header; Tailscale Serve/Funnel exposes the
|
|
13
16
|
* same gate through tailscaled, with Tailscale owning the access boundary.
|
|
17
|
+
* Every provider also passes through a path scope: `up` requires at least one
|
|
18
|
+
* `--allow-path <prefix>`, and the gate refuses any request outside it.
|
|
14
19
|
*
|
|
15
20
|
* State + config persisted under `.cache/tunnel/`. cloudflared auto-installs
|
|
16
21
|
* to ~/.local/bin/ on first run (Linux only; macOS users `brew install`);
|
|
@@ -98,6 +103,42 @@ function resolveHttpsPort(raw) {
|
|
|
98
103
|
}
|
|
99
104
|
return port;
|
|
100
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* The path scope for `up`. At least one prefix is required: a tunnel forwards
|
|
108
|
+
* to a whole local app, and publishing all of it has to be a stated choice
|
|
109
|
+
* (`--allow-path /`), never the result of leaving a flag off.
|
|
110
|
+
*/
|
|
111
|
+
function resolveAllowPaths(raw) {
|
|
112
|
+
let paths;
|
|
113
|
+
try {
|
|
114
|
+
paths = normalizeAllowPaths(raw ?? []);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
emit.error({
|
|
118
|
+
code: "tunnel_path_scope_invalid",
|
|
119
|
+
message: error instanceof Error ? error.message : String(error),
|
|
120
|
+
});
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
if (paths.length === 0) {
|
|
124
|
+
emit.error({
|
|
125
|
+
code: "tunnel_path_scope_empty",
|
|
126
|
+
message: "No path scope; refusing to start. Name what the tunnel shares with --allow-path <prefix> " +
|
|
127
|
+
"(repeatable), or pass --allow-path / to publish the whole upstream.",
|
|
128
|
+
});
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
return paths;
|
|
132
|
+
}
|
|
133
|
+
function samePaths(a, b) {
|
|
134
|
+
return a.length === b.length && [...a].sort().join("\n") === [...b].sort().join("\n");
|
|
135
|
+
}
|
|
136
|
+
function formatPaths(paths) {
|
|
137
|
+
return paths.length === 0 ? "(none: every path refused)" : paths.join(", ");
|
|
138
|
+
}
|
|
139
|
+
function collectPath(value, previous) {
|
|
140
|
+
return [...previous, value];
|
|
141
|
+
}
|
|
101
142
|
function gateScriptPath() {
|
|
102
143
|
return resolve(import.meta.dirname, "..", "lib", "tunnel", "gate.ts");
|
|
103
144
|
}
|
|
@@ -366,6 +407,7 @@ function spawnGate(o) {
|
|
|
366
407
|
HARNERY_TUNNEL_TARGET: o.target,
|
|
367
408
|
HARNERY_TUNNEL_VHOST: o.vhost,
|
|
368
409
|
HARNERY_TUNNEL_PORT: String(o.gatePort),
|
|
410
|
+
[ALLOW_PATHS_ENV]: o.allowPaths.join(","),
|
|
369
411
|
},
|
|
370
412
|
});
|
|
371
413
|
gateProc.unref();
|
|
@@ -389,6 +431,7 @@ async function up(opts) {
|
|
|
389
431
|
const tailscaleMode = provider === "tailscale" ? resolveTailscaleMode(opts.visibility) : undefined;
|
|
390
432
|
const tailscalePath = provider === "tailscale" ? resolveTailscalePath(opts.path, name) : undefined;
|
|
391
433
|
const tailscaleHttpsPort = provider === "tailscale" ? resolveHttpsPort(opts.httpsPort) : undefined;
|
|
434
|
+
const allowPaths = resolveAllowPaths(opts.allowPath);
|
|
392
435
|
const existing = readState(name);
|
|
393
436
|
if (existing && tunnelIsAlive(existing)) {
|
|
394
437
|
if (existing.provider !== provider) {
|
|
@@ -398,9 +441,18 @@ async function up(opts) {
|
|
|
398
441
|
});
|
|
399
442
|
process.exit(1);
|
|
400
443
|
}
|
|
444
|
+
if (!samePaths(existing.allow_paths, allowPaths)) {
|
|
445
|
+
emit.error({
|
|
446
|
+
code: "tunnel_instance_in_use",
|
|
447
|
+
message: `Tunnel [${name}] is already up sharing ${formatPaths(existing.allow_paths)}. ` +
|
|
448
|
+
`Stop it before starting it with ${formatPaths(allowPaths)}.`,
|
|
449
|
+
});
|
|
450
|
+
process.exit(1);
|
|
451
|
+
}
|
|
401
452
|
emit.text(`Already up [${name}]: ${existing.url}\n`);
|
|
402
453
|
emit.text(` Provider: ${existing.provider}\n`);
|
|
403
454
|
emit.text(` Forwarding: ${existing.target} (Host: ${existing.vhost})\n`);
|
|
455
|
+
emit.text(` Paths: ${formatPaths(existing.allow_paths)}\n`);
|
|
404
456
|
return;
|
|
405
457
|
}
|
|
406
458
|
if (existing)
|
|
@@ -438,6 +490,7 @@ async function up(opts) {
|
|
|
438
490
|
vhost,
|
|
439
491
|
provider,
|
|
440
492
|
allowedIps: cfg.allowed_ips,
|
|
493
|
+
allowPaths,
|
|
441
494
|
gateLogPath,
|
|
442
495
|
});
|
|
443
496
|
await sleep(800);
|
|
@@ -507,16 +560,19 @@ async function up(opts) {
|
|
|
507
560
|
target,
|
|
508
561
|
vhost,
|
|
509
562
|
gate_port: gatePort,
|
|
563
|
+
allow_paths: allowPaths,
|
|
510
564
|
tailscale_mode: tailscaleMode,
|
|
511
565
|
tailscale_path: tailscalePath,
|
|
512
566
|
tailscale_https_port: tailscaleHttpsPort,
|
|
513
567
|
};
|
|
514
568
|
writeState(state);
|
|
569
|
+
recordTunnelServer(state);
|
|
515
570
|
const stopHint = name === DEFAULT_INSTANCE ? "harn tunnel down" : `harn tunnel down --name ${name}`;
|
|
516
571
|
emit.text(`\n Instance: ${name}\n`);
|
|
517
572
|
emit.text(` Provider: ${provider}${tailscaleMode ? ` (${tailscaleMode})` : ""}\n`);
|
|
518
573
|
emit.text(` URL: ${url}\n\n`);
|
|
519
574
|
emit.text(` Forwarding: ${target} (Host: ${vhost})\n`);
|
|
575
|
+
emit.text(` Paths: ${formatPaths(allowPaths)}\n`);
|
|
520
576
|
emit.text(` Gate port: ${gatePort}\n`);
|
|
521
577
|
if (provider === "cloudflare") {
|
|
522
578
|
emit.text(` Allowed IPs: ${cfg.allowed_ips.join(", ")}\n\n`);
|
|
@@ -533,6 +589,14 @@ async function up(opts) {
|
|
|
533
589
|
emit.text(` Stop: ${stopHint}\n`);
|
|
534
590
|
emit.text(" Status: harn tunnel status\n");
|
|
535
591
|
}
|
|
592
|
+
/** The server registry lists tunnels beside every other local server. */
|
|
593
|
+
function registryRoot() {
|
|
594
|
+
return resolveCoordRoot() ?? process.cwd();
|
|
595
|
+
}
|
|
596
|
+
function recordTunnelServer(state) {
|
|
597
|
+
const coordRoot = registryRoot();
|
|
598
|
+
tryRegisterServer(tunnelServerInput(state, coordRoot), { coordRoot });
|
|
599
|
+
}
|
|
536
600
|
/** Tear down a single instance by name. Returns the number of processes killed. */
|
|
537
601
|
function downOne(name, killed) {
|
|
538
602
|
const before = killed.size;
|
|
@@ -558,6 +622,7 @@ function downOne(name, killed) {
|
|
|
558
622
|
// lost; they'd otherwise squat on the port and break the next `up`.
|
|
559
623
|
sweepStrays(state?.gate_port ?? DEFAULT_GATE_PORT, killed);
|
|
560
624
|
clearState(name);
|
|
625
|
+
unregisterServer(tunnelServerId(name), { coordRoot: registryRoot() });
|
|
561
626
|
return killed.size - before;
|
|
562
627
|
}
|
|
563
628
|
function down(opts) {
|
|
@@ -633,6 +698,14 @@ export async function reloadOne(state) {
|
|
|
633
698
|
if (!bunAvailable()) {
|
|
634
699
|
return { ok: false, message: `[${name}] bun is not on PATH, so the gate can't be respawned.` };
|
|
635
700
|
}
|
|
701
|
+
if (state.allow_paths.length === 0) {
|
|
702
|
+
return {
|
|
703
|
+
ok: false,
|
|
704
|
+
message: `[${name}] has no recorded path scope, so a reloaded gate would refuse every request. ` +
|
|
705
|
+
`Restart it with \`${bin} tunnel down --name ${name}\` and ` +
|
|
706
|
+
`\`${bin} tunnel up --name ${name} --allow-path <prefix>\`.`,
|
|
707
|
+
};
|
|
708
|
+
}
|
|
636
709
|
// Kill ONLY the gate. sweepStrays() is deliberately avoided here: it also
|
|
637
710
|
// matches `--url http://localhost:<port>`, which is the provider process we
|
|
638
711
|
// are going out of our way to keep alive.
|
|
@@ -662,6 +735,7 @@ export async function reloadOne(state) {
|
|
|
662
735
|
vhost: state.vhost,
|
|
663
736
|
provider: state.provider,
|
|
664
737
|
allowedIps: cfg.allowed_ips,
|
|
738
|
+
allowPaths: state.allow_paths,
|
|
665
739
|
gateLogPath: tunnelLogDestinations(name, state.provider).gate,
|
|
666
740
|
});
|
|
667
741
|
await sleep(800);
|
|
@@ -676,6 +750,7 @@ export async function reloadOne(state) {
|
|
|
676
750
|
};
|
|
677
751
|
}
|
|
678
752
|
writeState({ ...state, gate_pid: pid });
|
|
753
|
+
recordTunnelServer({ ...state, gate_pid: pid });
|
|
679
754
|
const allowNote = state.provider === "cloudflare" ? ` Allowlist: ${cfg.allowed_ips.length} IP(s).` : "";
|
|
680
755
|
return { ok: true, message: `[${name}] gate reloaded, URL unchanged: ${state.url}.${allowNote}` };
|
|
681
756
|
}
|
|
@@ -745,6 +820,7 @@ function statusDetail(state) {
|
|
|
745
820
|
emit.text(` Provider: ${state.provider}${state.tailscale_mode ? ` (${state.tailscale_mode})` : ""}\n`);
|
|
746
821
|
emit.text(` URL: ${state.url}\n`);
|
|
747
822
|
emit.text(` Forwarding: ${state.target} (Host: ${state.vhost})\n`);
|
|
823
|
+
emit.text(` Paths: ${formatPaths(state.allow_paths)}\n`);
|
|
748
824
|
emit.text(` Gate port: ${state.gate_port}\n`);
|
|
749
825
|
if (state.provider === "cloudflare") {
|
|
750
826
|
emit.text(` Allowed IPs: ${cfg.allowed_ips.join(", ")}\n`);
|
|
@@ -786,6 +862,7 @@ function status(opts) {
|
|
|
786
862
|
state: instanceState(s),
|
|
787
863
|
url: s.url,
|
|
788
864
|
fwd: `${s.target} (${s.vhost})`,
|
|
865
|
+
paths: formatPaths(s.allow_paths),
|
|
789
866
|
port: String(s.gate_port),
|
|
790
867
|
up: fmtUptime(s.started_at),
|
|
791
868
|
}));
|
|
@@ -795,12 +872,13 @@ function status(opts) {
|
|
|
795
872
|
state: 5,
|
|
796
873
|
url: Math.max(3, ...rows.map((r) => r.url.length)),
|
|
797
874
|
fwd: Math.max(10, ...rows.map((r) => r.fwd.length)),
|
|
875
|
+
paths: Math.max(5, ...rows.map((r) => r.paths.length)),
|
|
798
876
|
port: 4,
|
|
799
877
|
};
|
|
800
878
|
const pad = (s, n) => s.padEnd(n);
|
|
801
|
-
emit.text(`${pad("NAME", w.name)} ${pad("PROVIDER", w.provider)} ${pad("STATE", w.state)} ${pad("URL", w.url)} ${pad("FORWARDING", w.fwd)} ${pad("PORT", w.port)} UPTIME\n`);
|
|
879
|
+
emit.text(`${pad("NAME", w.name)} ${pad("PROVIDER", w.provider)} ${pad("STATE", w.state)} ${pad("URL", w.url)} ${pad("FORWARDING", w.fwd)} ${pad("PATHS", w.paths)} ${pad("PORT", w.port)} UPTIME\n`);
|
|
802
880
|
for (const r of rows) {
|
|
803
|
-
emit.text(`${pad(r.name, w.name)} ${pad(r.provider, w.provider)} ${pad(r.state, w.state)} ${pad(r.url, w.url)} ${pad(r.fwd, w.fwd)} ${pad(r.port, w.port)} ${r.up}\n`);
|
|
881
|
+
emit.text(`${pad(r.name, w.name)} ${pad(r.provider, w.provider)} ${pad(r.state, w.state)} ${pad(r.url, w.url)} ${pad(r.fwd, w.fwd)} ${pad(r.paths, w.paths)} ${pad(r.port, w.port)} ${r.up}\n`);
|
|
804
882
|
}
|
|
805
883
|
}
|
|
806
884
|
function logs(opts) {
|
|
@@ -886,6 +964,8 @@ export function registerTunnelCommand(program, emitParam, contextParam) {
|
|
|
886
964
|
.option("--visibility <visibility>", "Tailscale only: tailnet (Serve, default) or public (Funnel)", "tailnet")
|
|
887
965
|
.option("--path <path>", "Tailscale only: URL path mount (default: / for default, /<name> for named instances)")
|
|
888
966
|
.option("--https-port <port>", "Tailscale only: HTTPS listen port (default: 443)", "443")
|
|
967
|
+
.option("--allow-path <prefix>", "URL path prefix the tunnel shares (repeatable, required). Every other path is refused; " +
|
|
968
|
+
"pass / to publish the whole upstream.", collectPath, [])
|
|
889
969
|
.action(up);
|
|
890
970
|
cmd
|
|
891
971
|
.command("down")
|
|
@@ -17,6 +17,9 @@ export declare const BUILTIN_ADAPTER_PROFILES: {
|
|
|
17
17
|
readonly date: "2026-08-18";
|
|
18
18
|
readonly version: "2.1.233 (Claude Code)";
|
|
19
19
|
};
|
|
20
|
+
readonly fullAccess: {
|
|
21
|
+
readonly argv: readonly ["--permission-mode", "bypassPermissions", "--settings", "{\"sandbox\":{\"enabled\":false}}"];
|
|
22
|
+
};
|
|
20
23
|
readonly capabilities: AdapterCapabilities;
|
|
21
24
|
};
|
|
22
25
|
readonly codex: {
|
|
@@ -41,6 +44,9 @@ export declare const BUILTIN_ADAPTER_PROFILES: {
|
|
|
41
44
|
};
|
|
42
45
|
readonly writableRoots: true;
|
|
43
46
|
};
|
|
47
|
+
readonly fullAccess: {
|
|
48
|
+
readonly argv: readonly ["--sandbox", "danger-full-access"];
|
|
49
|
+
};
|
|
44
50
|
readonly capabilities: AdapterCapabilities;
|
|
45
51
|
};
|
|
46
52
|
readonly cursor: {
|
|
@@ -58,6 +64,9 @@ export declare const BUILTIN_ADAPTER_PROFILES: {
|
|
|
58
64
|
readonly date: "2026-09-20";
|
|
59
65
|
readonly version: "2026.09.18-9a7762b";
|
|
60
66
|
};
|
|
67
|
+
readonly fullAccess: {
|
|
68
|
+
readonly argv: readonly ["--sandbox", "disabled"];
|
|
69
|
+
};
|
|
61
70
|
readonly capabilities: AdapterCapabilities;
|
|
62
71
|
};
|
|
63
72
|
readonly opencode: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"profiles.d.ts","sourceRoot":"","sources":["../../../src/core/adapters/profiles.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,cAAc,EAAmB,MAAM,YAAY,CAAC;AAgCvF;gFACgF;AAChF,eAAO,MAAM,wBAAwB
|
|
1
|
+
{"version":3,"file":"profiles.d.ts","sourceRoot":"","sources":["../../../src/core/adapters/profiles.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,cAAc,EAAmB,MAAM,YAAY,CAAC;AAgCvF;gFACgF;AAChF,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuJc,CAAC;AAEpD,MAAM,MAAM,gBAAgB,GAAG,MAAM,OAAO,wBAAwB,CAAC;AAErE,eAAO,MAAM,mBAAmB,8DAE/B,CAAC;AAEF,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,EAAE,IAAI,gBAAgB,CAEnE;AAED,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS,CAE5E;AAED,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAUlF"}
|