@sleepy_panda_srl/portulan 0.3.0 → 0.4.1

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.
Files changed (90) hide show
  1. package/README.md +18 -6
  2. package/cli/README.md +56 -4
  3. package/cli/advisory.mjs +167 -53
  4. package/cli/cache-risk.mjs +69 -0
  5. package/cli/comments.mjs +8 -5
  6. package/cli/compile.mjs +492 -146
  7. package/cli/context.mjs +204 -43
  8. package/cli/doctor.mjs +211 -33
  9. package/cli/drills.mjs +32 -7
  10. package/cli/finish.md +34 -0
  11. package/cli/finish.mjs +9 -67
  12. package/cli/form.mjs +35 -19
  13. package/cli/frontmatter.mjs +39 -0
  14. package/cli/gate.mjs +14 -12
  15. package/cli/guidance-files.mjs +335 -0
  16. package/cli/handoff-policy.mjs +76 -0
  17. package/cli/html-entities.mjs +2130 -0
  18. package/cli/import-files.mjs +15 -0
  19. package/cli/index.mjs +77 -24
  20. package/cli/init-preview.mjs +553 -0
  21. package/cli/init.mjs +261 -46
  22. package/cli/instructions-journal.mjs +1185 -0
  23. package/cli/instructions.mjs +332 -57
  24. package/cli/ledger.mjs +123 -37
  25. package/cli/librarian.mjs +103 -19
  26. package/cli/persona-routing.mjs +209 -0
  27. package/cli/plugin-lint.mjs +77 -56
  28. package/cli/portulan.mjs +7 -2
  29. package/cli/recipe-output.mjs +91 -0
  30. package/cli/recipe-runner.md +68 -0
  31. package/cli/recipe-runner.mjs +131 -0
  32. package/cli/recipe-set.mjs +12 -3
  33. package/cli/ritual-contexts.mjs +103 -0
  34. package/cli/sessions.mjs +2 -1
  35. package/cli/spend-analysis.mjs +111 -0
  36. package/cli/spend-offer.md +53 -0
  37. package/cli/spend-offer.mjs +93 -0
  38. package/cli/stop-diagnostics.mjs +114 -0
  39. package/cli/stop-gate.mjs +133 -90
  40. package/cli/stop-policy.mjs +14 -0
  41. package/cli/symbols.mjs +55 -8
  42. package/cli/telemetry.mjs +19 -12
  43. package/cli/upgrade.mjs +62 -44
  44. package/cli/vendor.mjs +210 -126
  45. package/core/operating/context.md +368 -60
  46. package/core/operating/evolution.md +3 -2
  47. package/core/operating/loop.md +9 -4
  48. package/core/operating/sessions.md +6 -15
  49. package/core/personas/README.md +52 -10
  50. package/core/personas/implementer.md +3 -0
  51. package/core/personas/librarian.md +3 -0
  52. package/core/personas/reviewer.md +3 -0
  53. package/core/templates/handoff.md +6 -2
  54. package/evals/ab/personas/README.md +111 -0
  55. package/evals/ab/personas/acquisition.json +330 -0
  56. package/evals/ab/personas/bindings/implementer.txt +43 -0
  57. package/evals/ab/personas/bindings/librarian.txt +40 -0
  58. package/evals/ab/personas/bindings/reviewer.txt +41 -0
  59. package/evals/ab/personas/corpus.json +69 -0
  60. package/evals/ab/personas/grader.mjs +171 -0
  61. package/evals/ab/personas/implementer.json +774 -0
  62. package/evals/ab/personas/implementer.md +22 -0
  63. package/evals/ab/personas/librarian.json +758 -0
  64. package/evals/ab/personas/librarian.md +22 -0
  65. package/evals/ab/personas/protocol/continuation.mjs.txt +284 -0
  66. package/evals/ab/personas/protocol/first-outcome.mjs.txt +227 -0
  67. package/evals/ab/personas/protocol/first-probe.mjs.txt +172 -0
  68. package/evals/ab/personas/protocol/publisher.mjs.txt +126 -0
  69. package/evals/ab/personas/protocol/second-probe.mjs.txt +193 -0
  70. package/evals/ab/personas/repairs/first-control-answer.txt +23 -0
  71. package/evals/ab/personas/repairs/grader-before-decode-repair.mjs.txt +143 -0
  72. package/evals/ab/personas/reviewer.json +758 -0
  73. package/evals/ab/personas/reviewer.md +22 -0
  74. package/evals/releases/0.4.0.json +154 -0
  75. package/evals/releases/0.4.0.md +69 -0
  76. package/evals/releases/0.4.1.json +154 -0
  77. package/evals/releases/0.4.1.md +69 -0
  78. package/package.json +6 -2
  79. package/packs/rituals/checkpoints/README.md +35 -0
  80. package/packs/rituals/checkpoints/pack.json +3 -2
  81. package/packs/tools/local-llm/README.md +46 -6
  82. package/packs/tools/local-llm/runner/local-llm.mjs +127 -13
  83. package/packs/tools/local-llm/skills/delegate-local/SKILL.md +26 -2
  84. package/spec/README.md +37 -4
  85. package/spec/migrations/0009-instruction-sections-on-read.mjs +60 -11
  86. package/spec/migrations/0011-persona-bindings-compiled.mjs +23 -0
  87. package/spec/migrations/README.md +6 -0
  88. package/spec/pack.schema.json +12 -3
  89. package/spec/slots.md +262 -117
  90. package/spec/workspace.schema.json +67 -11
