knodin 0.7.4 → 0.7.6

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 (104) hide show
  1. package/README.md +4 -1
  2. package/dist/bin/cli.js +108 -1859
  3. package/dist/bin/launcher.js +3 -10
  4. package/dist/chunks/chunk-DMQAGX77.js +654 -0
  5. package/dist/chunks/chunk-F4Z3Z766.js +4 -0
  6. package/dist/chunks/chunk-SIJAQVSX.js +3 -0
  7. package/dist/chunks/chunk-X6M4HUUE.js +2 -0
  8. package/dist/chunks/chunk-YPRMY2LP.js +8 -0
  9. package/dist/chunks/pure-compression-cli-4TA2TQD5.js +5 -0
  10. package/dist/chunks/server-7EDF4CBY.js +14 -0
  11. package/dist/chunks/structural-fast-path-KD5KQSPX.js +4 -0
  12. package/dist/src/init-progress-worker.js +2 -106
  13. package/dist/src/repair-progress-worker.js +2 -120
  14. package/docs/INSTALLATION.md +17 -1
  15. package/docs/releases/0.7.6.md +25 -0
  16. package/package.json +1 -110
  17. package/dist/src/agent-integration.js +0 -238
  18. package/dist/src/artifact-refresh.js +0 -81
  19. package/dist/src/cli-args.js +0 -274
  20. package/dist/src/cli-model.js +0 -358
  21. package/dist/src/codeflow-replay.js +0 -80
  22. package/dist/src/compact-structural.js +0 -96
  23. package/dist/src/competitive-cold-mcp.js +0 -40
  24. package/dist/src/competitive-constraints.js +0 -20
  25. package/dist/src/competitive-manifest.js +0 -411
  26. package/dist/src/competitive-measurement.js +0 -183
  27. package/dist/src/competitive-runner.js +0 -487
  28. package/dist/src/competitive-sandbox.js +0 -108
  29. package/dist/src/context-export.js +0 -422
  30. package/dist/src/context.js +0 -102
  31. package/dist/src/diagnostics.js +0 -449
  32. package/dist/src/docs-sections.js +0 -141
  33. package/dist/src/doctor.js +0 -380
  34. package/dist/src/engine/ann-hnsw.js +0 -271
  35. package/dist/src/engine/embeddings.js +0 -193
  36. package/dist/src/engine/file-walker.js +0 -43
  37. package/dist/src/engine/git-history.js +0 -289
  38. package/dist/src/engine/index.js +0 -13377
  39. package/dist/src/engine/perf.js +0 -115
  40. package/dist/src/engine/prune.js +0 -112
  41. package/dist/src/engine/scip-import.js +0 -408
  42. package/dist/src/engine/source-policy.js +0 -69
  43. package/dist/src/engine/sqlite.js +0 -71
  44. package/dist/src/engine/symbol-delete.js +0 -58
  45. package/dist/src/execution-profile.js +0 -203
  46. package/dist/src/failure-diagnosis.js +0 -649
  47. package/dist/src/fleet.js +0 -7
  48. package/dist/src/git-executable.js +0 -31
  49. package/dist/src/graph-query-health.js +0 -115
  50. package/dist/src/hook-manager-integration.js +0 -156
  51. package/dist/src/index-activity.js +0 -125
  52. package/dist/src/init-progress.js +0 -155
  53. package/dist/src/init.js +0 -1254
  54. package/dist/src/lifecycle-health.js +0 -250
  55. package/dist/src/lsp-readonly.js +0 -217
  56. package/dist/src/output-compression.js +0 -629
  57. package/dist/src/output-telemetry.js +0 -363
  58. package/dist/src/pr-triage.js +0 -638
  59. package/dist/src/progressive-evidence.js +0 -473
  60. package/dist/src/pure-compression-cli.js +0 -101
  61. package/dist/src/relationship-adapters.js +0 -370
  62. package/dist/src/release-attestation.js +0 -533
  63. package/dist/src/release-preflight.js +0 -510
  64. package/dist/src/repair-progress.js +0 -262
  65. package/dist/src/repository-init-process.js +0 -173
  66. package/dist/src/repository-management.js +0 -1231
  67. package/dist/src/response-budget.js +0 -194
  68. package/dist/src/server.js +0 -73
  69. package/dist/src/structural-fast-path.js +0 -338
  70. package/dist/src/structural-snapshot.js +0 -33
  71. package/dist/src/system-config.js +0 -615
  72. package/dist/src/terminal-help.js +0 -83
  73. package/dist/src/tools/knodin-tools.js +0 -1529
  74. package/dist/src/update-ceremony.js +0 -158
  75. package/dist/src/update-policy.js +0 -944
  76. package/dist/src/update-trust.js +0 -503
  77. package/dist/src/version.js +0 -13
  78. package/dist/src/visualization.js +0 -162
  79. package/dist/src/wait-for-fresh.js +0 -98
  80. package/dist/src/worktree-lifecycle.js +0 -231
  81. package/docs/CONTAINED-EXECUTION.md +0 -77
  82. package/docs/DIAGNOSTICS.md +0 -45
  83. package/docs/GIT-HISTORY-REVIEW.md +0 -39
  84. package/docs/PROGRESSIVE-EVIDENCE.md +0 -37
  85. package/docs/PT-ACCESS-RECOMMENDATION.md +0 -91
  86. package/docs/RELEASE-0.3-EVIDENCE.md +0 -73
  87. package/docs/SCIP-IMPORT.md +0 -57
  88. package/docs/SIGNED-UPDATES.md +0 -151
  89. package/docs/TELEMETRY.md +0 -46
  90. package/docs/assets/knodin-favicon.svg +0 -4
  91. package/docs/releases/0.3.0.md +0 -46
  92. package/docs/releases/0.4.0.md +0 -68
  93. package/docs/releases/0.4.1.md +0 -28
  94. package/docs/releases/0.4.2.md +0 -27
  95. package/docs/releases/0.4.3.md +0 -23
  96. package/docs/releases/0.5.0.md +0 -29
  97. package/docs/releases/0.5.1.md +0 -17
  98. package/docs/releases/0.6.0.md +0 -18
  99. package/docs/releases/0.7.0.md +0 -24
  100. package/docs/releases/0.7.1.md +0 -21
  101. package/docs/releases/0.7.2.md +0 -21
  102. package/docs/releases/0.7.3.md +0 -23
  103. package/docs/releases/0.7.4.md +0 -17
  104. package/schemas/release-attestation-v1.schema.json +0 -210
