aiden-runtime 4.8.0 → 4.8.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.
- package/dist/cli/v4/chatSession.js +9 -7
- package/dist/cli/v4/commands/update.js +14 -2
- package/dist/cli/v4/display.js +70 -27
- package/dist/cli/v4/pasteIntercept.js +214 -70
- package/dist/cli/v4/replyRenderer.js +139 -0
- package/dist/core/v4/promptBuilder.js +6 -0
- package/dist/core/v4/update/executeInstall.js +10 -6
- package/dist/core/v4/update/installMethodDetect.js +7 -0
- package/dist/core/version.js +67 -2
- package/package.json +1 -3
|
@@ -907,13 +907,15 @@ class ChatSession {
|
|
|
907
907
|
// Phase 22 Task 4: status bar reflects the live phase. Set on
|
|
908
908
|
// entry, cleared in both success and error paths below.
|
|
909
909
|
this.setStatusState({ kind: 'generating', sinceMs: Date.now() });
|
|
910
|
-
//
|
|
911
|
-
// the
|
|
912
|
-
|
|
913
|
-
//
|
|
914
|
-
//
|
|
915
|
-
//
|
|
916
|
-
|
|
910
|
+
// v4.8.1 Slice 2 hotfix #3 — removed the prior Tier-3.1a dim
|
|
911
|
+
// rule between the user input echo and the agent reply. The dim
|
|
912
|
+
// colour read as a near-blank row in live smoke, and stacked
|
|
913
|
+
// with the indicator's erase-blank residue produced two visible
|
|
914
|
+
// separator rows above `▎ Aiden`. With the rule gone, the layout
|
|
915
|
+
// is:
|
|
916
|
+
// user input → [indicator paints, erases — 1 blank row] → ▎ Aiden
|
|
917
|
+
// = exactly one blank row between user input and Aiden header,
|
|
918
|
+
// matching the rhythm Shiva flagged in smoke.
|
|
917
919
|
const turnStartedAt = Date.now();
|
|
918
920
|
const userMsg = { role: 'user', content: userInput };
|
|
919
921
|
// Apply any queued system prompts (from skill slash commands) by
|
|
@@ -78,14 +78,26 @@ async function runInstall(ctx) {
|
|
|
78
78
|
return;
|
|
79
79
|
}
|
|
80
80
|
ctx.display.write(`Installing aiden-runtime v${status.latest} (current: v${status.installed})…\n`);
|
|
81
|
-
|
|
81
|
+
// v4.8.1 Slice 2 — reuse the v4.8.0 sliding-block shimmer indicator
|
|
82
|
+
// so the user sees motion while npm install runs (typically 5–15s
|
|
83
|
+
// on a warm cache, longer on cold). The indicator paints to a TTY
|
|
84
|
+
// only — non-TTY callers (CI, pipes) see the static "Installing…"
|
|
85
|
+
// line above and the result row below, no shimmer.
|
|
86
|
+
const indicator = ctx.display.activityIndicator('updating');
|
|
87
|
+
let result;
|
|
88
|
+
try {
|
|
89
|
+
result = await (0, executeInstall_1.executeInstall)();
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
indicator.stop();
|
|
93
|
+
}
|
|
82
94
|
if (result.success) {
|
|
83
95
|
const v = result.installedVersion ?? status.latest;
|
|
84
96
|
ctx.display.write(`\n ✓ aiden-runtime v${v} installed.\n`);
|
|
85
97
|
ctx.display.dim('Restart Aiden to apply: type /quit then re-run `aiden`.');
|
|
86
98
|
return;
|
|
87
99
|
}
|
|
88
|
-
ctx.display.
|
|
100
|
+
ctx.display.write(`\n ✗ Update failed: ${result.error ?? 'no error message'}\n`);
|
|
89
101
|
}
|
|
90
102
|
// ── v4.5 update system — skip + auto subcommands ───────────────────────────
|
|
91
103
|
async function runSkip(ctx) {
|
package/dist/cli/v4/display.js
CHANGED
|
@@ -658,10 +658,16 @@ class Display {
|
|
|
658
658
|
segments = [provModel, ctxSegFull, turnSeg, sessionSeg, stateDot];
|
|
659
659
|
}
|
|
660
660
|
else if (cols >= 100 && turnSeg) {
|
|
661
|
-
|
|
661
|
+
// v4.8.1 Slice 2 hotfix — was `elapsed` (bare); now uses
|
|
662
|
+
// `sessionSeg` which includes the ⌛ timer glyph. The previous
|
|
663
|
+
// mid-tier dropped the glyph for "denser" packing, but Shiva's
|
|
664
|
+
// smoke at 80–110 cols showed only ` 5.1s` (leading space, no
|
|
665
|
+
// glyph). The glyph is single-cell, cheap, and load-bearing as
|
|
666
|
+
// the timer's identity affordance.
|
|
667
|
+
segments = [provModel, ctxSegFull, turnSeg, sessionSeg || elapsed];
|
|
662
668
|
}
|
|
663
669
|
else {
|
|
664
|
-
segments = [provModel, ctxSegCompact, elapsed];
|
|
670
|
+
segments = [provModel, ctxSegCompact, sessionSeg || elapsed];
|
|
665
671
|
}
|
|
666
672
|
return ` ${segments.join(SEP)}`;
|
|
667
673
|
}
|
|
@@ -892,6 +898,13 @@ class Display {
|
|
|
892
898
|
let stopped = false;
|
|
893
899
|
let printed = false;
|
|
894
900
|
let tickTimer = null;
|
|
901
|
+
// v4.8.1 Slice 2 hotfix #4 — true once the indicator has paused
|
|
902
|
+
// and resumed at least once (i.e. a tool row interrupted it). When
|
|
903
|
+
// false at stop() time, the indicator is still in its initial-paint
|
|
904
|
+
// row immediately below the leading blank, so stop()'s erase can
|
|
905
|
+
// safely consume BOTH rows. When true, the leading blank is far
|
|
906
|
+
// above and stop() erases only the current indicator row.
|
|
907
|
+
let movedFromInitial = false;
|
|
895
908
|
// Tunable cadence. v4.1.4 Phase 3b' (Issue G): bumped from 400ms
|
|
896
909
|
// to 250ms after visual smoke — 400ms felt sluggish, made the
|
|
897
910
|
// indicator look static between seconds. 250ms gives ~4 dot
|
|
@@ -951,7 +964,12 @@ class Display {
|
|
|
951
964
|
: '';
|
|
952
965
|
// Shimmer prefix (or none, when opts.waveBar === false).
|
|
953
966
|
const prefix = shimmerEnabled ? `${buildShimmer()} ` : '';
|
|
954
|
-
|
|
967
|
+
// v4.8.1 Slice 2 hotfix #4 — 2-space leading indent so the
|
|
968
|
+
// indicator line aligns at col 2, matching `▎ Aiden`, the
|
|
969
|
+
// user-prompt ` ▲ `, the panel ` │ ` bar, and every other
|
|
970
|
+
// structured surface. Prior buildLine started at col 0 which
|
|
971
|
+
// read as misaligned against the rest of the v4.8 chrome.
|
|
972
|
+
return ` ${prefix}${verb}${dots.padEnd(3, ' ')}${elapsedStr}`;
|
|
955
973
|
};
|
|
956
974
|
// v4.1.5 Part 1a — Issue M (Windows ConPTY buffering fix).
|
|
957
975
|
//
|
|
@@ -1014,12 +1032,16 @@ class Display {
|
|
|
1014
1032
|
return;
|
|
1015
1033
|
out.write(`${ANSI_UP_ERASE}\n`);
|
|
1016
1034
|
};
|
|
1017
|
-
// Initial paint — only on TTY.
|
|
1018
|
-
//
|
|
1019
|
-
//
|
|
1020
|
-
// the user-
|
|
1021
|
-
//
|
|
1022
|
-
//
|
|
1035
|
+
// Initial paint — only on TTY.
|
|
1036
|
+
//
|
|
1037
|
+
// v4.8.1 Slice 2 hotfix #4 — leading `\n` restored to give one
|
|
1038
|
+
// blank row between the user-input row and the indicator (hotfix
|
|
1039
|
+
// #3 dropped the dim rule that previously provided that gap).
|
|
1040
|
+
// To keep the post-stop layout at "exactly one blank between
|
|
1041
|
+
// user input and ▎ Aiden", stop() now walks up TWO rows when
|
|
1042
|
+
// the indicator never moved (no pause/resume), consuming both
|
|
1043
|
+
// the indicator row AND the leading blank. The `movedFromInitial`
|
|
1044
|
+
// flag below tracks that state.
|
|
1023
1045
|
if (isTty) {
|
|
1024
1046
|
out.write(`\n${buildLine()}\n`);
|
|
1025
1047
|
printed = true;
|
|
@@ -1031,6 +1053,12 @@ class Display {
|
|
|
1031
1053
|
return;
|
|
1032
1054
|
paused = true;
|
|
1033
1055
|
stopTick();
|
|
1056
|
+
// v4.8.1 Slice 2 hotfix #4 — mark the indicator as "moved" so
|
|
1057
|
+
// a subsequent stop() does NOT walk up 2 rows. The leading
|
|
1058
|
+
// blank from initial paint is now far above the current row
|
|
1059
|
+
// and shouldn't be consumed; doing so would erase tool-row
|
|
1060
|
+
// content instead.
|
|
1061
|
+
movedFromInitial = true;
|
|
1034
1062
|
eraseLine();
|
|
1035
1063
|
// After erase the cursor is at column 0 of the indicator's
|
|
1036
1064
|
// (now empty) line. Caller is expected to write its own
|
|
@@ -1071,7 +1099,20 @@ class Display {
|
|
|
1071
1099
|
return;
|
|
1072
1100
|
stopped = true;
|
|
1073
1101
|
stopTick();
|
|
1074
|
-
|
|
1102
|
+
// v4.8.1 Slice 2 hotfix #4 — when the indicator never moved
|
|
1103
|
+
// (no pause/resume happened during the turn), walk up TWO
|
|
1104
|
+
// rows: erase the indicator row AND the leading blank above
|
|
1105
|
+
// it. The trailing `\n` then lands the cursor exactly one
|
|
1106
|
+
// row below the user-input echo, so the next writer
|
|
1107
|
+
// (agentHeader → ▎ Aiden) produces a clean single-blank gap.
|
|
1108
|
+
if (!printed || !isTty)
|
|
1109
|
+
return;
|
|
1110
|
+
if (movedFromInitial) {
|
|
1111
|
+
out.write(`${ANSI_UP_ERASE}\n`);
|
|
1112
|
+
}
|
|
1113
|
+
else {
|
|
1114
|
+
out.write(`${ANSI_UP_ERASE}${ANSI_UP_ERASE}\n`);
|
|
1115
|
+
}
|
|
1075
1116
|
},
|
|
1076
1117
|
isPaused: () => paused,
|
|
1077
1118
|
isStopped: () => stopped,
|
|
@@ -2051,23 +2092,25 @@ class Display {
|
|
|
2051
2092
|
this.out.write(this.uiTrailRow(`${ok ? '✓' : '✗'} ${framework}: ${parts.join(', ')}${dur}`, ok ? 'success' : 'error'));
|
|
2052
2093
|
this.streamLastEndedNewline = true;
|
|
2053
2094
|
}
|
|
2054
|
-
renderUiApprovalRequest(
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2095
|
+
renderUiApprovalRequest(_args) {
|
|
2096
|
+
// v4.8.1 Slice 1 — silent no-op. The Phase 2.5 wiring fires both
|
|
2097
|
+
// `ui_approval_request` (this method) AND `callbacks.promptApproval`
|
|
2098
|
+
// (which paints the framed approval panel via `renderApprovalBox`)
|
|
2099
|
+
// for every single approval request. The intent was complementary —
|
|
2100
|
+
// succinct event row above, structured kv panel below — but in live
|
|
2101
|
+
// smoke the two surfaces stack as a visual duplicate ("Approval
|
|
2102
|
+
// needed: file_write {...}" event row + "│ tool / │ reason / │ args"
|
|
2103
|
+
// panel). The panel is the canonical, information-rich surface; this
|
|
2104
|
+
// event-row paint is redundant.
|
|
2105
|
+
//
|
|
2106
|
+
// Behavioural change is renderer-side only: `approvalEngine` still
|
|
2107
|
+
// fires `onUiEvent('ui_approval_request', ...)` so any future
|
|
2108
|
+
// telemetry / daemon-side run_events subscriber will still see the
|
|
2109
|
+
// event. Nothing paints to the chat surface from this method.
|
|
2110
|
+
//
|
|
2111
|
+
// The `_args` parameter is retained for the dispatch signature
|
|
2112
|
+
// contract (`renderUiEvent` calls it positionally) and for the day
|
|
2113
|
+
// we re-introduce a single-paint surface keyed off args.risk_tier.
|
|
2071
2114
|
}
|
|
2072
2115
|
renderUiToast(args) {
|
|
2073
2116
|
const message = typeof args.message === 'string' ? args.message : '';
|
|
@@ -6,19 +6,52 @@
|
|
|
6
6
|
* Aiden — local-first agent.
|
|
7
7
|
*/
|
|
8
8
|
/**
|
|
9
|
-
* cli/v4/pasteIntercept.ts —
|
|
9
|
+
* cli/v4/pasteIntercept.ts — stdin pre-tap for bracketed paste.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
11
|
+
* Modern @inquirer/prompts treats any embedded `\n` as Enter and
|
|
12
|
+
* resolves early, so a multi-line paste would auto-submit one line
|
|
13
|
+
* at a time. This module intercepts paste payloads BEFORE inquirer
|
|
14
|
+
* sees them, persists them to a manifest, and substitutes a
|
|
15
|
+
* `[paste #<id>: <N> lines, <bytes>]` label on stdin. The user sees
|
|
16
|
+
* the label inside inquirer's input buffer, edits it like any other
|
|
17
|
+
* text, then presses Enter to submit; `chatSession.readUserInput`
|
|
18
|
+
* swaps the label back for the original via `getPasteOriginal(id)`
|
|
19
|
+
* before handing to the agent.
|
|
18
20
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
21
|
+
* v4.8.1 Slice 2 hotfix #6 — robustness rebuild for terminal-
|
|
22
|
+
* environment diversity:
|
|
23
|
+
*
|
|
24
|
+
* • State machine survives reads split across chunk boundaries.
|
|
25
|
+
* The begin or end marker can arrive partially in one chunk
|
|
26
|
+
* and be completed by the next; the parser keeps state in `buf`
|
|
27
|
+
* until a full marker is observed.
|
|
28
|
+
*
|
|
29
|
+
* • 800ms watchdog flushes a stuck `in_marker_paste` state if
|
|
30
|
+
* the terminal never delivers PASTE_END (mosh/tmux/SSH paths
|
|
31
|
+
* have all been observed to drop end markers under load).
|
|
32
|
+
*
|
|
33
|
+
* • Degraded marker forms get normalised to canonical at the
|
|
34
|
+
* intercept boundary. Visible-escape variants (`^[[200~`) are
|
|
35
|
+
* the common case from terminals that escape control sequences
|
|
36
|
+
* for display.
|
|
37
|
+
*
|
|
38
|
+
* • CRLF/CR → LF normalisation is applied universally on every
|
|
39
|
+
* incoming chunk, not just inside marker payloads. Some
|
|
40
|
+
* clipboard payloads carry CR-only line endings.
|
|
41
|
+
*
|
|
42
|
+
* • 30ms timing accumulation catches line-by-line paste delivery
|
|
43
|
+
* — the failure mode that surfaced after hotfix #5. When a
|
|
44
|
+
* terminal delivers a paste as N small `"<line>\n"` chunks
|
|
45
|
+
* instead of one bulk chunk, each chunk has a single trailing
|
|
46
|
+
* `\n` and would otherwise pass through as an Enter keystroke.
|
|
47
|
+
* The accumulator holds candidate chunks (`length > 1` so the
|
|
48
|
+
* bare Enter keystroke `"\n"` is never held) for a 30ms window;
|
|
49
|
+
* if another candidate arrives, both are accumulated as a
|
|
50
|
+
* multi-line paste and substituted with the placeholder before
|
|
51
|
+
* any `\n` reaches inquirer. If no follow-up arrives within the
|
|
52
|
+
* window, the held chunk is emitted unchanged (normal Enter).
|
|
53
|
+
* 30ms is imperceptible to humans and well below sustained
|
|
54
|
+
* keystroke timing.
|
|
22
55
|
*/
|
|
23
56
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
24
57
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
@@ -33,7 +66,16 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
33
66
|
const paths_1 = require("../../core/v4/paths");
|
|
34
67
|
const PASTE_BEGIN = '\x1b[200~';
|
|
35
68
|
const PASTE_END = '\x1b[201~';
|
|
36
|
-
/**
|
|
69
|
+
/**
|
|
70
|
+
* Degraded marker patterns observed in the wild. Each is rewritten
|
|
71
|
+
* to canonical at the normalisation boundary so the parser only
|
|
72
|
+
* needs to know about one form.
|
|
73
|
+
*/
|
|
74
|
+
const DEGRADED_BEGIN = /\^\[\[200~/g;
|
|
75
|
+
const DEGRADED_END = /\^\[\[201~/g;
|
|
76
|
+
const ACCUMULATION_MS = 30;
|
|
77
|
+
const WATCHDOG_MS = 800;
|
|
78
|
+
/** id → original text (in-memory swap table). Disk has /pastes/paste_<id>.txt as source of truth for /show. */
|
|
37
79
|
const originals = new Map();
|
|
38
80
|
function pastesDir() {
|
|
39
81
|
return node_path_1.default.join((0, paths_1.resolveAidenPaths)().root, 'pastes');
|
|
@@ -74,18 +116,17 @@ function compressSync(text) {
|
|
|
74
116
|
}
|
|
75
117
|
/**
|
|
76
118
|
* Look up the original text for a paste id. Returns undefined if the
|
|
77
|
-
* id was never seen by this process
|
|
78
|
-
*
|
|
79
|
-
*
|
|
119
|
+
* id was never seen by this process. Disk (/pastes/paste_<id>.txt)
|
|
120
|
+
* is the source of truth for /show <id>; this map is the fast path
|
|
121
|
+
* for the in-flight prompt swap.
|
|
80
122
|
*/
|
|
81
123
|
function getPasteOriginal(id) {
|
|
82
124
|
return originals.get(id);
|
|
83
125
|
}
|
|
84
126
|
/**
|
|
85
127
|
* Replace `[paste #N: …]` patterns in `input` with the corresponding
|
|
86
|
-
* original text
|
|
87
|
-
*
|
|
88
|
-
* string.
|
|
128
|
+
* original text. Patterns whose id we don't know are left intact
|
|
129
|
+
* (might be user-typed by hand).
|
|
89
130
|
*/
|
|
90
131
|
function expandPasteLabels(input) {
|
|
91
132
|
return input.replace(/\[paste #(\d+):[^\]]*\]/g, (m, id) => {
|
|
@@ -93,6 +134,38 @@ function expandPasteLabels(input) {
|
|
|
93
134
|
return orig !== undefined ? orig : m;
|
|
94
135
|
});
|
|
95
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* Universal normalisation applied at the intercept boundary:
|
|
139
|
+
* CRLF + bare CR → LF, then degraded marker variants → canonical.
|
|
140
|
+
*/
|
|
141
|
+
function normalize(text) {
|
|
142
|
+
let t = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
143
|
+
t = t.replace(DEGRADED_BEGIN, PASTE_BEGIN);
|
|
144
|
+
t = t.replace(DEGRADED_END, PASTE_END);
|
|
145
|
+
return t;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Decide whether `payload` should emit inline (small single-line) or
|
|
149
|
+
* be funnelled through the disk-backed placeholder system. Same
|
|
150
|
+
* thresholds for marker-wrapped and timing-accumulated paths so the
|
|
151
|
+
* user sees identical chrome regardless of how the paste arrived.
|
|
152
|
+
*/
|
|
153
|
+
function payloadToEmission(payload) {
|
|
154
|
+
const trimmed = payload.replace(/\n+$/, '');
|
|
155
|
+
if (!trimmed.includes('\n') && trimmed.length <= 500) {
|
|
156
|
+
return trimmed;
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
const { id, label } = compressSync(trimmed);
|
|
160
|
+
originals.set(id, trimmed);
|
|
161
|
+
return label;
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
// Disk failure: collapse newlines so the auto-submit we're
|
|
165
|
+
// preventing doesn't fire downstream.
|
|
166
|
+
return trimmed.replace(/\n/g, ' ');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
96
169
|
let installed = null;
|
|
97
170
|
/**
|
|
98
171
|
* Install the stdin pre-tap. Wraps `process.stdin.emit('data', …)`
|
|
@@ -103,72 +176,140 @@ let installed = null;
|
|
|
103
176
|
* MCP serve mode: never call this — `aiden mcp serve` doesn't run
|
|
104
177
|
* the REPL.
|
|
105
178
|
*/
|
|
106
|
-
function installPasteInterceptor(stdin) {
|
|
179
|
+
function installPasteInterceptor(stdin, opts = {}) {
|
|
107
180
|
if (installed)
|
|
108
181
|
return installed.restore;
|
|
182
|
+
const accumulationMs = opts.accumulationMs ?? ACCUMULATION_MS;
|
|
183
|
+
const watchdogMs = opts.watchdogMs ?? WATCHDOG_MS;
|
|
109
184
|
const origEmit = stdin.emit.bind(stdin);
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
185
|
+
// State machine —
|
|
186
|
+
// normal : default; chunks pass through or accumulate
|
|
187
|
+
// in_marker_paste : between PASTE_BEGIN and PASTE_END; buf accumulates payload
|
|
188
|
+
let mode = 'normal';
|
|
189
|
+
let buf = '';
|
|
190
|
+
let markerTimer = null;
|
|
191
|
+
let pendingChunk = null;
|
|
192
|
+
let pendingTimer = null;
|
|
193
|
+
function emitDownstream(text) {
|
|
194
|
+
if (text.length === 0)
|
|
195
|
+
return;
|
|
196
|
+
origEmit('data', Buffer.from(text, 'utf8'));
|
|
197
|
+
}
|
|
198
|
+
function clearMarkerWatchdog() {
|
|
199
|
+
if (markerTimer) {
|
|
200
|
+
clearTimeout(markerTimer);
|
|
201
|
+
markerTimer = null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
function armMarkerWatchdog() {
|
|
205
|
+
clearMarkerWatchdog();
|
|
206
|
+
markerTimer = setTimeout(() => {
|
|
207
|
+
// PASTE_END never arrived. Flush whatever we have and reset.
|
|
208
|
+
const payload = buf;
|
|
209
|
+
buf = '';
|
|
210
|
+
mode = 'normal';
|
|
211
|
+
markerTimer = null;
|
|
212
|
+
emitDownstream(payloadToEmission(payload));
|
|
213
|
+
}, watchdogMs);
|
|
214
|
+
}
|
|
215
|
+
function clearPending() {
|
|
216
|
+
if (pendingTimer) {
|
|
217
|
+
clearTimeout(pendingTimer);
|
|
218
|
+
pendingTimer = null;
|
|
219
|
+
}
|
|
220
|
+
pendingChunk = null;
|
|
221
|
+
}
|
|
222
|
+
function flushPendingAsIs() {
|
|
223
|
+
if (pendingChunk === null)
|
|
224
|
+
return;
|
|
225
|
+
const chunk = pendingChunk;
|
|
226
|
+
clearPending();
|
|
227
|
+
// Pending was a normal Enter — emit as-is, don't placeholder.
|
|
228
|
+
emitDownstream(chunk);
|
|
229
|
+
}
|
|
230
|
+
function flushPendingAsPaste() {
|
|
231
|
+
if (pendingChunk === null)
|
|
232
|
+
return;
|
|
233
|
+
const chunk = pendingChunk;
|
|
234
|
+
clearPending();
|
|
235
|
+
emitDownstream(payloadToEmission(chunk));
|
|
236
|
+
}
|
|
237
|
+
function processNormalised(text) {
|
|
113
238
|
let cursor = 0;
|
|
114
239
|
while (cursor < text.length) {
|
|
115
|
-
if (
|
|
240
|
+
if (mode === 'in_marker_paste') {
|
|
116
241
|
const endIdx = text.indexOf(PASTE_END, cursor);
|
|
117
242
|
if (endIdx === -1) {
|
|
118
|
-
|
|
243
|
+
buf += text.slice(cursor);
|
|
119
244
|
cursor = text.length;
|
|
245
|
+
// Watchdog stays armed — extending the buf without an end
|
|
246
|
+
// marker doesn't restart the clock; we still want to flush
|
|
247
|
+
// if the entire turn never produces PASTE_END.
|
|
120
248
|
}
|
|
121
249
|
else {
|
|
122
|
-
|
|
250
|
+
buf += text.slice(cursor, endIdx);
|
|
123
251
|
cursor = endIdx + PASTE_END.length;
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
// swallow the bytes pass through to readline, where they
|
|
127
|
-
// become an Enter event and auto-submit the prompt before
|
|
128
|
-
// the user has reviewed the paste. Eat at most one CR + one
|
|
129
|
-
// LF (in either order) right after PASTE_END.
|
|
130
|
-
if (text[cursor] === '\r')
|
|
131
|
-
cursor += 1;
|
|
252
|
+
// Swallow a trailing newline that some terminals emit
|
|
253
|
+
// immediately after PASTE_END.
|
|
132
254
|
if (text[cursor] === '\n')
|
|
133
255
|
cursor += 1;
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
if (!trimmed.includes('\n') && trimmed.length <= 500) {
|
|
140
|
-
// Single-line, small — emit as-is so user can edit.
|
|
141
|
-
out += trimmed;
|
|
142
|
-
}
|
|
143
|
-
else {
|
|
144
|
-
// Multi-line or large — disk-back + emit label.
|
|
145
|
-
try {
|
|
146
|
-
const { id, label } = compressSync(trimmed);
|
|
147
|
-
originals.set(id, trimmed);
|
|
148
|
-
out += label;
|
|
149
|
-
}
|
|
150
|
-
catch {
|
|
151
|
-
// Disk failure: fall back to a single-space substitute
|
|
152
|
-
// so internal newlines don't trigger auto-submit.
|
|
153
|
-
out += trimmed.replace(/\n/g, ' ');
|
|
154
|
-
}
|
|
155
|
-
}
|
|
256
|
+
mode = 'normal';
|
|
257
|
+
clearMarkerWatchdog();
|
|
258
|
+
const payload = buf;
|
|
259
|
+
buf = '';
|
|
260
|
+
emitDownstream(payloadToEmission(payload));
|
|
156
261
|
}
|
|
262
|
+
continue;
|
|
157
263
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
264
|
+
// mode === 'normal'
|
|
265
|
+
const beginIdx = text.indexOf(PASTE_BEGIN, cursor);
|
|
266
|
+
if (beginIdx !== -1) {
|
|
267
|
+
// Pre-marker content: flush any pending and emit inline so
|
|
268
|
+
// it lands in inquirer's buffer ahead of the placeholder
|
|
269
|
+
// (preserves typed prefix when the user pastes after typing).
|
|
270
|
+
flushPendingAsIs();
|
|
271
|
+
if (beginIdx > cursor)
|
|
272
|
+
emitDownstream(text.slice(cursor, beginIdx));
|
|
273
|
+
cursor = beginIdx + PASTE_BEGIN.length;
|
|
274
|
+
mode = 'in_marker_paste';
|
|
275
|
+
armMarkerWatchdog();
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
// No marker in the remainder.
|
|
279
|
+
const remainder = text.slice(cursor);
|
|
280
|
+
cursor = text.length;
|
|
281
|
+
const nlCount = (remainder.match(/\n/g) ?? []).length;
|
|
282
|
+
const hasInternalNl = nlCount > 1 || (nlCount === 1 && !remainder.endsWith('\n'));
|
|
283
|
+
if (hasInternalNl) {
|
|
284
|
+
// Single bulk chunk with internal newlines — instant
|
|
285
|
+
// placeholder. Flush pending first so any prior single-line
|
|
286
|
+
// candidate isn't lost.
|
|
287
|
+
flushPendingAsIs();
|
|
288
|
+
emitDownstream(payloadToEmission(remainder));
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
// Candidate paste-line: non-empty content ending in `\n` with
|
|
292
|
+
// length > 1 (excludes bare Enter keystroke `"\n"`).
|
|
293
|
+
const isCandidate = remainder.endsWith('\n') && remainder.length > 1;
|
|
294
|
+
if (isCandidate) {
|
|
295
|
+
if (pendingChunk !== null) {
|
|
296
|
+
// Already pending — append, restart the window.
|
|
297
|
+
pendingChunk += remainder;
|
|
298
|
+
if (pendingTimer)
|
|
299
|
+
clearTimeout(pendingTimer);
|
|
300
|
+
pendingTimer = setTimeout(flushPendingAsPaste, accumulationMs);
|
|
163
301
|
}
|
|
164
302
|
else {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
state.inPaste = true;
|
|
303
|
+
pendingChunk = remainder;
|
|
304
|
+
pendingTimer = setTimeout(flushPendingAsIs, accumulationMs);
|
|
168
305
|
}
|
|
306
|
+
continue;
|
|
169
307
|
}
|
|
308
|
+
// Non-candidate (bare Enter, or non-`\n`-terminated keystroke).
|
|
309
|
+
// Flush pending first since this chunk closes the window.
|
|
310
|
+
flushPendingAsIs();
|
|
311
|
+
emitDownstream(remainder);
|
|
170
312
|
}
|
|
171
|
-
return out;
|
|
172
313
|
}
|
|
173
314
|
const wrappedEmit = function (event, ...args) {
|
|
174
315
|
if (event !== 'data')
|
|
@@ -176,19 +317,22 @@ function installPasteInterceptor(stdin) {
|
|
|
176
317
|
const chunk = args[0];
|
|
177
318
|
if (chunk == null)
|
|
178
319
|
return origEmit(event, ...args);
|
|
179
|
-
const
|
|
320
|
+
const raw = Buffer.isBuffer(chunk)
|
|
180
321
|
? chunk.toString('utf8')
|
|
181
322
|
: (typeof chunk === 'string' ? chunk : String(chunk));
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
323
|
+
const normalised = normalize(raw);
|
|
324
|
+
processNormalised(normalised);
|
|
325
|
+
// We always claim to have handled the emit. Downstream listeners
|
|
326
|
+
// fire from `emitDownstream` immediately on the same tick OR
|
|
327
|
+
// from a deferred timer in the accumulation case.
|
|
328
|
+
return true;
|
|
187
329
|
};
|
|
188
330
|
stdin.emit = wrappedEmit;
|
|
189
331
|
const restore = () => {
|
|
190
332
|
if (!installed)
|
|
191
333
|
return;
|
|
334
|
+
clearPending();
|
|
335
|
+
clearMarkerWatchdog();
|
|
192
336
|
stdin.emit = origEmit;
|
|
193
337
|
installed = null;
|
|
194
338
|
};
|
|
@@ -39,6 +39,7 @@ const tokens_1 = require("./design/tokens");
|
|
|
39
39
|
// callsites in this file with `getBodyWidth()` and adds soft-wrap for
|
|
40
40
|
// code-block lines that previously overflowed the viewport.
|
|
41
41
|
const frame_1 = require("./display/frame");
|
|
42
|
+
const box_1 = require("./box");
|
|
42
43
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
43
44
|
const TerminalRenderer = require('marked-terminal').default ?? require('marked-terminal');
|
|
44
45
|
function paint(kind) {
|
|
@@ -638,6 +639,144 @@ function getReplyRenderer() {
|
|
|
638
639
|
const out = lines.join('\n');
|
|
639
640
|
return proto._listDepth === 0 ? out + '\n' : out + '\n';
|
|
640
641
|
};
|
|
642
|
+
// ── v4.8.1 Slice 2 — markdown table override ──────────────────────────
|
|
643
|
+
//
|
|
644
|
+
// Why: marked-terminal's default table renderer (cli-table3) auto-
|
|
645
|
+
// wraps cells but doesn't keep wrap-continuation lines aligned to
|
|
646
|
+
// the original row — wide tables with 5+ columns fragment into
|
|
647
|
+
// vertical pipe rails that don't read as rows. The narrow 2-col
|
|
648
|
+
// tables that smoke-tested fine were within the no-wrap budget.
|
|
649
|
+
//
|
|
650
|
+
// Strategy: own the entire render from the marked v15 token object.
|
|
651
|
+
// Use `parser.parseInline(cell.tokens)` to get ANSI-painted cell
|
|
652
|
+
// text, then proportionally distribute the terminal-width budget
|
|
653
|
+
// across columns (clamping to natural max width), wrap each cell
|
|
654
|
+
// to its column width, and render the box with the same row
|
|
655
|
+
// height for every cell in the row so visual rows stay tight.
|
|
656
|
+
//
|
|
657
|
+
// Token-source the box chars from `glyphs.chrome.*` so a single
|
|
658
|
+
// glyph swap propagates here automatically (consistent with the
|
|
659
|
+
// rest of v4.8.x chrome).
|
|
660
|
+
renderer.table = function (header, body) {
|
|
661
|
+
// marked v15 token: { header: [cellTok], rows: [[cellTok]] }.
|
|
662
|
+
// Older string-based API: (headerHtml, bodyHtml) — we fall back
|
|
663
|
+
// to a naive concatenation so the reply isn't lost entirely.
|
|
664
|
+
if (typeof header !== 'object' || header === null) {
|
|
665
|
+
return String(header ?? '') + (body !== undefined ? String(body) : '') + '\n';
|
|
666
|
+
}
|
|
667
|
+
const tok = header;
|
|
668
|
+
const parser = this.parser;
|
|
669
|
+
const renderCell = (c) => {
|
|
670
|
+
if (c.tokens && parser?.parseInline) {
|
|
671
|
+
try {
|
|
672
|
+
return parser.parseInline(c.tokens).trim();
|
|
673
|
+
}
|
|
674
|
+
catch {
|
|
675
|
+
return (c.text ?? '').trim();
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
return (c.text ?? '').trim();
|
|
679
|
+
};
|
|
680
|
+
const headers = (tok.header ?? []).map(renderCell);
|
|
681
|
+
const rows = (tok.rows ?? []).map((r) => r.map(renderCell));
|
|
682
|
+
const cols = headers.length;
|
|
683
|
+
if (cols === 0)
|
|
684
|
+
return '';
|
|
685
|
+
// Layout budget. Reply chrome family lives at col 2.
|
|
686
|
+
const indent = ' ';
|
|
687
|
+
const termCols = process.stdout.columns ?? 100;
|
|
688
|
+
const innerBudget = Math.max(40, Math.min(termCols, 110) - indent.length);
|
|
689
|
+
// Chrome per row = `│ ` (2) per col + trailing `│` (1) + 1 trailing
|
|
690
|
+
// space per cell already absorbed in the budget below.
|
|
691
|
+
const chromeCost = 3 * cols + 1;
|
|
692
|
+
const contentBudget = Math.max(cols * 4, innerBudget - chromeCost);
|
|
693
|
+
// Natural width = max(header, body) visible width per column.
|
|
694
|
+
const naturalW = headers.map((h, i) => {
|
|
695
|
+
const hw = (0, box_1.visibleLength)(h);
|
|
696
|
+
const cw = rows.reduce((m, r) => Math.max(m, (0, box_1.visibleLength)(r[i] ?? '')), 0);
|
|
697
|
+
return Math.max(hw, cw, 1);
|
|
698
|
+
});
|
|
699
|
+
// v4.8.1 Slice 2 hotfix #2 — header-floor + proportional allocation.
|
|
700
|
+
//
|
|
701
|
+
// Each column's minimum is `max(headerWidth, MIN_COL_W)` so column
|
|
702
|
+
// headers NEVER wrap — they are the column identifier; wrapping
|
|
703
|
+
// them ("Framework" → "Framew/ork") fragments scanability worse
|
|
704
|
+
// than wrapping body cells. Body content above the header width
|
|
705
|
+
// is what gets compressed under width pressure.
|
|
706
|
+
//
|
|
707
|
+
// Algorithm:
|
|
708
|
+
// 1. Compute `minPerCol = max(headerW[i], MIN_COL_W)` per column.
|
|
709
|
+
// 2. If sum(minPerCol) >= contentBudget (very narrow terminal),
|
|
710
|
+
// use minPerCol as-is — body cells will wrap to fit, headers
|
|
711
|
+
// stay intact.
|
|
712
|
+
// 3. Else if sum(naturalW) <= contentBudget, use natural widths
|
|
713
|
+
// (no wrap needed anywhere).
|
|
714
|
+
// 4. Else: floor at minPerCol, distribute remaining budget
|
|
715
|
+
// proportionally to each column's "extra need above min",
|
|
716
|
+
// then hand rounding leftover to widest-natural cols first.
|
|
717
|
+
const MIN_COL_W = 4;
|
|
718
|
+
const headerW = headers.map((h) => (0, box_1.visibleLength)(h));
|
|
719
|
+
const minPerCol = naturalW.map((_, i) => Math.max(headerW[i], MIN_COL_W));
|
|
720
|
+
const totalMin = minPerCol.reduce((a, b) => a + b, 0);
|
|
721
|
+
const totalNatW = naturalW.reduce((a, b) => a + b, 0);
|
|
722
|
+
let colWidths;
|
|
723
|
+
if (totalMin >= contentBudget) {
|
|
724
|
+
colWidths = minPerCol.slice();
|
|
725
|
+
}
|
|
726
|
+
else if (totalNatW <= contentBudget) {
|
|
727
|
+
colWidths = naturalW.slice();
|
|
728
|
+
}
|
|
729
|
+
else {
|
|
730
|
+
colWidths = minPerCol.slice();
|
|
731
|
+
const extraNeed = naturalW.map((w, i) => Math.max(0, w - minPerCol[i]));
|
|
732
|
+
const totalNeed = extraNeed.reduce((a, b) => a + b, 0);
|
|
733
|
+
const pool = contentBudget - totalMin;
|
|
734
|
+
if (totalNeed > 0) {
|
|
735
|
+
for (let i = 0; i < cols; i += 1) {
|
|
736
|
+
colWidths[i] += Math.floor((extraNeed[i] * pool) / totalNeed);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
let leftover = contentBudget - colWidths.reduce((a, b) => a + b, 0);
|
|
740
|
+
const order = naturalW.map((_, i) => i).sort((a, b) => naturalW[b] - naturalW[a]);
|
|
741
|
+
for (let k = 0; leftover > 0 && k < cols * 2; k += 1) {
|
|
742
|
+
const idx = order[k % cols];
|
|
743
|
+
if (colWidths[idx] < naturalW[idx]) {
|
|
744
|
+
colWidths[idx] += 1;
|
|
745
|
+
leftover -= 1;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
// ANSI-aware cell wrap. frameWrap handles colour-code-aware width.
|
|
750
|
+
const wrapCell = (text, w) => w <= 0 ? [''] : (0, frame_1.wrap)(text, w, { trim: false, hard: true }).split('\n');
|
|
751
|
+
const sk = (0, skinEngine_1.getSkinEngine)();
|
|
752
|
+
const ch = tokens_1.glyphs.chrome;
|
|
753
|
+
const rule = (l, m, r) => indent + sk.applyColors(l + colWidths.map((w) => ch.hLine.repeat(w + 2)).join(m) + r, 'muted');
|
|
754
|
+
const vBar = sk.applyColors(ch.vLine, 'muted');
|
|
755
|
+
const renderRow = (cells) => {
|
|
756
|
+
const height = Math.max(...cells.map((c) => c.length), 1);
|
|
757
|
+
const out = [];
|
|
758
|
+
for (let line = 0; line < height; line += 1) {
|
|
759
|
+
const cellLines = cells.map((cellLines2, ci) => {
|
|
760
|
+
const cellLine = cellLines2[line] ?? '';
|
|
761
|
+
const pad = Math.max(0, colWidths[ci] - (0, box_1.visibleLength)(cellLine));
|
|
762
|
+
return ' ' + cellLine + ' '.repeat(pad) + ' ';
|
|
763
|
+
});
|
|
764
|
+
out.push(indent + vBar + cellLines.join(vBar) + vBar);
|
|
765
|
+
}
|
|
766
|
+
return out.join('\n');
|
|
767
|
+
};
|
|
768
|
+
const wrappedHeader = headers.map((h, i) => wrapCell(h, colWidths[i]));
|
|
769
|
+
const wrappedRows = rows.map((r) => r.map((c, i) => wrapCell(c, colWidths[i])));
|
|
770
|
+
const lines = [rule(ch.topLeft, ch.teeDown, ch.topRight)];
|
|
771
|
+
if (headers.length > 0) {
|
|
772
|
+
lines.push(renderRow(wrappedHeader));
|
|
773
|
+
lines.push(rule(ch.teeRight, ch.cross, ch.teeLeft));
|
|
774
|
+
}
|
|
775
|
+
for (const row of wrappedRows)
|
|
776
|
+
lines.push(renderRow(row));
|
|
777
|
+
lines.push(rule(ch.botLeft, ch.teeUp, ch.botRight));
|
|
778
|
+
return lines.join('\n') + '\n';
|
|
779
|
+
};
|
|
641
780
|
cachedRenderer = {
|
|
642
781
|
render(text) {
|
|
643
782
|
try {
|
|
@@ -166,6 +166,12 @@ const UI_EVENTS_GUIDANCE = [
|
|
|
166
166
|
'Markdown text in your reply is for explanation, not status. Status goes',
|
|
167
167
|
'through events. Skip events entirely on single-shot queries that aren\'t',
|
|
168
168
|
'multi-step work.',
|
|
169
|
+
'',
|
|
170
|
+
'## Comparison formatting',
|
|
171
|
+
'',
|
|
172
|
+
'For comparison requests, prefer sectioned lists or narrow tables (3 cols max).',
|
|
173
|
+
'Wide tables (4+ columns or cells over ~30 chars) render imperfectly in the',
|
|
174
|
+
'CLI grid — break long content into sections with headers + bullets instead.',
|
|
169
175
|
].join('\n');
|
|
170
176
|
/**
|
|
171
177
|
* Llama-3.3-specific tool-call format guard. Adapter-side recovery picks
|
|
@@ -66,17 +66,21 @@ async function executeInstall(opts = {}) {
|
|
|
66
66
|
const platform = opts.platform ?? process.platform;
|
|
67
67
|
return new Promise((resolve) => {
|
|
68
68
|
const args = ['install', '-g', packageSpec];
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
69
|
+
// v4.8.1 Slice 2 — drop `shell: true`. Node 20+ emits
|
|
70
|
+
// `DeprecationWarning: Passing args to a child process with shell
|
|
71
|
+
// option true can lead to security vulnerabilities` whenever
|
|
72
|
+
// shell:true is paired with an args array. We don't need the
|
|
73
|
+
// shell either — on Windows we spawn `npm.cmd` explicitly (the
|
|
74
|
+
// shim that PATHEXT would otherwise resolve to); on POSIX we
|
|
75
|
+
// spawn `npm` directly. No user input flows into argv on either
|
|
76
|
+
// path so the prior shell-resolution wasn't load-bearing.
|
|
77
|
+
const cmd = platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
73
78
|
const spawnOpts = {
|
|
74
|
-
shell: platform === 'win32',
|
|
75
79
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
76
80
|
};
|
|
77
81
|
let child;
|
|
78
82
|
try {
|
|
79
|
-
child = spawn(
|
|
83
|
+
child = spawn(cmd, args, spawnOpts);
|
|
80
84
|
}
|
|
81
85
|
catch (err) {
|
|
82
86
|
resolve({
|
|
@@ -37,6 +37,13 @@ const NPM_GLOBAL_HINTS = [
|
|
|
37
37
|
/[/\\]npm-global[/\\]/,
|
|
38
38
|
/[/\\]\.nvm[/\\]versions[/\\]node[/\\][^/\\]+[/\\]lib[/\\]node_modules\b/,
|
|
39
39
|
/Program Files[/\\]nodejs[/\\]node_modules[/\\]aiden-runtime\b/i,
|
|
40
|
+
// v4.8.1 Slice 2 — Windows user-mode `npm install -g` lands in
|
|
41
|
+
// `C:\Users\<u>\AppData\Roaming\npm\node_modules\aiden-runtime\`.
|
|
42
|
+
// The leading `[/\\]npm[/\\]node_modules` hint above usually catches
|
|
43
|
+
// it, but tests on a non-default `npm config prefix` setup
|
|
44
|
+
// (Cmder, Scoop, etc.) can land outside the canonical path. The
|
|
45
|
+
// extra hint here is a belt-and-suspenders explicit AppData match.
|
|
46
|
+
/[/\\]AppData[/\\]Roaming[/\\]npm[/\\]/i,
|
|
40
47
|
];
|
|
41
48
|
function inferDirs(input) {
|
|
42
49
|
return {
|
package/dist/core/version.js
CHANGED
|
@@ -1,5 +1,70 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Shiva Deore (Taracod).
|
|
4
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
5
|
+
*
|
|
6
|
+
* Aiden — local-first agent.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* core/version.ts — runtime version reader.
|
|
10
|
+
*
|
|
11
|
+
* v4.8.1 Slice 2 — switched from build-time injection to a runtime
|
|
12
|
+
* `package.json` walk. The previous design relied on
|
|
13
|
+
* `scripts/inject-version.js` (a `prebuild:cli` / `prebuild:api` hook)
|
|
14
|
+
* to write a hardcoded VERSION constant into this file. That design
|
|
15
|
+
* had a subtle ordering bug:
|
|
16
|
+
*
|
|
17
|
+
* `npm run build` ran `tsc --outDir dist` BEFORE `inject-version.js`.
|
|
18
|
+
* tsc compiled `core/version.ts` (still at the previously-committed
|
|
19
|
+
* value) into `dist/core/version.js`. Inject then mutated the
|
|
20
|
+
* source, but only the esbuild bundle (`dist-bundle/cli.js`) picked
|
|
21
|
+
* up the fresh value. The `bin` entry uses the tsc tree
|
|
22
|
+
* (`dist/cli/v4/aidenCLI.js`), so the globally-installed CLI
|
|
23
|
+
* reported the stale version.
|
|
24
|
+
*
|
|
25
|
+
* Fix: read the version at module-load time by walking up from
|
|
26
|
+
* `__dirname` and parsing the first `package.json` we find whose
|
|
27
|
+
* `name` is `aiden-runtime`. This works for:
|
|
28
|
+
*
|
|
29
|
+
* - the tsc tree (`dist/core/version.js` → walk to `<install>/package.json`)
|
|
30
|
+
* - the esbuild bundle (`dist-bundle/cli.js` → walk to root)
|
|
31
|
+
* - source / tsx dev runs (`core/version.ts` → walk to repo root)
|
|
32
|
+
* - tests (any `__dirname` inside the repo lands on the right pkg)
|
|
33
|
+
*
|
|
34
|
+
* Failure mode: returns `'0.0.0-unknown'` if no aiden-runtime
|
|
35
|
+
* package.json is found within 6 parent directories. End-user
|
|
36
|
+
* deployments always have one within 3 levels; the 6-level budget
|
|
37
|
+
* keeps the function defensive without scanning the whole filesystem.
|
|
38
|
+
*/
|
|
2
39
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
40
|
exports.VERSION = void 0;
|
|
4
|
-
|
|
5
|
-
|
|
41
|
+
const node_fs_1 = require("node:fs");
|
|
42
|
+
const node_path_1 = require("node:path");
|
|
43
|
+
function readVersion() {
|
|
44
|
+
let dir = __dirname;
|
|
45
|
+
for (let i = 0; i < 6; i += 1) {
|
|
46
|
+
const candidate = (0, node_path_1.join)(dir, 'package.json');
|
|
47
|
+
if ((0, node_fs_1.existsSync)(candidate)) {
|
|
48
|
+
try {
|
|
49
|
+
const pkg = JSON.parse((0, node_fs_1.readFileSync)(candidate, 'utf8'));
|
|
50
|
+
if (pkg.name === 'aiden-runtime' && typeof pkg.version === 'string') {
|
|
51
|
+
return pkg.version;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
/* unreadable / non-JSON → keep walking */
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const parent = (0, node_path_1.dirname)(dir);
|
|
59
|
+
if (parent === dir)
|
|
60
|
+
break;
|
|
61
|
+
dir = parent;
|
|
62
|
+
}
|
|
63
|
+
return '0.0.0-unknown';
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Resolved at module-load time. Idempotent — multiple imports share
|
|
67
|
+
* the cached value. Re-reading on every access would be wasteful;
|
|
68
|
+
* the package.json version doesn't change during a process lifetime.
|
|
69
|
+
*/
|
|
70
|
+
exports.VERSION = readVersion();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aiden-runtime",
|
|
3
|
-
"version": "4.8.
|
|
3
|
+
"version": "4.8.1",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -60,9 +60,7 @@
|
|
|
60
60
|
"scripts": {
|
|
61
61
|
"dev": "electron electron/main.js",
|
|
62
62
|
"build": "tsc --outDir dist && npm run build:cli && npm run build:api",
|
|
63
|
-
"prebuild:cli": "node scripts/inject-version.js",
|
|
64
63
|
"build:cli": "esbuild cli/aiden.ts --bundle --platform=node --target=node18 --outfile=dist-bundle/cli.js --external:electron --external:cpu-features --external:ssh2 --external:bcrypt --external:playwright --external:playwright-core --external:@aws-sdk/client-s3",
|
|
65
|
-
"prebuild:api": "node scripts/inject-version.js",
|
|
66
64
|
"build:api": "esbuild api/entry.ts --bundle --platform=node --target=node18 --outfile=dist-bundle/index.js --external:electron --external:cpu-features --external:ssh2 --external:bcrypt --external:playwright --external:playwright-core --external:@aws-sdk/client-s3",
|
|
67
65
|
"prepublishOnly": "npm run typecheck && npm run build",
|
|
68
66
|
"typecheck": "tsc --noEmit",
|