@indigoai-us/hq-cli 5.273.0 → 5.274.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +32 -1
  3. package/dist/command-catalog.generated.d.ts +72 -0
  4. package/dist/command-catalog.generated.js +92 -0
  5. package/dist/command-registration-plan.d.ts +12 -0
  6. package/dist/command-registration-plan.js +2 -0
  7. package/dist/commands/cloud.js +40 -0
  8. package/dist/commands/core-checkpoint.js +3 -1
  9. package/dist/commands/core-command-catalog.d.ts +32 -0
  10. package/dist/commands/core-command-catalog.js +58 -0
  11. package/dist/commands/core-ontology.js +9 -4
  12. package/dist/commands/core.d.ts +4 -0
  13. package/dist/commands/core.js +79 -75
  14. package/dist/commands/deploy-comments.d.ts +3 -0
  15. package/dist/commands/deploy-comments.js +159 -0
  16. package/dist/commands/lanes-move.d.ts +12 -0
  17. package/dist/commands/lanes-move.js +342 -0
  18. package/dist/commands/lanes-resume.js +3 -1
  19. package/dist/commands/lanes-shared.d.ts +36 -1
  20. package/dist/commands/lanes-shared.js +59 -0
  21. package/dist/commands/lanes.d.ts +1 -1
  22. package/dist/commands/lanes.js +204 -456
  23. package/dist/commands/report.js +2 -14
  24. package/dist/fast-core.js +1 -1
  25. package/dist/{commands/scaffold-fast.d.ts → fast-scaffold.d.ts} +1 -1
  26. package/dist/{commands/scaffold-fast.js → fast-scaffold.js} +1 -1
  27. package/dist/index.d.ts +1 -1
  28. package/dist/index.js +2 -2
  29. package/dist/lib/checkpoint-ontology/apply.d.ts +8 -0
  30. package/dist/lib/checkpoint-ontology/apply.js +33 -0
  31. package/dist/lib/checkpoint-ontology/plan.d.ts +15 -0
  32. package/dist/lib/checkpoint-ontology/plan.js +15 -0
  33. package/dist/lib/deploy-api.d.ts +6 -0
  34. package/dist/lib/deploy-api.js +21 -0
  35. package/dist/lib/doctor/fix/apply.d.ts +17 -2
  36. package/dist/lib/doctor/fix/apply.js +111 -57
  37. package/dist/lib/doctor/fix/json-output.d.ts +9 -1
  38. package/dist/lib/doctor/fix/json-output.js +11 -0
  39. package/dist/lib/first-sync-next-step.d.ts +37 -0
  40. package/dist/lib/first-sync-next-step.js +189 -0
  41. package/dist/main.js +4 -1
  42. package/dist/utils/cli-telemetry.d.ts +7 -1
  43. package/dist/utils/cli-telemetry.js +84 -8
  44. package/dist/utils/contribution-table.d.ts +1 -1
  45. package/dist/utils/scaffold-fast-import-guard.d.ts +3 -0
  46. package/dist/utils/scaffold-fast-import-guard.js +28 -0
  47. package/dist/utils/scaffold-packlist-guard.d.ts +7 -0
  48. package/dist/utils/scaffold-packlist-guard.js +23 -0
  49. package/dist/utils/scaffold-sibling-references.d.ts +21 -0
  50. package/dist/utils/scaffold-sibling-references.js +156 -0
  51. package/package.json +1 -1
