tui-cap 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/timing.js ADDED
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.TimingTracker = exports.TIMING_SCHEMA_VERSION = void 0;
7
+ exports.timingPathFor = timingPathFor;
8
+ exports.writeTiming = writeTiming;
9
+ exports.readTiming = readTiming;
10
+ exports.timeAtOffset = timeAtOffset;
11
+ const promises_1 = require("node:fs/promises");
12
+ const node_path_1 = __importDefault(require("node:path"));
13
+ exports.TIMING_SCHEMA_VERSION = 1;
14
+ /** Sibling sidecar path for a capture file: `foo.ans` -> `foo.timing.json`. */
15
+ function timingPathFor(captureFile) {
16
+ const dir = node_path_1.default.dirname(captureFile);
17
+ const base = node_path_1.default.basename(captureFile, node_path_1.default.extname(captureFile));
18
+ return node_path_1.default.join(dir, `${base}.timing.json`);
19
+ }
20
+ /**
21
+ * Accumulates timing checkpoints while a capture streams in. Call {@link mark}
22
+ * with the byte length of each chunk *as it's written to the `.ans`* (so offsets
23
+ * line up with the file the parser later reads), then {@link build} to get the
24
+ * serializable sidecar.
25
+ */
26
+ class TimingTracker {
27
+ events = [];
28
+ startMs;
29
+ offset = 0;
30
+ constructor(startMs = Date.now()) {
31
+ this.startMs = startMs;
32
+ }
33
+ /** Record that `byteLength` more bytes were written at time `now`. */
34
+ mark(byteLength, now = Date.now()) {
35
+ if (byteLength <= 0)
36
+ return;
37
+ this.offset += byteLength;
38
+ this.events.push([Math.max(0, now - this.startMs), this.offset]);
39
+ }
40
+ /** Total bytes seen so far (the final stream length). */
41
+ get byteLength() {
42
+ return this.offset;
43
+ }
44
+ build() {
45
+ return {
46
+ schemaVersion: exports.TIMING_SCHEMA_VERSION,
47
+ startedAt: new Date(this.startMs).toISOString(),
48
+ events: this.events,
49
+ };
50
+ }
51
+ }
52
+ exports.TimingTracker = TimingTracker;
53
+ /** Write the timing sidecar next to `captureFile`. Returns the sidecar path. */
54
+ async function writeTiming(captureFile, timing) {
55
+ const out = timingPathFor(captureFile);
56
+ await (0, promises_1.writeFile)(out, `${JSON.stringify(timing)}\n`, 'utf8');
57
+ return out;
58
+ }
59
+ /**
60
+ * Read a capture's timing sidecar, or null if it's missing or unusable. Never
61
+ * throws — a missing/garbled sidecar simply means "no timing; fall back to a
62
+ * uniform default duration".
63
+ */
64
+ async function readTiming(captureFile) {
65
+ try {
66
+ const raw = await (0, promises_1.readFile)(timingPathFor(captureFile), 'utf8');
67
+ const parsed = JSON.parse(raw);
68
+ if (!Array.isArray(parsed.events))
69
+ return null;
70
+ const events = parsed.events.filter((e) => Array.isArray(e) &&
71
+ e.length === 2 &&
72
+ Number.isFinite(e[0]) &&
73
+ Number.isFinite(e[1]));
74
+ if (events.length === 0)
75
+ return null;
76
+ return {
77
+ schemaVersion: parsed.schemaVersion ?? exports.TIMING_SCHEMA_VERSION,
78
+ startedAt: parsed.startedAt ?? '',
79
+ events,
80
+ };
81
+ }
82
+ catch {
83
+ return null;
84
+ }
85
+ }
86
+ /**
87
+ * Map a byte offset in the captured stream to the moment (ms since start) it
88
+ * appeared on screen. A chunk ending at offset `o` arrived at time `t`, so every
89
+ * byte in `(prevOffset, o]` is attributed to `t`; we return the time of the
90
+ * first checkpoint whose end offset reaches `offset`. Offsets past the last
91
+ * checkpoint resolve to the final timestamp; an empty list yields 0.
92
+ */
93
+ function timeAtOffset(events, offset) {
94
+ if (events.length === 0)
95
+ return 0;
96
+ if (offset <= 0)
97
+ return events[0][0];
98
+ const lastIdx = events.length - 1;
99
+ if (offset >= events[lastIdx][1])
100
+ return events[lastIdx][0];
101
+ // Binary search for the first checkpoint whose offset >= `offset`.
102
+ let lo = 0;
103
+ let hi = lastIdx;
104
+ while (lo < hi) {
105
+ const mid = (lo + hi) >> 1;
106
+ if (events[mid][1] >= offset)
107
+ hi = mid;
108
+ else
109
+ lo = mid + 1;
110
+ }
111
+ return events[lo][0];
112
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,232 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.PACKAGE_NAME = void 0;
7
+ exports.currentVersion = currentVersion;
8
+ exports.parseVersion = parseVersion;
9
+ exports.compareVersions = compareVersions;
10
+ exports.isNewer = isNewer;
11
+ exports.installKind = installKind;
12
+ exports.updateCommand = updateCommand;
13
+ exports.fetchLatestVersion = fetchLatestVersion;
14
+ exports.checkForUpdate = checkForUpdate;
15
+ exports.runSelfUpdate = runSelfUpdate;
16
+ const node_child_process_1 = require("node:child_process");
17
+ const node_fs_1 = require("node:fs");
18
+ const node_path_1 = __importDefault(require("node:path"));
19
+ /** The published npm package name, shared by the registry lookup and update command. */
20
+ exports.PACKAGE_NAME = 'tui-cap';
21
+ /** npm registry endpoint for the latest published manifest (small payload). */
22
+ const REGISTRY_URL = `https://registry.npmjs.org/${exports.PACKAGE_NAME}/latest`;
23
+ let cachedVersion;
24
+ /**
25
+ * The `version` field of the package this module ships in, read once and cached.
26
+ * `package.json` sits one level above this module in both dev (`src/`) and the
27
+ * published build (`dist/`), and npm always includes it in the tarball.
28
+ */
29
+ function currentVersion() {
30
+ if (cachedVersion !== undefined)
31
+ return cachedVersion;
32
+ try {
33
+ const pkgPath = node_path_1.default.join(__dirname, '..', 'package.json');
34
+ const pkg = JSON.parse((0, node_fs_1.readFileSync)(pkgPath, 'utf8'));
35
+ cachedVersion = typeof pkg.version === 'string' ? pkg.version : '0.0.0';
36
+ }
37
+ catch {
38
+ cachedVersion = '0.0.0';
39
+ }
40
+ return cachedVersion;
41
+ }
42
+ /** Parse a semver-ish string (`1.2.3`, `v1.2.3`, `1.2.3-beta.1`), or null. */
43
+ function parseVersion(value) {
44
+ const m = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/.exec(value.trim());
45
+ if (!m)
46
+ return null;
47
+ return {
48
+ major: Number(m[1]),
49
+ minor: Number(m[2]),
50
+ patch: Number(m[3]),
51
+ pre: m[4] ? m[4].split('.') : [],
52
+ };
53
+ }
54
+ /** Compare two pre-release identifier lists per semver precedence rules. */
55
+ function comparePre(a, b) {
56
+ // A normal release outranks any pre-release of the same x.y.z.
57
+ if (a.length === 0 && b.length === 0)
58
+ return 0;
59
+ if (a.length === 0)
60
+ return 1;
61
+ if (b.length === 0)
62
+ return -1;
63
+ const n = Math.max(a.length, b.length);
64
+ for (let i = 0; i < n; i++) {
65
+ const ai = a[i];
66
+ const bi = b[i];
67
+ if (ai === undefined)
68
+ return -1; // shorter set is lower
69
+ if (bi === undefined)
70
+ return 1;
71
+ const aNum = /^\d+$/.test(ai);
72
+ const bNum = /^\d+$/.test(bi);
73
+ if (aNum && bNum) {
74
+ const d = Number(ai) - Number(bi);
75
+ if (d !== 0)
76
+ return d < 0 ? -1 : 1;
77
+ }
78
+ else if (aNum !== bNum) {
79
+ return aNum ? -1 : 1; // numeric identifiers are lower than alphanumeric
80
+ }
81
+ else if (ai !== bi) {
82
+ return ai < bi ? -1 : 1;
83
+ }
84
+ }
85
+ return 0;
86
+ }
87
+ /**
88
+ * Semver precedence compare: -1 if `a` < `b`, 0 if equal, 1 if `a` > `b`.
89
+ * Unparseable input compares as equal (0) so a garbled value never triggers a
90
+ * spurious update prompt.
91
+ */
92
+ function compareVersions(a, b) {
93
+ const pa = parseVersion(a);
94
+ const pb = parseVersion(b);
95
+ if (!pa || !pb)
96
+ return 0;
97
+ if (pa.major !== pb.major)
98
+ return pa.major < pb.major ? -1 : 1;
99
+ if (pa.minor !== pb.minor)
100
+ return pa.minor < pb.minor ? -1 : 1;
101
+ if (pa.patch !== pb.patch)
102
+ return pa.patch < pb.patch ? -1 : 1;
103
+ return comparePre(pa.pre, pb.pre);
104
+ }
105
+ /** True when `candidate` is a strictly newer version than `base`. */
106
+ function isNewer(candidate, base) {
107
+ return compareVersions(candidate, base) > 0;
108
+ }
109
+ /**
110
+ * Best-effort detection of how this CLI was installed. Uses the real (symlink-
111
+ * resolved) module directory: npm's npx cache lives under `…/_npx/…`, and a dev
112
+ * checkout sits under the current working directory; anything else is treated as
113
+ * a global install (the common case for `npm install -g`).
114
+ */
115
+ function installKind() {
116
+ let dir = __dirname;
117
+ try {
118
+ dir = (0, node_fs_1.realpathSync)(__dirname);
119
+ }
120
+ catch {
121
+ // fall back to the un-resolved path
122
+ }
123
+ const normalized = dir.split(node_path_1.default.sep).join('/');
124
+ if (normalized.includes('/_npx/'))
125
+ return 'npx';
126
+ let cwd = process.cwd();
127
+ try {
128
+ cwd = (0, node_fs_1.realpathSync)(cwd);
129
+ }
130
+ catch {
131
+ // use cwd as-is
132
+ }
133
+ if (dir === cwd || dir.startsWith(cwd + node_path_1.default.sep))
134
+ return 'local';
135
+ if (normalized.includes('/node_modules/'))
136
+ return 'global';
137
+ return 'unknown';
138
+ }
139
+ /** The shell command that upgrades a global install to the latest release. */
140
+ function updateCommand() {
141
+ return `npm install -g ${exports.PACKAGE_NAME}@latest`;
142
+ }
143
+ /**
144
+ * Fetch the latest published version from the npm registry, or null if the
145
+ * request fails or times out. Never throws — a missing answer simply means "no
146
+ * update prompt", so an offline machine degrades quietly.
147
+ */
148
+ async function fetchLatestVersion(timeoutMs = 3500) {
149
+ const controller = new AbortController();
150
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
151
+ try {
152
+ const res = await fetch(REGISTRY_URL, {
153
+ signal: controller.signal,
154
+ headers: {
155
+ // The abbreviated metadata document is smaller and cache-friendly.
156
+ accept: 'application/vnd.npm.install-v1+json, application/json',
157
+ },
158
+ });
159
+ if (!res.ok)
160
+ return null;
161
+ const body = (await res.json());
162
+ return typeof body.version === 'string' ? body.version : null;
163
+ }
164
+ catch {
165
+ return null;
166
+ }
167
+ finally {
168
+ clearTimeout(timer);
169
+ }
170
+ }
171
+ /** Gather everything the GUI/CLI needs to decide whether to prompt for an update. */
172
+ async function checkForUpdate(timeoutMs) {
173
+ const current = currentVersion();
174
+ const latest = await fetchLatestVersion(timeoutMs);
175
+ return {
176
+ name: exports.PACKAGE_NAME,
177
+ current,
178
+ latest,
179
+ updateAvailable: latest !== null && isNewer(latest, current),
180
+ installKind: installKind(),
181
+ updateCommand: updateCommand(),
182
+ };
183
+ }
184
+ /**
185
+ * Run `npm install -g tui-cap@latest` and resolve with the outcome. Captures a
186
+ * bounded tail of the command output so a failure (e.g. EACCES on a root-owned
187
+ * global prefix) can be shown without flooding. Never rejects.
188
+ */
189
+ function runSelfUpdate(timeoutMs = 120_000) {
190
+ const command = updateCommand();
191
+ const fromVersion = currentVersion();
192
+ return new Promise((resolve) => {
193
+ let output = '';
194
+ const capture = (buf) => {
195
+ output += buf.toString();
196
+ if (output.length > 20_000)
197
+ output = output.slice(-20_000);
198
+ };
199
+ let child;
200
+ try {
201
+ child = (0, node_child_process_1.spawn)('npm', ['install', '-g', `${exports.PACKAGE_NAME}@latest`], {
202
+ stdio: ['ignore', 'pipe', 'pipe'],
203
+ // npm is a `.cmd` shim on Windows; a shell lets it resolve on PATH.
204
+ shell: process.platform === 'win32',
205
+ });
206
+ }
207
+ catch (err) {
208
+ resolve({
209
+ ok: false,
210
+ fromVersion,
211
+ command,
212
+ output: err instanceof Error ? err.message : String(err),
213
+ code: null,
214
+ });
215
+ return;
216
+ }
217
+ const timer = setTimeout(() => {
218
+ output += '\nUpdate timed out.';
219
+ child.kill('SIGKILL');
220
+ }, timeoutMs);
221
+ child.stdout?.on('data', capture);
222
+ child.stderr?.on('data', capture);
223
+ child.on('error', (err) => {
224
+ clearTimeout(timer);
225
+ resolve({ ok: false, fromVersion, command, output: `${output}\n${err.message}`.trim(), code: null });
226
+ });
227
+ child.on('close', (code) => {
228
+ clearTimeout(timer);
229
+ resolve({ ok: code === 0, fromVersion, command, output: output.trim(), code });
230
+ });
231
+ });
232
+ }