@indigoai-us/hq-cli 5.345.53 → 5.345.55
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/CHANGELOG.md +24 -0
- package/assets/bot-workers/setup/context/USER-GUIDE.md +1 -1
- package/assets/bot-workers/setup/skills/agents-and-team.md +1 -1
- package/assets/bot-workers/setup/skills/pricing.md +1 -1
- package/dist/command-catalog.generated.d.ts +103 -2
- package/dist/command-catalog.generated.js +124 -2
- package/dist/command-registration-plan.d.ts +6 -1
- package/dist/command-registration-plan.js +2 -1
- package/dist/commands/cloud.d.ts +7 -0
- package/dist/commands/cloud.js +92 -16
- package/dist/commands/daemon.js +16 -0
- package/dist/commands/import-scan.d.ts +24 -0
- package/dist/commands/import-scan.js +75 -0
- package/dist/commands/lanes-session-start.d.ts +22 -0
- package/dist/commands/lanes-session-start.js +215 -0
- package/dist/commands/lanes-watch.d.ts +2 -0
- package/dist/commands/lanes-watch.js +2 -1
- package/dist/commands/lanes.js +4 -2
- package/dist/lib/billing/plan-lock.d.ts +2 -2
- package/dist/lib/billing/plan-lock.js +2 -2
- package/dist/lib/daemon/conflict-notices.d.ts +4 -0
- package/dist/lib/daemon/conflict-notices.js +80 -0
- package/dist/lib/import-scan/events.d.ts +135 -0
- package/dist/lib/import-scan/events.js +71 -0
- package/dist/lib/import-scan/run.d.ts +84 -0
- package/dist/lib/import-scan/run.js +308 -0
- package/dist/lib/import-scan/scan-event.schema.json +127 -0
- package/dist/lib/import-scan/validate.d.ts +31 -0
- package/dist/lib/import-scan/validate.js +111 -0
- package/dist/lib/lanes/flag-keys.d.ts +2 -0
- package/dist/lib/lanes/flag-keys.js +2 -0
- package/dist/lib/lanes/monitor.js +1 -1
- package/dist/lib/lanes/registry-flag.d.ts +2 -0
- package/dist/lib/lanes/registry-flag.js +1 -1
- package/dist/utils/manifest.d.ts +7 -1
- package/dist/utils/manifest.js +14 -6
- package/package.json +1 -1
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streaming context-scan events (contract v1).
|
|
3
|
+
*
|
|
4
|
+
* `hq import scan --json --stream` relays these as JSON Lines, one object per
|
|
5
|
+
* line. They are produced by the HQ import-context scanner
|
|
6
|
+
* (`.claude/skills/import-context/scan.sh --progress-json` in an HQ tree) and
|
|
7
|
+
* validated here before they reach stdout. The HQ desktop first-run setup
|
|
8
|
+
* renders them as a knowledge tree that grows while the scan runs.
|
|
9
|
+
*
|
|
10
|
+
* The canonical contract lives next to the scanner
|
|
11
|
+
* (`.claude/skills/import-context/progress-json.md` in hq-core) and in
|
|
12
|
+
* `docs/import-scan-stream.md` in this repo. The JSON Schema is
|
|
13
|
+
* `scan-event.schema.json` in this directory (shipped next to the compiled
|
|
14
|
+
* module as `dist/lib/import-scan/scan-event.schema.json`).
|
|
15
|
+
*
|
|
16
|
+
* Desktop consumers can import these types from
|
|
17
|
+
* `@indigoai-us/hq-cli/dist/lib/import-scan/events.js` or mirror them.
|
|
18
|
+
*
|
|
19
|
+
* Versioning: within v1, fields, source ids, count keys and messages may be
|
|
20
|
+
* added. Consumers ignore unknown fields and must not parse message text.
|
|
21
|
+
* Removing or retyping a field, or adding a new `type`, `status` or `basis`
|
|
22
|
+
* value, bumps `v`.
|
|
23
|
+
*/
|
|
24
|
+
export declare const SCAN_EVENT_VERSION: 1;
|
|
25
|
+
/** Event types, in the order a stream introduces them. */
|
|
26
|
+
export declare const SCAN_EVENT_TYPES: readonly ["start", "source", "count", "company", "project", "error", "done"];
|
|
27
|
+
export type ScanEventType = (typeof SCAN_EVENT_TYPES)[number];
|
|
28
|
+
export declare const SCAN_SOURCE_STATUSES: readonly ["scanning", "done", "skipped", "error"];
|
|
29
|
+
export type ScanSourceStatus = (typeof SCAN_SOURCE_STATUSES)[number];
|
|
30
|
+
export declare const SCAN_COMPANY_BASES: readonly ["hq-company", "repo-org", "folder"];
|
|
31
|
+
export type ScanCompanyBasis = (typeof SCAN_COMPANY_BASES)[number];
|
|
32
|
+
export declare const SCAN_PROJECT_BASES: readonly ["repo", "claude-code-cwd", "codex-cwd"];
|
|
33
|
+
export type ScanProjectBasis = (typeof SCAN_PROJECT_BASES)[number];
|
|
34
|
+
/**
|
|
35
|
+
* Source ids the v1 scanner emits, in scan order. Other ids may appear in
|
|
36
|
+
* later v1 scanners; show them by their `label`.
|
|
37
|
+
*/
|
|
38
|
+
export declare const KNOWN_SCAN_SOURCE_IDS: readonly ["hq", "repos", "claude-code", "codex", "grok", "claude-ai", "artifacts"];
|
|
39
|
+
export type KnownScanSourceId = (typeof KNOWN_SCAN_SOURCE_IDS)[number];
|
|
40
|
+
/**
|
|
41
|
+
* Source id used on `error` events that hq-cli itself emits (HQ not found,
|
|
42
|
+
* scanner missing or too old, scanner crashed). Never listed in `start`.
|
|
43
|
+
*/
|
|
44
|
+
export declare const SCAN_CLI_ERROR_SOURCE: "scanner";
|
|
45
|
+
/**
|
|
46
|
+
* Stable machine-readable codes on `error` events. hq-cli sets one on every
|
|
47
|
+
* error it emits itself (source "scanner"); scanner-emitted errors may omit it.
|
|
48
|
+
*
|
|
49
|
+
* scanner_outdated the HQ tree's scanner is missing or predates
|
|
50
|
+
* --progress-json; updating HQ fixes it
|
|
51
|
+
* scan_failed any other reason the scan could not run or finish
|
|
52
|
+
* (HQ not found, scanner crashed, cancelled, non-zero exit)
|
|
53
|
+
*
|
|
54
|
+
* Consumers branch on `code`, never on `message`. Treat an error without a
|
|
55
|
+
* code as non-fatal unless the stream ends with `report: null`.
|
|
56
|
+
*/
|
|
57
|
+
export declare const SCAN_ERROR_CODES: readonly ["scanner_outdated", "scan_failed"];
|
|
58
|
+
export type ScanErrorCode = (typeof SCAN_ERROR_CODES)[number];
|
|
59
|
+
/** Project ids: "p_" + 12 lowercase hex (pid() in scan-progress.sh). */
|
|
60
|
+
export declare const SCAN_PROJECT_ID_PATTERN: RegExp;
|
|
61
|
+
export interface ScanSourceInfo {
|
|
62
|
+
id: string;
|
|
63
|
+
label: string;
|
|
64
|
+
}
|
|
65
|
+
interface ScanEventBase {
|
|
66
|
+
v: typeof SCAN_EVENT_VERSION;
|
|
67
|
+
}
|
|
68
|
+
/** First line: the sources present on this machine, in scan order. */
|
|
69
|
+
export interface ScanStartEvent extends ScanEventBase {
|
|
70
|
+
type: "start";
|
|
71
|
+
sources: ScanSourceInfo[];
|
|
72
|
+
}
|
|
73
|
+
/** Source lifecycle: `scanning` once, then one of done/skipped/error. */
|
|
74
|
+
export interface ScanSourceEvent extends ScanEventBase {
|
|
75
|
+
type: "source";
|
|
76
|
+
id: string;
|
|
77
|
+
status: ScanSourceStatus;
|
|
78
|
+
/** Final counts, on `done`. Each value repeats the last `count` event for that key. */
|
|
79
|
+
counts?: Record<string, number>;
|
|
80
|
+
/** Plain-words reason, on `skipped` and `error`. */
|
|
81
|
+
message?: string;
|
|
82
|
+
}
|
|
83
|
+
/** Running count; fires at 1-2-5 milestones and once with the final value. */
|
|
84
|
+
export interface ScanCountEvent extends ScanEventBase {
|
|
85
|
+
type: "count";
|
|
86
|
+
source: string;
|
|
87
|
+
key: string;
|
|
88
|
+
value: number;
|
|
89
|
+
}
|
|
90
|
+
/** A company from the deterministic rule pass. Emitted once per id. */
|
|
91
|
+
export interface ScanCompanyEvent extends ScanEventBase {
|
|
92
|
+
type: "company";
|
|
93
|
+
id: string;
|
|
94
|
+
name: string;
|
|
95
|
+
basis: ScanCompanyBasis;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* A project. Upsert by `id`: an id repeats only when a later rule pass
|
|
99
|
+
* attaches a company to a project that had `company: null`.
|
|
100
|
+
*/
|
|
101
|
+
export interface ScanProjectEvent extends ScanEventBase {
|
|
102
|
+
type: "project";
|
|
103
|
+
/** "p_" + 12 lowercase hex, the same for the same project on any machine. */
|
|
104
|
+
id: string;
|
|
105
|
+
/** Folder or repository basename; never contains "/". */
|
|
106
|
+
name: string;
|
|
107
|
+
company: string | null;
|
|
108
|
+
basis: ScanProjectBasis;
|
|
109
|
+
}
|
|
110
|
+
/** Non-fatal problem, in plain words. */
|
|
111
|
+
export interface ScanErrorEvent extends ScanEventBase {
|
|
112
|
+
type: "error";
|
|
113
|
+
source: string;
|
|
114
|
+
message: string;
|
|
115
|
+
/** Stable code (SCAN_ERROR_CODES). Always set on hq-cli's own errors. */
|
|
116
|
+
code?: ScanErrorCode;
|
|
117
|
+
}
|
|
118
|
+
export interface ScanSummary {
|
|
119
|
+
companies: number;
|
|
120
|
+
projects: number;
|
|
121
|
+
sessions: number;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Last line. `report` is the absolute path of report.json, or `null` when
|
|
125
|
+
* hq-cli could not run the scan (then `summary` is all zeros and the command
|
|
126
|
+
* exits non-zero).
|
|
127
|
+
*/
|
|
128
|
+
export interface ScanDoneEvent extends ScanEventBase {
|
|
129
|
+
type: "done";
|
|
130
|
+
report: string | null;
|
|
131
|
+
summary: ScanSummary;
|
|
132
|
+
}
|
|
133
|
+
export type ScanEvent = ScanStartEvent | ScanSourceEvent | ScanCountEvent | ScanCompanyEvent | ScanProjectEvent | ScanErrorEvent | ScanDoneEvent;
|
|
134
|
+
export {};
|
|
135
|
+
//# sourceMappingURL=events.d.ts.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streaming context-scan events (contract v1).
|
|
3
|
+
*
|
|
4
|
+
* `hq import scan --json --stream` relays these as JSON Lines, one object per
|
|
5
|
+
* line. They are produced by the HQ import-context scanner
|
|
6
|
+
* (`.claude/skills/import-context/scan.sh --progress-json` in an HQ tree) and
|
|
7
|
+
* validated here before they reach stdout. The HQ desktop first-run setup
|
|
8
|
+
* renders them as a knowledge tree that grows while the scan runs.
|
|
9
|
+
*
|
|
10
|
+
* The canonical contract lives next to the scanner
|
|
11
|
+
* (`.claude/skills/import-context/progress-json.md` in hq-core) and in
|
|
12
|
+
* `docs/import-scan-stream.md` in this repo. The JSON Schema is
|
|
13
|
+
* `scan-event.schema.json` in this directory (shipped next to the compiled
|
|
14
|
+
* module as `dist/lib/import-scan/scan-event.schema.json`).
|
|
15
|
+
*
|
|
16
|
+
* Desktop consumers can import these types from
|
|
17
|
+
* `@indigoai-us/hq-cli/dist/lib/import-scan/events.js` or mirror them.
|
|
18
|
+
*
|
|
19
|
+
* Versioning: within v1, fields, source ids, count keys and messages may be
|
|
20
|
+
* added. Consumers ignore unknown fields and must not parse message text.
|
|
21
|
+
* Removing or retyping a field, or adding a new `type`, `status` or `basis`
|
|
22
|
+
* value, bumps `v`.
|
|
23
|
+
*/
|
|
24
|
+
export const SCAN_EVENT_VERSION = 1;
|
|
25
|
+
/** Event types, in the order a stream introduces them. */
|
|
26
|
+
export const SCAN_EVENT_TYPES = [
|
|
27
|
+
"start",
|
|
28
|
+
"source",
|
|
29
|
+
"count",
|
|
30
|
+
"company",
|
|
31
|
+
"project",
|
|
32
|
+
"error",
|
|
33
|
+
"done",
|
|
34
|
+
];
|
|
35
|
+
export const SCAN_SOURCE_STATUSES = ["scanning", "done", "skipped", "error"];
|
|
36
|
+
export const SCAN_COMPANY_BASES = ["hq-company", "repo-org", "folder"];
|
|
37
|
+
export const SCAN_PROJECT_BASES = ["repo", "claude-code-cwd", "codex-cwd"];
|
|
38
|
+
/**
|
|
39
|
+
* Source ids the v1 scanner emits, in scan order. Other ids may appear in
|
|
40
|
+
* later v1 scanners; show them by their `label`.
|
|
41
|
+
*/
|
|
42
|
+
export const KNOWN_SCAN_SOURCE_IDS = [
|
|
43
|
+
"hq",
|
|
44
|
+
"repos",
|
|
45
|
+
"claude-code",
|
|
46
|
+
"codex",
|
|
47
|
+
"grok",
|
|
48
|
+
"claude-ai",
|
|
49
|
+
"artifacts",
|
|
50
|
+
];
|
|
51
|
+
/**
|
|
52
|
+
* Source id used on `error` events that hq-cli itself emits (HQ not found,
|
|
53
|
+
* scanner missing or too old, scanner crashed). Never listed in `start`.
|
|
54
|
+
*/
|
|
55
|
+
export const SCAN_CLI_ERROR_SOURCE = "scanner";
|
|
56
|
+
/**
|
|
57
|
+
* Stable machine-readable codes on `error` events. hq-cli sets one on every
|
|
58
|
+
* error it emits itself (source "scanner"); scanner-emitted errors may omit it.
|
|
59
|
+
*
|
|
60
|
+
* scanner_outdated the HQ tree's scanner is missing or predates
|
|
61
|
+
* --progress-json; updating HQ fixes it
|
|
62
|
+
* scan_failed any other reason the scan could not run or finish
|
|
63
|
+
* (HQ not found, scanner crashed, cancelled, non-zero exit)
|
|
64
|
+
*
|
|
65
|
+
* Consumers branch on `code`, never on `message`. Treat an error without a
|
|
66
|
+
* code as non-fatal unless the stream ends with `report: null`.
|
|
67
|
+
*/
|
|
68
|
+
export const SCAN_ERROR_CODES = ["scanner_outdated", "scan_failed"];
|
|
69
|
+
/** Project ids: "p_" + 12 lowercase hex (pid() in scan-progress.sh). */
|
|
70
|
+
export const SCAN_PROJECT_ID_PATTERN = /^p_[0-9a-f]{12}$/;
|
|
71
|
+
//# sourceMappingURL=events.js.map
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runs the HQ import-context scanner with `--progress-json` and relays its
|
|
3
|
+
* JSON Lines event stream (contract: ./events.ts).
|
|
4
|
+
*
|
|
5
|
+
* The scanner is the HQ tree's own copy at
|
|
6
|
+
* `<hq root>/.claude/skills/import-context/scan.sh`, the same place the
|
|
7
|
+
* /import-context skill runs it from. The scan is local only: the scanner
|
|
8
|
+
* reads the filesystem and writes one report.json; nothing here touches the
|
|
9
|
+
* network.
|
|
10
|
+
*
|
|
11
|
+
* HQ root: `--hq-root` when given, else the root the HQ installer recorded in
|
|
12
|
+
* `~/.hq/menubar.json` (`hqPath`). Nothing else is trusted: no walk up from
|
|
13
|
+
* the current folder and no `$HQ_ROOT`, because this command runs a script
|
|
14
|
+
* from that tree.
|
|
15
|
+
*
|
|
16
|
+
* Output modes:
|
|
17
|
+
* stream every valid event, rebuilt from its v1 fields (invalid lines are
|
|
18
|
+
* dropped with a warning on stderr that never echoes the line)
|
|
19
|
+
* final only the final `done` object
|
|
20
|
+
* human short progress lines and a summary
|
|
21
|
+
*
|
|
22
|
+
* The `done` event is held until the scanner exits. When the scan cannot run
|
|
23
|
+
* or does not finish cleanly (HQ not found, scanner missing or too old,
|
|
24
|
+
* scanner exited non-zero or without `done`, cancelled), an `error` event
|
|
25
|
+
* (source "scanner") comes first, then `done` (zeroed with `report: null`
|
|
26
|
+
* unless the scanner produced one), and the command exits non-zero.
|
|
27
|
+
*
|
|
28
|
+
* Process handling: the scanner runs in its own process group. SIGINT/SIGTERM
|
|
29
|
+
* to hq-cli are forwarded to the whole group and escalated to SIGKILL after
|
|
30
|
+
* `killTimeoutMs`. When the scanner exits, anything still left in its group
|
|
31
|
+
* is terminated so an orphan holding stdout open cannot stall the command.
|
|
32
|
+
*
|
|
33
|
+
* Scanner stderr is not forwarded verbatim: each line is passed through with
|
|
34
|
+
* paths and token-like strings replaced, at most MAX_STDERR_LINES lines, then
|
|
35
|
+
* a count of the rest.
|
|
36
|
+
*/
|
|
37
|
+
export declare const SCANNER_RELATIVE_PATH: string;
|
|
38
|
+
export type ImportScanMode = "stream" | "final" | "human";
|
|
39
|
+
export interface ImportScanOptions {
|
|
40
|
+
mode: ImportScanMode;
|
|
41
|
+
/** Explicit HQ root (--hq-root). */
|
|
42
|
+
hqRoot?: string;
|
|
43
|
+
/** Home directory used to find ~/.hq/menubar.json (tests). */
|
|
44
|
+
home?: string;
|
|
45
|
+
/** Report path. Defaults to <hq>/workspace/imports/<scan id>/report.json. */
|
|
46
|
+
output?: string;
|
|
47
|
+
scopes?: string[];
|
|
48
|
+
claudeExport?: string;
|
|
49
|
+
noDefaultScopes?: boolean;
|
|
50
|
+
/** Shell used to run scan.sh (default "bash"). */
|
|
51
|
+
bash?: string;
|
|
52
|
+
env?: NodeJS.ProcessEnv;
|
|
53
|
+
now?: () => Date;
|
|
54
|
+
/** Grace period between forwarding a signal and SIGKILL (default 5000). */
|
|
55
|
+
killTimeoutMs?: number;
|
|
56
|
+
/** Cancels the scan like SIGTERM (for embedding callers and tests). */
|
|
57
|
+
abortSignal?: AbortSignal;
|
|
58
|
+
}
|
|
59
|
+
export interface ImportScanIo {
|
|
60
|
+
stdout: NodeJS.WritableStream;
|
|
61
|
+
stderr: NodeJS.WritableStream;
|
|
62
|
+
}
|
|
63
|
+
/** UTC scan id in the /import-context skill's format: YYYYMMDDTHHMMSSZ. */
|
|
64
|
+
export declare function scanId(date: Date): string;
|
|
65
|
+
export declare class HqRootError extends Error {
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* --hq-root, else the installer-recorded root in ~/.hq/menubar.json. Throws
|
|
69
|
+
* HqRootError with a plain-words message otherwise.
|
|
70
|
+
*/
|
|
71
|
+
export declare function resolveScanHqRoot(opts: {
|
|
72
|
+
hqRoot?: string;
|
|
73
|
+
home?: string;
|
|
74
|
+
}): string;
|
|
75
|
+
/**
|
|
76
|
+
* Replace paths and token-like strings in one scanner stderr line. A path
|
|
77
|
+
* runs from its first "/" (or a leading ~ / $HOME) to the next quote or the
|
|
78
|
+
* end of the line, because folder names may contain spaces: stopping at
|
|
79
|
+
* whitespace would leak the rest of "/Users/x/My Project/secret".
|
|
80
|
+
*/
|
|
81
|
+
export declare function redactScannerLine(line: string): string;
|
|
82
|
+
/** Run the scan. Resolves to the process exit code. */
|
|
83
|
+
export declare function runImportScan(opts: ImportScanOptions, io: ImportScanIo): Promise<number>;
|
|
84
|
+
//# sourceMappingURL=run.d.ts.map
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runs the HQ import-context scanner with `--progress-json` and relays its
|
|
3
|
+
* JSON Lines event stream (contract: ./events.ts).
|
|
4
|
+
*
|
|
5
|
+
* The scanner is the HQ tree's own copy at
|
|
6
|
+
* `<hq root>/.claude/skills/import-context/scan.sh`, the same place the
|
|
7
|
+
* /import-context skill runs it from. The scan is local only: the scanner
|
|
8
|
+
* reads the filesystem and writes one report.json; nothing here touches the
|
|
9
|
+
* network.
|
|
10
|
+
*
|
|
11
|
+
* HQ root: `--hq-root` when given, else the root the HQ installer recorded in
|
|
12
|
+
* `~/.hq/menubar.json` (`hqPath`). Nothing else is trusted: no walk up from
|
|
13
|
+
* the current folder and no `$HQ_ROOT`, because this command runs a script
|
|
14
|
+
* from that tree.
|
|
15
|
+
*
|
|
16
|
+
* Output modes:
|
|
17
|
+
* stream every valid event, rebuilt from its v1 fields (invalid lines are
|
|
18
|
+
* dropped with a warning on stderr that never echoes the line)
|
|
19
|
+
* final only the final `done` object
|
|
20
|
+
* human short progress lines and a summary
|
|
21
|
+
*
|
|
22
|
+
* The `done` event is held until the scanner exits. When the scan cannot run
|
|
23
|
+
* or does not finish cleanly (HQ not found, scanner missing or too old,
|
|
24
|
+
* scanner exited non-zero or without `done`, cancelled), an `error` event
|
|
25
|
+
* (source "scanner") comes first, then `done` (zeroed with `report: null`
|
|
26
|
+
* unless the scanner produced one), and the command exits non-zero.
|
|
27
|
+
*
|
|
28
|
+
* Process handling: the scanner runs in its own process group. SIGINT/SIGTERM
|
|
29
|
+
* to hq-cli are forwarded to the whole group and escalated to SIGKILL after
|
|
30
|
+
* `killTimeoutMs`. When the scanner exits, anything still left in its group
|
|
31
|
+
* is terminated so an orphan holding stdout open cannot stall the command.
|
|
32
|
+
*
|
|
33
|
+
* Scanner stderr is not forwarded verbatim: each line is passed through with
|
|
34
|
+
* paths and token-like strings replaced, at most MAX_STDERR_LINES lines, then
|
|
35
|
+
* a count of the rest.
|
|
36
|
+
*/
|
|
37
|
+
import { spawn } from "node:child_process";
|
|
38
|
+
import * as fs from "node:fs";
|
|
39
|
+
import * as os from "node:os";
|
|
40
|
+
import * as path from "node:path";
|
|
41
|
+
import * as readline from "node:readline";
|
|
42
|
+
import { SCAN_CLI_ERROR_SOURCE, SCAN_EVENT_VERSION, } from "./events.js";
|
|
43
|
+
import { parseScanEventLine } from "./validate.js";
|
|
44
|
+
export const SCANNER_RELATIVE_PATH = path.join(".claude", "skills", "import-context", "scan.sh");
|
|
45
|
+
const MAX_STDERR_LINES = 20;
|
|
46
|
+
const DEFAULT_KILL_TIMEOUT_MS = 5000;
|
|
47
|
+
const DRAIN_TIMEOUT_MS = 2000;
|
|
48
|
+
/** UTC scan id in the /import-context skill's format: YYYYMMDDTHHMMSSZ. */
|
|
49
|
+
export function scanId(date) {
|
|
50
|
+
return date.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
|
|
51
|
+
}
|
|
52
|
+
export class HqRootError extends Error {
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* --hq-root, else the installer-recorded root in ~/.hq/menubar.json. Throws
|
|
56
|
+
* HqRootError with a plain-words message otherwise.
|
|
57
|
+
*/
|
|
58
|
+
export function resolveScanHqRoot(opts) {
|
|
59
|
+
if (opts.hqRoot) {
|
|
60
|
+
const root = path.resolve(opts.hqRoot);
|
|
61
|
+
if (!isDirectory(root))
|
|
62
|
+
throw new HqRootError("The folder given with --hq-root does not exist.");
|
|
63
|
+
return root;
|
|
64
|
+
}
|
|
65
|
+
const menubar = path.join(opts.home ?? os.homedir(), ".hq", "menubar.json");
|
|
66
|
+
let recorded;
|
|
67
|
+
try {
|
|
68
|
+
recorded = JSON.parse(fs.readFileSync(menubar, "utf8")).hqPath;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
recorded = undefined;
|
|
72
|
+
}
|
|
73
|
+
if (typeof recorded !== "string" || !path.isAbsolute(recorded) || !isDirectory(recorded)) {
|
|
74
|
+
throw new HqRootError("Could not find your HQ folder. Pass --hq-root <path>, or open the HQ app once so it records where HQ is installed.");
|
|
75
|
+
}
|
|
76
|
+
return path.resolve(recorded);
|
|
77
|
+
}
|
|
78
|
+
function isDirectory(p) {
|
|
79
|
+
try {
|
|
80
|
+
return fs.statSync(p).isDirectory();
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Replace paths and token-like strings in one scanner stderr line. A path
|
|
88
|
+
* runs from its first "/" (or a leading ~ / $HOME) to the next quote or the
|
|
89
|
+
* end of the line, because folder names may contain spaces: stopping at
|
|
90
|
+
* whitespace would leak the rest of "/Users/x/My Project/secret".
|
|
91
|
+
*/
|
|
92
|
+
export function redactScannerLine(line) {
|
|
93
|
+
return line
|
|
94
|
+
.replace(/(?<![^\s'"`=:([])(?:\$HOME|~)?\/[^'"`]*/g, "<path>")
|
|
95
|
+
.replace(/\b[A-Za-z0-9_+=-]{24,}\b/g, "<redacted>")
|
|
96
|
+
.replace(/\S+@\S+/g, "<redacted>")
|
|
97
|
+
.slice(0, 300);
|
|
98
|
+
}
|
|
99
|
+
function zeroDone() {
|
|
100
|
+
return {
|
|
101
|
+
v: SCAN_EVENT_VERSION,
|
|
102
|
+
type: "done",
|
|
103
|
+
report: null,
|
|
104
|
+
summary: { companies: 0, projects: 0, sessions: 0 },
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function cliError(message, code) {
|
|
108
|
+
return { v: SCAN_EVENT_VERSION, type: "error", source: SCAN_CLI_ERROR_SOURCE, message, code };
|
|
109
|
+
}
|
|
110
|
+
const failed = (message) => ({ message, code: "scan_failed" });
|
|
111
|
+
const outdated = (message) => ({ message, code: "scanner_outdated" });
|
|
112
|
+
function humanLine(event, labels) {
|
|
113
|
+
switch (event.type) {
|
|
114
|
+
case "source": {
|
|
115
|
+
const label = labels.get(event.id) ?? event.id;
|
|
116
|
+
if (event.status === "scanning")
|
|
117
|
+
return `Scanning ${label}...`;
|
|
118
|
+
if (event.status === "done") {
|
|
119
|
+
const parts = Object.entries(event.counts ?? {})
|
|
120
|
+
.filter(([, n]) => n > 0)
|
|
121
|
+
.map(([k, n]) => `${n} ${k.replace(/_/g, " ")}`);
|
|
122
|
+
return ` ${label}: ${parts.length ? parts.join(", ") : "nothing found"}`;
|
|
123
|
+
}
|
|
124
|
+
return ` ${label}: ${event.status}${event.message ? ` - ${event.message}` : ""}`;
|
|
125
|
+
}
|
|
126
|
+
case "company":
|
|
127
|
+
return ` company: ${event.name}`;
|
|
128
|
+
case "error":
|
|
129
|
+
return ` warning: ${event.message}`;
|
|
130
|
+
case "done":
|
|
131
|
+
return (`Found ${event.summary.companies} companies, ${event.summary.projects} projects, ` +
|
|
132
|
+
`${event.summary.sessions} sessions.` + (event.report ? `\nReport: ${event.report}` : ""));
|
|
133
|
+
default:
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function signalGroup(child, signal) {
|
|
138
|
+
if (child.pid === undefined)
|
|
139
|
+
return;
|
|
140
|
+
try {
|
|
141
|
+
process.kill(-child.pid, signal);
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
try {
|
|
145
|
+
child.kill(signal);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
// already gone
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/** Run the scan. Resolves to the process exit code. */
|
|
153
|
+
export async function runImportScan(opts, io) {
|
|
154
|
+
const labels = new Map();
|
|
155
|
+
const write = (event) => {
|
|
156
|
+
if (event.type === "start")
|
|
157
|
+
for (const s of event.sources)
|
|
158
|
+
labels.set(s.id, s.label);
|
|
159
|
+
if (opts.mode === "stream") {
|
|
160
|
+
io.stdout.write(`${JSON.stringify(event)}\n`);
|
|
161
|
+
}
|
|
162
|
+
else if (opts.mode === "human") {
|
|
163
|
+
const line = humanLine(event, labels);
|
|
164
|
+
if (line)
|
|
165
|
+
io.stdout.write(`${line}\n`);
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
const finish = (done, error, code = 1) => {
|
|
169
|
+
if (error)
|
|
170
|
+
write(cliError(error.message, error.code));
|
|
171
|
+
write(done);
|
|
172
|
+
if (opts.mode === "final")
|
|
173
|
+
io.stdout.write(`${JSON.stringify(done)}\n`);
|
|
174
|
+
return error ? code : 0;
|
|
175
|
+
};
|
|
176
|
+
let hqRoot;
|
|
177
|
+
try {
|
|
178
|
+
hqRoot = resolveScanHqRoot({ hqRoot: opts.hqRoot, home: opts.home });
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
return finish(zeroDone(), failed(err instanceof HqRootError ? err.message : "Could not find your HQ folder."));
|
|
182
|
+
}
|
|
183
|
+
const scanner = path.join(hqRoot, SCANNER_RELATIVE_PATH);
|
|
184
|
+
if (!fs.existsSync(scanner)) {
|
|
185
|
+
return finish(zeroDone(), outdated("The import-context scanner is not installed in this HQ. Update HQ and try again."));
|
|
186
|
+
}
|
|
187
|
+
const output = path.resolve(opts.output ??
|
|
188
|
+
path.join(hqRoot, "workspace", "imports", scanId((opts.now ?? (() => new Date()))()), "report.json"));
|
|
189
|
+
const args = [scanner, `--hq-root=${hqRoot}`, `--output=${output}`, "--progress-json"];
|
|
190
|
+
for (const scope of opts.scopes ?? [])
|
|
191
|
+
args.push(`--scope=${scope}`);
|
|
192
|
+
if (opts.claudeExport)
|
|
193
|
+
args.push(`--claude-export=${opts.claudeExport}`);
|
|
194
|
+
if (opts.noDefaultScopes)
|
|
195
|
+
args.push("--no-default-scopes");
|
|
196
|
+
const child = spawn(opts.bash ?? "bash", args, {
|
|
197
|
+
cwd: hqRoot,
|
|
198
|
+
env: opts.env ?? process.env,
|
|
199
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
200
|
+
detached: true,
|
|
201
|
+
});
|
|
202
|
+
// Signal forwarding to the whole scanner process group, with escalation.
|
|
203
|
+
let cancelledBy;
|
|
204
|
+
let killTimer;
|
|
205
|
+
const onSignal = (signal) => {
|
|
206
|
+
if (!cancelledBy)
|
|
207
|
+
cancelledBy = signal;
|
|
208
|
+
signalGroup(child, signal);
|
|
209
|
+
if (!killTimer) {
|
|
210
|
+
killTimer = setTimeout(() => signalGroup(child, "SIGKILL"), opts.killTimeoutMs ?? DEFAULT_KILL_TIMEOUT_MS);
|
|
211
|
+
killTimer.unref();
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
const onInt = () => onSignal("SIGINT");
|
|
215
|
+
const onTerm = () => onSignal("SIGTERM");
|
|
216
|
+
process.on("SIGINT", onInt);
|
|
217
|
+
process.on("SIGTERM", onTerm);
|
|
218
|
+
opts.abortSignal?.addEventListener("abort", onTerm, { once: true });
|
|
219
|
+
// Scanner stderr: redacted, capped.
|
|
220
|
+
let stderrTail = "";
|
|
221
|
+
let stderrShown = 0;
|
|
222
|
+
let stderrHidden = 0;
|
|
223
|
+
const errLines = readline.createInterface({ input: child.stderr, crlfDelay: Infinity });
|
|
224
|
+
errLines.on("line", (line) => {
|
|
225
|
+
stderrTail = (stderrTail + line + "\n").slice(-4096);
|
|
226
|
+
if (!line.trim())
|
|
227
|
+
return;
|
|
228
|
+
if (stderrShown < MAX_STDERR_LINES) {
|
|
229
|
+
stderrShown += 1;
|
|
230
|
+
io.stderr.write(`hq import scan: scanner: ${redactScannerLine(line)}\n`);
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
stderrHidden += 1;
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
let lineNo = 0;
|
|
237
|
+
let dropped = 0;
|
|
238
|
+
let heldDone;
|
|
239
|
+
const lines = readline.createInterface({ input: child.stdout, crlfDelay: Infinity });
|
|
240
|
+
const consumed = (async () => {
|
|
241
|
+
for await (const raw of lines) {
|
|
242
|
+
lineNo += 1;
|
|
243
|
+
const line = raw.trim();
|
|
244
|
+
if (!line)
|
|
245
|
+
continue;
|
|
246
|
+
if (heldDone) {
|
|
247
|
+
dropped += 1;
|
|
248
|
+
io.stderr.write(`hq import scan: dropped scan event on line ${lineNo}: received after done\n`);
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
const parsed = parseScanEventLine(line);
|
|
252
|
+
if (!parsed.ok) {
|
|
253
|
+
dropped += 1;
|
|
254
|
+
io.stderr.write(`hq import scan: dropped scan event on line ${lineNo}: ${parsed.reason}\n`);
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
if (parsed.event.type === "done")
|
|
258
|
+
heldDone = parsed.event;
|
|
259
|
+
else
|
|
260
|
+
write(parsed.event);
|
|
261
|
+
}
|
|
262
|
+
})();
|
|
263
|
+
const exit = await new Promise((resolve) => {
|
|
264
|
+
child.once("error", (error) => resolve({ code: null, signal: null, error }));
|
|
265
|
+
child.once("exit", (code, signal) => resolve({ code, signal }));
|
|
266
|
+
});
|
|
267
|
+
// The scanner is gone; stop anything it left behind so stdout can close.
|
|
268
|
+
signalGroup(child, "SIGTERM");
|
|
269
|
+
const drained = await Promise.race([
|
|
270
|
+
consumed.then(() => true),
|
|
271
|
+
new Promise((r) => setTimeout(() => r(false), DRAIN_TIMEOUT_MS).unref()),
|
|
272
|
+
]);
|
|
273
|
+
if (!drained) {
|
|
274
|
+
signalGroup(child, "SIGKILL");
|
|
275
|
+
child.stdout?.destroy();
|
|
276
|
+
child.stderr?.destroy();
|
|
277
|
+
lines.close();
|
|
278
|
+
}
|
|
279
|
+
errLines.close();
|
|
280
|
+
if (killTimer)
|
|
281
|
+
clearTimeout(killTimer);
|
|
282
|
+
process.removeListener("SIGINT", onInt);
|
|
283
|
+
process.removeListener("SIGTERM", onTerm);
|
|
284
|
+
opts.abortSignal?.removeEventListener("abort", onTerm);
|
|
285
|
+
if (stderrHidden > 0) {
|
|
286
|
+
io.stderr.write(`hq import scan: ${stderrHidden} more scanner diagnostic line(s) not shown\n`);
|
|
287
|
+
}
|
|
288
|
+
if (dropped > 0 && opts.mode !== "stream") {
|
|
289
|
+
io.stderr.write(`hq import scan: ${dropped} invalid scan event(s) were dropped\n`);
|
|
290
|
+
}
|
|
291
|
+
if (exit.error) {
|
|
292
|
+
return finish(zeroDone(), failed("The scanner could not be started."));
|
|
293
|
+
}
|
|
294
|
+
if (cancelledBy) {
|
|
295
|
+
return finish(heldDone ?? zeroDone(), failed("The scan was cancelled."), cancelledBy === "SIGINT" ? 130 : 143);
|
|
296
|
+
}
|
|
297
|
+
if (!heldDone) {
|
|
298
|
+
if (exit.code === 2 && /unknown arg: --progress-json/.test(stderrTail)) {
|
|
299
|
+
return finish(zeroDone(), outdated("The installed import-context scanner does not support streaming. Update HQ and try again."));
|
|
300
|
+
}
|
|
301
|
+
return finish(zeroDone(), failed(`The scan stopped before it finished (scanner exit ${exit.code ?? exit.signal ?? "unknown"}).`));
|
|
302
|
+
}
|
|
303
|
+
if (exit.code !== 0) {
|
|
304
|
+
return finish(heldDone, failed(`The scanner reported a problem after finishing (exit ${exit.code ?? exit.signal ?? "unknown"}).`));
|
|
305
|
+
}
|
|
306
|
+
return finish(heldDone);
|
|
307
|
+
}
|
|
308
|
+
//# sourceMappingURL=run.js.map
|