@@ -0,0 +1,189 @@
1
+ import { createHash } from "node:crypto";
2
+ import * as fs from "node:fs";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ import { resolveProcessFlagGate } from "./flag-registry.js";
6
+ import { cognitoSubjectKey, isMachineIdentity, loadCachedTokens, } from "../utils/cognito-session.js";
7
+ export const CLI_FIRST_SYNC_NEXT_STEP_FLAG = "cli.first-sync-next-step";
8
+ export const FIRST_SYNC_NEXT_STEP_COPY = "Save a file in your HQ folder, then run `hq sync push`.";
9
+ export const FIRST_SYNC_CONFIRMATION_COPY = "Your first work sync is complete.";
10
+ const AUTOMATION_ENV_MARKERS = [
11
+ "CI",
12
+ "HQ_E2E",
13
+ "HQ_TEST",
14
+ "HQ_AUTOMATED",
15
+ "HQ_PROBE",
16
+ "HQ_AGENT_DIR",
17
+ ];
18
+ const pendingOutcomes = [];
19
+ export function shouldShowFirstSyncNextStep(input) {
20
+ return (input.flagEnabled &&
21
+ input.interactive &&
22
+ !input.alreadyShown &&
23
+ !input.alreadyCompleted);
24
+ }
25
+ export function shouldConfirmFirstWorkBearingSync(input) {
26
+ return (input.flagEnabled &&
27
+ input.interactive &&
28
+ !input.alreadyCompleted &&
29
+ input.workBearing);
30
+ }
31
+ export function isWorkBearingSyncResult(result) {
32
+ if (result.aborted === true)
33
+ return false;
34
+ return [
35
+ result.filesUploaded,
36
+ result.filesDownloaded,
37
+ result.sessionLogUploadedCount,
38
+ result.filesDeleted,
39
+ result.tombstonedCount,
40
+ ].some((count) => typeof count === "number" && Number.isFinite(count) && count > 0);
41
+ }
42
+ function isNonAutomatedHumanCliProcess(input) {
43
+ if (input.machineIdentity || input.env.NODE_ENV === "test")
44
+ return false;
45
+ return !AUTOMATION_ENV_MARKERS.some((name) => Boolean(input.env[name]?.trim()));
46
+ }
47
+ export function isHumanInteractiveCliSession(input) {
48
+ if (!input.stdoutIsTTY || !isNonAutomatedHumanCliProcess(input))
49
+ return false;
50
+ if (input.argv.some((arg) => ["--json", "--help", "-h", "--version", "-V"].includes(arg))) {
51
+ return false;
52
+ }
53
+ return true;
54
+ }
55
+ function markerDirectory(home) {
56
+ return path.join(home, ".hq", "first-sync-next-step");
57
+ }
58
+ export function firstSyncStepMarkerPaths(identity, home = process.env.HOME?.trim() || os.homedir()) {
59
+ const identityHash = createHash("sha256").update(identity).digest("hex");
60
+ const directory = markerDirectory(home);
61
+ return {
62
+ shown: path.join(directory, `${identityHash}.shown`),
63
+ completed: path.join(directory, `${identityHash}.completed`),
64
+ };
65
+ }
66
+ function reportMarkerError(action, err) {
67
+ const code = typeof err === "object" && err !== null && "code" in err
68
+ ? String(err.code)
69
+ : err instanceof Error
70
+ ? err.name
71
+ : "unknown error";
72
+ console.error(`hq: Could not ${action} first-sync state (${code}).`);
73
+ }
74
+ function firstSyncStepMarkerExists(kind, identity) {
75
+ try {
76
+ const paths = firstSyncStepMarkerPaths(identity);
77
+ return fs.existsSync(paths[kind]);
78
+ }
79
+ catch (err) {
80
+ reportMarkerError("read", err);
81
+ return false;
82
+ }
83
+ }
84
+ export function claimFirstSyncStepMarker(kind, identity, home = process.env.HOME?.trim() || os.homedir()) {
85
+ const paths = firstSyncStepMarkerPaths(identity, home);
86
+ const hqDirectory = path.join(home, ".hq");
87
+ try {
88
+ // Do not create ~/.hq on a signed-out machine. Authenticated CLI sessions
89
+ // already have it, and this state belongs only to the cached person.
90
+ if (!fs.existsSync(hqDirectory))
91
+ return false;
92
+ fs.mkdirSync(path.dirname(paths[kind]), { recursive: true, mode: 0o700 });
93
+ fs.writeFileSync(paths[kind], `${new Date().toISOString()}\n`, {
94
+ flag: "wx",
95
+ mode: 0o600,
96
+ });
97
+ return true;
98
+ }
99
+ catch (err) {
100
+ if (typeof err === "object" && err !== null && "code" in err && err.code === "EEXIST") {
101
+ return false;
102
+ }
103
+ reportMarkerError("write", err);
104
+ return false;
105
+ }
106
+ }
107
+ export function firstSyncStepTelemetryProperties(outcome) {
108
+ return {
109
+ step: "first_sync_next_step",
110
+ outcome,
111
+ flagKey: CLI_FIRST_SYNC_NEXT_STEP_FLAG,
112
+ };
113
+ }
114
+ export function isFirstSyncNextStepEnabled() {
115
+ return resolveProcessFlagGate(CLI_FIRST_SYNC_NEXT_STEP_FLAG, { fallback: false }, () => false);
116
+ }
117
+ function currentPersonIdentity() {
118
+ return cognitoSubjectKey(loadCachedTokens());
119
+ }
120
+ function isCurrentSessionEligible() {
121
+ return isHumanInteractiveCliSession({
122
+ stdoutIsTTY: Boolean(process.stdout.isTTY),
123
+ argv: process.argv,
124
+ env: process.env,
125
+ machineIdentity: isMachineIdentity(),
126
+ });
127
+ }
128
+ function queueOutcome(outcome) {
129
+ if (!pendingOutcomes.includes(outcome))
130
+ pendingOutcomes.push(outcome);
131
+ }
132
+ /** Called once after the first completed CLI action, before process finalization. */
133
+ export function maybeShowFirstSyncNextStep() {
134
+ // Keep the cheap eligibility/gate checks ahead of identity and filesystem
135
+ // reads; neither an automated invocation nor a flag-off session needs state.
136
+ if (!isCurrentSessionEligible() || !isFirstSyncNextStepEnabled())
137
+ return false;
138
+ const identity = currentPersonIdentity();
139
+ if (!identity)
140
+ return false;
141
+ const shouldShow = shouldShowFirstSyncNextStep({
142
+ flagEnabled: true,
143
+ interactive: true,
144
+ alreadyShown: firstSyncStepMarkerExists("shown", identity),
145
+ alreadyCompleted: firstSyncStepMarkerExists("completed", identity),
146
+ });
147
+ if (!shouldShow || !claimFirstSyncStepMarker("shown", identity))
148
+ return false;
149
+ console.log(FIRST_SYNC_NEXT_STEP_COPY);
150
+ queueOutcome("shown");
151
+ return true;
152
+ }
153
+ /** Capture the first positive CLI sync result without changing transfer logic. */
154
+ export function recordFirstWorkBearingSync(result) {
155
+ if (!isWorkBearingSyncResult(result))
156
+ return false;
157
+ if (!isNonAutomatedHumanCliProcess({
158
+ env: process.env,
159
+ machineIdentity: isMachineIdentity(),
160
+ }))
161
+ return false;
162
+ const identity = currentPersonIdentity();
163
+ if (!identity)
164
+ return false;
165
+ const alreadyCompleted = firstSyncStepMarkerExists("completed", identity);
166
+ if (alreadyCompleted || !claimFirstSyncStepMarker("completed", identity))
167
+ return false;
168
+ // Persist completion even while the rollout is off so enabling the flag later
169
+ // cannot prompt a person who already completed a work-bearing sync. JSON and
170
+ // other non-interactive human syncs count too; only the message is TTY-gated.
171
+ if (!isCurrentSessionEligible())
172
+ return false;
173
+ if (!shouldConfirmFirstWorkBearingSync({
174
+ flagEnabled: isFirstSyncNextStepEnabled(),
175
+ interactive: true,
176
+ alreadyCompleted: false,
177
+ workBearing: true,
178
+ }))
179
+ return false;
180
+ console.log(FIRST_SYNC_CONFIRMATION_COPY);
181
+ if (firstSyncStepMarkerExists("shown", identity))
182
+ queueOutcome("followed");
183
+ return true;
184
+ }
185
+ /** Drain outcomes once so each shown/followed event is sent at most once per run. */
186
+ export function takeFirstSyncStepOutcomes() {
187
+ return pendingOutcomes.splice(0, pendingOutcomes.length);
188
+ }
189
+ //# sourceMappingURL=first-sync-next-step.js.map
package/dist/main.js CHANGED
@@ -43,7 +43,7 @@ import { enforceVersionGate, shouldSkipGate, } from "./utils/version-gate.js";
43
43
  import { autoUpdateAndReexec } from "./utils/self-update.js";
