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,114 @@
|
|
|
1
|
+
import { type Command, InvalidArgumentError } from "commander";
|
|
2
|
+
import type { EmitContext } from "../commander.ts";
|
|
3
|
+
import {
|
|
4
|
+
type DiskGroup,
|
|
5
|
+
type DiskOptions,
|
|
6
|
+
type DiskReport,
|
|
7
|
+
diskUsage,
|
|
8
|
+
parseDiskSize,
|
|
9
|
+
} from "../lib/disk-usage.ts";
|
|
10
|
+
|
|
11
|
+
function positive(value: string): number {
|
|
12
|
+
if (!/^\d+$/.test(value) || !Number.isSafeInteger(Number(value)) || Number(value) < 1)
|
|
13
|
+
throw new InvalidArgumentError("requires a positive integer");
|
|
14
|
+
return Number(value);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function registerDiskCommand(program: Command, emit: EmitContext): void {
|
|
18
|
+
program
|
|
19
|
+
.command("disk [path]")
|
|
20
|
+
.description("Measure checkout disk usage, including nested repositories and ignored files")
|
|
21
|
+
.option(
|
|
22
|
+
"--git <states>",
|
|
23
|
+
"Filter: tracked, untracked, ignored, git-metadata, non-repository, unknown (comma-separated)",
|
|
24
|
+
)
|
|
25
|
+
.option(
|
|
26
|
+
"--type <types>",
|
|
27
|
+
"File extensions or video, audio, image, media, archive (comma-separated)",
|
|
28
|
+
)
|
|
29
|
+
.option(
|
|
30
|
+
"--exclude <paths>",
|
|
31
|
+
"Skip directory names at any depth or root-relative paths; repeatable, comma-separated",
|
|
32
|
+
(value: string, previous: string[]) => [...previous, value],
|
|
33
|
+
[],
|
|
34
|
+
)
|
|
35
|
+
.option("--group <by>", "Group by directory, repository, extension, git, or files", "directory")
|
|
36
|
+
.option("--depth <n>", "Directory grouping depth; does not limit the scan", positive, 1)
|
|
37
|
+
.option("--top <n>", "Maximum groups and largest files to display", positive, 20)
|
|
38
|
+
.option(
|
|
39
|
+
"--min-size <size>",
|
|
40
|
+
"Include individual files at least this size (e.g. 10MB, 1GiB)",
|
|
41
|
+
parseDiskSize,
|
|
42
|
+
)
|
|
43
|
+
.option("--apparent", "Rank and filter by file lengths instead of allocated blocks")
|
|
44
|
+
.option("--json", "Emit the versioned disk usage report")
|
|
45
|
+
.action(
|
|
46
|
+
(path: string | undefined, options: DiskOptions & { group: DiskGroup; json?: boolean }) => {
|
|
47
|
+
try {
|
|
48
|
+
const { json, ...filters } = options;
|
|
49
|
+
const report = diskUsage(path, filters);
|
|
50
|
+
if (json) {
|
|
51
|
+
emit.config({ format: "json" });
|
|
52
|
+
emit.data(report);
|
|
53
|
+
} else emit.text(renderDiskUsage(report));
|
|
54
|
+
if (!report.complete) emit.setExitCode(1);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
emit.error({
|
|
57
|
+
code: "disk_usage_failed",
|
|
58
|
+
message: error instanceof Error ? error.message : String(error),
|
|
59
|
+
});
|
|
60
|
+
emit.setExitCode(1);
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function renderDiskUsage(report: DiskReport): string {
|
|
67
|
+
const bytes = (value: number): string =>
|
|
68
|
+
value >= 1e9
|
|
69
|
+
? `${(value / 1e9).toFixed(2)} GB`
|
|
70
|
+
: value >= 1e6
|
|
71
|
+
? `${(value / 1e6).toFixed(2)} MB`
|
|
72
|
+
: value >= 1e3
|
|
73
|
+
? `${(value / 1e3).toFixed(2)} KB`
|
|
74
|
+
: `${value} B`;
|
|
75
|
+
const size = (row: { allocated_bytes: number | null; apparent_bytes: number }): string =>
|
|
76
|
+
bytes(
|
|
77
|
+
report.measurement === "allocated"
|
|
78
|
+
? (row.allocated_bytes ?? row.apparent_bytes)
|
|
79
|
+
: row.apparent_bytes,
|
|
80
|
+
);
|
|
81
|
+
const lines = [
|
|
82
|
+
`Disk usage: ${report.root}`,
|
|
83
|
+
`${size(report.totals)} in ${report.totals.files.toLocaleString()} regular files (${report.measurement})`,
|
|
84
|
+
`File lengths: ${bytes(report.totals.apparent_bytes)}. Directory metadata and symlinks are excluded.`,
|
|
85
|
+
"",
|
|
86
|
+
"Git state:",
|
|
87
|
+
];
|
|
88
|
+
for (const [state, totals] of Object.entries(report.git))
|
|
89
|
+
if (totals.files)
|
|
90
|
+
lines.push(
|
|
91
|
+
`${size(totals).padStart(12)} ${String(totals.files).padStart(8)} files ${state}`,
|
|
92
|
+
);
|
|
93
|
+
lines.push(
|
|
94
|
+
"",
|
|
95
|
+
`Largest ${report.filters.group} groups (${report.groups.length} of ${report.group_count}):`,
|
|
96
|
+
);
|
|
97
|
+
for (const row of report.groups)
|
|
98
|
+
lines.push(`${size(row).padStart(12)} ${String(row.files).padStart(8)} files ${row.path}`);
|
|
99
|
+
if (report.filters.group !== "files") {
|
|
100
|
+
lines.push("", "Largest files:");
|
|
101
|
+
for (const row of report.largest_files)
|
|
102
|
+
lines.push(`${size(row).padStart(12)} ${row.git.padEnd(14)} ${row.path}`);
|
|
103
|
+
}
|
|
104
|
+
if (report.skipped.symlinks || report.skipped.special_files || report.skipped.excluded)
|
|
105
|
+
lines.push(
|
|
106
|
+
"",
|
|
107
|
+
`Skipped: ${report.skipped.symlinks} symlinks, ${report.skipped.special_files} special files, ${report.skipped.excluded} excluded entries.`,
|
|
108
|
+
);
|
|
109
|
+
if (report.issues.length) {
|
|
110
|
+
lines.push("", "Incomplete scan:");
|
|
111
|
+
for (const issue of report.issues) lines.push(`${issue.path}: ${issue.message}`);
|
|
112
|
+
}
|
|
113
|
+
return `${lines.join("\n")}\n`;
|
|
114
|
+
}
|
package/src/commands/doctor.ts
CHANGED
|
@@ -18,7 +18,7 @@ import path from "node:path";
|
|
|
18
18
|
import type { Command } from "commander";
|
|
19
19
|
import type { EmitContext } from "../commander.ts";
|
|
20
20
|
import { BUILTIN_ADAPTER_IDS } from "../core/adapters/index.ts";
|
|
21
|
-
import { resolveBinName, ripgrepAutoInstall } from "../core/config.ts";
|
|
21
|
+
import { codexWindowsBridgeConfig, resolveBinName, ripgrepAutoInstall } from "../core/config.ts";
|
|
22
22
|
import {
|
|
23
23
|
countSummarizedSinceV3,
|
|
24
24
|
listDiagnosticSummariesV3,
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
type CodexWslBridgeStatus,
|
|
34
34
|
inspectCodexWslBridge,
|
|
35
35
|
} from "../core/hooks/codex-wsl-bridge.ts";
|
|
36
|
+
import { findUnfinishedHookRuns } from "../core/hooks/run-markers.ts";
|
|
36
37
|
import {
|
|
37
38
|
ADAPTER_BINARIES,
|
|
38
39
|
ADAPTER_INSTALL_HINTS,
|
|
@@ -115,6 +116,8 @@ export async function runChecks(): Promise<Check[]> {
|
|
|
115
116
|
const codexWslStatus = inspectCodexWslBridge();
|
|
116
117
|
const recentCodexMidFlightCount = root ? countRecentCodexMidFlightOnboardings(root) : 0;
|
|
117
118
|
const codexWslBridge = codexWslBridgeCheck(codexWslStatus, recentCodexMidFlightCount);
|
|
119
|
+
const codexHookRuns = codexHookRunsCheck(root);
|
|
120
|
+
const codexWindowsRoute = codexWindowsBridgeCheck(root);
|
|
118
121
|
// Probe the adapter CLIs before the hook check runs, not in output order:
|
|
119
122
|
// an unwired adapter only matters if its CLI is actually installed, and
|
|
120
123
|
// probing once here keeps that join to one spawn per CLI.
|
|
@@ -131,6 +134,8 @@ export async function runChecks(): Promise<Check[]> {
|
|
|
131
134
|
...(codexAuthorization ? [codexAuthorization] : []),
|
|
132
135
|
checkGitHookRegions(),
|
|
133
136
|
...(codexWslBridge ? [codexWslBridge] : []),
|
|
137
|
+
...(codexWindowsRoute ? [codexWindowsRoute] : []),
|
|
138
|
+
...(codexHookRuns ? [codexHookRuns] : []),
|
|
134
139
|
...workflow.map((w) => w.check),
|
|
135
140
|
checkRestic(),
|
|
136
141
|
checkRclone(),
|
|
@@ -189,6 +194,52 @@ export function codexWslBridgeCheck(
|
|
|
189
194
|
};
|
|
190
195
|
}
|
|
191
196
|
|
|
197
|
+
/**
|
|
198
|
+
* Hook runs that started but never finished (ADR 0197). Codex kills a hook at
|
|
199
|
+
* its timeout and the tool proceeds without that hook's checks; the start
|
|
200
|
+
* marker is the only trace such a run leaves.
|
|
201
|
+
*/
|
|
202
|
+
export function codexHookRunsCheck(
|
|
203
|
+
coordRoot: string | null,
|
|
204
|
+
nowMs = Date.now(),
|
|
205
|
+
isAlive?: (pid: number) => boolean,
|
|
206
|
+
): Check | null {
|
|
207
|
+
if (!coordRoot) return null;
|
|
208
|
+
const runs = findUnfinishedHookRuns(coordRoot, nowMs, undefined, isAlive);
|
|
209
|
+
if (runs.count === 0) return null;
|
|
210
|
+
const events = runs.events.map((e) => `${e.event} ×${e.count}`).join(", ");
|
|
211
|
+
return {
|
|
212
|
+
name: "codex:hook runs",
|
|
213
|
+
severity: "warn",
|
|
214
|
+
detail:
|
|
215
|
+
`${runs.count} hook run${runs.count === 1 ? "" : "s"} started but never finished in the last 24h ` +
|
|
216
|
+
`(${events}; latest ${runs.latest}); their tools proceeded without those hooks' checks`,
|
|
217
|
+
hint: "a hook that outlives its timeout is killed; look for a stalled WSL session or a slow hook",
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Report the per-project Windows bridge opt-in, or an invalid value init ignored. */
|
|
222
|
+
export function codexWindowsBridgeCheck(coordRoot: string | null): Check | null {
|
|
223
|
+
if (!coordRoot) return null;
|
|
224
|
+
const bridge = codexWindowsBridgeConfig(coordRoot);
|
|
225
|
+
if (bridge.error) {
|
|
226
|
+
return {
|
|
227
|
+
name: "codex:Windows hook route",
|
|
228
|
+
severity: "warn",
|
|
229
|
+
detail: `${bridge.error}; Codex hooks are wired without commandWindows`,
|
|
230
|
+
hint: "set hooks.codexWindowsBridge.entryPoint to a plain command name in .harnery/config.jsonc, then re-run init",
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
if (!bridge.entryPoint) return null;
|
|
234
|
+
return {
|
|
235
|
+
name: "codex:Windows hook route",
|
|
236
|
+
severity: "ok",
|
|
237
|
+
detail:
|
|
238
|
+
`opted in: Windows-native Codex hooks hand off to \`${bridge.entryPoint}\` from WSL paths ` +
|
|
239
|
+
`when it is on PATH, otherwise run the usual command; bridge health is reported by the bridge itself`,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
192
243
|
const CODEX_MID_FLIGHT_HISTORY_WINDOW_MS = 48 * 60 * 60 * 1000;
|
|
193
244
|
|
|
194
245
|
/** Count bounded, privacy-safe recovery diagnostics so a healthy environment
|
package/src/commands/files.ts
CHANGED
|
@@ -5,7 +5,21 @@ import type { EmitContext, HarneryProgramContext } from "../commander.ts";
|
|
|
5
5
|
import { resolveCoordRoot } from "../core/agents/coord-client.ts";
|
|
6
6
|
import { DEFAULT_WEB_PORT, resolveWebPort } from "../core/config.ts";
|
|
7
7
|
import { coordRootId, localCoordRootIdUrl } from "../lib/coord-root-id.ts";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
encodedRepoPath,
|
|
10
|
+
encodeLinkSafeComponent,
|
|
11
|
+
FILES_ORIGIN_HOST,
|
|
12
|
+
localFilesOriginUrl,
|
|
13
|
+
localFileViewerUrl,
|
|
14
|
+
} from "../lib/local-file-url.ts";
|
|
15
|
+
import { isPathAllowed } from "../lib/tunnel/path-scope.ts";
|
|
16
|
+
import {
|
|
17
|
+
isTunnelStateLive,
|
|
18
|
+
listStates,
|
|
19
|
+
type ProcessAliveCheck,
|
|
20
|
+
type TunnelState,
|
|
21
|
+
tunnelTargetPort,
|
|
22
|
+
} from "../lib/tunnel/state.ts";
|
|
9
23
|
|
|
10
24
|
export { FILES_ORIGIN_HOST } from "../lib/local-file-url.ts";
|
|
11
25
|
|
|
@@ -162,6 +176,72 @@ export function mintLocalFileUrl(input: string, options: LocalFileUrlOptions): L
|
|
|
162
176
|
return { url, relPath, mode };
|
|
163
177
|
}
|
|
164
178
|
|
|
179
|
+
export class TunnelLinkError extends Error {
|
|
180
|
+
constructor(
|
|
181
|
+
message: string,
|
|
182
|
+
readonly hint: string,
|
|
183
|
+
) {
|
|
184
|
+
super(message);
|
|
185
|
+
this.name = "TunnelLinkError";
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function vhostName(vhost: string): string {
|
|
190
|
+
return vhost.replace(/:\d+$/, "").toLowerCase();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function newestFirst(states: TunnelState[]): TunnelState[] {
|
|
194
|
+
return [...states].sort((a, b) => b.started_at.localeCompare(a.started_at));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Turn a minted local URL into the same file's URL on a live tunnel.
|
|
199
|
+
*
|
|
200
|
+
* HTML needs a tunnel in front of the isolated files origin (its vhost is
|
|
201
|
+
* FILES_ORIGIN_HOST), because only that origin renders the page and serves its
|
|
202
|
+
* relative assets; a dashboard tunnel can only show the viewer, which displays
|
|
203
|
+
* HTML as source. Other files need a dashboard tunnel that serves the viewer.
|
|
204
|
+
* In both cases the tunnel's path scope must forward the request, or its gate
|
|
205
|
+
* would refuse the link, so a tunnel that does not cover the file is skipped.
|
|
206
|
+
*/
|
|
207
|
+
export function tunnelFileUrl(
|
|
208
|
+
local: LocalFileUrl,
|
|
209
|
+
port: number,
|
|
210
|
+
states: TunnelState[],
|
|
211
|
+
processAlive?: ProcessAliveCheck,
|
|
212
|
+
): string {
|
|
213
|
+
const live = states.filter(
|
|
214
|
+
(state) => tunnelTargetPort(state) === port && isTunnelStateLive(state, processAlive),
|
|
215
|
+
);
|
|
216
|
+
if (local.mode === "raw") {
|
|
217
|
+
const path = `/${encodedRepoPath(local.relPath)}`;
|
|
218
|
+
const tunnel = newestFirst(live).find(
|
|
219
|
+
(state) =>
|
|
220
|
+
vhostName(state.vhost) === FILES_ORIGIN_HOST && isPathAllowed(path, state.allow_paths),
|
|
221
|
+
);
|
|
222
|
+
if (tunnel) return `${tunnel.url.replace(/\/+$/, "")}${path}`;
|
|
223
|
+
const folder = local.relPath.includes("/")
|
|
224
|
+
? `/${local.relPath.slice(0, local.relPath.lastIndexOf("/"))}`
|
|
225
|
+
: "/";
|
|
226
|
+
throw new TunnelLinkError(
|
|
227
|
+
`no live tunnel serves the files origin for /${local.relPath}`,
|
|
228
|
+
`Start one scoped to the page's folder, for example: tunnel up --name pages --target 127.0.0.1:${port} --vhost ${FILES_ORIGIN_HOST} --allow-path ${folder}`,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
const tunnel = newestFirst(live).find(
|
|
232
|
+
(state) =>
|
|
233
|
+
vhostName(state.vhost) !== FILES_ORIGIN_HOST &&
|
|
234
|
+
["/files", "/api/file", "/_next"].every((path) => isPathAllowed(path, state.allow_paths)),
|
|
235
|
+
);
|
|
236
|
+
if (tunnel) {
|
|
237
|
+
return `${tunnel.url.replace(/\/+$/, "")}/files?path=${encodeLinkSafeComponent(local.relPath)}`;
|
|
238
|
+
}
|
|
239
|
+
throw new TunnelLinkError(
|
|
240
|
+
`no live tunnel serves the dashboard file viewer on port ${port}`,
|
|
241
|
+
`Start one, for example: tunnel up --name files --target 127.0.0.1:${port} --allow-path /files --allow-path /api/file --allow-path /_next`,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
165
245
|
export function registerFilesCommand(
|
|
166
246
|
program: Command,
|
|
167
247
|
emit: EmitContext,
|
|
@@ -180,6 +260,10 @@ export function registerFilesCommand(
|
|
|
180
260
|
.option("--raw", "Use the isolated files origin, regardless of extension")
|
|
181
261
|
.option("--viewer", "Use the dashboard file viewer, regardless of extension")
|
|
182
262
|
.option("--no-verify", "Mint without checking the running dashboard's repository identity")
|
|
263
|
+
.option(
|
|
264
|
+
"--tunnel",
|
|
265
|
+
"Print the file's URL on a live tunnel instead of localhost (HTML uses a files-origin tunnel)",
|
|
266
|
+
)
|
|
183
267
|
.action(
|
|
184
268
|
async (
|
|
185
269
|
filePath: string,
|
|
@@ -189,6 +273,7 @@ export function registerFilesCommand(
|
|
|
189
273
|
raw?: boolean;
|
|
190
274
|
viewer?: boolean;
|
|
191
275
|
verify?: boolean;
|
|
276
|
+
tunnel?: boolean;
|
|
192
277
|
},
|
|
193
278
|
) => {
|
|
194
279
|
if (opts.raw && opts.viewer) {
|
|
@@ -224,8 +309,14 @@ export function registerFilesCommand(
|
|
|
224
309
|
mode: opts.raw ? "raw" : opts.viewer ? "viewer" : "auto",
|
|
225
310
|
});
|
|
226
311
|
if (opts.verify !== false) await verifyDashboardRoot(coordRoot, port);
|
|
227
|
-
|
|
312
|
+
const url = opts.tunnel ? tunnelFileUrl(result, port, listStates(coordRoot)) : result.url;
|
|
313
|
+
emit.text(`${url}\n`);
|
|
228
314
|
} catch (error) {
|
|
315
|
+
if (error instanceof TunnelLinkError) {
|
|
316
|
+
emit.error({ code: "no_tunnel_for_file", message: error.message, hint: error.hint });
|
|
317
|
+
emit.setExitCode(1);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
229
320
|
if (error instanceof DashboardRootError) {
|
|
230
321
|
emit.error({ code: error.code, message: error.message, hint: error.hint });
|
|
231
322
|
emit.setExitCode(1);
|
package/src/commands/init.ts
CHANGED
|
@@ -27,7 +27,12 @@ import { dirname, relative, resolve } from "node:path";
|
|
|
27
27
|
import { fileURLToPath } from "node:url";
|
|
28
28
|
import type { Command } from "commander";
|
|
29
29
|
import type { EmitContext } from "../commander.ts";
|
|
30
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
codexWindowsBridgeConfig,
|
|
32
|
+
DEFAULT_BIN_NAME,
|
|
33
|
+
pinnedBinName,
|
|
34
|
+
stripJsonComments,
|
|
35
|
+
} from "../core/config.ts";
|
|
31
36
|
import { canonicalJsonV3 } from "../core/events/v3/canonical.ts";
|
|
32
37
|
import { ADAPTER_CAPABILITY_PROFILES_V3 } from "../core/events/v3/capabilities.ts";
|
|
33
38
|
import type { EventV3ControlState } from "../core/events/v3/control.ts";
|
|
@@ -48,13 +53,17 @@ import {
|
|
|
48
53
|
CURSOR_STOP_FOLLOWUP_LOOP_LIMIT,
|
|
49
54
|
commandWiresSubcommand,
|
|
50
55
|
diffWiring,
|
|
56
|
+
expectedCommandWindows,
|
|
51
57
|
groupCommands,
|
|
52
58
|
type HookGroup,
|
|
53
59
|
hookCommand,
|
|
60
|
+
hookTimeoutFor,
|
|
54
61
|
isAgentHookCommand,
|
|
55
62
|
makeEntry,
|
|
63
|
+
orderHandlerKeys,
|
|
56
64
|
type SettingsFile,
|
|
57
65
|
summarizeAdapterWiring,
|
|
66
|
+
type WiringOptions,
|
|
58
67
|
} from "../core/hooks/adapter/wiring.ts";
|
|
59
68
|
import { detectInstalledAdapters } from "../core/workflow/adapters.ts";
|
|
60
69
|
import { applyIndexerExclusions } from "../lib/indexer-exclusions.ts";
|
|
@@ -147,7 +156,11 @@ function checkAdapterHooks(
|
|
|
147
156
|
let drift = false;
|
|
148
157
|
try {
|
|
149
158
|
const settings = JSON.parse(readFileSync(settingsPath, "utf8")) as SettingsFile;
|
|
150
|
-
const hookDiff = diffWiring(settings, spec, {
|
|
159
|
+
const hookDiff = diffWiring(settings, spec, {
|
|
160
|
+
agentHookPath: agentHook,
|
|
161
|
+
adapter,
|
|
162
|
+
windowsBridgeEntryPoint: codexWindowsBridgeConfig(projectRoot).entryPoint,
|
|
163
|
+
});
|
|
151
164
|
drift =
|
|
152
165
|
hookDiff.missing.length > 0 ||
|
|
153
166
|
hookDiff.stale.length > 0 ||
|
|
@@ -460,11 +473,16 @@ export function registerInitCommand(program: Command, emit: EmitContext, binName
|
|
|
460
473
|
} else {
|
|
461
474
|
settings = {};
|
|
462
475
|
}
|
|
476
|
+
const bridge = codexWindowsBridgeConfig(projectRoot);
|
|
477
|
+
if (bridge.error && spec.windowsBridgeHooks) {
|
|
478
|
+
actions.push(`⚠ ${bridge.error}; writing ${adapter} hooks without commandWindows`);
|
|
479
|
+
}
|
|
463
480
|
const { wired, already, removed, upgraded } = wireHooks(
|
|
464
481
|
settings,
|
|
465
482
|
spec,
|
|
466
483
|
agentHook,
|
|
467
484
|
adapter,
|
|
485
|
+
{ windowsBridgeEntryPoint: bridge.entryPoint },
|
|
468
486
|
);
|
|
469
487
|
|
|
470
488
|
if (wired === 0 && removed === 0 && upgraded === 0) {
|
|
@@ -588,6 +606,7 @@ export function wireHooks(
|
|
|
588
606
|
spec: AdapterSpec,
|
|
589
607
|
agentHookPath: string,
|
|
590
608
|
adapter: AdapterId,
|
|
609
|
+
options?: WiringOptions,
|
|
591
610
|
): { wired: number; already: number; removed: number; upgraded: number } {
|
|
592
611
|
if (spec.rootVersion !== undefined && settings.version === undefined) {
|
|
593
612
|
settings.version = spec.rootVersion;
|
|
@@ -606,6 +625,19 @@ export function wireHooks(
|
|
|
606
625
|
}
|
|
607
626
|
for (const { settingsKey, subcommand } of spec.events) {
|
|
608
627
|
const command = hookCommand(spec, agentHookPath, subcommand, adapter);
|
|
628
|
+
const timeout = hookTimeoutFor(spec, subcommand);
|
|
629
|
+
const commandWindows = expectedCommandWindows(
|
|
630
|
+
spec,
|
|
631
|
+
agentHookPath,
|
|
632
|
+
subcommand,
|
|
633
|
+
adapter,
|
|
634
|
+
options,
|
|
635
|
+
);
|
|
636
|
+
const handlerFields = {
|
|
637
|
+
timeout,
|
|
638
|
+
commandWindows,
|
|
639
|
+
windowsBridge: spec.windowsBridgeHooks === true,
|
|
640
|
+
};
|
|
609
641
|
let present = false;
|
|
610
642
|
for (const key of Object.keys(settings.hooks)) {
|
|
611
643
|
if (key === settingsKey) continue;
|
|
@@ -616,7 +648,7 @@ export function wireHooks(
|
|
|
616
648
|
const groups = settings.hooks[settingsKey] ?? [];
|
|
617
649
|
const nextGroups: HookGroup[] = [];
|
|
618
650
|
for (const group of groups) {
|
|
619
|
-
const normalized = normalizeEventGroup(group, subcommand, command, present);
|
|
651
|
+
const normalized = normalizeEventGroup(group, subcommand, command, handlerFields, present);
|
|
620
652
|
if (normalized.found && !present) present = true;
|
|
621
653
|
upgraded += normalized.upgraded;
|
|
622
654
|
removed += normalized.removed;
|
|
@@ -635,7 +667,7 @@ export function wireHooks(
|
|
|
635
667
|
command,
|
|
636
668
|
spec.entryShape === "cursor" && subcommand === "stop"
|
|
637
669
|
? { loop_limit: CURSOR_STOP_FOLLOWUP_LOOP_LIMIT }
|
|
638
|
-
:
|
|
670
|
+
: { timeout, commandWindows },
|
|
639
671
|
),
|
|
640
672
|
);
|
|
641
673
|
settings.hooks[settingsKey] = current;
|
|
@@ -658,6 +690,7 @@ function normalizeEventGroup(
|
|
|
658
690
|
group: HookGroup,
|
|
659
691
|
subcommand: string,
|
|
660
692
|
canonical: string,
|
|
693
|
+
fields: { timeout?: number; commandWindows?: string; windowsBridge: boolean },
|
|
661
694
|
alreadyFound: boolean,
|
|
662
695
|
): { group: HookGroup | null; found: boolean; upgraded: number; removed: number } {
|
|
663
696
|
let found = false;
|
|
@@ -676,7 +709,8 @@ function normalizeEventGroup(
|
|
|
676
709
|
}
|
|
677
710
|
if ("hooks" in group && Array.isArray(group.hooks)) {
|
|
678
711
|
const kept = [] as typeof group.hooks;
|
|
679
|
-
for (const
|
|
712
|
+
for (const original of group.hooks) {
|
|
713
|
+
let hook = original;
|
|
680
714
|
if (typeof hook.command !== "string" || !commandWiresSubcommand(hook.command, subcommand)) {
|
|
681
715
|
kept.push(hook);
|
|
682
716
|
continue;
|
|
@@ -686,8 +720,24 @@ function normalizeEventGroup(
|
|
|
686
720
|
continue;
|
|
687
721
|
}
|
|
688
722
|
found = true;
|
|
723
|
+
let changed = false;
|
|
689
724
|
if (hook.command !== canonical) {
|
|
690
725
|
hook.command = canonical;
|
|
726
|
+
changed = true;
|
|
727
|
+
}
|
|
728
|
+
if (fields.timeout !== undefined && hook.timeout !== fields.timeout) {
|
|
729
|
+
hook.timeout = fields.timeout;
|
|
730
|
+
changed = true;
|
|
731
|
+
}
|
|
732
|
+
// Harnery owns commandWindows on its own handlers for adapters that run
|
|
733
|
+
// it: write the opted-in command, or remove one a project opted out of.
|
|
734
|
+
if (fields.windowsBridge && hook.commandWindows !== fields.commandWindows) {
|
|
735
|
+
if (fields.commandWindows === undefined) delete hook.commandWindows;
|
|
736
|
+
else hook.commandWindows = fields.commandWindows;
|
|
737
|
+
changed = true;
|
|
738
|
+
}
|
|
739
|
+
if (changed) {
|
|
740
|
+
hook = orderHandlerKeys(hook);
|
|
691
741
|
upgraded++;
|
|
692
742
|
}
|
|
693
743
|
kept.push(hook);
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
4
|
+
import type { Command } from "commander";
|
|
5
|
+
import type { EmitContext, HarneryProgramContext } from "../commander.ts";
|
|
6
|
+
import { resolveCoordRoot } from "../core/agents/coord-client.ts";
|
|
7
|
+
import {
|
|
8
|
+
readCoordinationViewV3,
|
|
9
|
+
requireAuthoritySafeCoordinationViewV3,
|
|
10
|
+
} from "../core/events/v3/coordination-view.ts";
|
|
11
|
+
import { liveInstanceIdV3 } from "../core/events/v3/live-route-observer.ts";
|
|
12
|
+
import { resolveOwner } from "../core/hooks/resolve/owner.ts";
|
|
13
|
+
import { guardedRemove, type RemoveOptions } from "../lib/guarded-remove.ts";
|
|
14
|
+
|
|
15
|
+
export function registerRmCommand(
|
|
16
|
+
program: Command,
|
|
17
|
+
emit: EmitContext,
|
|
18
|
+
context?: HarneryProgramContext,
|
|
19
|
+
): void {
|
|
20
|
+
program
|
|
21
|
+
.command("rm <paths...>")
|
|
22
|
+
.description("Preview guarded removal of untracked files; --yes applies permanent deletion")
|
|
23
|
+
.requiredOption(
|
|
24
|
+
"--root <directory>",
|
|
25
|
+
"Allowed directory; each target must be strictly inside it",
|
|
26
|
+
)
|
|
27
|
+
.option("-r, --recursive", "Permit directory removal after inspecting its complete contents")
|
|
28
|
+
.option(
|
|
29
|
+
"--allow-protected <path>",
|
|
30
|
+
"Remove this one protected path (Git metadata, credentials, Harnery state, keys) inside a target; repeat per path",
|
|
31
|
+
(value: string, previous: string[] = []) => [...previous, value],
|
|
32
|
+
)
|
|
33
|
+
.option("--yes", "Permanently remove validated targets")
|
|
34
|
+
.option("--dry-run", "Preview only, even with --yes")
|
|
35
|
+
.option("--json", "Emit the versioned removal report")
|
|
36
|
+
.action((paths: string[], options: RemoveOptions & { json?: boolean }) => {
|
|
37
|
+
try {
|
|
38
|
+
const coordRoot = context?.resolveCoordRoot?.() ?? context?.repoRoot ?? resolveCoordRoot();
|
|
39
|
+
const checkClaims = (targets: string[]): void => {
|
|
40
|
+
if (!coordRoot || !existsSync(resolve(coordRoot, ".harnery"))) return;
|
|
41
|
+
const view = requireAuthoritySafeCoordinationViewV3(readCoordinationViewV3(coordRoot));
|
|
42
|
+
const owner = resolveOwner({ payload: null, coordRoot });
|
|
43
|
+
const ownId = owner ? liveInstanceIdV3(owner.instance_id) : undefined;
|
|
44
|
+
const ownSession = ownId ? view.instances[ownId]?.session_id : undefined;
|
|
45
|
+
for (const peer of Object.values(view.instances)) {
|
|
46
|
+
if (
|
|
47
|
+
!peer.authority_eligible ||
|
|
48
|
+
peer.instance_id === ownId ||
|
|
49
|
+
(ownSession && peer.session_id === ownSession)
|
|
50
|
+
)
|
|
51
|
+
continue;
|
|
52
|
+
for (const claim of peer.files_touched) {
|
|
53
|
+
const claimed = resolve(coordRoot, claim);
|
|
54
|
+
for (const target of targets) {
|
|
55
|
+
if (overlaps(target, claimed))
|
|
56
|
+
throw new Error(
|
|
57
|
+
`Target overlaps another agent's claim: ${claim} (${peer.instance_id})`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
// The workspace this command coordinates, and the checkout it runs in:
|
|
64
|
+
// their own .git and .harnery can never be allowed.
|
|
65
|
+
const liveRoots = [coordRoot, gitTopLevel(process.cwd())].filter((dir): dir is string =>
|
|
66
|
+
Boolean(dir),
|
|
67
|
+
);
|
|
68
|
+
const report = guardedRemove(paths, { ...options, liveRoots }, checkClaims);
|
|
69
|
+
if (options.json) {
|
|
70
|
+
emit.config({ format: "json" });
|
|
71
|
+
emit.data(report);
|
|
72
|
+
} else {
|
|
73
|
+
const lines = [
|
|
74
|
+
`${report.applied ? "Removed" : "Would remove"} ${report.targets.length} target(s), ${report.entries} entries, ${report.bytes} bytes:`,
|
|
75
|
+
...report.targets,
|
|
76
|
+
];
|
|
77
|
+
if (report.protected.length)
|
|
78
|
+
lines.push(
|
|
79
|
+
"Protected paths named with --allow-protected:",
|
|
80
|
+
...report.protected.map((path) => ` ${path}`),
|
|
81
|
+
);
|
|
82
|
+
if (report.links.length)
|
|
83
|
+
lines.push(
|
|
84
|
+
"Links removed as links (their targets are untouched):",
|
|
85
|
+
...report.links.map((link) => ` ${link.path} -> ${link.target}`),
|
|
86
|
+
);
|
|
87
|
+
if (!report.applied)
|
|
88
|
+
lines.push("Preview only. Add --yes to permanently delete these targets.");
|
|
89
|
+
emit.text(lines.join("\n"));
|
|
90
|
+
}
|
|
91
|
+
} catch (error) {
|
|
92
|
+
emit.error({
|
|
93
|
+
code: "removal_refused",
|
|
94
|
+
message: error instanceof Error ? error.message : String(error),
|
|
95
|
+
});
|
|
96
|
+
emit.setExitCode(1);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function gitTopLevel(dir: string): string | undefined {
|
|
102
|
+
const result = spawnSync("git", ["-C", dir, "rev-parse", "--show-toplevel"], {
|
|
103
|
+
encoding: "utf8",
|
|
104
|
+
timeout: 10_000,
|
|
105
|
+
});
|
|
106
|
+
return result.status === 0 ? result.stdout.trim() || undefined : undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function overlaps(a: string, b: string): boolean {
|
|
110
|
+
const inside = (parent: string, child: string) => {
|
|
111
|
+
const rel = relative(parent, child);
|
|
112
|
+
return rel === "" || (rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel));
|
|
113
|
+
};
|
|
114
|
+
return inside(a, b) || inside(b, a);
|
|
115
|
+
}
|