@oh-my-pi/pi-utils 15.10.10 → 15.10.12
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/types/dirs.d.ts +2 -0
- package/dist/types/env.d.ts +4 -0
- package/dist/types/logger.d.ts +15 -0
- package/dist/types/procmgr.d.ts +4 -0
- package/package.json +2 -2
- package/src/cli.ts +33 -8
- package/src/dirs.ts +5 -0
- package/src/env.ts +21 -1
- package/src/logger.ts +131 -51
- package/src/procmgr.ts +1 -1
- package/src/prompt.ts +155 -66
- package/src/snowflake.ts +11 -26
package/dist/types/dirs.d.ts
CHANGED
|
@@ -117,6 +117,8 @@ export declare function getAutoresearchDbPath(encodedProject: string): string;
|
|
|
117
117
|
export declare function getAutoresearchRunDir(encodedProject: string, runId: number): string;
|
|
118
118
|
/** Get the path to agent.db (SQLite database for settings and auth storage). */
|
|
119
119
|
export declare function getAgentDbPath(agentDir?: string): string;
|
|
120
|
+
/** Get the last-seen-changelog-version marker file (~/.omp/agent/last-changelog-version). */
|
|
121
|
+
export declare function getLastChangelogVersionPath(agentDir?: string): string;
|
|
120
122
|
/** Get the path to history.db (SQLite database for session history). */
|
|
121
123
|
export declare function getHistoryDbPath(agentDir?: string): string;
|
|
122
124
|
/** Get the path to models.db (model cache database). */
|
package/dist/types/env.d.ts
CHANGED
|
@@ -52,4 +52,8 @@ export declare function isBunTestRuntime(): boolean;
|
|
|
52
52
|
* first for cheap fast-path detection.
|
|
53
53
|
*/
|
|
54
54
|
export declare function isCompiledBinary(): boolean;
|
|
55
|
+
/** Called by CLI entrypoints whose main module dispatches worker argv selectors. */
|
|
56
|
+
export declare function declareWorkerHostEntry(): void;
|
|
57
|
+
/** Main-module path of the self-dispatching CLI host, or null outside it. */
|
|
58
|
+
export declare function workerHostEntry(): string | null;
|
|
55
59
|
export declare function $flag(name: string, def?: boolean): boolean;
|
package/dist/types/logger.d.ts
CHANGED
|
@@ -31,6 +31,15 @@ export declare function info(message: string, context?: Record<string, unknown>)
|
|
|
31
31
|
* @param context - The context to log.
|
|
32
32
|
*/
|
|
33
33
|
export declare function debug(message: string, context?: Record<string, unknown>): void;
|
|
34
|
+
/**
|
|
35
|
+
* Streaming startup markers, enabled by `PI_DEBUG_STARTUP`. Unlike the
|
|
36
|
+
* PI_TIMING tree (printed only after startup completes), these write one
|
|
37
|
+
* synchronous stderr line as each phase begins/ends, so a hard hang still
|
|
38
|
+
* shows the last phase that started. `fs.writeSync(2)` is used deliberately:
|
|
39
|
+
* it cannot be reordered or buffered past a synchronous block of the event
|
|
40
|
+
* loop (dlopen, sync fs on a dead mount, spawnSync).
|
|
41
|
+
*/
|
|
42
|
+
export declare function startupMarker(text: string): void;
|
|
34
43
|
export declare function timingModeIncludes(option: "full" | "x"): boolean;
|
|
35
44
|
export declare function shouldExitAfterTimings(): boolean;
|
|
36
45
|
/**
|
|
@@ -55,6 +64,12 @@ export declare function recordModuleLoadSpan(path: string, start: number, durati
|
|
|
55
64
|
* End timing window and clear buffers.
|
|
56
65
|
*/
|
|
57
66
|
export declare function endTiming(): void;
|
|
67
|
+
/**
|
|
68
|
+
* Ops of the currently-open span chain (root → deepest), following the most
|
|
69
|
+
* recently started unfinished child at each level. Lets a startup watchdog
|
|
70
|
+
* name the phase a stalled startup is stuck in.
|
|
71
|
+
*/
|
|
72
|
+
export declare function openSpanPath(): string[];
|
|
58
73
|
/**
|
|
59
74
|
* Time a span. Three forms:
|
|
60
75
|
* time(op) — point event (zero-duration breadcrumb)
|
package/dist/types/procmgr.d.ts
CHANGED
|
@@ -5,6 +5,10 @@ export interface ShellConfig {
|
|
|
5
5
|
env: Record<string, string>;
|
|
6
6
|
prefix: string | undefined;
|
|
7
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* Check if a shell binary is executable.
|
|
10
|
+
*/
|
|
11
|
+
export declare function isExecutable(path: string): boolean;
|
|
8
12
|
/**
|
|
9
13
|
* Resolve a basic shell (bash or sh) as fallback.
|
|
10
14
|
*/
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-utils",
|
|
4
|
-
"version": "15.10.
|
|
4
|
+
"version": "15.10.12",
|
|
5
5
|
"description": "Shared utilities for pi packages",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"fmt": "biome format --write ."
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@oh-my-pi/pi-natives": "15.10.
|
|
34
|
+
"@oh-my-pi/pi-natives": "15.10.12",
|
|
35
35
|
"beautiful-mermaid": "^1.1.3",
|
|
36
36
|
"handlebars": "^4.7.9",
|
|
37
37
|
"winston": "^3.19.0",
|
package/src/cli.ts
CHANGED
|
@@ -9,8 +9,25 @@
|
|
|
9
9
|
* - Lazy command imports (only the invoked command is loaded)
|
|
10
10
|
* - Typed `this.parse()` output matching oclif's API shape
|
|
11
11
|
*/
|
|
12
|
+
import * as fs from "node:fs";
|
|
12
13
|
import { parseArgs as nodeParseArgs } from "node:util";
|
|
13
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Streaming startup marker, enabled by `PI_DEBUG_STARTUP`. Local copy of
|
|
17
|
+
* `logger.startupMarker` so the minimal `--version`/bootstrap import graph
|
|
18
|
+
* stays free of the winston-backed logger module. Synchronous on purpose:
|
|
19
|
+
* a command module whose import hangs (dlopen, fs on a dead mount) must
|
|
20
|
+
* still leave its `:start` marker behind.
|
|
21
|
+
*/
|
|
22
|
+
function startupMarker(text: string): void {
|
|
23
|
+
if (!process.env.PI_DEBUG_STARTUP) return;
|
|
24
|
+
try {
|
|
25
|
+
fs.writeSync(2, `[startup] ${text}\n`);
|
|
26
|
+
} catch {
|
|
27
|
+
// stderr unavailable; markers are best-effort
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
14
31
|
// ---------------------------------------------------------------------------
|
|
15
32
|
// Flag & Arg descriptors
|
|
16
33
|
// ---------------------------------------------------------------------------
|
|
@@ -392,14 +409,14 @@ export async function run(opts: RunOptions): Promise<void> {
|
|
|
392
409
|
return;
|
|
393
410
|
}
|
|
394
411
|
|
|
395
|
-
// Per-command help
|
|
412
|
+
// Per-command help: load only the requested command. Loading the full
|
|
413
|
+
// command table here would make `omp <cmd> --help` hang or crash whenever
|
|
414
|
+
// any *unrelated* command module misbehaves at import time.
|
|
396
415
|
if (commandArgv.includes("--help") || commandArgv.includes("-h")) {
|
|
397
|
-
const config = await loadAllCommands(opts);
|
|
398
|
-
// Resolve aliases for help too
|
|
399
416
|
const entry = findEntry(opts.commands, commandId);
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
renderCommandHelp(bin, entry
|
|
417
|
+
if (entry) {
|
|
418
|
+
const Cmd = await loadEntry(entry);
|
|
419
|
+
renderCommandHelp(bin, entry.name, Cmd);
|
|
403
420
|
} else {
|
|
404
421
|
process.stderr.write(`Unknown command: ${commandId}\n`);
|
|
405
422
|
}
|
|
@@ -415,16 +432,24 @@ export async function run(opts: RunOptions): Promise<void> {
|
|
|
415
432
|
return;
|
|
416
433
|
}
|
|
417
434
|
|
|
418
|
-
const Cmd = await entry
|
|
435
|
+
const Cmd = await loadEntry(entry);
|
|
419
436
|
const config: CliConfig = { bin, version, commands: new Map([[entry.name, Cmd]]) };
|
|
420
437
|
const instance = new Cmd(commandArgv, config);
|
|
421
438
|
await instance.run();
|
|
422
439
|
}
|
|
423
440
|
|
|
441
|
+
/** Load one command module, leaving streaming markers around the import. */
|
|
442
|
+
async function loadEntry(entry: CommandEntry): Promise<CommandCtor> {
|
|
443
|
+
startupMarker(`cli:load:${entry.name}:start`);
|
|
444
|
+
const Cmd = await entry.load();
|
|
445
|
+
startupMarker(`cli:load:${entry.name}:done`);
|
|
446
|
+
return Cmd;
|
|
447
|
+
}
|
|
448
|
+
|
|
424
449
|
/** Resolve all command loaders for help/alias display. */
|
|
425
450
|
async function loadAllCommands(opts: RunOptions): Promise<CliConfig> {
|
|
426
451
|
const commands = new Map<string, CommandCtor>();
|
|
427
|
-
const loaded = await Promise.all(opts.commands.map(async e => [e.name, await e
|
|
452
|
+
const loaded = await Promise.all(opts.commands.map(async e => [e.name, await loadEntry(e)] as const));
|
|
428
453
|
for (const [name, Cmd] of loaded) {
|
|
429
454
|
commands.set(name, Cmd);
|
|
430
455
|
}
|
package/src/dirs.ts
CHANGED
|
@@ -398,6 +398,11 @@ export function getAgentDbPath(agentDir?: string): string {
|
|
|
398
398
|
return dirs.agentSubdir(agentDir, "agent.db", "data");
|
|
399
399
|
}
|
|
400
400
|
|
|
401
|
+
/** Get the last-seen-changelog-version marker file (~/.omp/agent/last-changelog-version). */
|
|
402
|
+
export function getLastChangelogVersionPath(agentDir?: string): string {
|
|
403
|
+
return dirs.agentSubdir(agentDir, "last-changelog-version", "state");
|
|
404
|
+
}
|
|
405
|
+
|
|
401
406
|
/** Get the path to history.db (SQLite database for session history). */
|
|
402
407
|
export function getHistoryDbPath(agentDir?: string): string {
|
|
403
408
|
return dirs.agentSubdir(agentDir, "history.db", "data");
|
package/src/env.ts
CHANGED
|
@@ -156,11 +156,31 @@ export function isBunTestRuntime(): boolean {
|
|
|
156
156
|
* first for cheap fast-path detection.
|
|
157
157
|
*/
|
|
158
158
|
export function isCompiledBinary(): boolean {
|
|
159
|
-
if (Bun.env.PI_COMPILED) return true;
|
|
159
|
+
if (process.env.PI_COMPILED || Bun.env.PI_COMPILED) return true;
|
|
160
160
|
const url = import.meta.url;
|
|
161
161
|
return url.includes("$bunfs") || url.includes("~BUN") || url.includes("%7EBUN");
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
/**
|
|
165
|
+
* Main-module path declared by self-dispatching CLI entrypoints — entries
|
|
166
|
+
* whose top-level argv handling routes hidden `__omp_*` worker selectors.
|
|
167
|
+
* Worker spawn sites re-enter this module via `new Worker(entry, { argv })`,
|
|
168
|
+
* so every distribution (source, npm bundle, compiled binary) needs exactly
|
|
169
|
+
* one JavaScript entrypoint. Never set under `bun test`, SDK embedding, or
|
|
170
|
+
* standalone package bins — those hosts load worker modules directly.
|
|
171
|
+
*/
|
|
172
|
+
let workerHostMain: string | null = null;
|
|
173
|
+
|
|
174
|
+
/** Called by CLI entrypoints whose main module dispatches worker argv selectors. */
|
|
175
|
+
export function declareWorkerHostEntry(): void {
|
|
176
|
+
workerHostMain = Bun.main;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Main-module path of the self-dispatching CLI host, or null outside it. */
|
|
180
|
+
export function workerHostEntry(): string | null {
|
|
181
|
+
return workerHostMain;
|
|
182
|
+
}
|
|
183
|
+
|
|
164
184
|
const TRUTHY: Dict<boolean> = {
|
|
165
185
|
"1": true,
|
|
166
186
|
Y: true,
|
package/src/logger.ts
CHANGED
|
@@ -47,25 +47,30 @@ function jsonReplacer(_key: string, value: unknown): unknown {
|
|
|
47
47
|
return value;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
/** Custom format that includes pid and flattens metadata */
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
50
|
+
/** Custom format that includes pid and flattens metadata; built on first use. */
|
|
51
|
+
let logFormat: winston.Logform.Format | undefined;
|
|
52
|
+
|
|
53
|
+
function getLogFormat(): winston.Logform.Format {
|
|
54
|
+
logFormat ??= winston.format.combine(
|
|
55
|
+
winston.format.timestamp({ format: "YYYY-MM-DDTHH:mm:ss.SSSZ" }),
|
|
56
|
+
winston.format.printf(({ timestamp, level, message, ...meta }) => {
|
|
57
|
+
const entry: Record<string, unknown> = {
|
|
58
|
+
timestamp,
|
|
59
|
+
level,
|
|
60
|
+
pid: process.pid,
|
|
61
|
+
message,
|
|
62
|
+
};
|
|
63
|
+
// Flatten metadata into entry
|
|
64
|
+
for (const [key, value] of Object.entries(meta)) {
|
|
65
|
+
if (key !== "level" && key !== "timestamp" && key !== "message") {
|
|
66
|
+
entry[key] = value;
|
|
67
|
+
}
|
|
64
68
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
+
return JSON.stringify(entry, jsonReplacer);
|
|
70
|
+
}),
|
|
71
|
+
);
|
|
72
|
+
return logFormat;
|
|
73
|
+
}
|
|
69
74
|
|
|
70
75
|
/** Build a rotating file transport, materializing the target directory lazily. */
|
|
71
76
|
function makeFileTransport(dir?: string): winston.transport {
|
|
@@ -80,17 +85,35 @@ function makeFileTransport(dir?: string): winston.transport {
|
|
|
80
85
|
}
|
|
81
86
|
|
|
82
87
|
function makeConsoleTransport(): winston.transport {
|
|
83
|
-
return new winston.transports.Console({ format:
|
|
88
|
+
return new winston.transports.Console({ format: getLogFormat() });
|
|
84
89
|
}
|
|
85
90
|
|
|
86
|
-
/**
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
91
|
+
/**
|
|
92
|
+
* Desired transport configuration, applied when the winston logger is built.
|
|
93
|
+
* Default: file ON (TUI-safe), console OFF.
|
|
94
|
+
*/
|
|
95
|
+
let transportOpts: { console?: boolean; file?: boolean | string } = { file: true };
|
|
96
|
+
|
|
97
|
+
/** The winston logger instance, created lazily on first log emission. */
|
|
98
|
+
let winstonLogger: winston.Logger | undefined;
|
|
99
|
+
|
|
100
|
+
function buildTransports(opts: { console?: boolean; file?: boolean | string }): winston.transport[] {
|
|
101
|
+
const transports: winston.transport[] = [];
|
|
102
|
+
if (opts.file) transports.push(makeFileTransport(typeof opts.file === "string" ? opts.file : undefined));
|
|
103
|
+
if (opts.console) transports.push(makeConsoleTransport());
|
|
104
|
+
return transports;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function getWinstonLogger(): winston.Logger {
|
|
108
|
+
winstonLogger ??= winston.createLogger({
|
|
109
|
+
level: "debug",
|
|
110
|
+
format: getLogFormat(),
|
|
111
|
+
transports: buildTransports(transportOpts),
|
|
112
|
+
// Don't exit on error - logging failures shouldn't crash the app
|
|
113
|
+
exitOnError: false,
|
|
114
|
+
});
|
|
115
|
+
return winstonLogger;
|
|
116
|
+
}
|
|
94
117
|
|
|
95
118
|
/**
|
|
96
119
|
* Replace the active log transports. Pass `console: true, file: false` for
|
|
@@ -98,11 +121,10 @@ const winstonLogger = winston.createLogger({
|
|
|
98
121
|
* logs piped into a process supervisor instead of the rotating file.
|
|
99
122
|
*/
|
|
100
123
|
export function setTransports(opts: { console?: boolean; file?: boolean | string }): void {
|
|
124
|
+
transportOpts = opts;
|
|
125
|
+
if (!winstonLogger) return; // applied lazily when the logger is first built
|
|
101
126
|
winstonLogger.clear();
|
|
102
|
-
|
|
103
|
-
winstonLogger.add(makeFileTransport(typeof opts.file === "string" ? opts.file : undefined));
|
|
104
|
-
}
|
|
105
|
-
if (opts.console) winstonLogger.add(makeConsoleTransport());
|
|
127
|
+
for (const transport of buildTransports(opts)) winstonLogger.add(transport);
|
|
106
128
|
}
|
|
107
129
|
|
|
108
130
|
/**
|
|
@@ -112,7 +134,7 @@ export function setTransports(opts: { console?: boolean; file?: boolean | string
|
|
|
112
134
|
*/
|
|
113
135
|
export function error(message: string, context?: Record<string, unknown>): void {
|
|
114
136
|
try {
|
|
115
|
-
|
|
137
|
+
getWinstonLogger().error(message, context);
|
|
116
138
|
} catch {
|
|
117
139
|
// Silently ignore logging failures
|
|
118
140
|
}
|
|
@@ -125,7 +147,7 @@ export function error(message: string, context?: Record<string, unknown>): void
|
|
|
125
147
|
*/
|
|
126
148
|
export function warn(message: string, context?: Record<string, unknown>): void {
|
|
127
149
|
try {
|
|
128
|
-
|
|
150
|
+
getWinstonLogger().warn(message, context);
|
|
129
151
|
} catch {
|
|
130
152
|
// Silently ignore logging failures
|
|
131
153
|
}
|
|
@@ -138,7 +160,7 @@ export function warn(message: string, context?: Record<string, unknown>): void {
|
|
|
138
160
|
*/
|
|
139
161
|
export function info(message: string, context?: Record<string, unknown>): void {
|
|
140
162
|
try {
|
|
141
|
-
|
|
163
|
+
getWinstonLogger().info(message, context);
|
|
142
164
|
} catch {
|
|
143
165
|
// Silently ignore logging failures
|
|
144
166
|
}
|
|
@@ -151,12 +173,29 @@ export function info(message: string, context?: Record<string, unknown>): void {
|
|
|
151
173
|
*/
|
|
152
174
|
export function debug(message: string, context?: Record<string, unknown>): void {
|
|
153
175
|
try {
|
|
154
|
-
|
|
176
|
+
getWinstonLogger().debug(message, context);
|
|
155
177
|
} catch {
|
|
156
178
|
// Silently ignore logging failures
|
|
157
179
|
}
|
|
158
180
|
}
|
|
159
181
|
|
|
182
|
+
/**
|
|
183
|
+
* Streaming startup markers, enabled by `PI_DEBUG_STARTUP`. Unlike the
|
|
184
|
+
* PI_TIMING tree (printed only after startup completes), these write one
|
|
185
|
+
* synchronous stderr line as each phase begins/ends, so a hard hang still
|
|
186
|
+
* shows the last phase that started. `fs.writeSync(2)` is used deliberately:
|
|
187
|
+
* it cannot be reordered or buffered past a synchronous block of the event
|
|
188
|
+
* loop (dlopen, sync fs on a dead mount, spawnSync).
|
|
189
|
+
*/
|
|
190
|
+
export function startupMarker(text: string): void {
|
|
191
|
+
if (!process.env.PI_DEBUG_STARTUP) return;
|
|
192
|
+
try {
|
|
193
|
+
fs.writeSync(2, `[startup] ${text}\n`);
|
|
194
|
+
} catch {
|
|
195
|
+
// stderr unavailable; markers are best-effort
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
160
199
|
const LOGGED_TIMING_THRESHOLD_MS = 0.5;
|
|
161
200
|
|
|
162
201
|
interface Span {
|
|
@@ -329,6 +368,29 @@ export function endTiming(): void {
|
|
|
329
368
|
gRecordTimings = false;
|
|
330
369
|
}
|
|
331
370
|
|
|
371
|
+
/**
|
|
372
|
+
* Ops of the currently-open span chain (root → deepest), following the most
|
|
373
|
+
* recently started unfinished child at each level. Lets a startup watchdog
|
|
374
|
+
* name the phase a stalled startup is stuck in.
|
|
375
|
+
*/
|
|
376
|
+
export function openSpanPath(): string[] {
|
|
377
|
+
const ops: string[] = [];
|
|
378
|
+
let node = gRootSpan;
|
|
379
|
+
while (node) {
|
|
380
|
+
let next: Span | undefined;
|
|
381
|
+
for (let i = node.children.length - 1; i >= 0; i--) {
|
|
382
|
+
if (node.children[i].end === undefined) {
|
|
383
|
+
next = node.children[i];
|
|
384
|
+
break;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
if (!next) break;
|
|
388
|
+
ops.push(next.op);
|
|
389
|
+
node = next;
|
|
390
|
+
}
|
|
391
|
+
return ops;
|
|
392
|
+
}
|
|
393
|
+
|
|
332
394
|
function durationOf(span: Span): number {
|
|
333
395
|
if (span.point || span.end === undefined) return 0;
|
|
334
396
|
return span.end - span.start;
|
|
@@ -550,33 +612,51 @@ function isParallel(span: Span): boolean {
|
|
|
550
612
|
export function time(op: string): void;
|
|
551
613
|
export function time<T, A extends unknown[]>(op: string, fn: (...args: A) => T, ...args: A): T;
|
|
552
614
|
export function time<T, A extends unknown[]>(op: string, fn?: (...args: A) => T, ...args: A): T | undefined {
|
|
553
|
-
|
|
554
|
-
if (fn === undefined) return undefined as T;
|
|
555
|
-
return fn(...args);
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
const parent = spanStorage.getStore() ?? gRootSpan;
|
|
559
|
-
const span: Span = { op, start: performance.now(), parent, children: [] };
|
|
560
|
-
parent.children.push(span);
|
|
615
|
+
const recording = gRecordTimings && gRootSpan !== undefined;
|
|
561
616
|
|
|
562
617
|
if (fn === undefined) {
|
|
563
|
-
|
|
564
|
-
|
|
618
|
+
startupMarker(op);
|
|
619
|
+
if (!recording) return undefined as T;
|
|
620
|
+
const parent = spanStorage.getStore() ?? gRootSpan!;
|
|
621
|
+
const now = performance.now();
|
|
622
|
+
parent.children.push({ op, start: now, end: now, parent, children: [], point: true });
|
|
565
623
|
return undefined as T;
|
|
566
624
|
}
|
|
567
625
|
|
|
568
|
-
|
|
569
|
-
|
|
626
|
+
if (!recording && !process.env.PI_DEBUG_STARTUP) {
|
|
627
|
+
return fn(...args);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
startupMarker(`${op}:start`);
|
|
631
|
+
let span: Span | undefined;
|
|
632
|
+
if (recording) {
|
|
633
|
+
const parent = spanStorage.getStore() ?? gRootSpan!;
|
|
634
|
+
span = { op, start: performance.now(), parent, children: [] };
|
|
635
|
+
parent.children.push(span);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const finish = (ok: boolean): void => {
|
|
639
|
+
if (span) span.end = performance.now();
|
|
640
|
+
startupMarker(ok ? `${op}:done` : `${op}:fail`);
|
|
570
641
|
};
|
|
571
642
|
try {
|
|
572
|
-
const result = spanStorage.run(span, () => fn(...args));
|
|
643
|
+
const result = span ? spanStorage.run(span, () => fn(...args)) : fn(...args);
|
|
573
644
|
if (isPromise(result)) {
|
|
574
|
-
return result.
|
|
645
|
+
return result.then(
|
|
646
|
+
value => {
|
|
647
|
+
finish(true);
|
|
648
|
+
return value;
|
|
649
|
+
},
|
|
650
|
+
error => {
|
|
651
|
+
finish(false);
|
|
652
|
+
throw error;
|
|
653
|
+
},
|
|
654
|
+
) as T;
|
|
575
655
|
}
|
|
576
|
-
finish();
|
|
656
|
+
finish(true);
|
|
577
657
|
return result;
|
|
578
658
|
} catch (error) {
|
|
579
|
-
finish();
|
|
659
|
+
finish(false);
|
|
580
660
|
throw error;
|
|
581
661
|
}
|
|
582
662
|
}
|
package/src/procmgr.ts
CHANGED
|
@@ -16,7 +16,7 @@ let cachedShellConfig: ShellConfig | null = null;
|
|
|
16
16
|
/**
|
|
17
17
|
* Check if a shell binary is executable.
|
|
18
18
|
*/
|
|
19
|
-
function isExecutable(path: string): boolean {
|
|
19
|
+
export function isExecutable(path: string): boolean {
|
|
20
20
|
try {
|
|
21
21
|
fs.accessSync(path, fs.constants.X_OK);
|
|
22
22
|
return true;
|
package/src/prompt.ts
CHANGED
|
@@ -13,14 +13,53 @@ export interface PromptFormatOptions {
|
|
|
13
13
|
|
|
14
14
|
// Opening XML tag (not self-closing, not closing)
|
|
15
15
|
const OPENING_XML = /^<([a-z_-]+)(?:\s+[^>]*)?>$/;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Closing XML tag matcher, manual equivalent of `/^<\/([a-z_-]+)>$/` — avoids a
|
|
19
|
+
* RegExp exec (and match array allocation) per `<`-prefixed line. Caller
|
|
20
|
+
* guarantees `s` starts `</`.
|
|
21
|
+
*/
|
|
22
|
+
function closingTagName(s: string): string | null {
|
|
23
|
+
const n = s.length;
|
|
24
|
+
if (n < 4 || s.charCodeAt(n - 1) !== 62 /* > */) return null;
|
|
25
|
+
for (let j = 2; j < n - 1; j++) {
|
|
26
|
+
const c = s.charCodeAt(j);
|
|
27
|
+
if (!((c >= 97 /* a */ && c <= 122) /* z */ || c === 45 /* - */ || c === 95) /* _ */) return null;
|
|
28
|
+
}
|
|
29
|
+
return s.slice(2, n - 1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Manual equivalent of {@link OPENING_XML}. Caller guarantees `s` starts with
|
|
34
|
+
* `<` but not `</`. Falls back to the regex when the char after the tag name
|
|
35
|
+
* is non-ASCII (possible unicode whitespace).
|
|
36
|
+
*/
|
|
37
|
+
function openingTagName(s: string): string | null {
|
|
38
|
+
const n = s.length;
|
|
39
|
+
if (n < 3 || s.charCodeAt(n - 1) !== 62 /* > */) return null;
|
|
40
|
+
let j = 1;
|
|
41
|
+
while (j < n - 1) {
|
|
42
|
+
const c = s.charCodeAt(j);
|
|
43
|
+
if ((c >= 97 /* a */ && c <= 122) /* z */ || c === 45 /* - */ || c === 95 /* _ */) j++;
|
|
44
|
+
else break;
|
|
45
|
+
}
|
|
46
|
+
if (j === 1) return null;
|
|
47
|
+
if (j === n - 1) return s.slice(1, j); // `<tag>`
|
|
48
|
+
const c = s.charCodeAt(j);
|
|
49
|
+
if (c !== 32 /* space */ && c !== 9 /* tab */) {
|
|
50
|
+
if (c < 128) return null;
|
|
51
|
+
const match = OPENING_XML.exec(s);
|
|
52
|
+
return match ? match[1] : null;
|
|
53
|
+
}
|
|
54
|
+
// `\s+[^>]*>$` ⇔ no further `>` before the final char.
|
|
55
|
+
return s.indexOf(">", j + 1) === n - 1 ? s.slice(1, j) : null;
|
|
56
|
+
}
|
|
20
57
|
// Table row
|
|
21
58
|
const TABLE_ROW = /^\|.*\|$/;
|
|
22
59
|
// Table separator (|---|---|)
|
|
23
60
|
const TABLE_SEP = /^\|[-:\s|]+\|$/;
|
|
61
|
+
// Any non-whitespace char — blank-line check without allocating a trimmed copy
|
|
62
|
+
const NON_BLANK = /\S/;
|
|
24
63
|
|
|
25
64
|
/**
|
|
26
65
|
* RFC 2119 keywords (plus project aliases NEVER/AVOID) wrapped in markdown bold
|
|
@@ -28,6 +67,19 @@ const TABLE_SEP = /^\|[-:\s|]+\|$/;
|
|
|
28
67
|
*/
|
|
29
68
|
const RFC2119_BOLD = /\*\*(MUST NOT|SHOULD NOT|RECOMMENDED|REQUIRED|OPTIONAL|SHOULD|MUST|MAY|NEVER|AVOID)\*\*/g;
|
|
30
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Fast pre-check for {@link normalizeRfc2119}: a line that lacks every one of
|
|
72
|
+
* these substrings is untouched by all three replacements, so the
|
|
73
|
+
* split/replace/join machinery can be skipped entirely.
|
|
74
|
+
*/
|
|
75
|
+
const RFC2119_GUARD = /\*\*(?:MUST|SHOULD|RECOMMENDED|REQUIRED|OPTIONAL|MAY|NEVER|AVOID)|MUST NOT|SHOULD NOT/;
|
|
76
|
+
const MUST_NOT = /\bMUST NOT\b/g;
|
|
77
|
+
const SHOULD_NOT = /\bSHOULD NOT\b/g;
|
|
78
|
+
|
|
79
|
+
function applyRfc2119(text: string): string {
|
|
80
|
+
return text.replace(RFC2119_BOLD, "$1").replace(MUST_NOT, "NEVER").replace(SHOULD_NOT, "AVOID");
|
|
81
|
+
}
|
|
82
|
+
|
|
31
83
|
/**
|
|
32
84
|
* Normalize RFC 2119 markers per project convention:
|
|
33
85
|
* - Strip `**KEYWORD**` bold (visual noise, no semantics).
|
|
@@ -35,12 +87,11 @@ const RFC2119_BOLD = /\*\*(MUST NOT|SHOULD NOT|RECOMMENDED|REQUIRED|OPTIONAL|SHO
|
|
|
35
87
|
* Skips spans inside inline code (`` `…` ``) so alias definitions can be quoted literally.
|
|
36
88
|
*/
|
|
37
89
|
function normalizeRfc2119(line: string): string {
|
|
90
|
+
if (!RFC2119_GUARD.test(line)) return line;
|
|
91
|
+
if (!line.includes("`")) return applyRfc2119(line);
|
|
38
92
|
const segments = line.split("`");
|
|
39
93
|
for (let i = 0; i < segments.length; i += 2) {
|
|
40
|
-
segments[i] = segments[i]
|
|
41
|
-
.replace(RFC2119_BOLD, "$1")
|
|
42
|
-
.replace(/\bMUST NOT\b/g, "NEVER")
|
|
43
|
-
.replace(/\bSHOULD NOT\b/g, "AVOID");
|
|
94
|
+
segments[i] = applyRfc2119(segments[i]);
|
|
44
95
|
}
|
|
45
96
|
return segments.join("`");
|
|
46
97
|
}
|
|
@@ -73,19 +124,31 @@ type HtmlCommentState = {
|
|
|
73
124
|
inHtmlComment: boolean;
|
|
74
125
|
};
|
|
75
126
|
|
|
127
|
+
// Single-pass alternation equivalent to the former chain of seven .replace()
|
|
128
|
+
// calls. Alternative order mirrors the old sequential order (`<->` before
|
|
129
|
+
// `->`/`<-`), and every replacement emits a non-ASCII char, so one pass
|
|
130
|
+
// produces byte-identical output to the sequential passes.
|
|
131
|
+
const ASCII_SYMBOLS = /\.{3}|<->|->|<-|!=|<=|>=/g;
|
|
132
|
+
const ASCII_SYMBOL_REPLACEMENTS: Record<string, string> = {
|
|
133
|
+
"...": "…",
|
|
134
|
+
"<->": "↔",
|
|
135
|
+
"->": "→",
|
|
136
|
+
"<-": "←",
|
|
137
|
+
"!=": "≠",
|
|
138
|
+
"<=": "≤",
|
|
139
|
+
">=": "≥",
|
|
140
|
+
};
|
|
141
|
+
const replaceAsciiSymbol = (match: string): string => ASCII_SYMBOL_REPLACEMENTS[match];
|
|
142
|
+
|
|
76
143
|
function replaceCommonAsciiSymbols(line: string): string {
|
|
77
|
-
return line
|
|
78
|
-
.replace(/\.{3}/g, "…")
|
|
79
|
-
.replace(/<->/g, "↔")
|
|
80
|
-
.replace(/->/g, "→")
|
|
81
|
-
.replace(/<-/g, "←")
|
|
82
|
-
.replace(/!=/g, "≠")
|
|
83
|
-
.replace(/<=/g, "≤")
|
|
84
|
-
.replace(/>=/g, "≥");
|
|
144
|
+
return line.replace(ASCII_SYMBOLS, replaceAsciiSymbol);
|
|
85
145
|
}
|
|
86
146
|
|
|
87
147
|
function replaceCommonAsciiSymbolsOutsideHtmlComments(line: string, state: HtmlCommentState): string {
|
|
88
|
-
|
|
148
|
+
// When not inside a comment, a line without `<!--` takes the fast path even
|
|
149
|
+
// if it contains `-->`: the slow path would hit openIndex === -1 and replace
|
|
150
|
+
// the whole line identically.
|
|
151
|
+
if (!state.inHtmlComment && !line.includes(HTML_COMMENT_OPEN)) {
|
|
89
152
|
return replaceCommonAsciiSymbols(line);
|
|
90
153
|
}
|
|
91
154
|
|
|
@@ -133,86 +196,111 @@ export function format(content: string, options: PromptFormatOptions = {}): stri
|
|
|
133
196
|
} = options;
|
|
134
197
|
const isPreRender = renderPhase === "pre-render";
|
|
135
198
|
const lines = content.split("\n");
|
|
136
|
-
const result: string[] =
|
|
199
|
+
const result: string[] = new Array(lines.length);
|
|
200
|
+
let n = 0; // logical length of `result` (pops are n--)
|
|
137
201
|
let inCodeBlock = false;
|
|
138
202
|
|
|
139
203
|
const htmlCommentState: HtmlCommentState = { inHtmlComment: false };
|
|
140
204
|
const topLevelTags: string[] = [];
|
|
141
205
|
|
|
142
206
|
for (let i = 0; i < lines.length; i++) {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
207
|
+
const raw = lines[i];
|
|
208
|
+
// charCode fast paths: only pay for trimEnd when the last char might be
|
|
209
|
+
// whitespace (<= 0x20 ASCII ws/controls, >= 0x80 unicode ws). Untouched
|
|
210
|
+
// lines are pushed as the original string — no allocation.
|
|
211
|
+
const last = raw.charCodeAt(raw.length - 1);
|
|
212
|
+
let line = last <= 32 || last >= 128 ? raw.trimEnd() : raw;
|
|
213
|
+
// Locate the first non-whitespace char without allocating a trimStart
|
|
214
|
+
// copy; `s` is the indent width, `first` the char code there (NaN when
|
|
215
|
+
// the line is blank).
|
|
216
|
+
let s = 0;
|
|
217
|
+
let first = line.charCodeAt(0);
|
|
218
|
+
while (first === 32 /* space */ || first === 9 /* tab */) first = line.charCodeAt(++s);
|
|
219
|
+
if (first >= 128) {
|
|
220
|
+
// Possible unicode leading whitespace — defer to trimStart for exactness.
|
|
221
|
+
s = line.length - line.trimStart().length;
|
|
222
|
+
first = line.charCodeAt(s);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if ((first === 96 /* ` */ || first === 126) /* ~ */ && (line.startsWith("```", s) || line.startsWith("~~~", s))) {
|
|
146
226
|
inCodeBlock = !inCodeBlock;
|
|
147
|
-
result
|
|
227
|
+
result[n++] = line;
|
|
148
228
|
continue;
|
|
149
229
|
}
|
|
150
230
|
|
|
151
231
|
if (inCodeBlock) {
|
|
152
|
-
result
|
|
232
|
+
result[n++] = line;
|
|
153
233
|
continue;
|
|
154
234
|
}
|
|
155
235
|
|
|
156
236
|
if (replaceAsciiSymbols) {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
237
|
+
const replaced = replaceCommonAsciiSymbolsOutsideHtmlComments(line, htmlCommentState);
|
|
238
|
+
if (replaced !== line) {
|
|
239
|
+
line = replaced;
|
|
240
|
+
s = 0;
|
|
241
|
+
first = line.charCodeAt(0);
|
|
242
|
+
while (first === 32 || first === 9) first = line.charCodeAt(++s);
|
|
243
|
+
if (first >= 128) {
|
|
244
|
+
s = line.length - line.trimStart().length;
|
|
245
|
+
first = line.charCodeAt(s);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
166
248
|
}
|
|
167
249
|
|
|
168
|
-
|
|
169
|
-
if (
|
|
170
|
-
const
|
|
171
|
-
if (
|
|
172
|
-
|
|
250
|
+
let isClosingLine = false;
|
|
251
|
+
if (first === 60 /* < */) {
|
|
252
|
+
const trimmedStart = s === 0 ? line : line.slice(s);
|
|
253
|
+
if (trimmedStart.charCodeAt(1) === 47 /* / */) {
|
|
254
|
+
const tagName = closingTagName(trimmedStart);
|
|
255
|
+
if (tagName !== null) {
|
|
256
|
+
isClosingLine = true;
|
|
257
|
+
if (topLevelTags.length > 0 && topLevelTags[topLevelTags.length - 1] === tagName) {
|
|
258
|
+
topLevelTags.pop();
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
} else if (s === 0 && !trimmedStart.endsWith("/>")) {
|
|
262
|
+
const tagName = openingTagName(trimmedStart);
|
|
263
|
+
if (tagName !== null) topLevelTags.push(tagName);
|
|
264
|
+
}
|
|
265
|
+
} else if (first === 124 /* | */) {
|
|
266
|
+
const trimmedStart = s === 0 ? line : line.slice(s);
|
|
267
|
+
if (TABLE_SEP.test(trimmedStart)) {
|
|
268
|
+
line = `${line.slice(0, s)}${compactTableSep(trimmedStart)}`;
|
|
269
|
+
} else if (TABLE_ROW.test(trimmedStart)) {
|
|
270
|
+
line = `${line.slice(0, s)}${compactTableRow(trimmedStart)}`;
|
|
173
271
|
}
|
|
174
|
-
} else if (isPreRender && trimmedStart.startsWith("{{")) {
|
|
175
|
-
/* keep indentation as-is in pre-render for Handlebars markers */
|
|
176
|
-
} else if (TABLE_SEP.test(trimmedStart)) {
|
|
177
|
-
const leadingWhitespace = line.slice(0, line.length - trimmedStart.length);
|
|
178
|
-
line = `${leadingWhitespace}${compactTableSep(trimmedStart)}`;
|
|
179
|
-
} else if (TABLE_ROW.test(trimmedStart)) {
|
|
180
|
-
const leadingWhitespace = line.slice(0, line.length - trimmedStart.length);
|
|
181
|
-
line = `${leadingWhitespace}${compactTableRow(trimmedStart)}`;
|
|
182
272
|
}
|
|
183
273
|
|
|
184
274
|
if (shouldNormalizeRfc2119) {
|
|
185
275
|
line = normalizeRfc2119(line);
|
|
186
276
|
}
|
|
187
277
|
|
|
188
|
-
if (
|
|
189
|
-
|
|
278
|
+
if (s >= line.length) {
|
|
279
|
+
// Blank line (`line` carries no trailing whitespace, so it is "").
|
|
280
|
+
const next = lines[i + 1];
|
|
190
281
|
// Strip any run of 2+ consecutive blank lines entirely; preserve a single blank.
|
|
191
|
-
if (
|
|
192
|
-
while (
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
282
|
+
if (next === undefined || next.length === 0 || !NON_BLANK.test(next)) {
|
|
283
|
+
while (n > 0 && result[n - 1].length === 0) n--;
|
|
284
|
+
let j = i + 1;
|
|
285
|
+
while (j < lines.length && (lines[j].length === 0 || !NON_BLANK.test(lines[j]))) j++;
|
|
286
|
+
i = j - 1;
|
|
196
287
|
continue;
|
|
197
288
|
}
|
|
198
|
-
|
|
199
|
-
if (prevLine === "") {
|
|
289
|
+
if (n === 0 || result[n - 1].length === 0) {
|
|
200
290
|
continue;
|
|
201
291
|
}
|
|
202
292
|
}
|
|
203
293
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
}
|
|
294
|
+
// CLOSING_HBS (`/^\{\{\//`) ⇔ startsWith("{{/") at the indent offset.
|
|
295
|
+
if (isClosingLine || (isPreRender && first === 123 /* { */ && line.startsWith("{{/", s))) {
|
|
296
|
+
while (n > 0 && result[n - 1].length === 0) n--;
|
|
208
297
|
}
|
|
209
298
|
|
|
210
|
-
result
|
|
299
|
+
result[n++] = line;
|
|
211
300
|
}
|
|
212
301
|
|
|
213
|
-
while (
|
|
214
|
-
|
|
215
|
-
}
|
|
302
|
+
while (n > 0 && result[n - 1].length === 0) n--;
|
|
303
|
+
result.length = n;
|
|
216
304
|
|
|
217
305
|
return result.join("\n");
|
|
218
306
|
}
|
|
@@ -454,13 +542,14 @@ function disambiguateClosingBraces(template: string): string {
|
|
|
454
542
|
const compiledTemplateCache = new Map<string, (context: TemplateContext) => string>();
|
|
455
543
|
|
|
456
544
|
export function compile(template: string): (context: TemplateContext) => string {
|
|
457
|
-
|
|
458
|
-
|
|
545
|
+
// Keyed on the raw template so repeat renders skip disambiguateClosingBraces
|
|
546
|
+
// (a full-template regex pass) as well as the Handlebars compile.
|
|
547
|
+
const cached = compiledTemplateCache.get(template);
|
|
459
548
|
if (cached) return cached;
|
|
460
|
-
const compiled = handlebars.compile(
|
|
549
|
+
const compiled = handlebars.compile(disambiguateClosingBraces(template), { noEscape: true, strict: false }) as (
|
|
461
550
|
context: TemplateContext,
|
|
462
551
|
) => string;
|
|
463
|
-
compiledTemplateCache.set(
|
|
552
|
+
compiledTemplateCache.set(template, compiled);
|
|
464
553
|
return compiled;
|
|
465
554
|
}
|
|
466
555
|
|
package/src/snowflake.ts
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
// 16-bit hex lookup table (65536 entries) for fast conversion
|
|
2
|
-
const HEX4 = Array.from({ length: 65536 }, (_, i) => i.toString(16).padStart(4, "0"));
|
|
3
|
-
|
|
4
1
|
function randu32() {
|
|
5
2
|
return crypto.getRandomValues(new Uint32Array(1))[0];
|
|
6
3
|
}
|
|
@@ -28,29 +25,14 @@ namespace Snowflake {
|
|
|
28
25
|
//
|
|
29
26
|
export const MAX_SEQUENCE = MAX_SEQ;
|
|
30
27
|
|
|
31
|
-
// Parses a hex string or bigint to bigint.
|
|
32
|
-
//
|
|
33
|
-
function toBigInt(value: Snowflake): bigint {
|
|
34
|
-
const hi = Number.parseInt(value.substring(0, 8), 16);
|
|
35
|
-
const lo = Number.parseInt(value.substring(8, 16), 16);
|
|
36
|
-
return (BigInt(hi) << 32n) | BigInt(lo);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
28
|
// Formats a sequence and timestamp into a snowflake hex string.
|
|
40
29
|
//
|
|
30
|
+
// dt fits well within BigInt range: (dt << 22) | seq stays under 2^64 for
|
|
31
|
+
// any dt < 2^42 (~year 2154), so a single 64-bit format is exact — and
|
|
32
|
+
// measures ~1.7x faster than stitching four 16-bit hex segments.
|
|
33
|
+
//
|
|
41
34
|
export function formatParts(dt: number, seq: number): Snowflake {
|
|
42
|
-
|
|
43
|
-
// dt is ~39 bits; dt<<22 would be ~61 bits, so we split at bit 10:
|
|
44
|
-
// lo32 = (dtLo << 22) | seq (10+22 = 32 bits, no overlap)
|
|
45
|
-
// hi32 = dtHi (~29 bits)
|
|
46
|
-
const dtLo = dt % 1024;
|
|
47
|
-
const hi = (dt - dtLo) / 1024; // dt >>> 10
|
|
48
|
-
const lo = ((dtLo << 22) | seq) >>> 0;
|
|
49
|
-
const hi1 = (hi >>> 16) & 0xffff;
|
|
50
|
-
const hi2 = hi & 0xffff;
|
|
51
|
-
const lo1 = (lo >>> 16) & 0xffff;
|
|
52
|
-
const lo2 = lo & 0xffff;
|
|
53
|
-
return `${HEX4[hi1]}${HEX4[hi2]}${HEX4[lo1]}${HEX4[lo2]}` as Snowflake;
|
|
35
|
+
return ((BigInt(dt) << 22n) | BigInt(seq)).toString(16).padStart(16, "0") as Snowflake;
|
|
54
36
|
}
|
|
55
37
|
|
|
56
38
|
// Snowflake generator type.
|
|
@@ -85,8 +67,9 @@ namespace Snowflake {
|
|
|
85
67
|
|
|
86
68
|
// Gets the next snowflake given the timestamp.
|
|
87
69
|
//
|
|
88
|
-
|
|
70
|
+
let defaultSource: Source | undefined;
|
|
89
71
|
export function next(timestamp = Date.now()): Snowflake {
|
|
72
|
+
defaultSource ??= new Source();
|
|
90
73
|
return defaultSource.generate(timestamp);
|
|
91
74
|
}
|
|
92
75
|
|
|
@@ -125,8 +108,10 @@ namespace Snowflake {
|
|
|
125
108
|
return Number.parseInt(value.substring(8, 16), 16) & MAX_SEQ;
|
|
126
109
|
}
|
|
127
110
|
export function getTimestamp(value: Snowflake) {
|
|
128
|
-
const
|
|
129
|
-
|
|
111
|
+
const hi = Number.parseInt(value.substring(0, 8), 16);
|
|
112
|
+
const lo = Number.parseInt(value.substring(8, 16), 16);
|
|
113
|
+
// (hi:lo) >> 22 == hi * 2^10 + (lo >>> 22); at most ~2^42, exact in a double.
|
|
114
|
+
return hi * 1024 + (lo >>> 22) + EPOCH;
|
|
130
115
|
}
|
|
131
116
|
export function getDate(value: Snowflake) {
|
|
132
117
|
return new Date(getTimestamp(value));
|