44
44
  import { CLI_VERSION } from "./cli-version.js";
45
45
  import { findLazyCommand, registerCommandCatalog } from "./lazy-commands.js";
46
- import { emitCliSessionStarted } from "./utils/cli-telemetry.js";
46
+ import { emitCliFirstSyncNextStepOutcome, emitCliSessionStarted, takeFirstSyncStepOutcomes, maybeShowFirstSyncNextStep, } from "./utils/cli-telemetry.js";
47
47
  import { reportCliClientHealthInvocation } from "./utils/client-health.js";
48
48
  import { settleWithin } from "./utils/settle-with-timeout.js";
49
49
  import { emitPlanLimitNag } from "./lib/plan-limit-nag.js";
@@ -275,6 +275,7 @@ export async function runCli() {
275
275
  return;
276
276
  }
277
277
  await program.parseAsync();
278
+ maybeShowFirstSyncNextStep();
278
279
  }
279
280
  catch (err) {
280
281
  await handleTopLevelError(err);
@@ -292,9 +293,11 @@ export async function runCli() {
292
293
  // lifecycle boundary for this invocation. The budget reserves this 500 ms
293
294
  // slice even when the command itself was long-running.
294
295
  const releaseHealthTimeoutMs = foregroundNetworkBudget.timeoutFor(FINALIZATION_FOREGROUND_WAIT_MS);
296
+ const firstSyncOutcomeFlushes = takeFirstSyncStepOutcomes().map((outcome) => emitCliFirstSyncNextStepOutcome(outcome, releaseHealthTimeoutMs));
295
297
  await settleWithin([
296
298
  refreshVersionCache({ timeoutMs: releaseHealthTimeoutMs }),
297
299
  Sentry.flush(releaseHealthTimeoutMs),
300
+ ...firstSyncOutcomeFlushes,
298
301
  ], releaseHealthTimeoutMs);
299
302
  });
300
303
  // Last, so it wins over anything the (skipped) command path would have
@@ -1,9 +1,13 @@
1
+ import { type FirstSyncStepOutcome } from "../lib/first-sync-next-step.js";
2
+ export { CLI_FIRST_SYNC_NEXT_STEP_FLAG, FIRST_SYNC_CONFIRMATION_COPY, FIRST_SYNC_NEXT_STEP_COPY, claimFirstSyncStepMarker, firstSyncStepMarkerPaths, firstSyncStepTelemetryProperties, isFirstSyncNextStepEnabled, isHumanInteractiveCliSession, isWorkBearingSyncResult, maybeShowFirstSyncNextStep, recordFirstWorkBearingSync, shouldConfirmFirstWorkBearingSync, shouldShowFirstSyncNextStep, takeFirstSyncStepOutcomes, type FirstSyncStepOutcome, } from "../lib/first-sync-next-step.js";
1
3
  export declare const TELEMETRY_TIMEOUT_MS = 1200;
