@sayknow-cli/utils 0.7.11 → 0.7.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/broken-pipe.d.ts +4 -3
- package/dist/types/canonical-log-dir.d.ts +20 -0
- package/dist/types/cli-metadata.d.ts +33 -0
- package/dist/types/cli.d.ts +12 -0
- package/dist/types/crash-fingerprint.d.ts +82 -0
- package/dist/types/crash-journal.d.ts +84 -0
- package/dist/types/crash-redaction.d.ts +20 -0
- package/dist/types/dirs.d.ts +127 -36
- package/dist/types/env-file.d.ts +59 -0
- package/dist/types/env.d.ts +19 -1
- package/dist/types/error-classification.d.ts +12 -0
- package/dist/types/fetch-retry.d.ts +6 -3
- package/dist/types/header-value.d.ts +10 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/logger.d.ts +22 -8
- package/dist/types/native-process.d.ts +12 -0
- package/dist/types/procmgr.d.ts +1 -35
- package/dist/types/safe-error.d.ts +20 -0
- package/dist/types/sanitize-text.d.ts +10 -0
- package/dist/types/shell-config.d.ts +58 -0
- package/dist/types/stream.d.ts +2 -1
- package/dist/types/which.d.ts +1 -1
- package/package.json +2 -2
- package/src/broken-pipe.ts +16 -4
- package/src/canonical-log-dir.ts +59 -0
- package/src/cli-metadata.ts +74 -0
- package/src/cli.ts +30 -2
- package/src/crash-fingerprint.ts +294 -0
- package/src/crash-journal.ts +292 -0
- package/src/crash-redaction.ts +558 -0
- package/src/dirs.ts +583 -165
- package/src/env-file.ts +326 -20
- package/src/env.ts +321 -35
- package/src/error-classification.ts +31 -0
- package/src/fetch-retry.ts +23 -10
- package/src/frontmatter.ts +28 -2
- package/src/glob.ts +13 -10
- package/src/header-value.ts +15 -0
- package/src/index.ts +6 -0
- package/src/logger.ts +252 -97
- package/src/native-process.ts +19 -0
- package/src/postmortem.ts +1 -55
- package/src/procmgr.ts +16 -212
- package/src/ptree.ts +3 -2
- package/src/safe-error.ts +40 -0
- package/src/sanitize-text.ts +14 -0
- package/src/shell-config.ts +329 -0
- package/src/stream.ts +32 -1
- package/src/which.ts +6 -5
|
@@ -22,15 +22,18 @@ export interface FetchWithRetryOptions extends RequestInit {
|
|
|
22
22
|
/**
|
|
23
23
|
* Per-delay cap. Server-provided `Retry-After` hints exceeding this return
|
|
24
24
|
* the current response immediately — caller deals with the `!response.ok`.
|
|
25
|
-
* Default
|
|
25
|
+
* Scheduled values are also capped at the platform timer ceiling. Default
|
|
26
|
+
* `60_000`.
|
|
26
27
|
*/
|
|
27
28
|
maxDelayMs?: number;
|
|
28
29
|
/**
|
|
29
30
|
* Fallback delay schedule when no server hint is present. Number, array
|
|
30
31
|
* (indexed by attempt, clamped to last), or function. Default exponential
|
|
31
|
-
* `500ms * 2 ** attempt` capped at `maxDelayMs
|
|
32
|
+
* `500ms * 2 ** attempt` capped at `maxDelayMs` and the platform timer
|
|
33
|
+
* ceiling. Values that remain negative or non-finite after capping retry
|
|
34
|
+
* immediately.
|
|
32
35
|
*/
|
|
33
|
-
defaultDelayMs?: number | readonly number[] | ((attempt: number) => number);
|
|
36
|
+
defaultDelayMs?: number | readonly number[] | ((attempt: number, error?: unknown) => number);
|
|
34
37
|
/**
|
|
35
38
|
* Optional per-attempt overlay merged into the base `RequestInit` each try.
|
|
36
39
|
* Headers from the overlay shallow-merge over the base. Useful for auth
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sanitize a value destined for an HTTP header.
|
|
3
|
+
*
|
|
4
|
+
* OS-derived components (kernel release, hostname, os.version) can contain
|
|
5
|
+
* non-ASCII characters — e.g. Android kernel releases such as
|
|
6
|
+
* `4.4.302-Minimal™-EAS-QTI_Haptic-R26` — which `Headers`/`fetch` reject
|
|
7
|
+
* before the request is ever sent. Strip everything outside printable
|
|
8
|
+
* ASCII so header construction can never throw on runtime-derived values.
|
|
9
|
+
*/
|
|
10
|
+
export declare function sanitizeHeaderComponent(value: string): string;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -2,13 +2,18 @@ export { createAbortableStream, once, untilAborted } from "./abortable";
|
|
|
2
2
|
export * from "./async";
|
|
3
3
|
export * from "./broken-pipe";
|
|
4
4
|
export * from "./color";
|
|
5
|
+
export * from "./crash-fingerprint";
|
|
6
|
+
export * from "./crash-journal";
|
|
7
|
+
export * from "./crash-redaction";
|
|
5
8
|
export * from "./dirs";
|
|
6
9
|
export * from "./env";
|
|
10
|
+
export * from "./error-classification";
|
|
7
11
|
export * from "./fetch-retry";
|
|
8
12
|
export * from "./format";
|
|
9
13
|
export * from "./frontmatter";
|
|
10
14
|
export * from "./fs-error";
|
|
11
15
|
export * from "./glob";
|
|
16
|
+
export * from "./header-value";
|
|
12
17
|
export * from "./hook-fetch";
|
|
13
18
|
export * from "./json";
|
|
14
19
|
export * as logger from "./logger";
|
|
@@ -20,6 +25,7 @@ export * as procmgr from "./procmgr";
|
|
|
20
25
|
export * as prompt from "./prompt";
|
|
21
26
|
export * as ptree from "./ptree";
|
|
22
27
|
export { AbortError, ChildProcess, Exception, NonZeroExitError } from "./ptree";
|
|
28
|
+
export * from "./safe-error";
|
|
23
29
|
export * from "./safe-stderr";
|
|
24
30
|
export * from "./sanitize-text";
|
|
25
31
|
export * from "./snowflake";
|
package/dist/types/logger.d.ts
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
type LogPathPlatform = NodeJS.Platform | "posix";
|
|
2
|
+
/** A filesystem root. On POSIX a backslash is a file name, not a root or a separator. */
|
|
3
|
+
export declare function isFsRoot(target: string, platform?: LogPathPlatform): boolean;
|
|
4
|
+
/**
|
|
5
|
+
* Drop trailing separators without changing the directory name.
|
|
6
|
+
* POSIX removes only `/`. Windows also removes `\`.
|
|
7
|
+
* A root keeps one separator so `C:\` does not become the drive-relative name `C:`.
|
|
8
|
+
*/
|
|
9
|
+
export declare function lexicalLogPath(target: string, platform?: LogPathPlatform): string;
|
|
10
|
+
/** True when "." / ".." collapse to a filesystem root. Does not follow symlinks. */
|
|
11
|
+
export declare function resolvesAsFsRoot(target: string, platform?: LogPathPlatform): boolean;
|
|
1
12
|
/**
|
|
2
13
|
* Replace the active log transports. Pass `console: true, file: false` for
|
|
3
14
|
* long-running services (the auth broker, etc.) that want their structured
|
|
@@ -39,16 +50,18 @@ export declare function debug(message: string, context?: Record<string, unknown>
|
|
|
39
50
|
export declare function printTimings(): void;
|
|
40
51
|
/**
|
|
41
52
|
* Begin recording startup timings under a new root span.
|
|
42
|
-
* Idempotent: a second call while already recording is a no-op so that
|
|
43
|
-
*
|
|
53
|
+
* Idempotent: a second call while already recording is a no-op so that the
|
|
54
|
+
* early starter (cli.ts, the first CLI statement) and the explicit starter
|
|
55
|
+
* (main.ts) can coexist.
|
|
56
|
+
*
|
|
57
|
+
* The root is anchored at the process-start origin rather than at the call:
|
|
58
|
+
* `performance.now()` counts milliseconds since `performance.timeOrigin`
|
|
59
|
+
* (process start) on Bun, so `start: 0` makes `Total` the true
|
|
60
|
+
* time-since-process-start. Runtime bootstrap plus static module linking and
|
|
61
|
+
* evaluation then land in the root's self time instead of being invisible
|
|
62
|
+
* before the first statement of cli.ts.
|
|
44
63
|
*/
|
|
45
64
|
export declare function startTiming(): void;
|
|
46
|
-
/**
|
|
47
|
-
* Record an externally-measured span as a leaf child of the active span (or root
|
|
48
|
-
* when no span is active). Used by the module-load timing plugin to splice load
|
|
49
|
-
* events into the tree retroactively.
|
|
50
|
-
*/
|
|
51
|
-
export declare function recordModuleLoadSpan(path: string, start: number, durationMs: number): void;
|
|
52
65
|
/**
|
|
53
66
|
* End timing window and clear buffers.
|
|
54
67
|
*/
|
|
@@ -64,3 +77,4 @@ export declare function endTiming(): void;
|
|
|
64
77
|
*/
|
|
65
78
|
export declare function time(op: string): void;
|
|
66
79
|
export declare function time<T, A extends unknown[]>(op: string, fn: (...args: A) => T, ...args: A): T;
|
|
80
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lazy synchronous binding for @sayknow-cli/natives process control.
|
|
3
|
+
*
|
|
4
|
+
* The natives package entry loads the compiled addon at import time, so a
|
|
5
|
+
* static import anywhere in the `@sayknow-cli/utils` root barrel graph would
|
|
6
|
+
* materialize the addon for every barrel consumer. The W5b S1/idle
|
|
7
|
+
* module-trace gate requires that merely importing the barrel never loads
|
|
8
|
+
* @sayknow-cli/natives; process-control callers bind at first real use instead.
|
|
9
|
+
*/
|
|
10
|
+
type NativeProcessBindings = Pick<typeof import("@sayknow-cli/natives"), "Process" | "ProcessStatus">;
|
|
11
|
+
export declare function nativeProcessBindings(): NativeProcessBindings;
|
|
12
|
+
export {};
|
package/dist/types/procmgr.d.ts
CHANGED
|
@@ -1,39 +1,5 @@
|
|
|
1
1
|
import type { Subprocess } from "bun";
|
|
2
|
-
export
|
|
3
|
-
shell: string;
|
|
4
|
-
args: string[];
|
|
5
|
-
env: Record<string, string>;
|
|
6
|
-
prefix: string | undefined;
|
|
7
|
-
}
|
|
8
|
-
/**
|
|
9
|
-
* Strip disabled macOS malloc-stack-logging vars from `process.env` in place.
|
|
10
|
-
*
|
|
11
|
-
* macOS leaves `MallocStackLogging=0` (or similar) inherited by debug-attached
|
|
12
|
-
* shells. Bun's libc init then prints `MallocStackLogging: can't turn off
|
|
13
|
-
* malloc stack logging because it was not enabled.` to stderr for every
|
|
14
|
-
* subprocess. Scrubbing once at startup means every child we spawn — bash,
|
|
15
|
-
* bun subagents, plugin installs, ptree commands — inherits a clean env.
|
|
16
|
-
*/
|
|
17
|
-
export declare function scrubProcessEnv(): void;
|
|
18
|
-
/**
|
|
19
|
-
* Resolve a basic shell (bash or sh) as fallback.
|
|
20
|
-
*/
|
|
21
|
-
export declare function resolveBasicShell(): string | undefined;
|
|
22
|
-
/**
|
|
23
|
-
* Get shell configuration based on platform.
|
|
24
|
-
* Resolution order:
|
|
25
|
-
* 1. User-specified shellPath in settings.json
|
|
26
|
-
* 2. On Windows: Git Bash in known locations, then bash on PATH
|
|
27
|
-
* 3. On Unix: $SHELL if bash/zsh, then fallback paths
|
|
28
|
-
* 4. Fallback: sh
|
|
29
|
-
*/
|
|
30
|
-
export declare function getShellConfig(customShellPath?: string): ShellConfig;
|
|
31
|
-
/**
|
|
32
|
-
* Clear the memoized shell configuration so the next {@link getShellConfig}
|
|
33
|
-
* call re-resolves the shell and re-reads the environment (shell selection and
|
|
34
|
-
* the bash CI/login flags). Primarily for tests that vary those inputs.
|
|
35
|
-
*/
|
|
36
|
-
export declare function resetShellConfigCache(): void;
|
|
2
|
+
export { getShellConfig, resetShellConfigCache, resolveBasicShell, type ShellConfig, scrubProcessEnv, } from "./shell-config";
|
|
37
3
|
/**
|
|
38
4
|
* Check if a process is running.
|
|
39
5
|
*/
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Throw-proof error description.
|
|
3
|
+
*
|
|
4
|
+
* Hostile or exotic thrown values can make `String(value)` throw (a `toString`
|
|
5
|
+
* that throws, a revoked proxy, a null-prototype object) and a `message` getter
|
|
6
|
+
* can throw too. Logging or recording a failure must never become a second
|
|
7
|
+
* failure, so every failure path that stringifies a caught value uses this.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { safeErrorDescription } from "@sayknow-cli/utils";
|
|
12
|
+
*
|
|
13
|
+
* try {
|
|
14
|
+
* await risky();
|
|
15
|
+
* } catch (error) {
|
|
16
|
+
* logger.warn("risky failed", { error: safeErrorDescription(error) });
|
|
17
|
+
* }
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export declare function safeErrorDescription(value: unknown): string;
|
|
@@ -12,3 +12,13 @@
|
|
|
12
12
|
* string after the control probe.
|
|
13
13
|
*/
|
|
14
14
|
export declare function sanitizeText(text: string): string;
|
|
15
|
+
/**
|
|
16
|
+
* Sanitize untrusted text that must occupy exactly one rendered row.
|
|
17
|
+
*
|
|
18
|
+
* {@link sanitizeText} deliberately preserves `\n`, and width-based truncation
|
|
19
|
+
* treats it as zero-width, so a value carrying line breaks can still inject
|
|
20
|
+
* extra rows and evade a single-line width budget. Flatten every CR/LF run to a
|
|
21
|
+
* single space before the usual control/ANSI strip, then remove directional
|
|
22
|
+
* format controls that can visually reorder an otherwise safe row.
|
|
23
|
+
*/
|
|
24
|
+
export declare function sanitizeDisplayLine(text: string): string;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export interface ShellConfig {
|
|
2
|
+
shell: string;
|
|
3
|
+
args: string[];
|
|
4
|
+
env: Record<string, string>;
|
|
5
|
+
prefix: string | undefined;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Strip disabled macOS malloc-stack-logging vars from `process.env` in place.
|
|
9
|
+
*
|
|
10
|
+
* macOS leaves `MallocStackLogging=0` (or similar) inherited by debug-attached
|
|
11
|
+
* shells. Bun's libc init then prints `MallocStackLogging: can't turn off
|
|
12
|
+
* malloc stack logging because it was not enabled.` to stderr for every
|
|
13
|
+
* subprocess. Scrubbing once at startup means every child we spawn — bash,
|
|
14
|
+
* bun subagents, plugin installs, ptree commands — inherits a clean env.
|
|
15
|
+
*/
|
|
16
|
+
export declare function scrubProcessEnv(): void;
|
|
17
|
+
/**
|
|
18
|
+
* Names of hook-capable variables whose live value came from the project `.env`.
|
|
19
|
+
*
|
|
20
|
+
* Provenance comes from `$inheritedEnv`, the same launch snapshot that guards
|
|
21
|
+
* credentials: a value the launching shell exported with a different value than
|
|
22
|
+
* the project declaration is kept. Bun does not reveal which source supplied a
|
|
23
|
+
* value, so an inherited value the project repeats verbatim, or a key the project
|
|
24
|
+
* declares dynamically, is ambiguous and fails closed (dropped).
|
|
25
|
+
*/
|
|
26
|
+
export declare function projectDeclaredSpawnHookNames(env?: Record<string, string | undefined>): string[];
|
|
27
|
+
/**
|
|
28
|
+
* Remove project-declared hook variables from a spawn environment in place.
|
|
29
|
+
* If removing a home selector leaves `HOME` unset, provide the trusted account
|
|
30
|
+
* home when one exists rather than allowing a Windows profile fallback.
|
|
31
|
+
*/
|
|
32
|
+
export declare function stripProjectSpawnHooks(env: Record<string, string>): Record<string, string>;
|
|
33
|
+
/**
|
|
34
|
+
* The process environment without project-declared hook variables, for spawning
|
|
35
|
+
* the bash tool's shell processes (isolated worker chain and PTY sessions).
|
|
36
|
+
*/
|
|
37
|
+
export declare function projectSafeSpawnEnv(): Record<string, string>;
|
|
38
|
+
/** Provide the trusted HOME override needed when PTY hook filtering removes a home selector. */
|
|
39
|
+
export declare function projectSafeHomeOverride(explicit?: Record<string, string>): Record<string, string>;
|
|
40
|
+
/**
|
|
41
|
+
* Resolve a basic shell (bash or sh) as fallback.
|
|
42
|
+
*/
|
|
43
|
+
export declare function resolveBasicShell(): string | undefined;
|
|
44
|
+
/**
|
|
45
|
+
* Get shell configuration based on platform.
|
|
46
|
+
* Resolution order:
|
|
47
|
+
* 1. User-specified shellPath in settings.json
|
|
48
|
+
* 2. On Windows: Git Bash in known locations, then bash on PATH
|
|
49
|
+
* 3. On Unix: $SHELL if bash/zsh, then fallback paths
|
|
50
|
+
* 4. Fallback: sh
|
|
51
|
+
*/
|
|
52
|
+
export declare function getShellConfig(customShellPath?: string): ShellConfig;
|
|
53
|
+
/**
|
|
54
|
+
* Clear the memoized shell configuration so the next {@link getShellConfig}
|
|
55
|
+
* call re-resolves the shell and re-reads the environment (shell selection and
|
|
56
|
+
* the bash CI/login flags). Primarily for tests that vary those inputs.
|
|
57
|
+
*/
|
|
58
|
+
export declare function resetShellConfigCache(): void;
|
package/dist/types/stream.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export declare function readLines(stream: ReadableStream<Uint8Array>, signal?: AbortSignal): AsyncGenerator<Uint8Array>;
|
|
2
|
-
export
|
|
2
|
+
export type JsonlLineObserver = (raw: string) => void;
|
|
3
|
+
export declare function readJsonl<T>(stream: ReadableStream<Uint8Array>, signal?: AbortSignal, onLine?: JsonlLineObserver): AsyncGenerator<T>;
|
|
3
4
|
/**
|
|
4
5
|
* Stream parsed JSON objects from SSE `data:` lines.
|
|
5
6
|
*
|
package/dist/types/which.d.ts
CHANGED
|
@@ -26,7 +26,7 @@ export interface WhichOptions extends Bun.WhichOptions {
|
|
|
26
26
|
*/
|
|
27
27
|
cache?: WhichCachePolicy;
|
|
28
28
|
}
|
|
29
|
-
declare function darwinWhich(command: string,
|
|
29
|
+
declare function darwinWhich(command: string, options?: Bun.WhichOptions): string | null;
|
|
30
30
|
export declare const whichFresh: typeof darwinWhich;
|
|
31
31
|
/**
|
|
32
32
|
* Locate binary on PATH (with flexible caching).
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@sayknow-cli/utils",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.12",
|
|
5
5
|
"description": "Shared utilities for pi packages",
|
|
6
6
|
"homepage": "https://sayknow-cli.com",
|
|
7
7
|
"author": "jaybeyond",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"fmt": "biome format --write ."
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@sayknow-cli/natives": "0.7.
|
|
34
|
+
"@sayknow-cli/natives": "0.7.12",
|
|
35
35
|
"beautiful-mermaid": "^1.1.3",
|
|
36
36
|
"handlebars": "^4.7.9",
|
|
37
37
|
"winston": "^3.19.0",
|
package/src/broken-pipe.ts
CHANGED
|
@@ -9,6 +9,16 @@
|
|
|
9
9
|
import * as fs from "node:fs";
|
|
10
10
|
|
|
11
11
|
const KNOWN_SINK_PEER_CLOSED_CODES = new Set(["EPIPE", "ERR_STREAM_DESTROYED"]);
|
|
12
|
+
/**
|
|
13
|
+
* Errno codes that mean the process's own stdout sink has gone away because
|
|
14
|
+
* the peer (terminal emulator, pager, pipe reader, or pty) was torn down. On
|
|
15
|
+
* macOS a torn-down pty slave produces `EIO`; a closed fd produces `EBADF`;
|
|
16
|
+
* a closed pipe produces `EPIPE`. All three are the same benign condition:
|
|
17
|
+
* the terminal disconnected.
|
|
18
|
+
*
|
|
19
|
+
* Mirrors `CLOSED_STDERR_ERROR_CODES` in `safe-stderr.ts`; keep in sync.
|
|
20
|
+
*/
|
|
21
|
+
const PROCESS_STDOUT_PEER_CLOSED_CODES = new Set(["EIO", "EPIPE", "EBADF"]);
|
|
12
22
|
|
|
13
23
|
type ErrorProperty = "code" | "fd" | "syscall";
|
|
14
24
|
|
|
@@ -70,9 +80,10 @@ export function isBrokenPipeError(error: unknown): boolean {
|
|
|
70
80
|
}
|
|
71
81
|
|
|
72
82
|
/**
|
|
73
|
-
* A classifier for process-level stdout
|
|
74
|
-
* evidence is private to each factory instance, so
|
|
75
|
-
* intercepted `process.stdout.write` can mark an error
|
|
83
|
+
* A classifier for process-level stdout peer-closed errors (`EPIPE`, `EIO`,
|
|
84
|
+
* `EBADF`). Its direct-write evidence is private to each factory instance, so
|
|
85
|
+
* only the owner that intercepted `process.stdout.write` can mark an error
|
|
86
|
+
* for this classifier.
|
|
76
87
|
*/
|
|
77
88
|
export interface ProcessStdoutEpipeClassifier {
|
|
78
89
|
markDirectProcessStdoutWriteError(error: unknown): void;
|
|
@@ -109,7 +120,8 @@ export function createProcessStdoutEpipeClassifier(): ProcessStdoutEpipeClassifi
|
|
|
109
120
|
if (!isObjectLike(error)) return false;
|
|
110
121
|
|
|
111
122
|
const code = readErrorProperty(error, "code");
|
|
112
|
-
if (!code.available || code.value !== "
|
|
123
|
+
if (!code.available || typeof code.value !== "string" || !PROCESS_STDOUT_PEER_CLOSED_CODES.has(code.value))
|
|
124
|
+
return false;
|
|
113
125
|
|
|
114
126
|
if (directProcessStdoutWriteErrors.has(error)) return true;
|
|
115
127
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
|
|
3
|
+
interface ProjectEnvSnapshotLike {
|
|
4
|
+
values: Record<string, string>;
|
|
5
|
+
dynamic: Set<string>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface CanonicalLogDirInput {
|
|
9
|
+
home: string;
|
|
10
|
+
env: Record<string, string | undefined>;
|
|
11
|
+
projectEnv: ProjectEnvSnapshotLike;
|
|
12
|
+
xdgEligible: boolean;
|
|
13
|
+
pathExists?: (target: string) => boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const APP_NAME = "skc";
|
|
17
|
+
const DEFAULT_CONFIG_DIR_NAME = ".skc";
|
|
18
|
+
|
|
19
|
+
function canonicalEnvKey(name: string): string {
|
|
20
|
+
return process.platform === "win32" ? name.toUpperCase() : name;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function sanitizeConfigDirName(value: string | undefined): string | undefined {
|
|
24
|
+
const trimmed = value?.trim();
|
|
25
|
+
if (!trimmed || path.normalize(trimmed).split(/[\\/]/).includes("..")) return undefined;
|
|
26
|
+
return trimmed;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Resolve a caller environment value only when its dotenv provenance is trusted. */
|
|
30
|
+
function trustedValue(name: string, input: CanonicalLogDirInput): string | undefined {
|
|
31
|
+
const value = input.env[name];
|
|
32
|
+
if (!value) return undefined;
|
|
33
|
+
const key = canonicalEnvKey(name);
|
|
34
|
+
if (input.projectEnv.dynamic.has(key) || input.projectEnv.values[key] === value) return undefined;
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Resolve the canonical user log directory without mutating the environment.
|
|
40
|
+
*
|
|
41
|
+
* The caller supplies filesystem existence checks so this leaf stays free of
|
|
42
|
+
* filesystem and resolver side effects. Both the production directory resolver
|
|
43
|
+
* and the test preload use this exact path selection logic.
|
|
44
|
+
*/
|
|
45
|
+
export function resolveCanonicalLogsDir(input: CanonicalLogDirInput): string {
|
|
46
|
+
const configDirName =
|
|
47
|
+
sanitizeConfigDirName(trustedValue("SKC_CONFIG_DIR", input)) ??
|
|
48
|
+
sanitizeConfigDirName(trustedValue("PI_CONFIG_DIR", input)) ??
|
|
49
|
+
DEFAULT_CONFIG_DIR_NAME;
|
|
50
|
+
const xdgStateHome =
|
|
51
|
+
input.xdgEligible && (process.platform === "linux" || process.platform === "darwin")
|
|
52
|
+
? trustedValue("XDG_STATE_HOME", input)?.trim()
|
|
53
|
+
: undefined;
|
|
54
|
+
if (xdgStateHome) {
|
|
55
|
+
const xdgRoot = path.join(xdgStateHome, APP_NAME);
|
|
56
|
+
if (input.pathExists?.(xdgRoot) === true) return path.join(xdgRoot, "logs");
|
|
57
|
+
}
|
|
58
|
+
return path.join(input.home, configDirName, "logs");
|
|
59
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure CLI metadata leaf.
|
|
3
|
+
*
|
|
4
|
+
* The actual `skc` entry (`packages/coding-agent/src/cli.ts`) must stay inert on
|
|
5
|
+
* import: no directory resolution, logger construction, NSS lookup, native load
|
|
6
|
+
* or environment probing may run before argv routing. `dirs.ts` performs all of
|
|
7
|
+
* those at module scope, so the handful of constants the entry really needs live
|
|
8
|
+
* here and `dirs.ts` re-exports them for its existing callers.
|
|
9
|
+
*
|
|
10
|
+
* Only build-time package metadata may be imported here.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { engines, version } from "../package.json" with { type: "json" };
|
|
14
|
+
|
|
15
|
+
/** App name (e.g. "skc") */
|
|
16
|
+
export const APP_NAME: string = "skc";
|
|
17
|
+
|
|
18
|
+
/** Version (e.g. "1.0.0") */
|
|
19
|
+
export const VERSION: string = version;
|
|
20
|
+
|
|
21
|
+
/** Minimum Bun version */
|
|
22
|
+
export const MIN_BUN_VERSION: string = engines.bun.replace(/[^0-9.]/g, "");
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Build the diagnostic shown when the Bun runtime executing `skc` is older
|
|
26
|
+
* than {@link MIN_BUN_VERSION}. This is the most common Windows native-install
|
|
27
|
+
* failure (issue #525): `bun install -g sayknow-cli` probes a recent Bun while
|
|
28
|
+
* the `skc` launcher resolves an older Bun still on PATH. The message names the
|
|
29
|
+
* exact detected runtime path and gives a platform-specific upgrade + PATH fix
|
|
30
|
+
* instead of a bare `bun upgrade`.
|
|
31
|
+
*
|
|
32
|
+
* Pure and platform-parameterized so it can be unit-tested cross-platform.
|
|
33
|
+
*/
|
|
34
|
+
export function formatBunRuntimeError(opts: {
|
|
35
|
+
currentVersion: string;
|
|
36
|
+
minVersion: string;
|
|
37
|
+
execPath?: string;
|
|
38
|
+
platform?: NodeJS.Platform;
|
|
39
|
+
}): string {
|
|
40
|
+
const platform = opts.platform ?? process.platform;
|
|
41
|
+
const lines = [
|
|
42
|
+
`error: ${APP_NAME} requires Bun >= ${opts.minVersion}, but the running Bun is v${opts.currentVersion}.`,
|
|
43
|
+
];
|
|
44
|
+
if (opts.execPath) {
|
|
45
|
+
lines.push(` detected Bun runtime: ${opts.execPath}`);
|
|
46
|
+
}
|
|
47
|
+
if (platform === "win32") {
|
|
48
|
+
lines.push(
|
|
49
|
+
"",
|
|
50
|
+
"The 'skc' launcher is using an older Bun than the one used to install it.",
|
|
51
|
+
"Upgrade Bun, then restart your terminal so PATH and the runtime refresh:",
|
|
52
|
+
"",
|
|
53
|
+
' powershell -c "irm bun.sh/install.ps1|iex"',
|
|
54
|
+
"",
|
|
55
|
+
"After restarting the terminal, verify both versions match:",
|
|
56
|
+
" bun --version",
|
|
57
|
+
" skc --version",
|
|
58
|
+
"",
|
|
59
|
+
"If 'skc' still loads the old runtime, make sure %USERPROFILE%\\.bun\\bin is",
|
|
60
|
+
"first on PATH and remove any stale Bun installs shadowing it.",
|
|
61
|
+
);
|
|
62
|
+
} else {
|
|
63
|
+
lines.push(
|
|
64
|
+
"",
|
|
65
|
+
"Upgrade Bun, then restart your terminal:",
|
|
66
|
+
" bun upgrade",
|
|
67
|
+
"",
|
|
68
|
+
"Then verify:",
|
|
69
|
+
" bun --version",
|
|
70
|
+
" skc --version",
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
return `${lines.join("\n")}\n`;
|
|
74
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -23,6 +23,8 @@ export interface FlagDescriptor<K extends "string" | "boolean" | "integer" = "st
|
|
|
23
23
|
multiple?: boolean;
|
|
24
24
|
options?: readonly string[];
|
|
25
25
|
required?: boolean;
|
|
26
|
+
/** The string value may be omitted; parsers decide whether to consume it. */
|
|
27
|
+
optionalValue?: boolean;
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
export interface ArgDescriptor {
|
|
@@ -40,6 +42,8 @@ interface FlagInput {
|
|
|
40
42
|
multiple?: boolean;
|
|
41
43
|
options?: readonly string[];
|
|
42
44
|
required?: boolean;
|
|
45
|
+
/** The string value may be omitted; parsers decide whether to consume it. */
|
|
46
|
+
optionalValue?: boolean;
|
|
43
47
|
}
|
|
44
48
|
|
|
45
49
|
interface ArgInput {
|
|
@@ -352,7 +356,14 @@ function renderCommandBody(lines: string[], Cmd: CommandCtor): void {
|
|
|
352
356
|
for (const [name, desc] of flagEntries) {
|
|
353
357
|
const charPart = desc.char ? `-${desc.char}, ` : " ";
|
|
354
358
|
const namePart = `--${name}`;
|
|
355
|
-
const typePart =
|
|
359
|
+
const typePart =
|
|
360
|
+
desc.kind === "boolean"
|
|
361
|
+
? ""
|
|
362
|
+
: desc.kind === "integer"
|
|
363
|
+
? "=<int>"
|
|
364
|
+
: desc.optionalValue
|
|
365
|
+
? "[=<value>]"
|
|
366
|
+
: "=<value>";
|
|
356
367
|
formatted.push([` ${charPart}${namePart}${typePart}`, desc.description ?? ""]);
|
|
357
368
|
}
|
|
358
369
|
const maxLeft = Math.max(...formatted.map(([l]) => l.length));
|
|
@@ -383,6 +394,15 @@ export interface CommandEntry {
|
|
|
383
394
|
name: string;
|
|
384
395
|
load: () => Promise<CommandCtor>;
|
|
385
396
|
aliases?: string[];
|
|
397
|
+
/** Owns parsing, help, loading and failures for this family when supplied. */
|
|
398
|
+
dispatch?: (argv: string[], context: CommandEntryContext) => Promise<void>;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export interface CommandEntryContext {
|
|
402
|
+
bin: string;
|
|
403
|
+
version: string;
|
|
404
|
+
/** Canonical registered entry name, including when invoked through an alias. */
|
|
405
|
+
command: string;
|
|
386
406
|
}
|
|
387
407
|
|
|
388
408
|
export interface RunOptions {
|
|
@@ -428,9 +448,17 @@ export async function run(opts: RunOptions): Promise<void> {
|
|
|
428
448
|
return;
|
|
429
449
|
}
|
|
430
450
|
|
|
451
|
+
const dispatchedEntry = findEntry(opts.commands, commandId);
|
|
452
|
+
if (dispatchedEntry?.dispatch) {
|
|
453
|
+
await dispatchedEntry.dispatch(commandArgv, { bin, version, command: dispatchedEntry.name });
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
|
|
431
457
|
// Per-command help. Commands with nested subcommands can opt into receiving
|
|
432
458
|
// help flags themselves so `cmd subcommand --help` can render subcommand help.
|
|
433
|
-
|
|
459
|
+
const delimiterIndex = commandArgv.indexOf("--");
|
|
460
|
+
const helpArgs = delimiterIndex === -1 ? commandArgv : commandArgv.slice(0, delimiterIndex);
|
|
461
|
+
if (helpArgs.includes("--help") || helpArgs.includes("-h")) {
|
|
434
462
|
const entry = findEntry(opts.commands, commandId);
|
|
435
463
|
if (!entry) {
|
|
436
464
|
process.stderr.write(`Unknown command: ${commandId}\n`);
|