@indigoai-us/hq-cli 5.241.0 → 5.243.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.
@@ -32,6 +32,7 @@
32
32
  * actually written (we do NOT rely solely on the pre-flight scan or on the
33
33
  * tar binary's own protections).
34
34
  */
35
+ import { execFileSync } from 'child_process';
35
36
  export interface ExtractCaps {
36
37
  /** Max total uncompressed bytes across all entries. Default 256 MiB. */
37
38
  maxUncompressedBytes: number;
@@ -109,9 +110,13 @@ export declare function resolveContainedPath(targetDir: string, entryName: strin
109
110
  export declare function assertLinkTargetContained(packDir: string, entryAbs: string, linkTarget: string): void;
110
111
  /**
111
112
  * List a gzip-compressed tarball's entries via `tar -tvf` (no extraction).
112
- * argv form — never a shell string — so a hostile filename can't break out.
113
+ * argv form, never a shell string, keeps hostile filenames out of shell syntax.
114
+ * When the default-off compatibility flag is enabled, Windows drive paths use
115
+ * a basename relative to the archive directory. GNU tar cannot parse `C:\\...`
116
+ * as remote `host:path`, and Windows' bundled bsdtar accepts the same relative
117
+ * argv without GNU-only `--force-local`.
113
118
  */
114
- export declare function listTarball(tarballPath: string): TarEntry[];
119
+ export declare function listTarball(tarballPath: string, opts?: Pick<SafeExtractOptions, 'platform' | 'useRelativeWindowsDrivePath' | 'execFileSync'>): TarEntry[];
115
120
  /**
116
121
  * Pre-flight validation against the entry headers. Enforces (a) containment of
117
122
  * every entry name under `targetDir`, (b) containment of every link target
@@ -129,6 +134,10 @@ export declare function validateEntries(entries: TarEntry[], targetDir: string,
129
134
  export declare function assertExtractContained(stagingDir: string): void;
130
135
  export interface SafeExtractOptions {
131
136
  caps?: ExtractCaps;
137
+ /** Test seams for platform-specific argv construction; runtime gating uses hq-flags. */
138
+ platform?: NodeJS.Platform;
139
+ useRelativeWindowsDrivePath?: boolean;
140
+ execFileSync?: typeof execFileSync;
132
141
  /**
133
142
  * Test seam: invoked AFTER `tar` extracts into the staging dir but BEFORE
134
143
  * the post-extract sweep / atomic rename. Throwing here simulates a failure
@@ -35,6 +35,7 @@
35
35
  import { execFileSync } from 'child_process';
36
36
  import * as fs from 'fs';
37
37
  import * as path from 'path';
38
+ import { installPathCompatibilityEnabled } from '../lib/install-path-compatibility-flag.js';
38
39
  export const DEFAULT_CAPS = {
39
40
  // A content pack is docs/skills/policies/small scripts. 256 MiB is already
40
41
  // generous; anything larger is almost certainly a bomb or a mistake.
@@ -203,18 +204,34 @@ export function assertLinkTargetContained(packDir, entryAbs, linkTarget) {
203
204
  `"${linkTarget}" -> "${resolvedTarget}", outside pack dir "${path.resolve(packDir)}".`);
204
205
  }
205
206
  }
206
- // ---------------------------------------------------------------------------
207
- // Pre-flight: list entries, enforce containment + caps WITHOUT extracting
208
- // ---------------------------------------------------------------------------
207
+ function tarballArgvLocation(tarballPath, platform, useRelativeWindowsDrivePath) {
208
+ if (platform === 'win32' &&
209
+ useRelativeWindowsDrivePath &&
210
+ /^[A-Za-z]:[\\/]/.test(tarballPath)) {
211
+ return {
212
+ archiveArg: path.win32.basename(tarballPath),
213
+ cwd: path.win32.dirname(tarballPath),
214
+ };
215
+ }
216
+ return { archiveArg: tarballPath };
217
+ }
209
218
  /**
210
219
  * List a gzip-compressed tarball's entries via `tar -tvf` (no extraction).
211
- * argv form — never a shell string — so a hostile filename can't break out.
220
+ * argv form, never a shell string, keeps hostile filenames out of shell syntax.
221
+ * When the default-off compatibility flag is enabled, Windows drive paths use
222
+ * a basename relative to the archive directory. GNU tar cannot parse `C:\\...`
223
+ * as remote `host:path`, and Windows' bundled bsdtar accepts the same relative
224
+ * argv without GNU-only `--force-local`.
212
225
  */
213
- export function listTarball(tarballPath) {
214
- const out = execFileSync('tar', ['-tvf', tarballPath], {
226
+ export function listTarball(tarballPath, opts = {}) {
227
+ const platform = opts.platform ?? process.platform;
228
+ const useRelativeWindowsDrivePath = opts.useRelativeWindowsDrivePath ?? installPathCompatibilityEnabled();
229
+ const location = tarballArgvLocation(tarballPath, platform, useRelativeWindowsDrivePath);
230
+ const out = (opts.execFileSync ?? execFileSync)('tar', ['-tvf', location.archiveArg], {
215
231
  encoding: 'utf-8',
216
232
  maxBuffer: 64 * 1024 * 1024,
217
233
  stdio: ['ignore', 'pipe', 'pipe'],
234
+ ...(location.cwd ? { cwd: location.cwd } : {}),
218
235
  });
219
236
  return parseTarListing(out);
220
237
  }
@@ -311,8 +328,17 @@ export function safeExtractTarball(tarballPath, finalDir, opts = {}) {
311
328
  if (fs.existsSync(finalDir)) {
312
329
  throw new Error(`safeExtractTarball: refusing to extract over existing dir "${finalDir}".`);
313
330
  }
331
+ // Resolve once so listing and extraction use the same archive location.
332
+ const platform = opts.platform ?? process.platform;
333
+ const useRelativeWindowsDrivePath = opts.useRelativeWindowsDrivePath ?? installPathCompatibilityEnabled();
334
+ const exec = opts.execFileSync ?? execFileSync;
335
+ const location = tarballArgvLocation(tarballPath, platform, useRelativeWindowsDrivePath);
314
336
  // 1. Pre-flight against headers (no bytes written yet).
315
- const entries = listTarball(tarballPath);
337
+ const entries = listTarball(tarballPath, {
338
+ platform,
339
+ useRelativeWindowsDrivePath,
340
+ execFileSync: exec,
341
+ });
316
342
  validateEntries(entries, finalDir, caps);
317
343
  // 2. Sibling staging dir on the same filesystem so the rename is atomic.
318
344
  const parent = path.dirname(path.resolve(finalDir));
@@ -322,7 +348,10 @@ export function safeExtractTarball(tarballPath, finalDir, opts = {}) {
322
348
  // `--no-same-owner` avoids chown surprises when run as root; argv form, no
323
349
  // shell. We deliberately re-extract here (not the pre-flight listing) and
324
350
  // re-validate after, rather than trusting tar to honor any single flag.
325
- execFileSync('tar', ['-xzf', tarballPath, '-C', stagingDir, '--no-same-owner'], { stdio: 'inherit' });
351
+ exec('tar', ['-xzf', location.archiveArg, '-C', stagingDir, '--no-same-owner'], {
352
+ stdio: 'inherit',
353
+ ...(location.cwd ? { cwd: location.cwd } : {}),
354
+ });
326
355
  // 3. Independent backstop sweep over what actually landed.
327
356
  assertExtractContained(stagingDir);
328
357
  // 4. Failure-injection seam (tests).
@@ -0,0 +1,8 @@
1
+ import type { RememberedLoginProvider } from "../utils/login-provider.js";
2
+ export declare const LOGIN_PROVIDER_STATE_FILE_NAME = "auth-login-provider.json";
3
+ export declare function getLastLoginProviderStatePath(): string;
4
+ /** Read only the provider preference; malformed or unknown state is ignored. */
5
+ export declare function loadLastLoginProvider(): RememberedLoginProvider | undefined;
6
+ /** Store only the provider name, clearing stale state when the choice is unknown. */
7
+ export declare function storeLastLoginProvider(provider: RememberedLoginProvider | undefined): void;
8
+ //# sourceMappingURL=auth-login-provider-state.d.ts.map
@@ -0,0 +1,63 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
3
+ import path from "node:path";
4
+ import { personTokenCacheFile } from "../utils/cognito-session.js";
5
+ export const LOGIN_PROVIDER_STATE_FILE_NAME = "auth-login-provider.json";
6
+ export function getLastLoginProviderStatePath() {
7
+ return path.join(path.dirname(personTokenCacheFile()), LOGIN_PROVIDER_STATE_FILE_NAME);
8
+ }
9
+ function isRememberedProvider(value) {
10
+ return value === "google" || value === "microsoft";
11
+ }
12
+ /** Read only the provider preference; malformed or unknown state is ignored. */
13
+ export function loadLastLoginProvider() {
14
+ let raw;
15
+ try {
16
+ raw = readFileSync(getLastLoginProviderStatePath(), "utf8");
17
+ }
18
+ catch (error) {
19
+ if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
20
+ return undefined;
21
+ }
22
+ throw error;
23
+ }
24
+ let value;
25
+ try {
26
+ value = JSON.parse(raw);
27
+ }
28
+ catch {
29
+ return undefined;
30
+ }
31
+ if (!value || typeof value !== "object" || Array.isArray(value))
32
+ return undefined;
33
+ const state = value;
34
+ const keys = Object.keys(state).sort();
35
+ if (keys.length !== 2
36
+ || keys[0] !== "lastProvider"
37
+ || keys[1] !== "schemaVersion"
38
+ || state.schemaVersion !== 1
39
+ || !isRememberedProvider(state.lastProvider)) {
40
+ return undefined;
41
+ }
42
+ return state.lastProvider;
43
+ }
44
+ /** Store only the provider name, clearing stale state when the choice is unknown. */
45
+ export function storeLastLoginProvider(provider) {
46
+ const filePath = getLastLoginProviderStatePath();
47
+ if (!provider) {
48
+ rmSync(filePath, { force: true });
49
+ return;
50
+ }
51
+ const directory = path.dirname(filePath);
52
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
53
+ const temporaryPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
54
+ try {
55
+ const state = { schemaVersion: 1, lastProvider: provider };
56
+ writeFileSync(temporaryPath, JSON.stringify(state), { mode: 0o600, flag: "wx" });
57
+ renameSync(temporaryPath, filePath);
58
+ }
59
+ finally {
60
+ rmSync(temporaryPath, { force: true });
61
+ }
62
+ }
63
+ //# sourceMappingURL=auth-login-provider-state.js.map
@@ -0,0 +1,9 @@
1
+ export declare const INSTALL_PATH_COMPATIBILITY_FLAG = "cli.install-path-compatibility";
2
+ export declare const INSTALL_PATH_COMPATIBILITY_FALLBACK = false;
3
+ /**
4
+ * Read the default-off rollout flag from the process snapshot. Missing,
5
+ * unreadable, or not-yet-loaded registry state keeps the compatibility fixes
6
+ * disabled until the operator enables a canary.
7
+ */
8
+ export declare function installPathCompatibilityEnabled(): boolean;
9
+ //# sourceMappingURL=install-path-compatibility-flag.d.ts.map
@@ -0,0 +1,12 @@
1
+ import { resolveProcessFlagGate } from "./flag-registry.js";
2
+ export const INSTALL_PATH_COMPATIBILITY_FLAG = "cli.install-path-compatibility";
3
+ export const INSTALL_PATH_COMPATIBILITY_FALLBACK = false;
4
+ /**
5
+ * Read the default-off rollout flag from the process snapshot. Missing,
6
+ * unreadable, or not-yet-loaded registry state keeps the compatibility fixes
7
+ * disabled until the operator enables a canary.
8
+ */
9
+ export function installPathCompatibilityEnabled() {
10
+ return resolveProcessFlagGate(INSTALL_PATH_COMPATIBILITY_FLAG, {}, () => INSTALL_PATH_COMPATIBILITY_FALLBACK);
11
+ }
12
+ //# sourceMappingURL=install-path-compatibility-flag.js.map
@@ -10,13 +10,12 @@
10
10
  import * as fs from "node:fs";
11
11
  import { probeProviderCompactionsSince } from "./compaction.js";
12
12
  import { tryReadLane } from "./store.js";
13
- import { type LaneCompanyRef, type LaneRecord, type LaneState, type QuestionRecord } from "./types.js";
13
+ import { type LaneRecord, type LaneState, type QuestionRecord } from "./types.js";
14
14
  export declare const DROPBOX_DIR = "dropbox";
15
15
  export declare const INBOX_NAME = "inbox.jsonl";
16
16
  export declare const DRAINING_DIR = "draining";
17
17
  export declare const DELIVERED_DIR = "delivered";
18
18
  export declare const INFLIGHT_DIR = "inflight";
19
- export { LANE_MESSAGE_COMPACTION_REPLAY_FLAG } from "./message-replay.js";
20
19
  export interface DropboxMessage {
21
20
  at?: string;
22
21
  message_id?: string;
@@ -59,10 +58,6 @@ export interface LaneDrainHookInput {
59
58
  hook_event_name?: string;
60
59
  }
61
60
  export interface RunDrainHookDeps {
62
- readFlag?: (flag: string, options: {
63
- company?: LaneCompanyRef | null;
64
- env: NodeJS.ProcessEnv;
65
- }) => Promise<boolean>;
66
61
  readLane?: typeof tryReadLane;
67
62
  probeCompactionsSince?: typeof probeProviderCompactionsSince;
68
63
  }
@@ -12,8 +12,7 @@ import * as fs from "node:fs";
12
12
  import * as path from "node:path";
13
13
  import { probeProviderCompactionsSince, providerTranscriptSize, } from "./compaction.js";
14
14
  import { LanesError } from "./errors.js";
15
- import { readLanesRegistryFlag } from "./registry-flag.js";
16
- import { LANE_MESSAGE_COMPACTION_REPLAY_FLAG, MESSAGE_COMPACTION_REPLAY_NOTE, MESSAGE_REPLAY_STATE_NAME, } from "./message-replay.js";
15
+ import { MESSAGE_COMPACTION_REPLAY_NOTE, MESSAGE_REPLAY_STATE_NAME, } from "./message-replay.js";
17
16
  import { LANES_RUNS_REL, readProcStarttime } from "./spawn.js";
18
17
  import { tryReadLane } from "./store.js";
19
18
  export const DROPBOX_DIR = "dropbox";
@@ -21,7 +20,6 @@ export const INBOX_NAME = "inbox.jsonl";
21
20
  export const DRAINING_DIR = "draining";
22
21
  export const DELIVERED_DIR = "delivered";
23
22
  export const INFLIGHT_DIR = "inflight";
24
- export { LANE_MESSAGE_COMPACTION_REPLAY_FLAG } from "./message-replay.js";
25
23
  function defaultDrainDeps() {
26
24
  return {
27
25
  mkdirSync: fs.mkdirSync.bind(fs),
@@ -492,8 +490,8 @@ function readReplayState(filePath, turnId) {
492
490
  (state.flag_enabled !== undefined && typeof state.flag_enabled !== "boolean")) {
493
491
  throw new LanesError("message_replay_state_invalid", `Replay state ${filePath} does not match the current provider turn`);
494
492
  }
495
- // Older state files persisted this per-turn cache. Accept it, but remove it
496
- // before the next write so the flag is refreshed for every compaction.
493
+ // Older state files persisted the removed per-turn flag. Accept it, but
494
+ // remove it before the next write so the stale cache does not live on.
497
495
  delete state.flag_enabled;
498
496
  return state;
499
497
  }
@@ -602,12 +600,9 @@ export async function runDrainHook(env, input = {}, deps = {}) {
602
600
  }
603
601
  else if (delta.count > 0) {
604
602
  state.last_compaction_marker += delta.count;
605
- const flagEnabled = await (deps.readFlag ?? readLanesRegistryFlag)(LANE_MESSAGE_COMPACTION_REPLAY_FLAG, { company: lane.company, env });
606
- if (flagEnabled) {
607
- replayed = deliveredMessagesForTurn(hqRoot, laneId, turnId);
608
- if (replayed.length > 0)
609
- replayNote = MESSAGE_COMPACTION_REPLAY_NOTE;
610
- }
603
+ replayed = deliveredMessagesForTurn(hqRoot, laneId, turnId);
604
+ if (replayed.length > 0)
605
+ replayNote = MESSAGE_COMPACTION_REPLAY_NOTE;
611
606
  }
612
607
  }
613
608
  }
@@ -1,4 +1,3 @@
1
- export declare const LANE_MESSAGE_COMPACTION_REPLAY_FLAG = "lanes.message-compaction-replay";
2
1
  export declare const MESSAGE_REPLAY_STATE_NAME = "message-compaction-replay.json";
3
2
  export declare const MESSAGE_COMPACTION_REPLAY_NOTE = "These messages were delivered before a context compaction and still apply.";
4
3
  export interface MessageReplayState {
@@ -1,4 +1,3 @@
1
- export const LANE_MESSAGE_COMPACTION_REPLAY_FLAG = "lanes.message-compaction-replay";
2
1
  export const MESSAGE_REPLAY_STATE_NAME = "message-compaction-replay.json";
3
2
  export const MESSAGE_COMPACTION_REPLAY_NOTE = "These messages were delivered before a context compaction and still apply.";
4
3
  //# sourceMappingURL=message-replay.js.map
package/dist/main.js CHANGED
@@ -180,12 +180,15 @@ program.hook("preAction", async () => {
180
180
  export async function runCli() {
181
181
  const foregroundNetworkBudget = new ForegroundNetworkBudget();
182
182
  activeForegroundNetworkBudget = foregroundNetworkBudget;
183
- // Begin one registry refresh without awaiting it. Every command gate reads
184
- // only the client's held snapshot and falls back locally, so an offline or
185
- // slow registry cannot delay command parsing or alter a command failure.
186
- runGlobalNetworkLifecycle(process.argv, () => {
187
- void kickFlagRegistryReadiness();
188
- });
183
+ // Login must be usable before any identity or company is available, so do
184
+ // not make a pre-auth feature-flag request on this path. Other commands
185
+ // retain the shared asynchronous registry refresh.
186
+ const isAuthLogin = process.argv[2] === "auth" && process.argv[3] === "login";
187
+ if (!isAuthLogin) {
188
+ runGlobalNetworkLifecycle(process.argv, () => {
189
+ void kickFlagRegistryReadiness();
190
+ });
191
+ }
189
192
  // Set when a self-update re-exec'd this command on a newer CLI: the child
190
193
  // already did the work, so this process only has to carry its exit status
191
194
  // out (after the finally block's telemetry, hence not process.exit here).
@@ -5,7 +5,14 @@ declare const PROVIDER_TO_COGNITO_IDP: {
5
5
  readonly picker: undefined;
6
6
  };
7
7
  export type LoginProvider = keyof typeof PROVIDER_TO_COGNITO_IDP;
8
+ export type RememberedLoginProvider = Exclude<LoginProvider, "picker">;
9
+ export interface LoginProviderSelection {
10
+ config: CognitoAuthConfig;
11
+ providerToRemember?: RememberedLoginProvider;
12
+ }
8
13
  export declare function parseLoginProvider(value: string | undefined): LoginProvider | undefined;
9
14
  export declare function cognitoConfigForLoginProvider(value: string | undefined): CognitoAuthConfig;
15
+ /** Resolve CLI, environment, remembered, then first-login choice-page precedence. */
16
+ export declare function resolveLoginProviderSelection(explicitProvider: string | undefined, env: NodeJS.ProcessEnv, rememberedProvider?: RememberedLoginProvider): LoginProviderSelection;
10
17
  export {};
11
18
  //# sourceMappingURL=login-provider.d.ts.map
@@ -22,4 +22,38 @@ export function cognitoConfigForLoginProvider(value) {
22
22
  identityProvider: PROVIDER_TO_COGNITO_IDP[provider],
23
23
  };
24
24
  }
25
+ function rememberedProviderForCognitoProvider(value) {
26
+ const normalized = value?.trim().toLowerCase();
27
+ if (normalized === "google")
28
+ return "google";
29
+ if (normalized === "microsoft" || normalized === "microsoftpersonal")
30
+ return "microsoft";
31
+ return undefined;
32
+ }
33
+ /** Resolve CLI, environment, remembered, then first-login choice-page precedence. */
34
+ export function resolveLoginProviderSelection(explicitProvider, env, rememberedProvider) {
35
+ if (explicitProvider !== undefined) {
36
+ const provider = parseLoginProvider(explicitProvider);
37
+ return {
38
+ config: cognitoConfigForLoginProvider(explicitProvider),
39
+ ...(provider && provider !== "picker" ? { providerToRemember: provider } : {}),
40
+ };
41
+ }
42
+ if (env.HQ_COGNITO_IDENTITY_PROVIDER !== undefined) {
43
+ const identityProvider = env.HQ_COGNITO_IDENTITY_PROVIDER.trim() || undefined;
44
+ return {
45
+ config: { ...DEFAULT_COGNITO, identityProvider },
46
+ ...(rememberedProviderForCognitoProvider(identityProvider)
47
+ ? { providerToRemember: rememberedProviderForCognitoProvider(identityProvider) }
48
+ : {}),
49
+ };
50
+ }
51
+ if (rememberedProvider) {
52
+ return {
53
+ config: cognitoConfigForLoginProvider(rememberedProvider),
54
+ providerToRemember: rememberedProvider,
55
+ };
56
+ }
57
+ return { config: cognitoConfigForLoginProvider("picker") };
58
+ }
25
59
  //# sourceMappingURL=login-provider.js.map
@@ -52,8 +52,8 @@ export declare function npmPrefixFromPackageDir(pkgDir: string): string | null;
52
52
  * on PATH:
53
53
  *
54
54
  * $PNPM_HOME/hq <- shim on PATH
55
- * $PNPM_HOME/global/5/node_modules/@indigoai-us/hq-cli <- symlink
56
- * $PNPM_HOME/global/5/.pnpm/@indigoai-us+hq-cli@5.61.0/node_modules/…
55
+ * pnpm through v10: $PNPM_HOME/global/5/node_modules/@indigoai-us/hq-cli
56
+ * pnpm v11+: $PNPM_HOME/global/v11/<hash>/node_modules/@indigoai-us/hq-cli
57
57
  *
58
58
  * Both the symlinked and the resolved (`.pnpm`) form are recognised, because
59
59
  * whether `import.meta.url` reports the link or its target depends on how node
@@ -67,7 +67,7 @@ export declare function npmPrefixFromPackageDir(pkgDir: string): string | null;
67
67
  * i.e. the gate would re-fire and re-install globally on every subsequent run.
68
68
  * Those layouts fall through to the ordinary npm-prefix/manual handling.
69
69
  */
70
- export declare function isPnpmManagedPackageDir(pkgDir: string): boolean;
70
+ export declare function isPnpmManagedPackageDir(pkgDir: string, acceptVersionPrefixedGlobalDir?: boolean): boolean;
71
71
  /** Whether the running package lives inside Bun's global install tree. */
72
72
  export declare function isBunManagedPackageDir(pkgDir: string): boolean;
73
73
  /**
@@ -28,6 +28,7 @@
28
28
  * to silence both check + gate).
29
29
  */
30
30
  import { spawnSync } from "node:child_process";
31
+ import { installPathCompatibilityEnabled } from "../lib/install-path-compatibility-flag.js";
31
32
  import { buildSpawnPlan, quoteForWindowsShell } from "./windows-spawn.js";
32
33
  import { accessSync, closeSync, constants as fsConstants, existsSync, mkdtempSync, openSync, readdirSync, readFileSync, rmSync, } from "node:fs";
33
34
  import os from "node:os";
@@ -93,8 +94,8 @@ function pnpmHomeGlobalRoot() {
93
94
  * on PATH:
94
95
  *
95
96
  * $PNPM_HOME/hq <- shim on PATH
96
- * $PNPM_HOME/global/5/node_modules/@indigoai-us/hq-cli <- symlink
97
- * $PNPM_HOME/global/5/.pnpm/@indigoai-us+hq-cli@5.61.0/node_modules/…
97
+ * pnpm through v10: $PNPM_HOME/global/5/node_modules/@indigoai-us/hq-cli
98
+ * pnpm v11+: $PNPM_HOME/global/v11/<hash>/node_modules/@indigoai-us/hq-cli
98
99
  *
99
100
  * Both the symlinked and the resolved (`.pnpm`) form are recognised, because
100
101
  * whether `import.meta.url` reports the link or its target depends on how node
@@ -108,18 +109,26 @@ function pnpmHomeGlobalRoot() {
108
109
  * i.e. the gate would re-fire and re-install globally on every subsequent run.
109
110
  * Those layouts fall through to the ordinary npm-prefix/manual handling.
110
111
  */
111
- export function isPnpmManagedPackageDir(pkgDir) {
112
+ export function isPnpmManagedPackageDir(pkgDir, acceptVersionPrefixedGlobalDir = false) {
112
113
  const normalized = pkgDir.replace(/\\/g, "/").replace(/\/+$/, "");
113
114
  const segments = normalized.split("/").filter(Boolean);
114
- // `global/<digits>` — pnpm's global root, in either the symlinked or the
115
- // resolved (`.pnpm`) form, and independent of where PNPM_HOME points.
115
+ // pnpm through v10 uses `global/<digits>`; pnpm v11+ uses
116
+ // `global/v<major>`. Keep the newer form behind the default-off rollout
117
+ // flag, and require a documented version segment so `global/next` or a
118
+ // `v11` directory elsewhere cannot be mistaken for a global install.
116
119
  for (let i = 0; i < segments.length - 1; i += 1) {
117
- if (segments[i] === "global" && /^\d+$/.test(segments[i + 1]))
120
+ if (segments[i] !== "global")
121
+ continue;
122
+ const globalDirVersion = segments[i + 1];
123
+ if (/^\d+$/.test(globalDirVersion))
118
124
  return true;
125
+ if (acceptVersionPrefixedGlobalDir && /^v\d+$/.test(globalDirVersion)) {
126
+ return true;
127
+ }
119
128
  }
120
- // Belt-and-braces for a future pnpm layout whose store dir is not numeric:
121
- // anything under `$PNPM_HOME/global` is global by construction. Still keyed
122
- // on the `global` segment so `$PNPM_HOME/store/**` (dlx caches) stays out.
129
+ // PNPM_HOME/global is pnpm's configured global root by construction, so
130
+ // keep this fallback in both flag states. The flag above gates only the new
131
+ // documented global/v<major> segment match outside that configured root.
123
132
  const globalRoot = pnpmHomeGlobalRoot();
124
133
  if (globalRoot) {
125
134
  if (normalized === globalRoot || normalized.startsWith(`${globalRoot}/`)) {
@@ -246,7 +255,7 @@ export function resolveRunningInstall() {
246
255
  const packageRoot = findRunningPackageRoot();
247
256
  if (!packageRoot)
248
257
  return { manager: "npm", prefix: null, packageRoot: null };
249
- if (isPnpmManagedPackageDir(packageRoot)) {
258
+ if (isPnpmManagedPackageDir(packageRoot, installPathCompatibilityEnabled())) {
250
259
  return { manager: "pnpm", prefix: null, packageRoot };
251
260
  }
252
261
  if (isBunManagedPackageDir(packageRoot)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.241.0",
3
+ "version": "5.243.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -34,6 +34,7 @@
34
34
  "test:db": "vitest run src/lib/db test/commands/db.test.ts test/commands/db-tenant-isolation.test.ts",
35
35
  "test:e2e": "vitest run --config vitest.e2e.config.ts",
36
36
  "coverage": "vitest run --coverage --coverage.reporter=text-summary --coverage.reporter=json-summary",
37
+ "check:lanes-module-cycles": "node scripts/check-lanes-module-cycles.mjs",
37
38
  "vitest": "vitest",
38
39
  "clean": "rm -rf dist",
39
40
  "postinstall": "node scripts/mesh-daemon-if-stale.mjs || true",