2
4
  /**
3
5
  * Exact stderr line printed once on the first signed-in TTY run that is not
4
- * `--json`. Wording matches the event body: CLI version + timestamp only.
6
+ * `--json`. Keep this notice and its marker stable for the default telemetry
7
+ * path; first-sync fields have a separate opt-in disclosure below.
5
8
  */
6
9
  export declare const TELEMETRY_DISCLOSURE_LINE = "HQ sends a usage event when you run a signed-in command (CLI version and a timestamp, never command names, file contents, or secrets) to improve the CLI. Set HQ_TELEMETRY=0 to turn this off.";
10
+ export declare const FIRST_SYNC_TELEMETRY_DISCLOSURE_LINE = "HQ sends a usage event when you run a signed-in command (CLI version and a timestamp; when enabled, first-sync guidance shown or followed and the flag key cli.first-sync-next-step; never command names, file contents, or secrets) to improve the CLI. Set HQ_TELEMETRY=0 to turn this off.";
7
11
  /**
8
12
  * `HQ_TELEMETRY=0` (also `false` and `off`, case-insensitive) disables every
9
13
  * CLI POST to `/v1/telemetry/events`. `HQ_TELEMETRY_TIMEOUT_MS` is unchanged.
@@ -25,4 +29,6 @@ export declare function maybeDiscloseCliTelemetry(stderr?: {
25
29
  * This deliberately never refreshes a session or opens a browser.
26
30
  */
27
31
  export declare function emitCliSessionStarted(timeoutMs?: number): Promise<void>;
32
+ /** Emit one authenticated measurement for a first-sync prompt outcome. */
33
+ export declare function emitCliFirstSyncNextStepOutcome(outcome: FirstSyncStepOutcome, timeoutMs?: number): Promise<void>;
28
34
  //# sourceMappingURL=cli-telemetry.d.ts.map
@@ -4,14 +4,19 @@ import * as path from "node:path";
4
4
  import { CLI_VERSION } from "../cli-version.js";
5
5
  import { isExpiring, isMachineIdentity, loadCachedTokens, } from "./cognito-session.js";
6
6
  import { vaultApiFetch } from "./vault-api.js";
7
+ import { firstSyncStepTelemetryProperties, isFirstSyncNextStepEnabled, isHumanInteractiveCliSession, } from "../lib/first-sync-next-step.js";
8
+ export { CLI_FIRST_SYNC_NEXT_STEP_FLAG, FIRST_SYNC_CONFIRMATION_COPY, FIRST_SYNC_NEXT_STEP_COPY, claimFirstSyncStepMarker, firstSyncStepMarkerPaths, firstSyncStepTelemetryProperties, isFirstSyncNextStepEnabled, isHumanInteractiveCliSession, isWorkBearingSyncResult, maybeShowFirstSyncNextStep, recordFirstWorkBearingSync, shouldConfirmFirstWorkBearingSync, shouldShowFirstSyncNextStep, takeFirstSyncStepOutcomes, } from "../lib/first-sync-next-step.js";
7
9
  export const TELEMETRY_TIMEOUT_MS = 1_200;
8
10
  /**
9
11
  * Exact stderr line printed once on the first signed-in TTY run that is not
10
- * `--json`. Wording matches the event body: CLI version + timestamp only.
12
+ * `--json`. Keep this notice and its marker stable for the default telemetry
13
+ * path; first-sync fields have a separate opt-in disclosure below.
11
14
  */
12
15
  export const TELEMETRY_DISCLOSURE_LINE = "HQ sends a usage event when you run a signed-in command (CLI version and a timestamp, never command names, file contents, or secrets) to improve the CLI. Set HQ_TELEMETRY=0 to turn this off.";
16
+ export const FIRST_SYNC_TELEMETRY_DISCLOSURE_LINE = "HQ sends a usage event when you run a signed-in command (CLI version and a timestamp; when enabled, first-sync guidance shown or followed and the flag key cli.first-sync-next-step; never command names, file contents, or secrets) to improve the CLI. Set HQ_TELEMETRY=0 to turn this off.";
13
17
  const TELEMETRY_DISABLED_VALUES = new Set(["0", "false", "off"]);
14
18
  const DISCLOSURE_MARKER = "cli-telemetry-disclosed";
19
+ const FIRST_SYNC_DISCLOSURE_MARKER = "cli-telemetry-disclosed-first-sync-v1";
15
20
  let cliSessionStartedPromise;