@@ -1,115 +0,0 @@
1
- function hasNonReconciliableDamage(graph) {
2
- if (Object.values(graph.orphaned).some((count) => count > 0))
3
- return true;
4
- const lifecycleIssues = new Set(graph.lifecycle?.issues ?? []);
5
- const reconcilablePrefixes = [
6
- "indexed file no longer exists: ",
7
- "indexed file is no longer eligible: ",
8
- "indexed snapshot differs from disk: ",
9
- ];
10
- return graph.missing.records.some((record) => !lifecycleIssues.has(record) &&
11
- !reconcilablePrefixes.some((prefix) => record.startsWith(prefix)));
12
- }
13
- function unavailableState(graph) {
14
- if (graph.freshness?.state === "queued")
15
- return "queued";
16
- if (graph.freshness?.state === "indexing")
17
- return "indexing";
18
- if (graph.status === "indexing")
19
- return "indexing";
20
- if (graph.schemaVersion === 0)
21
- return graph.coverage.sourceFiles === 0 ? "empty-repository" : "not-initialized";
22
- if (graph.coverage.sourceFiles === 0)
23
- return "empty-repository";
24
- if (graph.coverage.indexedFiles === 0)
25
- return "empty-index";
26
- if (graph.status === "repair-needed" && hasNonReconciliableDamage(graph))
27
- return "repair-needed";
28
- return null;
29
- }
30
- export async function inspectGraphQueryHealth(repo, status) {
31
- try {
32
- const graph = await status(repo);
33
- const state = unavailableState(graph);
34
- if (!state)
35
- return {
36
- available: true,
37
- state: graph.lifecycle && graph.lifecycle.status !== "healthy"
38
- ? "lifecycle-degraded"
39
- : "healthy",
40
- graph,
41
- };
42
- return {
43
- available: false,
44
- status: "unavailable",
45
- state,
46
- graph,
47
- remediation: graph.repairSteps,
48
- };
49
- }
50
- catch (error) {
51
- return {
52
- available: false,
53
- status: "unavailable",
54
- state: "unknown",
55
- remediation: ["Run `knodin doctor` and `knodin status --deep`."],
56
- error: error instanceof Error ? error.message : String(error),
57
- };
58
- }
59
- }
60
- function hasNoMatches(value) {
61
- if (Array.isArray(value.results))
62
- return value.results.length === 0;
63
- if (typeof value.count === "number")
64
- return value.count === 0;
65
- if (typeof value.changedFileCount === "number")
66
- return value.changedFileCount === 0;
67
- if (typeof value.summary === "string" && /\bnot found\b/i.test(value.summary))
68
- return true;
69
- if (typeof value.refused === "string" &&
70
- /\b(?:not found|no edit sites|no matches)\b/i.test(value.refused))
71
- return true;
72
- return false;
73
- }
74
- /** Add an explicit healthy/no-match outcome without replacing operation data. */
75
- export function decorateGraphQueryResult(value, graphState = "healthy", freshness) {
76
- if (value === null || value === undefined) {
77
- return {
78
- status: "no-match",
79
- result: value ?? null,
80
- ...(freshness ? { freshness } : {}),
81
- availability: { state: "no-match", graphState, outcome: "no-match" },
82
- };
83
- }
84
- if (Array.isArray(value)) {
85
- return {
86
- status: value.length === 0 ? "no-match" : "ok",
87
- results: value,
88
- ...(freshness ? { freshness } : {}),
89
- availability: {
90
- state: value.length === 0 ? "no-match" : graphState,
91
- graphState,
92
- outcome: value.length === 0 ? "no-match" : "matches",
93
- },
94
- };
95
- }
96
- if (typeof value !== "object") {
97
- return {
98
- result: value,
99
- ...(freshness ? { freshness } : {}),
100
- availability: { state: graphState, outcome: "complete" },
101
- };
102
- }
103
- const root = value;
104
- const noMatch = hasNoMatches(root) && root.ambiguity === undefined;
105
- return {
106
- ...root,
107
- ...(freshness ? { freshness } : {}),
108
- ...(noMatch && root.status === undefined ? { status: "no-match" } : {}),
109
- availability: {
110
- state: noMatch ? "no-match" : graphState,
111
- graphState,
112
- outcome: noMatch ? "no-match" : "matches",
113
- },
114
- };
115
- }
@@ -1,156 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { isMap, parseDocument } from "yaml";
4
- export const KNODIN_LEFTHOOK_COMMAND = "knodin-refresh";
5
- const MANAGED_COMMENT = "Managed locally by knodin init; repository hook configuration is not modified.";
6
- const LEFTHOOK_CONFIGS = [
7
- { main: "lefthook.yml", local: "lefthook-local.yml" },
8
- { main: "lefthook.yaml", local: "lefthook-local.yaml" },
9
- { main: ".lefthook.yml", local: ".lefthook-local.yml" },
10
- { main: ".lefthook.yaml", local: ".lefthook-local.yaml" },
11
- { main: ".config/lefthook.yml", local: ".config/lefthook-local.yml" },
12
- { main: ".config/lefthook.yaml", local: ".config/lefthook-local.yaml" },
13
- ];
14
- const HOOK_COMMANDS = {
15
- "post-commit": { run: ".knodin/hooks/manager-dispatch.sh post-commit" },
16
- "post-checkout": {
17
- run: ".knodin/hooks/manager-dispatch.sh post-checkout {1} {2} {3}",
18
- },
19
- "post-merge": { run: ".knodin/hooks/manager-dispatch.sh post-merge {1}" },
20
- "post-rewrite": {
21
- run: ".knodin/hooks/manager-dispatch.sh post-rewrite {1}",
22
- use_stdin: true,
23
- },
24
- };
25
- export const LEFTHOOK_DISPATCHER = `#!/bin/sh
26
- # knodin manager-native lifecycle dispatcher. Generated by knodin init.
27
- set -u
28
- [ "\${KNODIN_WRAPPER_ACTIVE:-0}" = "1" ] && exit 0
29
- REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || true)"
30
- [ -n "$REPO_ROOT" ] || exit 0
31
- BACKGROUND="$REPO_ROOT/.knodin/hooks/background-index.sh"
32
- [ -x "$BACKGROUND" ] || exit 0
33
- HOOK="\${1:-}"
34
- shift || true
35
- case "$HOOK" in
36
- post-commit)
37
- set -- commit
38
- ;;
39
- post-checkout)
40
- [ "$#" -ge 2 ] || exit 0
41
- set -- checkout "$1" "$2"
42
- ;;
43
- post-merge)
44
- BEFORE="$(git rev-parse ORIG_HEAD 2>/dev/null || true)"
45
- AFTER="$(git rev-parse HEAD 2>/dev/null || true)"
46
- [ -n "$BEFORE" ] && [ -n "$AFTER" ] || exit 0
47
- set -- merge "$BEFORE" "$AFTER"
48
- ;;
49
- post-rewrite)
50
- INPUT="$(mktemp "$REPO_ROOT/.knodin/hooks/rewrite-input.XXXXXX")" || exit 0
51
- cat > "$INPUT"
52
- set -- rewrite "$INPUT"
53
- ;;
54
- *) exit 0 ;;
55
- esac
56
- nohup "$BACKGROUND" "$@" >/dev/null 2>&1 &
57
- exit 0
58
- `;
59
- function lefthookFiles(repo) {
60
- for (const candidate of LEFTHOOK_CONFIGS) {
61
- if (fs.existsSync(path.join(repo, candidate.main)))
62
- return candidate;
63
- }
64
- return null;
65
- }
66
- function assertRegularOrMissing(target) {
67
- try {
68
- const stat = fs.lstatSync(target);
69
- if (!stat.isFile() || stat.isSymbolicLink())
70
- throw new Error(`knodin init: refusing non-regular hook-manager config: ${target}`);
71
- }
72
- catch (error) {
73
- if (error.code !== "ENOENT")
74
- throw error;
75
- }
76
- }
77
- function atomicWrite(target, content, mode) {
78
- fs.mkdirSync(path.dirname(target), { recursive: true });
79
- const temporary = `${target}.${process.pid}.${Date.now()}.tmp`;
80
- fs.writeFileSync(temporary, content, { encoding: "utf-8", mode, flag: "wx" });
81
- fs.renameSync(temporary, target);
82
- if (mode !== undefined)
83
- fs.chmodSync(target, mode);
84
- }
85
- function ensureMap(document, keys) {
86
- const existing = document.getIn(keys, true);
87
- if (existing !== undefined && !isMap(existing))
88
- throw new Error(`knodin init: cannot merge Lefthook integration because ${keys.join(".")} is not a map`);
89
- }
90
- /**
91
- * Add a local-only Lefthook overlay. The repository-owned main configuration is
92
- * never edited; an existing local overlay is parsed and merged by key.
93
- */
94
- export function installHookManagerIntegration(repoPath) {
95
- const repo = path.resolve(repoPath);
96
- const files = lefthookFiles(repo);
97
- if (!files)
98
- return null;
99
- const localPath = path.join(repo, files.local);
100
- assertRegularOrMissing(localPath);
101
- const existing = fs.existsSync(localPath) ? fs.readFileSync(localPath, "utf-8") : "";
102
- const document = parseDocument(existing || "{}\n", { keepSourceTokens: true });
103
- if (document.errors.length > 0)
104
- throw new Error(`knodin init: cannot parse ${files.local}: ${document.errors[0]?.message}`);
105
- if (!isMap(document.contents))
106
- throw new Error(`knodin init: cannot merge ${files.local}: root must be a map`);
107
- if (!existing)
108
- document.commentBefore = MANAGED_COMMENT;
109
- for (const [hook, command] of Object.entries(HOOK_COMMANDS)) {
110
- ensureMap(document, [hook]);
111
- ensureMap(document, [hook, "commands"]);
112
- document.setIn([hook, "commands", KNODIN_LEFTHOOK_COMMAND], command);
113
- }
114
- atomicWrite(localPath, document.toString({ lineWidth: 0 }));
115
- const dispatcher = path.join(repo, ".knodin", "hooks", "manager-dispatch.sh");
116
- assertRegularOrMissing(dispatcher);
117
- atomicWrite(dispatcher, LEFTHOOK_DISPATCHER, 0o755);
118
- return {
119
- manager: "lefthook",
120
- localConfig: files.local,
121
- dispatcher: ".knodin/hooks/manager-dispatch.sh",
122
- };
123
- }
124
- export function inspectLefthookIntegration(repoPath) {
125
- const repo = path.resolve(repoPath);
126
- const files = lefthookFiles(repo);
127
- if (!files)
128
- return { configured: false, localConfig: null };
129
- try {
130
- const document = parseDocument(fs.readFileSync(path.join(repo, files.local), "utf-8"));
131
- if (document.errors.length > 0)
132
- return { configured: false, localConfig: files.local };
133
- for (const [hook, command] of Object.entries(HOOK_COMMANDS)) {
134
- const run = document.getIn([hook, "commands", KNODIN_LEFTHOOK_COMMAND, "run"]);
135
- if (run !== command.run)
136
- return { configured: false, localConfig: files.local };
137
- if ("use_stdin" in command &&
138
- document.getIn([hook, "commands", KNODIN_LEFTHOOK_COMMAND, "use_stdin"]) !== true)
139
- return { configured: false, localConfig: files.local };
140
- }
141
- const dispatcher = path.join(repo, ".knodin", "hooks", "manager-dispatch.sh");
142
- const stat = fs.statSync(dispatcher);
143
- return {
144
- configured: stat.isFile() &&
145
- (stat.mode & 0o111) !== 0 &&
146
- fs.readFileSync(dispatcher, "utf-8") === LEFTHOOK_DISPATCHER,
147
- localConfig: files.local,
148
- };
149
- }
150
- catch {
151
- return { configured: false, localConfig: files.local };
152
- }
153
- }
154
- export function isActiveLefthookHook(content) {
155
- return /\bcall_lefthook\b|\blefthook(?:\s|")/.test(content);
156
- }
@@ -1,125 +0,0 @@
1
- import child_process from "node:child_process";
2
- import fs from "node:fs";
3
- import path from "node:path";
4
- function activityPath(repo) {
5
- return path.join(path.resolve(repo), ".knodin", "index-activity.json");
6
- }
7
- const ACTIVITY_CLOCK_SKEW_MS = 2_000;
8
- const MAX_ACTIVITY_AGE_MS = 7 * 24 * 60 * 60 * 1_000;
9
- const INDEX_PHASES = new Set([
10
- "starting",
11
- "collecting-files",
12
- "indexing-files",
13
- "embedding-model",
14
- "embedding-symbols",
15
- "finalizing",
16
- "verifying",
17
- "completed",
18
- ]);
19
- function processIsAlive(pid) {
20
- try {
21
- process.kill(pid, 0);
22
- return true;
23
- }
24
- catch {
25
- return false;
26
- }
27
- }
28
- function processStartedAt(pid) {
29
- if (pid === process.pid)
30
- return Date.now() - process.uptime() * 1_000;
31
- if (process.platform === "win32")
32
- return null;
33
- try {
34
- const output = child_process.execFileSync("ps", ["-o", "lstart=", "-p", String(pid)], {
35
- encoding: "utf8",
36
- stdio: ["ignore", "pipe", "ignore"],
37
- });
38
- const startedAt = Date.parse(output.trim());
39
- return Number.isFinite(startedAt) ? startedAt : null;
40
- }
41
- catch {
42
- return null;
43
- }
44
- }
45
- /** Cross-process, source-safe status for a currently running index operation. */
46
- export function createIndexActivityReporter(repo) {
47
- const target = activityPath(repo);
48
- const startedAt = new Date().toISOString();
49
- const write = (activity) => {
50
- fs.mkdirSync(path.dirname(target), { recursive: true });
51
- const temporary = `${target}.${process.pid}.tmp`;
52
- try {
53
- fs.writeFileSync(temporary, `${JSON.stringify(activity)}\n`, "utf8");
54
- fs.renameSync(temporary, target);
55
- }
56
- finally {
57
- fs.rmSync(temporary, { force: true });
58
- }
59
- };
60
- return {
61
- start() {
62
- write({
63
- pid: process.pid,
64
- startedAt,
65
- updatedAt: startedAt,
66
- phase: "starting",
67
- phaseCompleted: 0,
68
- message: "Opening local graph database",
69
- });
70
- },
71
- update(event) {
72
- write({
73
- pid: process.pid,
74
- startedAt,
75
- updatedAt: new Date().toISOString(),
76
- phase: event.phase,
77
- phaseCompleted: event.phaseCompleted,
78
- ...(event.phaseTotal === undefined ? {} : { phaseTotal: event.phaseTotal }),
79
- message: event.message,
80
- });
81
- },
82
- stop() {
83
- try {
84
- const current = JSON.parse(fs.readFileSync(target, "utf8"));
85
- if (current.pid === process.pid)
86
- fs.unlinkSync(target);
87
- }
88
- catch { }
89
- },
90
- };
91
- }
92
- /** Return only a live operation; crash-left activity files never claim freshness. */
93
- export function readIndexActivity(repo) {
94
- try {
95
- const activity = JSON.parse(fs.readFileSync(activityPath(repo), "utf8"));
96
- if (!Number.isInteger(activity.pid) || activity.pid <= 0 || !processIsAlive(activity.pid))
97
- return null;
98
- const now = Date.now();
99
- const startedAt = Date.parse(activity.startedAt);
100
- const updatedAt = Date.parse(activity.updatedAt);
101
- if (!Number.isFinite(startedAt) ||
102
- !Number.isFinite(updatedAt) ||
103
- startedAt > now + ACTIVITY_CLOCK_SKEW_MS ||
104
- updatedAt < startedAt ||
105
- updatedAt > now + ACTIVITY_CLOCK_SKEW_MS ||
106
- now - startedAt > MAX_ACTIVITY_AGE_MS ||
107
- !INDEX_PHASES.has(activity.phase) ||
108
- !Number.isFinite(activity.phaseCompleted) ||
109
- activity.phaseCompleted < 0 ||
110
- (activity.phaseTotal !== undefined &&
111
- (!Number.isFinite(activity.phaseTotal) ||
112
- activity.phaseTotal < 0 ||
113
- activity.phaseCompleted > activity.phaseTotal)) ||
114
- typeof activity.message !== "string") {
115
- return null;
116
- }
117
- const processStart = processStartedAt(activity.pid);
118
- if (processStart !== null && processStart > startedAt + ACTIVITY_CLOCK_SKEW_MS)
119
- return null;
120
- return activity;
121
- }
122
- catch {
123
- return null;
124
- }
125
- }
@@ -1,155 +0,0 @@
1
- const HEARTBEAT_MS = 15_000;
2
- const TTY_THROTTLE_MS = 100;
3
- const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
4
- const CLEAR_TTY_LINE = "\r\x1b[2K";
5
- export function normalizeTerminalDimension(value, fallback) {
6
- return Number.isFinite(value) && (value ?? 0) > 0 ? Math.floor(value) : fallback;
7
- }
8
- function formatCount(value) {
9
- return value.toLocaleString();
10
- }
11
- function formatBytes(value) {
12
- if (value < 1024 * 1024)
13
- return `${Math.round(value / 1024)} KB`;
14
- return `${(value / (1024 * 1024)).toFixed(1)} MB`;
15
- }
16
- function formatDuration(seconds) {
17
- if (seconds < 1)
18
- return "<1s";
19
- if (seconds < 60)
20
- return `${Math.ceil(seconds)}s`;
21
- return `${Math.floor(seconds / 60)}m ${Math.ceil(seconds % 60)}s`;
22
- }
23
- export function formatProgress(event, ratePerSecond, operation = "init") {
24
- const model = event.modelFile ? ` (${event.modelFile})` : "";
25
- const total = event.phaseTotal;
26
- if (total === undefined)
27
- return `[${operation}:${event.phase}] ${event.message}${model}`;
28
- const counter = event.phase === "embedding-model"
29
- ? `${formatBytes(event.phaseCompleted)} / ${formatBytes(total)}`
30
- : `${formatCount(event.phaseCompleted)} / ${formatCount(total)}`;
31
- const percent = total > 0 ? ` (${Math.floor((event.phaseCompleted / total) * 100)}%)` : "";
32
- const eta = ratePerSecond && total > event.phaseCompleted
33
- ? ` • ~${formatDuration((total - event.phaseCompleted) / ratePerSecond)} remaining`
34
- : "";
35
- const rate = ratePerSecond && event.phase !== "embedding-model"
36
- ? ` • ${ratePerSecond.toFixed(ratePerSecond >= 10 ? 0 : 1)}/s`
37
- : "";
38
- return `[${operation}:${event.phase}] ${counter}${percent} ${event.message}${model}${rate}${eta}`;
39
- }
40
- /**
41
- * Human-only init feedback. It writes to stderr, leaving stdout stable for the
42
- * final result, redraws a TTY at most 10 times/sec, and heartbeats a quiet phase.
43
- */
44
- export function createInitProgressRenderer(options) {
45
- const operation = options.operation ?? "init";
46
- const now = options.now ?? Date.now;
47
- const schedule = options.setTimeout ??
48
- ((callback, intervalMs) => setTimeout(callback, intervalMs));
49
- const unschedule = options.clearTimeout ?? ((handle) => clearTimeout(handle));
50
- const tty = options.stderr.isTTY === true;
51
- let started = false;
52
- let stopped = false;
53
- let hasTtyLine = false;
54
- let startedAt = 0;
55
- let heartbeat;
56
- let spinner;
57
- let spinnerFrame = 0;
58
- let displayLine = `[${operation}:starting] Opening local graph database`;
59
- let lastWriteAt = Number.NEGATIVE_INFINITY;
60
- let lastPhase = "starting";
61
- let lastPercentBucket = -1;
62
- let phaseStartedElapsedMs = 0;
63
- let phaseStartedCompleted = 0;
64
- const scheduleHeartbeat = () => {
65
- if (heartbeat !== undefined || stopped)
66
- return;
67
- heartbeat = schedule(() => {
68
- heartbeat = undefined;
69
- if (stopped)
70
- return;
71
- write(`${displayLine} (still working; ${Math.floor((now() - startedAt) / 1000)}s elapsed)`, false);
72
- scheduleHeartbeat();
73
- }, HEARTBEAT_MS);
74
- if (typeof heartbeat === "object" && heartbeat !== null && "unref" in heartbeat) {
75
- heartbeat.unref();
76
- }
77
- };
78
- const scheduleSpinner = () => {
79
- if (!tty || spinner !== undefined || stopped)
80
- return;
81
- spinner = schedule(() => {
82
- spinner = undefined;
83
- if (stopped)
84
- return;
85
- spinnerFrame = (spinnerFrame + 1) % SPINNER_FRAMES.length;
86
- options.stderr.write(`${CLEAR_TTY_LINE}${SPINNER_FRAMES[spinnerFrame]} ${displayLine} (${Math.floor((now() - startedAt) / 1000)}s elapsed)`);
87
- hasTtyLine = true;
88
- scheduleSpinner();
89
- }, TTY_THROTTLE_MS);
90
- if (typeof spinner === "object" && spinner !== null && "unref" in spinner) {
91
- spinner.unref();
92
- }
93
- };
94
- const write = (line, updateDisplay = true) => {
95
- if (updateDisplay)
96
- displayLine = line;
97
- if (tty) {
98
- options.stderr.write(`${CLEAR_TTY_LINE}${SPINNER_FRAMES[spinnerFrame]} ${line} (${Math.floor((now() - startedAt) / 1000)}s elapsed)`);
99
- hasTtyLine = true;
100
- }
101
- else {
102
- options.stderr.write(`${line}\n`);
103
- }
104
- lastWriteAt = now();
105
- scheduleHeartbeat();
106
- };
107
- return {
108
- start() {
109
- if (started || stopped)
110
- return;
111
- started = true;
112
- startedAt = now();
113
- scheduleSpinner();
114
- write(displayLine);
115
- },
116
- onProgress(event) {
117
- if (stopped)
118
- return;
119
- if (!started)
120
- this.start();
121
- const percentage = event.phaseTotal && event.phaseTotal > 0
122
- ? Math.floor((event.phaseCompleted / event.phaseTotal) * 100)
123
- : undefined;
124
- const bucket = percentage === undefined ? -1 : Math.floor(percentage / 5);
125
- const phaseChanged = event.phase !== lastPhase;
126
- if (phaseChanged) {
127
- phaseStartedElapsedMs = event.elapsedMs;
128
- phaseStartedCompleted = event.phaseCompleted;
129
- }
130
- const phaseElapsedSeconds = Math.max(0, event.elapsedMs - phaseStartedElapsedMs) / 1_000;
131
- const ratePerSecond = phaseElapsedSeconds > 0 && event.phaseCompleted > phaseStartedCompleted
132
- ? (event.phaseCompleted - phaseStartedCompleted) / phaseElapsedSeconds
133
- : undefined;
134
- const completed = event.phaseTotal !== undefined && event.phaseCompleted >= event.phaseTotal;
135
- if (tty && !phaseChanged && !completed && now() - lastWriteAt < TTY_THROTTLE_MS)
136
- return;
137
- if (!tty && !phaseChanged && bucket <= lastPercentBucket && !completed)
138
- return;
139
- write(formatProgress(event, ratePerSecond, operation));
140
- lastPhase = event.phase;
141
- lastPercentBucket = Math.max(lastPercentBucket, bucket);
142
- },
143
- stop() {
144
- if (stopped)
145
- return;
146
- stopped = true;
147
- if (heartbeat !== undefined)
148
- unschedule(heartbeat);
149
- if (spinner !== undefined)
150
- unschedule(spinner);
151
- if (tty && hasTtyLine)
152
- options.stderr.write(`${CLEAR_TTY_LINE}\n`);
153
- },
154
- };
155
- }