package/cli/finish.mjs CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  import fs from "node:fs";
10
10
  import path from "node:path";
11
- import { spawn, spawnSync } from "node:child_process";
11
+ import { spawnSync } from "node:child_process";
12
12
  import { pathToFileURL } from "node:url";
13
13
 
14
14
  import { AUTO, discoverPackRoots, namedWithAuto } from "./discover.mjs";
@@ -16,22 +16,19 @@ import { packRoots, resolvePack } from "./compile.mjs";
16
16
  import { CHANGES_DIR, CHANGES_README } from "./form.mjs";
17
17
  import { CHANGE_NAME } from "./index.mjs";
18
18
  import { recipeSet, resolverFor } from "./recipe-set.mjs";
19
+ import { INTERRUPTIONS, runRecipe, tailKeeper } from "./recipe-runner.mjs";
20
+ export { runRecipe, tailKeeper };
19
21
 
20
22
  export const RECIPE_TIMEOUT_MS = 10 * 60 * 1000;
21
23
 
22
24
  export const TAIL_LINES = 25;
23
25
 
24
- const TAIL_BYTES = 64 * 1024;
25
-
26
- const DRAIN_MS = 1000;
27
-
28
26
  const GIT_TIMEOUT_MS = 2 * 60 * 1000;
29
27
 