16
21
  /**
17
22
  * `HQ_TELEMETRY=0` (also `false` and `off`, case-insensitive) disables every
@@ -30,27 +35,30 @@ function hqConfigDir() {
30
35
  export function telemetryDisclosureMarkerPath() {
31
36
  return path.join(hqConfigDir(), DISCLOSURE_MARKER);
32
37
  }
38
+ function firstSyncTelemetryDisclosureMarkerPath() {
39
+ return path.join(hqConfigDir(), FIRST_SYNC_DISCLOSURE_MARKER);
40
+ }
33
41
  function wantsJsonOutput(argv = process.argv) {
34
42
  return argv.includes("--json");
35
43
  }
36
44
  function canPrintDisclosure(stdout = process.stdout, argv = process.argv) {
37
45
  return Boolean(stdout.isTTY) && !wantsJsonOutput(argv);
38
46
  }
39
- function hasShownDisclosure() {
47
+ function hasShownDisclosure(markerPath) {
40
48
  try {
41
- return fs.existsSync(telemetryDisclosureMarkerPath());
49
+ return fs.existsSync(markerPath);
42
50
  }
43
51
  catch {
44
52
  return false;
45
53
  }
46
54
  }
47
- function recordDisclosureShown() {
55
+ function recordDisclosureShown(markerPath) {
48
56
  try {
49
57
  const dir = hqConfigDir();
50
58
  // Do not create ~/.hq. Signed-in runs already have it (cached tokens).
51
59
  if (!fs.existsSync(dir))
52
60
  return;
53
- fs.writeFileSync(telemetryDisclosureMarkerPath(), `${new Date().toISOString()}\n`);
61
+ fs.writeFileSync(markerPath, `${new Date().toISOString()}\n`);
54
62
  }
55
63
  catch {
56
64
  // Marker write is best effort; a repeat print is better than crashing.
@@ -62,17 +70,23 @@ function recordDisclosureShown() {
62
70
  * non-TTY stdout skip the print and do not record the marker.
63
71
  */
64
72
  export function maybeDiscloseCliTelemetry(stderr = process.stderr, stdout = process.stdout, argv = process.argv) {
65
- if (hasShownDisclosure())
73
+ return maybeDiscloseTelemetry(TELEMETRY_DISCLOSURE_LINE, telemetryDisclosureMarkerPath(), stderr, stdout, argv);
74
+ }
75
+ function maybeDiscloseFirstSyncTelemetry() {
76
+ return maybeDiscloseTelemetry(FIRST_SYNC_TELEMETRY_DISCLOSURE_LINE, firstSyncTelemetryDisclosureMarkerPath());
77
+ }
78
+ function maybeDiscloseTelemetry(line, markerPath, stderr = process.stderr, stdout = process.stdout, argv = process.argv) {
79
+ if (hasShownDisclosure(markerPath))
66
80
  return true;
67
81
  if (!canPrintDisclosure(stdout, argv))
68
82
  return false;
69
83
  try {
70
- stderr.write(`${TELEMETRY_DISCLOSURE_LINE}\n`);
84
+ stderr.write(`${line}\n`);
71
85
  }
72
86
  catch {
73
87
  return false;
74
88
  }
75
- recordDisclosureShown();
89
+ recordDisclosureShown(markerPath);
76
90
  return true;
77
91
  }
78
92
  /**
@@ -131,4 +145,66 @@ async function emitCachedCliSessionStarted(timeoutMs) {
131
145
  // Cached-token reads are also best effort (for example, a malformed cache).
132
146
  }
133
147
  }
148
+ /** Emit one authenticated measurement for a first-sync prompt outcome. */
149
+ export async function emitCliFirstSyncNextStepOutcome(outcome, timeoutMs = TELEMETRY_TIMEOUT_MS) {
150
+ try {
151
+ if (isCliTelemetryDisabled())
152
+ return;
153
+ if (!isFirstSyncNextStepEnabled())
154
+ return;
155
+ const cached = loadCachedTokens();
156
+ const machineIdentity = isMachineIdentity();
157
+ if (!cached ||
158
+ isExpiring(cached, 120) ||
159
+ !isHumanInteractiveCliSession({
160
+ stdoutIsTTY: Boolean(process.stdout.isTTY),
161
+ argv: process.argv,
162
+ env: process.env,
163
+ machineIdentity,
164
+ }))
165
+ return;
166
+ const token = cached.accessToken;
167
+ if (!token)
168
+ return;
169
+ const consentBasis = maybeDiscloseFirstSyncTelemetry() ? "disclosed" : "no-consent";
170
+ const controller = new AbortController();
171
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
172
+ try {
173
+ const response = await vaultApiFetch({
174
+ token,
175
+ path: "/v1/telemetry/events",
176
+ method: "POST",
177
+ body: {
178
+ events: [
179
+ {
180
+ eventName: "cli_session_started",
181
+ app: "hq-cli",
182
+ source: "cli",
183
+ occurredAt: new Date().toISOString(),
184
+ consentBasis,
185
+ schemaVersion: 1,
186
+ properties: {
187
+ version: CLI_VERSION,
188
+ ...firstSyncStepTelemetryProperties(outcome),
189
+ },
190
+ },
191
+ ],
192
+ },
193
+ signal: controller.signal,
194
+ });
195
+ if (!response.ok)
196
+ return;
197
+ }
198
+ catch {
199
+ // Telemetry is best effort and must never affect or follow as an error
200
+ // after the completed CLI command.
201
+ }
202
+ finally {
203
+ clearTimeout(timeout);
204
+ }
205
+ }
206
+ catch {
207
+ // Cached-token reads are also best effort (for example, a malformed cache).
208
+ }
209
+ }
134
210
  //# sourceMappingURL=cli-telemetry.js.map
