heedvane 0.0.0-stage → 0.2.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/README.md +62 -2
- package/dist/apply.d.ts +104 -0
- package/dist/apply.js +388 -0
- package/dist/backend/client.d.ts +40 -0
- package/dist/backend/client.js +32 -0
- package/dist/backend/credentials.d.ts +15 -0
- package/dist/backend/credentials.js +76 -0
- package/dist/backend/login.d.ts +33 -0
- package/dist/backend/login.js +265 -0
- package/dist/backend/repo-remote.d.ts +15 -0
- package/dist/backend/repo-remote.js +61 -0
- package/dist/backend/sign-in-change.d.ts +12 -0
- package/dist/backend/sign-in-change.js +72 -0
- package/dist/best-effort.d.ts +4 -0
- package/dist/best-effort.js +25 -0
- package/dist/cli.d.ts +29 -0
- package/dist/cli.js +666 -0
- package/dist/clients/definitions.d.ts +113 -0
- package/dist/clients/definitions.js +427 -0
- package/dist/clients/hermes-plugin.d.ts +33 -0
- package/dist/clients/hermes-plugin.js +247 -0
- package/dist/commands/heedvane.d.ts +15 -0
- package/dist/commands/heedvane.js +74 -0
- package/dist/commands/memory.d.ts +10 -0
- package/dist/commands/memory.js +106 -0
- package/dist/config-doc.d.ts +97 -0
- package/dist/config-doc.js +465 -0
- package/dist/dispatchers/posix.d.ts +41 -0
- package/dist/dispatchers/posix.js +208 -0
- package/dist/dispatchers/snippets.d.ts +6 -0
- package/dist/dispatchers/snippets.js +39 -0
- package/dist/dispatchers/windows.d.ts +15 -0
- package/dist/dispatchers/windows.js +50 -0
- package/dist/doctor.d.ts +37 -0
- package/dist/doctor.js +303 -0
- package/dist/events/contract.d.ts +54 -0
- package/dist/events/contract.js +87 -0
- package/dist/events/normalize.d.ts +36 -0
- package/dist/events/normalize.js +143 -0
- package/dist/guards.d.ts +18 -0
- package/dist/guards.js +28 -0
- package/dist/heed-file.d.ts +78 -0
- package/dist/heed-file.js +316 -0
- package/dist/installer.d.ts +47 -0
- package/dist/installer.js +174 -0
- package/dist/listener/background.d.ts +8 -0
- package/dist/listener/background.js +36 -0
- package/dist/listener/endpoint.d.ts +26 -0
- package/dist/listener/endpoint.js +125 -0
- package/dist/listener/forward.d.ts +36 -0
- package/dist/listener/forward.js +257 -0
- package/dist/listener/serve.d.ts +2 -0
- package/dist/listener/serve.js +155 -0
- package/dist/listener/server.d.ts +48 -0
- package/dist/listener/server.js +251 -0
- package/dist/listener/spool.d.ts +28 -0
- package/dist/listener/spool.js +60 -0
- package/dist/mcp-server.d.ts +9 -0
- package/dist/mcp-server.js +187 -0
- package/dist/mcp.d.ts +57 -0
- package/dist/mcp.js +83 -0
- package/dist/memory/private-memory.d.ts +12 -0
- package/dist/memory/private-memory.js +54 -0
- package/dist/memory/table-of-contents.d.ts +10 -0
- package/dist/memory/table-of-contents.js +80 -0
- package/dist/memory/team-memory.d.ts +60 -0
- package/dist/memory/team-memory.js +54 -0
- package/dist/plan.d.ts +77 -0
- package/dist/plan.js +288 -0
- package/dist/render.d.ts +32 -0
- package/dist/render.js +132 -0
- package/dist/reply/harness-output.d.ts +57 -0
- package/dist/reply/harness-output.js +88 -0
- package/dist/reply/reply.d.ts +65 -0
- package/dist/reply/reply.js +169 -0
- package/dist/reply/verdict.d.ts +28 -0
- package/dist/reply/verdict.js +17 -0
- package/dist/skill/bundle.d.ts +23 -0
- package/dist/skill/bundle.js +102 -0
- package/dist/skill/content.d.ts +16 -0
- package/dist/skill/content.js +182 -0
- package/dist/skills/approved.d.ts +31 -0
- package/dist/skills/approved.js +123 -0
- package/dist/skills/check.d.ts +21 -0
- package/dist/skills/check.js +83 -0
- package/dist/state.d.ts +51 -0
- package/dist/state.js +118 -0
- package/dist/steering/rules.d.ts +49 -0
- package/dist/steering/rules.js +165 -0
- package/dist/steering/steer.d.ts +29 -0
- package/dist/steering/steer.js +64 -0
- package/dist/steering/violations.d.ts +27 -0
- package/dist/steering/violations.js +24 -0
- package/dist/uninstall.d.ts +23 -0
- package/dist/uninstall.js +159 -0
- package/package.json +35 -4
- package/skills/heedvane-hooks/SKILL.md +87 -0
- package/skills/heedvane-hooks/references/events.md +69 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,666 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* heedvane-hook — command routing: init | setup | doctor | uninstall |
|
|
4
|
+
* listener | status | mcp. Plain argv parsing (no commander); all state
|
|
5
|
+
* mutations flow through plan → apply; --dry-run writes nothing.
|
|
6
|
+
*/
|
|
7
|
+
import { createWriteStream, existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
9
|
+
import { constants as osConstants, homedir, platform } from "node:os";
|
|
10
|
+
import { dirname, join } from "node:path";
|
|
11
|
+
import { supportedClients } from "./clients/definitions.js";
|
|
12
|
+
import { installHermesPlugin } from "./clients/hermes-plugin.js";
|
|
13
|
+
import { applyEntry } from "./apply.js";
|
|
14
|
+
import { DEFAULT_HEED_YAML_TEMPLATE, heedvaneConfigAt, loadHeedFile, loadHeedFileAt, withInstallChoices, withServerUrl } from "./heed-file.js";
|
|
15
|
+
import { planActions, desiredEntriesFor, isClientDetected } from "./plan.js";
|
|
16
|
+
import { appliedFailed, capabilitySummary, renderApplied, renderDetection, renderPlan, wouldInstallSummary } from "./render.js";
|
|
17
|
+
import { runDoctor } from "./doctor.js";
|
|
18
|
+
import { installSkill } from "./skill/bundle.js";
|
|
19
|
+
import { canonicalSkillFiles, skillDigest } from "./skill/content.js";
|
|
20
|
+
import { emptyState, readState, writeStateAtomic } from "./state.js";
|
|
21
|
+
import { writeDispatcherScript } from "./dispatchers/posix.js";
|
|
22
|
+
import { removeDeferredHookGroups, removeStaleHookGroups, uninstall } from "./uninstall.js";
|
|
23
|
+
import { statusText, runMcpStdio } from "./mcp-server.js";
|
|
24
|
+
import { errorMessage } from "./guards.js";
|
|
25
|
+
import { ensureEndpoint } from "./listener/endpoint.js";
|
|
26
|
+
import { listenerLogPath, startListenerInBackground } from "./listener/background.js";
|
|
27
|
+
import { serveHooks } from "./listener/serve.js";
|
|
28
|
+
import { cmdAuth, cmdLogin, cmdSkills } from "./commands/heedvane.js";
|
|
29
|
+
import { cmdMemory } from "./commands/memory.js";
|
|
30
|
+
import { backendAt } from "./backend/credentials.js";
|
|
31
|
+
import { readLogin } from "./backend/login.js";
|
|
32
|
+
import { agentLines, askChoices, brandIntro, confirmInstall, installCancelled, installFinished, installWithSpinner, listenerStep, promptLine, showInstalled } from "./installer.js";
|
|
33
|
+
const USAGE = ` heedvane-hook — cross-agent steering, guardrails & analysis
|
|
34
|
+
|
|
35
|
+
Usage: npx heedvane [command] [flags] (installed: heedvane <command>)
|
|
36
|
+
npx heedvane on its own, at a terminal, opens the installer: tick agents and features, confirm.
|
|
37
|
+
|
|
38
|
+
Commands:
|
|
39
|
+
connect set up this machine: the installer at a terminal; with --yes (or in a script) every detected agent at once
|
|
40
|
+
run -- <cmd> CI, containers and software factories: Heedvane answers the hooks for the length of one agent run
|
|
41
|
+
init write ~/.heed/heed.yaml (if missing), plan, apply
|
|
42
|
+
setup plan + apply from an existing heed.yaml
|
|
43
|
+
doctor scoped, remediation-carrying health check
|
|
44
|
+
uninstall marker-verified removal (never touches heed.yaml)
|
|
45
|
+
listener listener controls (start): answers hooks with skill checks, steering and memory
|
|
46
|
+
login sign in to Heedvane in the browser (~/.heed/login.json, 0600; refreshed automatically)
|
|
47
|
+
auth org CI/headless: store an organization API key read from stdin (~/.heed/credentials.json, 0600)
|
|
48
|
+
skills sync install the organization's approved skills and cache the list hooks enforce
|
|
49
|
+
memory list|show|search browse this repository's team memory (a folder, one memory, or a question)
|
|
50
|
+
status quick state summary
|
|
51
|
+
mcp run the MCP stdio server (what agents register)
|
|
52
|
+
|
|
53
|
+
Flags: --dry-run --yes --json --agent <id> (repeatable) --client <id> --purge-config --project
|
|
54
|
+
--server <url> (connect/init: the Heedvane app to use, saved as heedvane.url; --url also works)
|
|
55
|
+
--org <id> (connect/init: the organization the sign-in must belong to)`;
|
|
56
|
+
function resolveHome(env) {
|
|
57
|
+
return env.HEED_HOME ?? homedir();
|
|
58
|
+
}
|
|
59
|
+
/** Repeated flag values, comma-split: `--agent a,b` and `--agent a --agent b` both feed the list. */
|
|
60
|
+
function parseRepeatedFlag(args, flag) {
|
|
61
|
+
const values = [];
|
|
62
|
+
for (let i = 0; i < args.length; i++) {
|
|
63
|
+
if (args[i] === flag) {
|
|
64
|
+
values.push(...(args[i + 1] ?? "").split(",").filter(Boolean));
|
|
65
|
+
i += 1;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return values;
|
|
69
|
+
}
|
|
70
|
+
/** The value after `name` in the raw arguments, if the flag is there. */
|
|
71
|
+
function flagValue(args, name) {
|
|
72
|
+
const at = args.indexOf(name);
|
|
73
|
+
return at < 0 ? undefined : args[at + 1];
|
|
74
|
+
}
|
|
75
|
+
/** Positional (non-flag) arguments, skipping the values of the repeatable flags. */
|
|
76
|
+
function positionalArgs(args, repeatableFlags) {
|
|
77
|
+
const positional = [];
|
|
78
|
+
for (let i = 0; i < args.length; i++) {
|
|
79
|
+
if (repeatableFlags.includes(args[i])) {
|
|
80
|
+
i += 1; // skip the value
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (!args[i].startsWith("--"))
|
|
84
|
+
positional.push(args[i]);
|
|
85
|
+
}
|
|
86
|
+
return positional;
|
|
87
|
+
}
|
|
88
|
+
/** The parsed argv: boolean flags, repeatable flag values, and positional command words. */
|
|
89
|
+
function parseInvocation(args) {
|
|
90
|
+
return {
|
|
91
|
+
flags: {
|
|
92
|
+
dryRun: args.includes("--dry-run"),
|
|
93
|
+
yes: args.includes("--yes"),
|
|
94
|
+
json: args.includes("--json"),
|
|
95
|
+
purge: args.includes("--purge-config"),
|
|
96
|
+
},
|
|
97
|
+
agents: parseRepeatedFlag(args, "--agent"),
|
|
98
|
+
clients: parseRepeatedFlag(args, "--client"),
|
|
99
|
+
server: flagValue(args, "--server") ?? flagValue(args, "--url"),
|
|
100
|
+
org: flagValue(args, "--org"),
|
|
101
|
+
positional: positionalArgs(args, ["--agent", "--client", "--folder", "--offset", "--url", "--server", "--org", "--token"]),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
async function main(argv, opts = {}) {
|
|
105
|
+
const ctx = commandContext(argv, opts);
|
|
106
|
+
const command = ctx.positional[0];
|
|
107
|
+
if (command === undefined && takesInstaller(ctx))
|
|
108
|
+
return cmdInstall(ctx);
|
|
109
|
+
if (!command || command === "help" || command === "--help") {
|
|
110
|
+
ctx.out(USAGE);
|
|
111
|
+
return command ? 0 : 2;
|
|
112
|
+
}
|
|
113
|
+
return routeCommand(command, ctx);
|
|
114
|
+
}
|
|
115
|
+
function commandContext(argv, opts) {
|
|
116
|
+
const env = opts.env ?? process.env;
|
|
117
|
+
const args = [...argv];
|
|
118
|
+
return {
|
|
119
|
+
...parseInvocation(args),
|
|
120
|
+
home: resolveHome(env),
|
|
121
|
+
env,
|
|
122
|
+
out: opts.write ?? ((line) => console.log(line)),
|
|
123
|
+
args,
|
|
124
|
+
readStdin: opts.readStdin ?? readProcessStdin,
|
|
125
|
+
openBrowser: opts.openBrowser ?? openSystemBrowser,
|
|
126
|
+
interactive: opts.interactive ?? (process.stdin.isTTY === true && process.stdout.isTTY === true),
|
|
127
|
+
cwd: opts.cwd ?? process.cwd(),
|
|
128
|
+
prompts: opts.prompts ?? {},
|
|
129
|
+
startListener: opts.startListener ?? ((home) => startListenerInBackground(home, fileURLToPath(import.meta.url), env)),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
async function routeCommand(command, ctx) {
|
|
133
|
+
switch (command) {
|
|
134
|
+
case "connect":
|
|
135
|
+
return takesInstaller(ctx) ? cmdInstall(ctx) : cmdInit({ ...ctx, flags: { ...ctx.flags, yes: true } });
|
|
136
|
+
case "init":
|
|
137
|
+
return cmdInit(ctx);
|
|
138
|
+
case "setup":
|
|
139
|
+
return cmdSetup(ctx);
|
|
140
|
+
case "doctor":
|
|
141
|
+
return cmdDoctor(ctx);
|
|
142
|
+
case "uninstall":
|
|
143
|
+
return cmdUninstall({ home: ctx.home, env: ctx.env, out: ctx.out, agents: ctx.agents, purge: ctx.flags.purge });
|
|
144
|
+
default:
|
|
145
|
+
return routeServiceCommand(command, ctx);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
async function routeServiceCommand(command, ctx) {
|
|
149
|
+
switch (command) {
|
|
150
|
+
case "status": {
|
|
151
|
+
ctx.out(await statusText());
|
|
152
|
+
return 0;
|
|
153
|
+
}
|
|
154
|
+
case "mcp": {
|
|
155
|
+
runMcpStdio({ write: (line) => ctx.out(line) });
|
|
156
|
+
return 0;
|
|
157
|
+
}
|
|
158
|
+
case "listener": {
|
|
159
|
+
return cmdListener({ home: ctx.home, out: ctx.out, rest: ctx.args.slice(ctx.args.indexOf("listener") + 1) });
|
|
160
|
+
}
|
|
161
|
+
case "run":
|
|
162
|
+
return cmdRun({ home: ctx.home, env: ctx.env, command: ctx.args.slice(ctx.args.indexOf("--") + 1), separated: ctx.args.includes("--") });
|
|
163
|
+
case "login":
|
|
164
|
+
return cmdLogin(backendCommandContext(ctx));
|
|
165
|
+
case "auth":
|
|
166
|
+
case "skills":
|
|
167
|
+
return (command === "auth" ? cmdAuth : cmdSkills)(backendCommandContext(ctx));
|
|
168
|
+
case "memory":
|
|
169
|
+
return cmdMemory({ home: ctx.home, out: ctx.out, words: ctx.positional.slice(1), flag: (name) => flagValue(ctx.args, name), cwd: ctx.cwd });
|
|
170
|
+
default:
|
|
171
|
+
ctx.out(` ✗ unknown command: ${command}`);
|
|
172
|
+
ctx.out(USAGE);
|
|
173
|
+
return 2;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function banner(out) {
|
|
177
|
+
out(" Heedvane Agent Hooks — cross-agent steering, guardrails & analysis");
|
|
178
|
+
out("");
|
|
179
|
+
}
|
|
180
|
+
function detectionRows(home, env) {
|
|
181
|
+
return supportedClients({ home, platform: platform(), env }).map((client) => ({
|
|
182
|
+
client,
|
|
183
|
+
detected: isClientDetected(client),
|
|
184
|
+
markerRel: client.markerPath.replace(home, "~"),
|
|
185
|
+
}));
|
|
186
|
+
}
|
|
187
|
+
function loadOrTemplate(home) {
|
|
188
|
+
const path = join(home, ".heed", "heed.yaml");
|
|
189
|
+
if (!existsSync(path))
|
|
190
|
+
return { heedFile: loadHeedFile(DEFAULT_HEED_YAML_TEMPLATE), existed: false };
|
|
191
|
+
try {
|
|
192
|
+
return { heedFile: loadHeedFileAt(path), existed: true };
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
return { heedFile: loadHeedFile(DEFAULT_HEED_YAML_TEMPLATE), existed: true, error: errorMessage(err) };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function planFor(plan) {
|
|
199
|
+
const effective = plan.agents.length > 0 ? { ...plan.heedFile, agents: [...plan.agents] } : plan.heedFile;
|
|
200
|
+
return planActions({ home: plan.home, platform: platform(), env: plan.env, heedFile: effective, forced: plan.agents, skillDigest: skillDigest(canonicalSkillFiles()) });
|
|
201
|
+
}
|
|
202
|
+
async function cmdInit(ctx) {
|
|
203
|
+
const { home, env, out, flags, agents } = ctx;
|
|
204
|
+
banner(out);
|
|
205
|
+
const rows = detectionRows(home, env);
|
|
206
|
+
for (const line of renderDetection(rows))
|
|
207
|
+
out(line);
|
|
208
|
+
const loaded = loadOrTemplate(home);
|
|
209
|
+
if (loaded.error) {
|
|
210
|
+
out("");
|
|
211
|
+
out(` ✗ ~/.heed/heed.yaml does not parse: ${loaded.error}`);
|
|
212
|
+
out(` remediation: fix or remove it, then run npx heedvane init`);
|
|
213
|
+
return 1;
|
|
214
|
+
}
|
|
215
|
+
let serverEdit;
|
|
216
|
+
try {
|
|
217
|
+
serverEdit = ctx.server === undefined ? null : withServer(home, loaded, ctx.server);
|
|
218
|
+
}
|
|
219
|
+
catch (err) {
|
|
220
|
+
out("");
|
|
221
|
+
out(` ✗ --server ${ctx.server} cannot be used: ${errorMessage(err)}`);
|
|
222
|
+
return 1;
|
|
223
|
+
}
|
|
224
|
+
const { heedFile, existed } = serverEdit ?? loaded;
|
|
225
|
+
const actions = planFor({ home, env, heedFile, agents });
|
|
226
|
+
// Plain init (no --yes, no --dry-run): show the plan summary, write nothing.
|
|
227
|
+
if (!flags.yes && !flags.dryRun) {
|
|
228
|
+
out("");
|
|
229
|
+
out(wouldInstallSummary(actions));
|
|
230
|
+
out(" Re-run with --yes to apply, or --dry-run for the full file-by-file plan.");
|
|
231
|
+
return 0;
|
|
232
|
+
}
|
|
233
|
+
if (flags.dryRun) {
|
|
234
|
+
out("");
|
|
235
|
+
for (const line of renderPlan(actions))
|
|
236
|
+
out(line);
|
|
237
|
+
out("");
|
|
238
|
+
out(wouldInstallSummary(actions));
|
|
239
|
+
if (flags.json)
|
|
240
|
+
outJson(out, { status: "planned", plannedActions: actions, appliedActions: [] });
|
|
241
|
+
return 0; // zero writes — the fixture home is untouched
|
|
242
|
+
}
|
|
243
|
+
// --yes: first non-dry run has approval; proceed to write.
|
|
244
|
+
return applyAndReport(ctx, { heedFile, agents, existed, ...(serverEdit === null ? {} : { text: serverEdit.text }) });
|
|
245
|
+
}
|
|
246
|
+
/** heed.yaml (or the template) with `--server` as `heedvane.url`; null when it already names that server. Throws on an unusable URL. */
|
|
247
|
+
function withServer(home, loaded, server) {
|
|
248
|
+
const text = withServerUrl(heedText(home, loaded.existed), server);
|
|
249
|
+
const heedFile = loadHeedFile(text);
|
|
250
|
+
if (loaded.existed && heedFile.heedvane.url === loaded.heedFile.heedvane.url)
|
|
251
|
+
return null;
|
|
252
|
+
return { heedFile, existed: loaded.existed, text };
|
|
253
|
+
}
|
|
254
|
+
async function applyAndReport(ctx, spec) {
|
|
255
|
+
const { out, flags } = ctx;
|
|
256
|
+
const { actions, applied } = await installPlan(ctx, spec);
|
|
257
|
+
for (const line of renderApplied(applied))
|
|
258
|
+
out(line);
|
|
259
|
+
if (!flags.dryRun)
|
|
260
|
+
await signInIfNeeded(ctx, spec.heedFile.heedvane.url);
|
|
261
|
+
out("");
|
|
262
|
+
out(" Next: open a new agent session and run /heedvane-status.");
|
|
263
|
+
out(" Re-run `npx heedvane doctor` after any heed.yaml edit.");
|
|
264
|
+
const failed = applied.some(appliedFailed);
|
|
265
|
+
if (flags.json)
|
|
266
|
+
outJson(out, { status: failed ? "failed" : "applied", plannedActions: actions, appliedActions: applied });
|
|
267
|
+
return failed ? 1 : 0;
|
|
268
|
+
}
|
|
269
|
+
/** Writes heed.yaml when it is missing, or when `--server` changed its URL. */
|
|
270
|
+
function writeControlFile({ home, out }, { heedFile, existed, text }) {
|
|
271
|
+
if (existed && text === undefined)
|
|
272
|
+
return;
|
|
273
|
+
mkdirSync(join(home, ".heed"), { recursive: true, mode: 0o700 });
|
|
274
|
+
writeFileSync(join(home, ".heed", "heed.yaml"), text ?? DEFAULT_HEED_YAML_TEMPLATE, { mode: 0o600 });
|
|
275
|
+
out(existed ? ` ✓ Set heedvane.url in ~/.heed/heed.yaml to ${heedFile.heedvane.url}` : ` ✓ Wrote ~/.heed/heed.yaml (your control file — edit any time)`);
|
|
276
|
+
}
|
|
277
|
+
/** Without a sign-in to `url` (and to `--org`, when named), sign in now when a person is at the terminal, else say how. Never fails setup. */
|
|
278
|
+
/** Writes heed.yaml, applies the plan, issues the listener endpoint and records the machine state. Prints only what applying prints. */
|
|
279
|
+
async function installPlan(ctx, spec) {
|
|
280
|
+
const { home, env, out } = ctx;
|
|
281
|
+
const { heedFile, agents } = spec;
|
|
282
|
+
// The control file comes first (mock order): "✓ Wrote ~/.heed/heed.yaml".
|
|
283
|
+
writeControlFile(ctx, spec);
|
|
284
|
+
const actions = planFor({ home, env, heedFile, agents });
|
|
285
|
+
const dispatcher = {
|
|
286
|
+
denySkillsWhenUnchecked: heedFile.heedvane.skills.enforce && heedFile.heedvane.skills.whenUnknown === "deny",
|
|
287
|
+
gating: heedFile.gating.mode,
|
|
288
|
+
nodeDir: dirname(process.execPath),
|
|
289
|
+
};
|
|
290
|
+
const applied = await applyPlan(actions, { home, env, out, dispatcher });
|
|
291
|
+
// The listener port and token every dispatcher reads; kept once issued.
|
|
292
|
+
await ensureEndpoint(join(home, ".heed"));
|
|
293
|
+
// Machine state: CLI-owned; preserve an existing endpoint.
|
|
294
|
+
const root = join(home, ".heed");
|
|
295
|
+
const existing = await readState(root);
|
|
296
|
+
const state = existing ?? emptyState();
|
|
297
|
+
// Record the skill only when this run installed it (or found it current).
|
|
298
|
+
const skillInstalled = applied.some((a) => a.kind === "skill" && (a.status === "installed" || a.status === "unchanged"));
|
|
299
|
+
if (skillInstalled)
|
|
300
|
+
state.skill = { version: "1", digest: skillDigest(canonicalSkillFiles()) };
|
|
301
|
+
await writeStateAtomic(root, state);
|
|
302
|
+
return { actions, applied };
|
|
303
|
+
}
|
|
304
|
+
async function signInIfNeeded(ctx, url) {
|
|
305
|
+
const root = join(ctx.home, ".heed");
|
|
306
|
+
if ((await backendAt(root, url)) !== null && (await onRequestedOrg(root, ctx.org)))
|
|
307
|
+
return;
|
|
308
|
+
if (!ctx.interactive) {
|
|
309
|
+
ctx.out(" Next: sign in with `npx heedvane login` (CI and headless machines: npx heedvane auth org < key-file).");
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
await cmdLogin({ home: ctx.home, out: ctx.out, words: [], project: false, readStdin: async () => "", openBrowser: ctx.openBrowser });
|
|
313
|
+
if (!(await onRequestedOrg(root, ctx.org))) {
|
|
314
|
+
ctx.out(` ✗ This sign-in belongs to another organization than ${ctx.org}. Run \`npx heedvane login\` and pick that organization on the consent page.`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
/** True without `--org`; else whether the saved login was granted for that organization. */
|
|
318
|
+
async function onRequestedOrg(root, org) {
|
|
319
|
+
if (org === undefined)
|
|
320
|
+
return true;
|
|
321
|
+
const saved = await readLogin(root).catch(() => null);
|
|
322
|
+
return saved?.organizationId === org;
|
|
323
|
+
}
|
|
324
|
+
async function cmdSetup(ctx) {
|
|
325
|
+
const { home, out } = ctx;
|
|
326
|
+
const path = join(home, ".heed", "heed.yaml");
|
|
327
|
+
if (!existsSync(path)) {
|
|
328
|
+
out(" ✗ ~/.heed/heed.yaml not found — setup compiles your control file into agent configs.");
|
|
329
|
+
out(" remediation: run npx heedvane init");
|
|
330
|
+
return 1;
|
|
331
|
+
}
|
|
332
|
+
let heedFile;
|
|
333
|
+
try {
|
|
334
|
+
heedFile = loadHeedFileAt(path, out);
|
|
335
|
+
}
|
|
336
|
+
catch (err) {
|
|
337
|
+
out(` ✗ ~/.heed/heed.yaml does not parse: ${errorMessage(err)}`);
|
|
338
|
+
out(" remediation: fix it, then re-run npx heedvane setup");
|
|
339
|
+
return 1;
|
|
340
|
+
}
|
|
341
|
+
banner(out);
|
|
342
|
+
if (ctx.flags.dryRun) {
|
|
343
|
+
const actions = planFor({ home, env: ctx.env, heedFile, agents: ctx.agents });
|
|
344
|
+
for (const line of renderPlan(actions))
|
|
345
|
+
out(line);
|
|
346
|
+
out("");
|
|
347
|
+
out(wouldInstallSummary(actions));
|
|
348
|
+
if (ctx.flags.json)
|
|
349
|
+
outJson(out, { status: "planned", plannedActions: actions, appliedActions: [] });
|
|
350
|
+
return 0; // zero writes
|
|
351
|
+
}
|
|
352
|
+
return applyAndReport(ctx, { heedFile, agents: ctx.agents, existed: true });
|
|
353
|
+
}
|
|
354
|
+
/** A person at a terminal who did not ask for the one-shot path (`--yes`, `--dry-run`, `--json`) or for help. */
|
|
355
|
+
function takesInstaller(ctx) {
|
|
356
|
+
return ctx.interactive && !ctx.flags.yes && !ctx.flags.dryRun && !ctx.flags.json && !ctx.args.includes("--help");
|
|
357
|
+
}
|
|
358
|
+
/** heed.yaml's text, or the template when there is none yet. */
|
|
359
|
+
function heedText(home, existed) {
|
|
360
|
+
return existed ? readFileSync(join(home, ".heed", "heed.yaml"), "utf8") : DEFAULT_HEED_YAML_TEMPLATE;
|
|
361
|
+
}
|
|
362
|
+
/** The installer: pick agents, integrations and features, confirm, install, start the listener, sign in. */
|
|
363
|
+
async function cmdInstall(ctx) {
|
|
364
|
+
const base = installerBase(ctx);
|
|
365
|
+
if (typeof base === "string") {
|
|
366
|
+
ctx.out(` ✗ ${base}`);
|
|
367
|
+
return 1;
|
|
368
|
+
}
|
|
369
|
+
const { prompts } = ctx;
|
|
370
|
+
brandIntro(base.heedFile.heedvane.url, ctx.org, prompts);
|
|
371
|
+
const choices = await askChoices(agentOptions(ctx), base.heedFile, prompts);
|
|
372
|
+
if (choices === null)
|
|
373
|
+
return installCancelled(prompts);
|
|
374
|
+
const text = withInstallChoices(base.text, choices);
|
|
375
|
+
const spec = { heedFile: loadHeedFile(text), agents: choices.agents === "auto" ? [] : choices.agents, existed: base.existed, text };
|
|
376
|
+
const names = new Map(detectionRows(ctx.home, ctx.env).map((row) => [row.client.id, row.client.displayName]));
|
|
377
|
+
const planned = agentLines(planFor({ home: ctx.home, env: ctx.env, ...spec }), names);
|
|
378
|
+
if (!(await confirmInstall(planned, prompts)))
|
|
379
|
+
return installCancelled(prompts);
|
|
380
|
+
const notes = [];
|
|
381
|
+
const { actions, applied } = await installWithSpinner(planned.length, () => installPlan({ ...ctx, out: (line) => notes.push(line) }, spec), prompts);
|
|
382
|
+
notes.forEach(promptLine(prompts));
|
|
383
|
+
showInstalled(agentLines(actions, names, applied), prompts);
|
|
384
|
+
const listening = await listenerStep(() => ctx.startListener(ctx.home), listenerLogPath(join(ctx.home, ".heed")), prompts);
|
|
385
|
+
await signInIfNeeded({ ...ctx, out: promptLine(prompts) }, spec.heedFile.heedvane.url);
|
|
386
|
+
installFinished(nextSteps(listening, spec.heedFile.heedvane.url), prompts);
|
|
387
|
+
return applied.some(appliedFailed) ? 1 : 0;
|
|
388
|
+
}
|
|
389
|
+
/** heed.yaml's current text (or the template) with `--server` applied, parsed; a message when either cannot be used. */
|
|
390
|
+
function installerBase({ home, server }) {
|
|
391
|
+
const loaded = loadOrTemplate(home);
|
|
392
|
+
if (loaded.error)
|
|
393
|
+
return `~/.heed/heed.yaml does not parse: ${loaded.error}`;
|
|
394
|
+
try {
|
|
395
|
+
const current = heedText(home, loaded.existed);
|
|
396
|
+
const text = server === undefined ? current : withServerUrl(current, server);
|
|
397
|
+
return { heedFile: loadHeedFile(text), existed: loaded.existed, text };
|
|
398
|
+
}
|
|
399
|
+
catch (err) {
|
|
400
|
+
return `--server ${server} cannot be used: ${errorMessage(err)}`;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
function agentOptions({ home, env }) {
|
|
404
|
+
return detectionRows(home, env).map(({ client, detected }) => ({ id: client.id, name: client.displayName, detected, capability: capabilitySummary(client) }));
|
|
405
|
+
}
|
|
406
|
+
function nextSteps(listening, server) {
|
|
407
|
+
return [
|
|
408
|
+
"Open a new agent session: Heedvane joins it at session start.",
|
|
409
|
+
`Watch your sessions: ${server}`,
|
|
410
|
+
...(listening ? [] : ["Heedvane starts by itself with your next agent session. To see why it did not start now: npx heedvane listener start"]),
|
|
411
|
+
"Check the install: npx heedvane doctor",
|
|
412
|
+
"Change your choices: npx heedvane",
|
|
413
|
+
];
|
|
414
|
+
}
|
|
415
|
+
/** Hermes' hooks capability rides its native plugin; installHermesPlugin owns creation and ownership checks. */
|
|
416
|
+
async function applyHermesPlugin(client, ctx) {
|
|
417
|
+
const path = join(ctx.home, ".hermes", "plugins", "heedvane-hooks", "plugin.yaml");
|
|
418
|
+
const status = await installHermesPlugin(ctx.home, ctx.dispatcher.gating);
|
|
419
|
+
if (status === "skipped") {
|
|
420
|
+
return { clientId: client.id, kind: "hooks", status: "skipped", path, reason: "another plugin owns ~/.hermes/plugins/heedvane-hooks — left untouched" };
|
|
421
|
+
}
|
|
422
|
+
return { clientId: client.id, kind: "hooks", status: status === "installed" ? "create" : "unchanged", path };
|
|
423
|
+
}
|
|
424
|
+
/** Drop our groups under event keys this client no longer installs; a failed removal becomes the result. */
|
|
425
|
+
async function withoutStaleHookGroups(client, writes, result) {
|
|
426
|
+
let worst = result;
|
|
427
|
+
for (const { target, containerKey } of writes) {
|
|
428
|
+
if (containerKey !== "hooks")
|
|
429
|
+
continue;
|
|
430
|
+
for (const item of await removeStaleHookGroups(client, target)) {
|
|
431
|
+
if (item.status === "failed")
|
|
432
|
+
return { status: "failed", path: item.path, reason: item.detail };
|
|
433
|
+
if (worst.status === "unchanged")
|
|
434
|
+
worst = { status: "update", path: item.path };
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return worst;
|
|
438
|
+
}
|
|
439
|
+
/** Patch every desired entry into the client's config files, then refresh the dispatcher script. */
|
|
440
|
+
async function applyConfigEntries(action, client, ctx) {
|
|
441
|
+
const desireds = desiredEntriesFor(client, action.kind === "hooks" ? "hooks" : "mcp", { home: ctx.home, env: ctx.env });
|
|
442
|
+
// One write per file and container: every hook event lands (and is backed up) together.
|
|
443
|
+
const writes = new Map();
|
|
444
|
+
for (const d of desireds) {
|
|
445
|
+
const key = `${d.path}\0${d.containerKey}`;
|
|
446
|
+
const write = writes.get(key) ?? { target: { path: d.path, format: d.format }, containerKey: d.containerKey, entries: {} };
|
|
447
|
+
write.entries[d.entryName] = d.entry;
|
|
448
|
+
writes.set(key, write);
|
|
449
|
+
}
|
|
450
|
+
let worst = { status: "unchanged", path: action.path };
|
|
451
|
+
for (const { target, containerKey, entries } of writes.values()) {
|
|
452
|
+
const res = await applyEntry(target, { containerKey, entries });
|
|
453
|
+
if (res.status === "failed" || res.status === "invalid") {
|
|
454
|
+
worst = res;
|
|
455
|
+
break;
|
|
456
|
+
}
|
|
457
|
+
if (res.status !== "unchanged")
|
|
458
|
+
worst = res;
|
|
459
|
+
}
|
|
460
|
+
if (action.kind === "hooks" && worst.status !== "failed" && worst.status !== "invalid") {
|
|
461
|
+
worst = await withoutStaleHookGroups(client, writes.values(), worst);
|
|
462
|
+
}
|
|
463
|
+
// The dispatcher script the snippet delegates to.
|
|
464
|
+
await writeDispatcherScript(ctx.home, client.id, ctx.dispatcher);
|
|
465
|
+
return {
|
|
466
|
+
clientId: action.clientId,
|
|
467
|
+
kind: action.kind,
|
|
468
|
+
status: worst.status,
|
|
469
|
+
path: worst.path,
|
|
470
|
+
backupPath: worst.backupPath,
|
|
471
|
+
reason: worst.reason,
|
|
472
|
+
remediation: worst.remediation,
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
/** One planned action → one applied result. */
|
|
476
|
+
async function applyAction(action, client, ctx) {
|
|
477
|
+
if (action.kind === "skill") {
|
|
478
|
+
const status = await installSkill(join(ctx.home, ".agents", "skills"));
|
|
479
|
+
return { clientId: action.clientId, kind: "skill", status, path: action.path };
|
|
480
|
+
}
|
|
481
|
+
if (client.id === "hermes" && action.kind === "hooks") {
|
|
482
|
+
return applyHermesPlugin(client, ctx);
|
|
483
|
+
}
|
|
484
|
+
return applyConfigEntries(action, client, ctx);
|
|
485
|
+
}
|
|
486
|
+
async function recordDeferredHookRemovals(client, results) {
|
|
487
|
+
if (!client)
|
|
488
|
+
return;
|
|
489
|
+
for (const item of await removeDeferredHookGroups(client)) {
|
|
490
|
+
if (item.status === "removed") {
|
|
491
|
+
results.push({ clientId: client.id, kind: "hooks", status: "update", path: item.path });
|
|
492
|
+
}
|
|
493
|
+
else if (item.status === "failed") {
|
|
494
|
+
results.push({ clientId: client.id, kind: "hooks", status: "failed", path: item.path, reason: item.detail });
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
async function applyPlan(actions, ctx) {
|
|
499
|
+
const results = [];
|
|
500
|
+
const done = new Set(); // unique (kind,path) — skill is shared across clients
|
|
501
|
+
const clients = supportedClients({ home: ctx.home, platform: platform(), env: ctx.env });
|
|
502
|
+
for (const action of actions) {
|
|
503
|
+
const client = clients.find((c) => c.id === action.clientId);
|
|
504
|
+
if (action.kind === "skip") {
|
|
505
|
+
await recordDeferredHookRemovals(client, results);
|
|
506
|
+
results.push({ clientId: action.clientId, kind: "skip", status: "skipped", path: action.path, reason: action.reason });
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
const key = `${action.kind}:${action.path}`;
|
|
510
|
+
if (done.has(key))
|
|
511
|
+
continue;
|
|
512
|
+
done.add(key);
|
|
513
|
+
if (!client)
|
|
514
|
+
continue;
|
|
515
|
+
results.push(await applyAction(action, client, ctx));
|
|
516
|
+
}
|
|
517
|
+
return results;
|
|
518
|
+
}
|
|
519
|
+
function outJson(out, envelope) {
|
|
520
|
+
out(JSON.stringify(envelope));
|
|
521
|
+
}
|
|
522
|
+
// ---------------------------------------------------------------------------
|
|
523
|
+
// doctor / uninstall / listener
|
|
524
|
+
// ---------------------------------------------------------------------------
|
|
525
|
+
/** Glyph for a doctor check line: pass, warn-only, or fail. */
|
|
526
|
+
function checkGlyph(check) {
|
|
527
|
+
if (check.ok)
|
|
528
|
+
return "✓";
|
|
529
|
+
return check.warnOnly ? "·" : "✗";
|
|
530
|
+
}
|
|
531
|
+
async function cmdDoctor(ctx) {
|
|
532
|
+
const report = await runDoctor({ home: ctx.home, env: ctx.env, clients: ctx.clients.length > 0 ? ctx.clients : undefined, json: ctx.flags.json });
|
|
533
|
+
if (ctx.flags.json) {
|
|
534
|
+
ctx.out(JSON.stringify({ status: report.healthy ? "healthy" : "unhealthy", ...report }));
|
|
535
|
+
return report.healthy ? 0 : 1;
|
|
536
|
+
}
|
|
537
|
+
ctx.out(` Heedvane doctor — ${report.scopedTo ? `scoped to ${report.scopedTo.join(", ")}` : "all checks"}`);
|
|
538
|
+
ctx.out("");
|
|
539
|
+
for (const check of report.checks) {
|
|
540
|
+
ctx.out(` ${checkGlyph(check)} ${check.name}: ${check.detail}`);
|
|
541
|
+
if (check.remediation)
|
|
542
|
+
ctx.out(` remediation: ${check.remediation}`);
|
|
543
|
+
}
|
|
544
|
+
ctx.out("");
|
|
545
|
+
ctx.out(` ${report.healthy ? "All scoped checks pass." : "Some checks failed — see remediations above."}`);
|
|
546
|
+
return report.healthy ? 0 : 1;
|
|
547
|
+
}
|
|
548
|
+
/** Glyph for an uninstall item line: removed, failed, or skipped/absent. */
|
|
549
|
+
function uninstallGlyph(status) {
|
|
550
|
+
if (status === "removed")
|
|
551
|
+
return "✓";
|
|
552
|
+
return status === "failed" ? "✗" : "·";
|
|
553
|
+
}
|
|
554
|
+
async function cmdUninstall(ctx) {
|
|
555
|
+
const items = await uninstall({ home: ctx.home, env: ctx.env, agents: ctx.agents.length > 0 ? ctx.agents : undefined, purgeConfig: ctx.purge });
|
|
556
|
+
for (const item of items) {
|
|
557
|
+
ctx.out(` ${uninstallGlyph(item.status)} ${item.name} ${item.path}${item.detail ? ` (${item.detail})` : ""}`);
|
|
558
|
+
}
|
|
559
|
+
ctx.out("");
|
|
560
|
+
ctx.out(" heed.yaml was left untouched (your file). Pass --purge-config to delete it too.");
|
|
561
|
+
return 0;
|
|
562
|
+
}
|
|
563
|
+
async function cmdListener(ctx) {
|
|
564
|
+
const sub = ctx.rest[0] ?? "";
|
|
565
|
+
if (sub !== "start") {
|
|
566
|
+
ctx.out(" usage: npx heedvane listener start");
|
|
567
|
+
ctx.out(" v1 runs the listener in the foreground; service install (launchd/systemd) is opt-in later.");
|
|
568
|
+
return sub === "" ? 2 : 1;
|
|
569
|
+
}
|
|
570
|
+
const started = await serveHooks(ctx.home, ctx.out);
|
|
571
|
+
ctx.out(` ✓ listener on 127.0.0.1:${started.port}, ${started.replayed} spooled event(s) replayed (Ctrl-C to stop)`);
|
|
572
|
+
await new Promise((resolve) => {
|
|
573
|
+
process.on("SIGINT", () => {
|
|
574
|
+
started.server.close(() => resolve());
|
|
575
|
+
});
|
|
576
|
+
});
|
|
577
|
+
return 0;
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* `run -- <agent command>`: a listener for exactly one agent run, for CI, containers and software
|
|
581
|
+
* factories where nothing stays running. The agent keeps stdin, stdout and stderr; the listener writes to
|
|
582
|
+
* ~/.heed/listener.log and Heedvane's own lines go to stderr, so an agent's JSON output stays parseable.
|
|
583
|
+
* Returns the agent's exit code (128 + signal number when a signal ended it).
|
|
584
|
+
*/
|
|
585
|
+
async function cmdRun(ctx) {
|
|
586
|
+
const say = (line) => void process.stderr.write(`heedvane: ${line}\n`);
|
|
587
|
+
if (!ctx.separated || ctx.command.length === 0) {
|
|
588
|
+
say('usage: npx heedvane run -- <agent command>, e.g. npx heedvane run -- claude -p "fix the failing tests"');
|
|
589
|
+
return 2;
|
|
590
|
+
}
|
|
591
|
+
const root = join(ctx.home, ".heed");
|
|
592
|
+
mkdirSync(root, { recursive: true, mode: 0o700 }); // a fresh container has no ~/.heed yet
|
|
593
|
+
const log = createWriteStream(listenerLogPath(root), { flags: "a", mode: 0o600 });
|
|
594
|
+
const started = await serveHooks(ctx.home, (line) => void log.write(`${line}\n`));
|
|
595
|
+
if ((await backendAt(root, heedvaneConfigAt(root).url)) === null)
|
|
596
|
+
say("not signed in, so guardrails are off: printf '%s' \"$HEEDVANE_ORG_KEY\" | npx heedvane auth org");
|
|
597
|
+
say(`listening on 127.0.0.1:${started.port}; log: ${listenerLogPath(root)}`);
|
|
598
|
+
try {
|
|
599
|
+
// The agent's hooks find this listener's endpoint through HEED_AGENT_HOOK_ROOT, also when HEED_HOME is not $HOME.
|
|
600
|
+
return await runAgent(ctx.command, { ...ctx.env, HEED_HOME: ctx.home, HEED_AGENT_HOOK_ROOT: root }, say);
|
|
601
|
+
}
|
|
602
|
+
finally {
|
|
603
|
+
await new Promise((resolve) => started.server.close(() => resolve()));
|
|
604
|
+
await new Promise((resolve) => log.end(resolve));
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
/** Runs the agent with this process's terminal, passes Ctrl-C and `docker stop` on to it, and resolves with its exit code. */
|
|
608
|
+
function runAgent([program, ...args], env, say) {
|
|
609
|
+
return new Promise((resolve) => {
|
|
610
|
+
const child = spawn(program, args, { stdio: "inherit", env });
|
|
611
|
+
const forward = (signal) => void child.kill(signal);
|
|
612
|
+
process.on("SIGINT", forward).on("SIGTERM", forward);
|
|
613
|
+
const settle = (code) => {
|
|
614
|
+
process.off("SIGINT", forward).off("SIGTERM", forward);
|
|
615
|
+
resolve(code);
|
|
616
|
+
};
|
|
617
|
+
child.on("error", (err) => {
|
|
618
|
+
say(`could not start ${program}: ${errorMessage(err)}`);
|
|
619
|
+
settle(127);
|
|
620
|
+
});
|
|
621
|
+
child.on("exit", (code, signal) => settle(code ?? 128 + (signal === null ? 0 : osConstants.signals[signal])));
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
function backendCommandContext(ctx) {
|
|
625
|
+
return { home: ctx.home, out: ctx.out, words: ctx.positional.slice(1), project: ctx.args.includes("--project"), readStdin: ctx.readStdin, openBrowser: ctx.openBrowser };
|
|
626
|
+
}
|
|
627
|
+
/** All of stdin as text (where `auth` reads its key). */
|
|
628
|
+
/** Open `url` in the system browser; the URL is printed too, so a failure here only costs a click. */
|
|
629
|
+
/**
|
|
630
|
+
* The command that opens `url` in the default browser. Windows goes through rundll32, not `cmd /c start`:
|
|
631
|
+
* cmd would cut the sign-in URL at its first `&`.
|
|
632
|
+
*/
|
|
633
|
+
export function browserOpener(platform, url) {
|
|
634
|
+
const openers = { darwin: ["open", [url]], win32: ["rundll32", ["url.dll,FileProtocolHandler", url]] };
|
|
635
|
+
return openers[platform] ?? ["xdg-open", [url]];
|
|
636
|
+
}
|
|
637
|
+
function openSystemBrowser(url) {
|
|
638
|
+
const [command, args] = browserOpener(process.platform, url);
|
|
639
|
+
const child = spawn(command, args, { detached: true, stdio: "ignore" });
|
|
640
|
+
child.on("error", () => undefined); // no browser opener: the printed URL is the way in
|
|
641
|
+
child.unref();
|
|
642
|
+
}
|
|
643
|
+
async function readProcessStdin() {
|
|
644
|
+
const chunks = [];
|
|
645
|
+
for await (const chunk of process.stdin)
|
|
646
|
+
chunks.push(chunk);
|
|
647
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
648
|
+
}
|
|
649
|
+
export { main };
|
|
650
|
+
// Bin entry: when executed directly (npx / ./dist/cli.js), run the CLI.
|
|
651
|
+
// When imported (tests, programmatic use), main is just exported.
|
|
652
|
+
import { fileURLToPath } from "node:url";
|
|
653
|
+
import { argv } from "node:process";
|
|
654
|
+
const invokedDirectly = (() => {
|
|
655
|
+
try {
|
|
656
|
+
return argv[1] !== undefined && realpathSync(argv[1]) === fileURLToPath(import.meta.url);
|
|
657
|
+
}
|
|
658
|
+
catch {
|
|
659
|
+
return false;
|
|
660
|
+
}
|
|
661
|
+
})();
|
|
662
|
+
if (invokedDirectly) {
|
|
663
|
+
void main(argv.slice(2)).then((code) => {
|
|
664
|
+
process.exitCode = code;
|
|
665
|
+
});
|
|
666
|
+
}
|