30
- const CANNOT_RUN = new Set([2, 126, 127]);
31
-
32
28
  const USAGE = [
33
29
  "usage: node cli/finish.mjs -m <subject> [-m <paragraph>]... [options]",
34
30
  " node cli/finish.mjs -F <file | -> [options]",
31
+ " portulan finish <the same arguments>, where the package is installed",
35
32
  "",
36
33
  " -m <text> a paragraph of the commit message, the first its subject, as git takes -m",
37
34
  " -F <file | -> the whole commit message, from a file or from standard input",
@@ -124,6 +121,9 @@ const textOf = (thrown) => {
124
121
  };
125
122
 
126
123
  const resultOf = (id, r) => {
124
+ if (INTERRUPTIONS.includes(r?.interrupted)) {
125
+ return { id, outcome: "could not run", code: null, interrupted: r.interrupted, output: typeof r.output === "string" ? r.output : `interrupted by ${r.interrupted}` };
126
+ }
127
127
  if (r?.outcome === "green") return { id, outcome: "green" };
128
128
  if (r?.outcome === "red" || r?.outcome === "could not run") {
129
129
  return { id, outcome: r.outcome, code: Number.isInteger(r.code) ? r.code : null, output: typeof r.output === "string" ? r.output : "" };
@@ -206,65 +206,6 @@ export function recipesOf({ root, workspaceDir, named, forced }) {
206
206
  return set.ok ? { recipes: set.recipes } : { why: set.reason };
207
207
  }
208
208
 
209
- /** A stream's last `limit` bytes, kept as it arrives: what is held never passes `limit` and one chunk. */
210
- export function tailKeeper(limit = TAIL_BYTES) {
211
- const chunks = [];
212
- let held = 0;
213
- return {
214
- add(chunk) {
215
- chunks.push(chunk);
216
- held += chunk.length;
217
- while (held - chunks[0].length >= limit) held -= chunks.shift().length;
218
- },
219
- get held() {
220
- return held;
221
- },
222
- text: () => Buffer.concat(chunks).subarray(-limit).toString("utf8"),
223
- };
224
- }
225
-
226
- export function runRecipe(recipe, { root, env, timeout = RECIPE_TIMEOUT_MS }) {
227
- return new Promise((resolve) => {
228
- const kept = tailKeeper();
229
- let failure = null;
230
- let late = false;
231
- let limit;
232
- let drain;
233
- const settle = (status) => {
234
- clearTimeout(limit);
235
- clearTimeout(drain);
236
- const code = failure || late ? null : status;
237
- if (code === 0) return resolve({ id: recipe.id, outcome: "green" });
238
- const why = failure ? `\n${failure.message}` : late ? `\nkilled at its time limit of ${timeout / 1000} s` : "";
239
- const cannot = code === null || CANNOT_RUN.has(code);
240
- resolve({ id: recipe.id, outcome: cannot ? "could not run" : "red", code, output: `${kept.text()}${why}` });
241
- };
242
- // As CI and the Stop-gate run a recipe, its stderr joined to its stdout so its lines keep their order.
243
- const child = spawn("bash", ["-c", `exec 2>&1\n${recipe.run}`], { cwd: root, env, stdio: ["ignore", "pipe", "pipe"] });
244
- const stop = () => {
245
- child.stdout.destroy();
246
- child.stderr.destroy();
247
- };
248
- child.stdout.on("data", kept.add);
249
- child.stderr.on("data", kept.add);
250
- child.on("exit", () => {
251
- clearTimeout(limit);
252
- // A process the shell left running is not waited for: once the pipe closes, the child's next write kills it with SIGPIPE, or fails with EPIPE where it ignores that signal, and a child that never writes keeps running.
253
- drain = setTimeout(stop, DRAIN_MS);
254
- });
255
- child.on("error", (error) => {
256
- failure = error;
257
- settle(null);
258
- });
259
- child.on("close", settle);
260
- limit = setTimeout(() => {
261
- late = true;
262
- child.kill();
263
- stop();
264
- }, timeout);
265
- });
266
- }
267
-
268
209
  /** Resolves to `{ code, lines }`: the exit code and what to print, the first line saying what happened. */
269
210
  export async function finish(options, { cwd = process.cwd(), env = process.env, readStdin, runOne = runRecipe } = {}) {
270
211
  const stop = (code, ...lines) => ({ code, lines: [`finish: ${lines[0]}`, ...lines.slice(1)] });
@@ -374,10 +315,11 @@ export async function finish(options, { cwd = process.cwd(), env = process.env,
374
315
  } catch (error) {
375
316
  results.push({ id: recipe.id, outcome: "could not run", code: null, output: textOf(error) });
376
317
  }
318
+ if (results.at(-1).interrupted) break;
377
319
  }
378
320
  const failed = results.filter((r) => r.outcome !== "green");
379
321
  if (failed.length) {
380
- const code = failed.some((r) => r.outcome === "red") ? 1 : 2;
322
+ const code = failed.some((r) => r.interrupted) ? 2 : failed.some((r) => r.outcome === "red") ? 1 : 2;
381
323
  const named = failed.map((r) => r.id).join(", ");
382
324
  return stop(
383
325
  code,
package/cli/form.mjs CHANGED
@@ -10,6 +10,7 @@ import { BOOT_CARD_LINE, BOOT_CARD_UNIT, GUIDANCE_RULES_DIR, leadsOfText } from
10
10
  import { CHANGE_SECTIONS, readChanges, renderChanges } from "./index.mjs";
11
11
  import { instructionsState, shellWord, splitCommand } from "./instructions.mjs";
12
12
  import { recipeSet } from "./recipe-set.mjs";
13
+ import { isInside } from "./inside.mjs";
13
14
 
14
15
  /** Anything that means the form could not be read. Carries no verdict. */
15
16
  export class FormError extends Error {}
@@ -40,12 +41,23 @@ function present(file) {
40
41
  }
41
42
  }
42
43
 
43
- function readOrNull(file) {
44
+ function readOrNull(file, boundary) {
44
45
  try {
45
- return fs.readFileSync(file, "utf8");
46
+ fs.lstatSync(file);
46
47
  } catch (cause) {
47
48
  if (cause.code === "ENOENT" || cause.code === "ENOTDIR") return null;
48
- throw new FormError(`${file} could not be read — ${cause.code ?? cause.message}`);
49
+ throw new FormError(`${file} could not be inspected — ${cause.code ?? "read error"}`);
50
+ }
51
+ try {
52
+ const real = fs.realpathSync(file);
53
+ if (!isInside(fs.realpathSync(boundary), real)) throw new FormError(`${file} is outside the form's source boundary`);
54
+ if (!fs.statSync(real).isFile()) throw new FormError(`${file} is not a regular form source`);
55
+ const raw = fs.readFileSync(real), text = raw.toString("utf8");
56
+ if (!Buffer.from(text, "utf8").equals(raw)) throw new FormError(`${file} is not valid UTF-8`);
57
+ return text;
58
+ } catch (cause) {
59
+ if (cause instanceof FormError) throw cause;
60
+ throw new FormError(`${file} could not be read — ${cause.code ?? "read error"}`);
49
61
  }
50
62
  }
51
63
 
@@ -87,7 +99,9 @@ export function handoffsReadme() {
87
99
  A handoff carries work still open, so the next session starts from where things stand. Write one only
88
100
  when a session ends with work not committed and pushed: committed work carries its why in its commit
89
101
  message, and needs none. Name it \`YYYY-MM-DD-<slug>.md\`, keep only the parts that have something to say,
90
- and five lines is a valid handoff. Drafted from the engine's \`core/templates/handoff.md\`.
102
+ and five lines is a valid handoff. The H1 names the outcome; the filename already gives its date.
103
+ Link measured figures and durable rules in their owning records. Keep decisions and their why when
104
+ compressing to a declared token cap. Drafted from the engine's \`core/templates/handoff.md\`.
91
105
 
92
106
  **State.** Where things stand: what is committed, and what is in progress and uncommitted.
93
107
 
@@ -108,12 +122,14 @@ export function handoffIndexIgnore(rel, workspaceRel) {
108
122
  }
109
123
 
110
124
  export function withIgnoreLines(text, lines) {
111
- const have = new Set((text ?? "").split(/\r?\n/).map((l) => l.trim()));
112
- const patterns = lines.filter((l) => !l.startsWith("#"));
113
- if (patterns.every((l) => have.has(l))) return text ?? "";
114
- const base = text ?? "";
115
- const sep = base === "" ? "" : base.endsWith("\n\n") ? "" : base.endsWith("\n") ? "\n" : "\n\n";
116
- return `${base}${sep}${lines.join("\n")}\n`;
125
+ const bytes = Buffer.isBuffer(text), base = text ?? "";
126
+ const existing = bytes ? base.toString("latin1") : base;
127
+ const have = new Set(existing.split(/\r?\n/).map(l => bytes ? l.replace(/^[ \t\r]+|[ \t\r]+$/g, "") : l.trim()));
128
+ const patterns = lines.filter(l => !l.startsWith("#"));
129
+ if (patterns.every(l => have.has(bytes ? Buffer.from(l).toString("latin1") : l))) return base;
130
+ const sep = existing === "" || existing.endsWith("\n\n") ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
131
+ const addition = `${sep}${lines.join("\n")}\n`;
132
+ return bytes ? Buffer.concat([base, Buffer.from(addition)]) : base + addition;
117
133
  }
118
134
 
119
135
  export const COMMENTS_RECIPE = "comments";
@@ -181,9 +197,9 @@ exit 2
181
197
 
182
198
  // ------------------------------------------------------------------------- git
183
199
 
184
- export function gitIn(root) {
200
+ export function gitIn(root, { env, timeout, noFsmonitor = false } = {}) {
185
201
  const run = (...args) => {
186
- const out = spawnSync("git", ["-C", root, ...args], { encoding: "utf8" });
202
+ const out = spawnSync("git", [...(noFsmonitor ? ["-c", "core.fsmonitor=false"] : []), "-C", root, ...args], { encoding: "utf8", timeout, killSignal: "SIGKILL", env });
187
203
  if (out.error) return { status: null, stdout: "", stderr: String(out.error.code ?? out.error.message) };
188
204
  return { status: out.status, stdout: out.stdout, stderr: out.stderr };
189
205
  };
@@ -296,7 +312,7 @@ export function markdownOnDisk(root, keep = null) {
296
312
  export function sessionLogsIn(root, files) {
297
313
  const found = [];
298
314
  for (const rel of files) {
299
- const text = readOrNull(path.join(root, ...rel.split("/")));
315
+ const text = readOrNull(path.join(root, ...rel.split("/")), root);
300
316
  if (text === null || !text.includes("Session log")) continue;
301
317
  const logs = sessionLogSections(text).filter((s) => s.entries);
302
318
  if (logs.length) found.push({ file: rel, line: logs[0].line });
@@ -527,7 +543,7 @@ export function draftCard(manifest, read, { workspace, inTree, repoCards = [] })
527
543
  }
528
544
  section(
529
545
  "Records",
530
- 'Close a change with `git add <new files> && node <plugin root>/cli/finish.mjs -m "<subject>" -m "<why>"` after writing its changelog entry, a one-bullet file `changes/<slug>.<section>.md`: it commits and pushes only when every verify recipe passes.',
546
+ 'Close a change with `git add <new files> && portulan finish -m "<subject>" -m "<why>"` after writing its changelog entry, a one-bullet file `changes/<slug>.<section>.md`: it commits and pushes only when every verify recipe passes. Where `portulan` is not on the path, run `npx @sleepy_panda_srl/portulan finish` instead, and do not search the disk for it.',
531
547
  ...(slots.handoffs ? [`A session ending with work not committed and pushed leaves a dated handoff in ${shown(slots.handoffs)}.`] : []),
532
548
  );
533
549
  section(
@@ -567,7 +583,7 @@ export function formOf(workspaceDir, manifest) {
567
583
  const tree = typeof manifest?.tree === "string" ? path.resolve(wsDir, manifest.tree) : null;
568
584
  if (tree === null) return { tree, pieces };
569
585
 
570
- const changelog = readOrNull(path.join(tree, CHANGELOG));
586
+ const changelog = readOrNull(path.join(tree, CHANGELOG), tree);
571
587
  const fragments = present(path.join(tree, CHANGES_README));
572
588
  if (changelog !== null || fragments) add("changes", fragments ? "new" : "today", fragments ? `changelog fragments in ${CHANGES_DIR}/` : `no ${CHANGES_README}`);
573
589
  const entries = changelog === null ? null : unreleasedCount(changelog);
@@ -593,16 +609,16 @@ export function formOf(workspaceDir, manifest) {
593
609
  if (typeof context !== "string") {
594
610
  add("card", "today", "no boot card: `slots.context` is undeclared");
595
611
  } else {
596
- const source = readOrNull(path.resolve(wsDir, context, `${BOOT_CARD_UNIT}.md`));
612
+ const source = readOrNull(path.resolve(wsDir, context, `${BOOT_CARD_UNIT}.md`), wsDir);
597
613
  if (source === null) add("card", "new", "no boot card, by choice: `slots.context` holds no `boot` unit");
598
614
  else if (withReading(source) !== null) add("card", "today", "a boot card drafted before it carried the engine's rules on reading and the cache");
599
615
  else if (!carriesReading(source)) {
600
616
  pieces.push({ id: "card", state: "today", hand: true, text: `a boot card without the engine's rules on reading and the cache, whose head \`upgrade\` does not recognise: add a section holding the line \`${READING_LINE}\`` });
601
- } else if (readOrNull(path.join(tree, COMPILED_CARD)) !== null) add("card", "new", "a compiled boot card");
602
- else if ((readOrNull(path.join(tree, "AGENTS.md")) ?? "").split(/\r?\n/).includes(BOOT_CARD_LINE)) add("card", "new", "a boot card at the head of AGENTS.md, which this host reads");
617
+ } else if (readOrNull(path.join(tree, COMPILED_CARD), tree) !== null) add("card", "new", "a compiled boot card");
618
+ else if ((readOrNull(path.join(tree, "AGENTS.md"), tree) ?? "").split(/\r?\n/).includes(BOOT_CARD_LINE)) add("card", "new", "a boot card at the head of AGENTS.md, which this host reads");
603
619
  else add("card", "today", `a boot card not yet compiled to ${COMPILED_CARD}: run \`portulan compile\``);
604
620
  }
605
- const marked = instructionsState(tree, (rel) => readOrNull(path.join(tree, ...rel.split("/"))));
621
+ const marked = instructionsState(tree, (rel) => readOrNull(path.join(tree, ...rel.split("/")), tree));
606
622
  if (marked.files.length) {
607
623
  const files = marked.files.join(" and ");
608
624
  const count = (n) => `${n} section${n === 1 ? "" : "s"}`;
@@ -0,0 +1,39 @@
1
+ // Flat frontmatter for plugin components and host bindings.
2
+
3
+ const BLOCK_SCALAR = /^[|>][+-]?$/;
4
+
5
+ /** A flat-YAML reader: `fields` is null without a usable block, and `error` is set when that is a defect. */
6
+ export function parseFrontmatter(text) {
7
+ const lines = text.split(/\r?\n/);
8
+ if (lines[0]?.trim() !== "---") return { fields: null };
9
+
10
+ const close = lines.indexOf("---", 1);
11
+ if (close === -1) {
12
+ return { fields: null, error: "the frontmatter block is never closed" };
13
+ }
14
+
15
+ const fields = {};
16
+ const body = lines.slice(1, close);
17
+ for (let i = 0; i < body.length; i += 1) {
18
+ const match = /^([A-Za-z_][\w-]*)\s*:\s*(.*)$/.exec(body[i]);
19
+ if (!match) continue;
20
+ const key = match[1];
21
+ let value = match[2].trim();
22
+
23
+ if (BLOCK_SCALAR.test(value)) {
24
+ const collected = [];
25
+ while (i + 1 < body.length && (body[i + 1].trim() === "" || /^\s+/.test(body[i + 1]))) {
26
+ i += 1;
27
+ collected.push(body[i].trim());
28
+ }
29
+ value = collected.join(" ").trim();
30
+ } else if (
31
+ (value.startsWith('"') && value.endsWith('"') && value.length >= 2) ||
32
+ (value.startsWith("'") && value.endsWith("'") && value.length >= 2)
33
+ ) {
34
+ value = value.slice(1, -1);
35
+ }
36
+ fields[key] = value;
37
+ }
38
+ return { fields };
39
+ }
package/cli/gate.mjs CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  import fs from "node:fs";
9
9
  import path from "node:path";
10
- import { fileURLToPath } from "node:url";
10
+ import { pathToFileURL } from "node:url";
11
11
 
12
12
  import { matchesRule, policyPath, packContributions, composeFragments, parse } from "./compile.mjs";
13
13
 
@@ -64,17 +64,19 @@ async function main() {
64
64
  const rule = decide(payload, yielded(policy));
65
65
  if (!rule) stepAside();
66
66
 
67
- const decision = rule.tier === "prohibited" ? "deny" : "ask";
68
- process.stdout.write(
69
- `${JSON.stringify({
70
- hookSpecificOutput: {
71
- hookEventName: "PreToolUse",
72
- permissionDecision: decision,
73
- permissionDecisionReason: `PORTULAN GATE \`${rule.id}\` (${rule.tier}) — ${rule.reason}`,
74
- },
75
- })}\n`,
76
- );
67
+ process.stdout.write(`${gateOutput(rule)}\n`);
77
68
  process.exit(0);
78
69
  }
79
70
 
80
- main().catch(stepAside);
71
+ export function gateOutput(rule) {
72
+ if (!rule) return null;
73
+ return JSON.stringify({
74
+ hookSpecificOutput: {
75
+ hookEventName: "PreToolUse",
76
+ permissionDecision: rule.tier === "prohibited" ? "deny" : "ask",
77
+ permissionDecisionReason: `PORTULAN GATE \`${rule.id}\` (${rule.tier}) — ${rule.reason}`,
78
+ },
79
+ });
80
+ }
81
+
82
+ if (process.argv[1] && import.meta.url === pathToFileURL(fs.realpathSync(process.argv[1])).href) main().catch(stepAside);