@@ -99,5 +99,5 @@ export declare const CONTRIBUTION_KEYS: ContributionKey[];
99
99
  */
100
100
  export declare function payloadFor(key: ContributionKey, item: string): string;
101
101
  /** Keys whose contributions are wired by a host symlink (skip `merge` rows). */
102
- export declare const SYMLINK_KEYS: ("workers" | "mcp" | "skills" | "knowledge" | "commands" | "hooks" | "policies" | "scripts")[];
102
+ export declare const SYMLINK_KEYS: ("workers" | "mcp" | "commands" | "skills" | "knowledge" | "hooks" | "policies" | "scripts")[];
103
103
  //# sourceMappingURL=contribution-table.d.ts.map
@@ -0,0 +1,3 @@
1
+ export declare function forbiddenFastPathModules(loadedModules: readonly string[], distDirectory: string): string[];
2
+ export declare function assertFastPathStaysCommandGraphFree(loadedModules: readonly string[], distDirectory: string): void;
3
+ //# sourceMappingURL=scaffold-fast-import-guard.d.ts.map
@@ -0,0 +1,28 @@
1
+ import * as path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ export function forbiddenFastPathModules(loadedModules, distDirectory) {
4
+ const dist = path.resolve(distDirectory);
5
+ const main = path.join(dist, "main.js");
6
+ const commands = `${path.join(dist, "commands")}${path.sep}`;
7
+ return [...new Set(loadedModules.map((loaded) => {
8
+ if (loaded.startsWith("file:")) {
9
+ try {
10
+ return fileURLToPath(loaded);
11
+ }
12
+ catch {
13
+ return loaded;
14
+ }
15
+ }
16
+ return path.resolve(loaded);
17
+ }))]
18
+ .filter((loaded) => path.resolve(loaded) === main ||
19
+ path.resolve(loaded).startsWith(commands))
20
+ .sort();
21
+ }
22
+ export function assertFastPathStaysCommandGraphFree(loadedModules, distDirectory) {
23
+ const forbidden = forbiddenFastPathModules(loadedModules, distDirectory);
24
+ if (forbidden.length > 0) {
25
+ throw new Error(`fast core loaded forbidden command modules:\n${forbidden.join("\n")}`);
26
+ }
27
+ }
28
+ //# sourceMappingURL=scaffold-fast-import-guard.js.map
@@ -0,0 +1,7 @@
1
+ export type ScaffoldPacklistCheck = {
2
+ missingAssets: string[];
3
+ forbiddenTestData: string[];
4
+ };
5
+ export declare function checkScaffoldPacklist(listedAssets: readonly string[], packedPaths: ReadonlySet<string>, assetPrefix: string): ScaffoldPacklistCheck;
6
+ export declare function assertScaffoldPacklist(listedAssets: readonly string[], packedPaths: ReadonlySet<string>, assetPrefix: string): void;
7
+ //# sourceMappingURL=scaffold-packlist-guard.d.ts.map
@@ -0,0 +1,23 @@
1
+ export function checkScaffoldPacklist(listedAssets, packedPaths, assetPrefix) {
2
+ const prefix = assetPrefix.replace(/\/$/, "");
3
+ const missingAssets = listedAssets
4
+ .map((asset) => `${prefix}/${asset}`)
5
+ .filter((assetPath) => !packedPaths.has(assetPath))
6
+ .sort();
7
+ const forbiddenTestData = [...packedPaths]
8
+ .filter((packedPath) => packedPath === "test/differential" ||
9
+ packedPath.startsWith("test/differential/") ||
10
+ packedPath.split("/").some((part) => /corpus/i.test(part)))
11
+ .sort();
12
+ return { missingAssets, forbiddenTestData };
13
+ }
14
+ export function assertScaffoldPacklist(listedAssets, packedPaths, assetPrefix) {
15
+ const check = checkScaffoldPacklist(listedAssets, packedPaths, assetPrefix);
16
+ const failures = [
17
+ ...check.missingAssets.map((asset) => `missing listed asset ${asset}`),
18
+ ...check.forbiddenTestData.map((file) => `test data leaked into packlist: ${file}`),
19
+ ];
20
+ if (failures.length > 0)
21
+ throw new Error(failures.join("\n"));
22
+ }
23
+ //# sourceMappingURL=scaffold-packlist-guard.js.map
@@ -0,0 +1,21 @@
1
+ export type SiblingAssetReference = {
2
+ referringAsset: string;
3
+ targetAsset: string;
4
+ };
5
+ export type HqRootAssetReference = {
6
+ referringAsset: string;
7
+ reference: string;
8
+ };
9
+ export type ScaffoldReferenceScan = {
10
+ missing: SiblingAssetReference[];
11
+ allowedHqRoot: HqRootAssetReference[];
12
+ };
13
+ /**
14
+ * Scan every asset below assets/scaffold for local file loads. Node source is
15
+ * identified by extension or shebang; unknown text assets use shell scanning.
16
+ * `${HQ_ROOT}/...` paths stay rooted in the live HQ tree and are returned as
17
+ * allowed references for review instead of being mistaken for package assets.
18
+ */
19
+ export declare function scanScaffoldSiblingReferences(scaffoldRoot: string): ScaffoldReferenceScan;
20
+ export declare function assertNoMissingScaffoldSiblings(scan: ScaffoldReferenceScan): void;
21
+ //# sourceMappingURL=scaffold-sibling-references.d.ts.map
@@ -0,0 +1,156 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ const SCRIPT_ROOT_TOKENS = /(?:\$\{SCRIPT_DIR\}|\$SCRIPT_DIR)/g;
4
+ const BASH_SOURCE_TOKEN = String.raw `(?:\$\{BASH_SOURCE\[0\](?::-\$0)?\}|\$\{?0\}?)`;
5
+ function relativePath(root, target) {
6
+ return path.relative(root, target).split(path.sep).join("/") || ".";
7
+ }
8
+ function filesBelow(root) {
9
+ const files = [];
10
+ const pending = [root];
11
+ while (pending.length > 0) {
12
+ const current = pending.pop();
13
+ if (!current)
14
+ continue;
15
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
16
+ const fullPath = path.join(current, entry.name);
17
+ if (entry.isDirectory())
18
+ pending.push(fullPath);
19
+ else if (entry.isFile())
20
+ files.push(fullPath);
21
+ }
22
+ }
23
+ return files.sort();
24
+ }
25
+ function shellLocationAliases(contents) {
26
+ // Asset-relative values point into the package. HQ-root values are live-tree
27
+ // paths, so keep them visible as allowed references instead of package deps.
28
+ const aliases = new Map([
29
+ ["SCRIPT_DIR", { kind: "asset", baseDirectory: "." }],
30
+ ["HQ_ROOT", { kind: "hq-root", baseDirectory: "." }],
31
+ ]);
32
+ for (const line of contents.split(/\r?\n/)) {
33
+ const assignment = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
34
+ if (!assignment)
35
+ continue;
36
+ const [, name, value] = assignment;
37
+ if (name === "HQ_ROOT" || /\$\{?(?:HQ_ROOT|CLAUDE_PROJECT_DIR)(?:\}|(?=[:/}]))/.test(value)) {
38
+ aliases.set(name, { kind: "hq-root", baseDirectory: "." });
39
+ continue;
40
+ }
41
+ SCRIPT_ROOT_TOKENS.lastIndex = 0;
42
+ const scriptRoot = SCRIPT_ROOT_TOKENS.exec(value);
43
+ if (scriptRoot) {
44
+ const suffix = value.slice(scriptRoot.index + scriptRoot[0].length)
45
+ .match(/^\/([^"'`\s;|&)]+)/)?.[1] ?? ".";
46
+ aliases.set(name, { kind: "asset", baseDirectory: path.posix.normalize(suffix) });
47
+ continue;
48
+ }
49
+ const dirname = new RegExp(String.raw `\$\(\s*dirname\s+["']?${BASH_SOURCE_TOKEN}["']?\s*\)(\/[^"'\x60\s;|&)]+)?`).exec(value);
50
+ if (dirname) {
51
+ const suffix = dirname[1]?.replace(/^\/+/, "") ?? ".";
52
+ aliases.set(name, { kind: "asset", baseDirectory: path.posix.normalize(suffix) });
53
+ }
54
+ }
55
+ return aliases;
56
+ }
57
+ function isNodeAsset(file, contents) {
58
+ if ([".js", ".mjs", ".cjs"].includes(path.extname(file).toLowerCase()))
59
+ return true;
60
+ const shebang = contents.split(/\r?\n/, 1)[0] ?? "";
61
+ return /^#!.*(?:[\s/])node(?:js)?(?:\s|$)/.test(shebang);
62
+ }
63
+ /**
64
+ * Scan every asset below assets/scaffold for local file loads. Node source is
65
+ * identified by extension or shebang; unknown text assets use shell scanning.
66
+ * `${HQ_ROOT}/...` paths stay rooted in the live HQ tree and are returned as
67
+ * allowed references for review instead of being mistaken for package assets.
68
+ */
69
+ export function scanScaffoldSiblingReferences(scaffoldRoot) {
70
+ const root = path.resolve(scaffoldRoot);
71
+ const bundledFiles = filesBelow(root);
72
+ const bundledPaths = new Set(bundledFiles.map((file) => relativePath(root, file)));
73
+ const missing = [];
74
+ const allowedHqRoot = new Map();
75
+ const missingSet = new Set();
76
+ const recordRelative = (sourceFile, specifier, baseDirectory = ".") => {
77
+ const referringAsset = relativePath(root, sourceFile);
78
+ const targetPath = path.resolve(path.dirname(sourceFile), baseDirectory, specifier.replace(/^\/+/, ""));
79
+ const targetAsset = relativePath(root, targetPath);
80
+ if (bundledPaths.has(targetAsset))
81
+ return;
82
+ const key = `${referringAsset}\0${targetAsset}`;
83
+ if (missingSet.has(key))
84
+ return;
85
+ missingSet.add(key);
86
+ missing.push({ referringAsset, targetAsset });
87
+ };
88
+ for (const sourceFile of bundledFiles) {
89
+ const contents = fs.readFileSync(sourceFile, "utf8");
90
+ const referringAsset = relativePath(root, sourceFile);
91
+ for (const line of contents.split(/\r?\n/)) {
92
+ if (/^\s*#/.test(line))
93
+ continue;
94
+ for (const match of line.matchAll(/\$\{HQ_ROOT(?::-[^}]*)?\}\/([^"'`\s;|&)]+)/g)) {
95
+ const reference = "${HQ_ROOT}/" + match[1];
96
+ allowedHqRoot.set(`${referringAsset}\0${reference}`, { referringAsset, reference });
97
+ }
98
+ for (const match of line.matchAll(/\$HQ_ROOT\/([^"'`\s;|&)]+)/g)) {
99
+ const reference = `$HQ_ROOT/${match[1]}`;
100
+ allowedHqRoot.set(`${referringAsset}\0${reference}`, { referringAsset, reference });
101
+ }
102
+ }
103
+ if (isNodeAsset(sourceFile, contents)) {
104
+ const nodePatterns = [
105
+ /new\s+URL\(\s*(['"])(\.\.?\/[^'"]+)\1\s*,\s*import\.meta\.url\s*\)/g,
106
+ /path\.join\(\s*__dirname\s*,\s*(['"])(\.\.?\/[^'"]+|[^.'"/][^'"]*)\1\s*\)/g,
107
+ ];
108
+ for (const pattern of nodePatterns) {
109
+ for (const match of contents.matchAll(pattern))
110
+ recordRelative(sourceFile, match[2]);
111
+ }
112
+ continue;
113
+ }
114
+ const aliases = shellLocationAliases(contents);
115
+ const commandLines = contents.split(/\r?\n/).filter((line) => !/^\s*#/.test(line) &&
116
+ (/\b(?:source|exec|bash|sh|node)\b/.test(line) || /^\s*\.\s+/.test(line)));
117
+ for (const line of commandLines) {
118
+ SCRIPT_ROOT_TOKENS.lastIndex = 0;
119
+ for (const match of line.matchAll(/(?:\$\{SCRIPT_DIR\}|\$SCRIPT_DIR)\/([^"'`\s;|&)]+)/g)) {
120
+ recordRelative(sourceFile, match[1]);
121
+ }
122
+ const dirnamePath = new RegExp(String.raw `\$\(\s*dirname\s+["']?${BASH_SOURCE_TOKEN}["']?\s*\)(\/[^"'\x60\s;|&)]+)`, "g");
123
+ for (const match of line.matchAll(dirnamePath))
124
+ recordRelative(sourceFile, match[1]);
125
+ for (const [alias, location] of aliases) {
126
+ const escapedAlias = alias.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
127
+ const aliasPath = new RegExp(String.raw `(?:\$\{${escapedAlias}\}|\$${escapedAlias})\/([^"'\x60\s;|&)]+)`, "g");
128
+ for (const match of line.matchAll(aliasPath)) {
129
+ if (location.kind === "hq-root") {
130
+ const reference = match[0];
131
+ allowedHqRoot.set(`${referringAsset}\0${reference}`, { referringAsset, reference });
132
+ }
133
+ else {
134
+ recordRelative(sourceFile, match[1], location.baseDirectory);
135
+ }
136
+ }
137
+ }
138
+ const relativeCommand = /(?:\b(?:source|exec|bash|sh|node)\s+|^\s*\.\s+)(?:"(\.\.?\/[^"]+)"|'(\.\.?\/[^']+)'|(\.\.?\/[^\s;|&)]+))/g;
139
+ for (const match of line.matchAll(relativeCommand)) {
140
+ recordRelative(sourceFile, match[1] ?? match[2] ?? match[3]);
141
+ }
142
+ }
143
+ }
144
+ return {
145
+ missing: missing.sort((a, b) => `${a.referringAsset}\0${a.targetAsset}`.localeCompare(`${b.referringAsset}\0${b.targetAsset}`)),
146
+ allowedHqRoot: [...allowedHqRoot.values()].sort((a, b) => `${a.referringAsset}\0${a.reference}`.localeCompare(`${b.referringAsset}\0${b.reference}`)),
147
+ };
148
+ }
149
+ export function assertNoMissingScaffoldSiblings(scan) {
150
+ if (scan.missing.length === 0)
151
+ return;
152
+ throw new Error(scan.missing
153
+ .map(({ referringAsset, targetAsset }) => `${referringAsset} references missing ${targetAsset}`)
154
+ .join("\n"));
155
+ }
156
+ //# sourceMappingURL=scaffold-sibling-references.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.273.0",
3
+ "version": "5.274.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {