@indigoai-us/hq-cli 5.71.0 → 5.72.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 (115) hide show
  1. package/dist/bin/hq-auth-refresh.js +1 -4
  2. package/dist/cli-version.js +1 -4
  3. package/dist/commands/__fixtures__/make-tar.js +1 -4
  4. package/dist/commands/_patterns.js +1 -4
  5. package/dist/commands/add.js +1 -4
  6. package/dist/commands/agents.js +1 -4
  7. package/dist/commands/api-keys.js +1 -4
  8. package/dist/commands/auth.js +1 -4
  9. package/dist/commands/billing.js +1 -4
  10. package/dist/commands/channels.js +1 -4
  11. package/dist/commands/cloud-demote.js +1 -4
  12. package/dist/commands/cloud-provision.js +1 -4
  13. package/dist/commands/cloud.js +1 -4
  14. package/dist/commands/company.js +1 -4
  15. package/dist/commands/creators.js +1 -4
  16. package/dist/commands/crm.js +1 -4
  17. package/dist/commands/db-migrate.js +1 -4
  18. package/dist/commands/db-provision.js +1 -4
  19. package/dist/commands/db-sql.js +1 -4
  20. package/dist/commands/db-status.js +1 -4
  21. package/dist/commands/db.js +1 -4
  22. package/dist/commands/dm.js +1 -4
  23. package/dist/commands/feedback.js +1 -4
  24. package/dist/commands/files-browse.js +1 -4
  25. package/dist/commands/files.js +1 -4
  26. package/dist/commands/group-grants.js +1 -4
  27. package/dist/commands/groups.js +1 -4
  28. package/dist/commands/integrations.js +1 -4
  29. package/dist/commands/list.js +1 -4
  30. package/dist/commands/login.js +1 -4
  31. package/dist/commands/logout.js +1 -4
  32. package/dist/commands/mcp-registration.js +1 -4
  33. package/dist/commands/mcp-status.js +1 -4
  34. package/dist/commands/meetings.js +1 -4
  35. package/dist/commands/members.js +1 -4
  36. package/dist/commands/onboard-identity-guard.js +1 -4
  37. package/dist/commands/onboard-join.js +1 -4
  38. package/dist/commands/onboard-warning.js +1 -4
  39. package/dist/commands/onboard.js +1 -4
  40. package/dist/commands/outposts.d.ts +23 -1
  41. package/dist/commands/outposts.js +172 -8
  42. package/dist/commands/pack-install.js +1 -4
  43. package/dist/commands/packs.js +1 -4
  44. package/dist/commands/people.js +1 -4
  45. package/dist/commands/pkg-install.js +1 -4
  46. package/dist/commands/pkg-list.js +1 -4
  47. package/dist/commands/pkg-remove.js +1 -4
  48. package/dist/commands/pkg-update.js +1 -4
  49. package/dist/commands/publish.js +1 -4
  50. package/dist/commands/reindex.js +1 -4
  51. package/dist/commands/rescue.js +1 -4
  52. package/dist/commands/run.js +1 -4
  53. package/dist/commands/safe-extract.js +1 -4
  54. package/dist/commands/secrets-scope.js +1 -4
  55. package/dist/commands/secrets.js +1 -4
  56. package/dist/commands/signals.js +1 -4
  57. package/dist/commands/skill.js +1 -4
  58. package/dist/commands/sources.js +1 -4
  59. package/dist/commands/sync-mode.js +1 -4
  60. package/dist/commands/sync-narrow.js +1 -4
  61. package/dist/commands/sync.js +1 -4
  62. package/dist/commands/team-sync.js +1 -4
  63. package/dist/commands/update.js +1 -4
  64. package/dist/commands/whoami.js +1 -4
  65. package/dist/commands/workers.js +1 -4
  66. package/dist/index.js +1 -4
  67. package/dist/lib/db/control-plane.js +1 -4
  68. package/dist/lib/db/local.js +1 -4
  69. package/dist/lib/db/migrate.js +1 -4
  70. package/dist/lib/db/paths.js +1 -4
  71. package/dist/lib/db/remote-engine.js +1 -4
  72. package/dist/lib/db/remote-sql.js +1 -4
  73. package/dist/lib/db/sql.js +1 -4
  74. package/dist/lib/local-tree-diff.js +1 -4
  75. package/dist/lib/narrow-hint-banner.js +1 -4
  76. package/dist/main.js +1 -4
  77. package/dist/node-preflight.js +1 -4
  78. package/dist/run/discover-schemas.js +1 -4
  79. package/dist/run/hq-plugin.js +1 -4
  80. package/dist/sentry-before-send.js +1 -4
  81. package/dist/sentry-dsn.generated.js +1 -4
  82. package/dist/sentry.js +1 -4
  83. package/dist/strategies/link.js +1 -4
  84. package/dist/strategies/merge.js +1 -4
  85. package/dist/types.js +1 -4
  86. package/dist/utils/billing-gate.js +1 -4
  87. package/dist/utils/breadcrumb-buffer.js +1 -4
  88. package/dist/utils/cli-telemetry.js +1 -4
  89. package/dist/utils/cognito-session.js +1 -4
  90. package/dist/utils/contribution-table.js +1 -4
  91. package/dist/utils/environmental-error.js +1 -4
  92. package/dist/utils/epipe.js +1 -4
  93. package/dist/utils/feedback-diagnostics.js +1 -4
  94. package/dist/utils/feedback-screenshots.js +1 -4
  95. package/dist/utils/feedback-versions.js +1 -4
  96. package/dist/utils/git.js +1 -4
  97. package/dist/utils/integrity.js +1 -4
  98. package/dist/utils/intercepted-process-exit.js +1 -4
  99. package/dist/utils/login-provider.js +1 -4
  100. package/dist/utils/manifest.js +1 -4
  101. package/dist/utils/pack-contributions.js +1 -4
  102. package/dist/utils/people.js +1 -4
  103. package/dist/utils/registry-client.js +1 -4
  104. package/dist/utils/registry.js +1 -4
  105. package/dist/utils/sandbox-runner-client.js +1 -4
  106. package/dist/utils/secrets-cache.js +1 -4
  107. package/dist/utils/sentry-identity.js +1 -4
  108. package/dist/utils/vault-api.js +1 -4
  109. package/dist/utils/version-check.js +1 -4
  110. package/dist/utils/version-gate.js +1 -4
  111. package/package.json +3 -2
  112. package/scripts/chmod-bins.mjs +49 -0
  113. package/src/commands/outposts-replica-sync.test.ts +272 -0
  114. package/src/commands/outposts.ts +258 -5
  115. package/src/packaging.test.ts +55 -0
@@ -1,5 +1,3 @@
1
-
2
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="1ca7cb4e-47e2-5cd0-a400-bbdaafa2b0d5")}catch(e){}}();
3
1
  import { loadCachedTokens, isMachineIdentity, loadMachineCreds, } from "@indigoai-us/hq-cloud";
4
2
  /** Decode the (untrusted) id-token payload — for attribution/display only. */
5
3
  function peekIdToken(idToken) {
@@ -39,5 +37,4 @@ export function getCachedSentryUser() {
39
37
  return null;
40
38
  }
41
39
  }
42
- //# sourceMappingURL=sentry-identity.js.map
43
- //# debugId=1ca7cb4e-47e2-5cd0-a400-bbdaafa2b0d5
40
+ //# sourceMappingURL=sentry-identity.js.map
@@ -1,5 +1,3 @@
1
-
2
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="6f96bd3e-9a86-54ba-ad09-951c57afac62")}catch(e){}}();
3
1
  import { DEFAULT_VAULT_API_URL } from './cognito-session.js';
4
2
  import { Sentry } from '../sentry.js';
5
3
  export async function vaultApiFetch(opts) {
@@ -229,5 +227,4 @@ export async function getEntityUid(token, opts) {
229
227
  }
230
228
  return getCompanyUid(token, opts.companySlug);
231
229
  }
232
- //# sourceMappingURL=vault-api.js.map
233
- //# debugId=6f96bd3e-9a86-54ba-ad09-951c57afac62
230
+ //# sourceMappingURL=vault-api.js.map
@@ -1,5 +1,3 @@
1
-
2
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="f7058fc6-251e-5e11-af6c-de15f6e256f3")}catch(e){}}();
3
1
  import * as fs from "fs";
4
2
  import * as os from "os";
5
3
  import * as path from "path";
@@ -152,5 +150,4 @@ export const __test__ = {
152
150
  CACHE_TTL_MS,
153
151
  isKnownNoninteractiveStatusProbe,
154
152
  };
155
- //# sourceMappingURL=version-check.js.map
156
- //# debugId=f7058fc6-251e-5e11-af6c-de15f6e256f3
153
+ //# sourceMappingURL=version-check.js.map
@@ -27,8 +27,6 @@
27
27
  * Opt-out: `HQ_NO_UPDATE_CHECK=1` (same env as `version-check.ts` — one knob
28
28
  * to silence both check + gate).
29
29
  */
30
-
31
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="00fa2aa9-f5d1-57a7-92db-c441c6bef906")}catch(e){}}();
32
30
  import { spawnSync } from "node:child_process";
33
31
  import { readFileSync } from "node:fs";
34
32
  import path from "node:path";
@@ -278,5 +276,4 @@ export const __test__ = {
278
276
  runUpdateCommand,
279
277
  resolveRunningPrefix,
280
278
  };
281
- //# sourceMappingURL=version-gate.js.map
282
- //# debugId=00fa2aa9-f5d1-57a7-92db-c441c6bef906
279
+ //# sourceMappingURL=version-gate.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.71.0",
3
+ "version": "5.72.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -8,7 +8,8 @@
8
8
  "hq-auth-refresh": "dist/bin/hq-auth-refresh.js"
9
9
  },
10
10
  "scripts": {
11
- "build": "node scripts/generate-dsn.mjs && tsc",
11
+ "build": "node scripts/generate-dsn.mjs && tsc && node scripts/chmod-bins.mjs",
12
+ "prepublishOnly": "npm run build",
12
13
  "typecheck": "tsc --noEmit",
13
14
  "lint": "eslint .",
14
15
  "test": "vitest run",
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Make every declared `bin` executable after `tsc` emits it.
4
+ *
5
+ * WHY: the build is plain `tsc`, and TypeScript does NOT set the executable
6
+ * bit on its output — `dist/index.js` lands as 0644 despite carrying a
7
+ * `#!/usr/bin/env node` shebang. Most `npm install` paths re-chmod bin targets
8
+ * to 0755 while linking, but not all do: a root global install on a fresh
9
+ * Amazon Linux outpost (`npm install -g @indigoai-us/hq-cli`) has been observed
10
+ * to leave the linked target at 0644, so `/usr/bin/hq` resolves to a
11
+ * non-executable file and EVERY `hq …` invocation on the box dies with
12
+ * "Permission denied" — the outpost never becomes CLI-ready.
13
+ *
14
+ * Fixing it here means the published tarball ships its bins already 0755, so
15
+ * the runnable bit no longer depends on the installer re-chmod'ing. Runs from
16
+ * `build` (local + CI) and `prepublishOnly` (guarantees a fresh publish is
17
+ * always executable, whatever state the publisher's dist was in).
18
+ *
19
+ * Cross-platform: uses fs.chmod (no `chmod` shell dependency); a no-op on
20
+ * Windows where the mode bits are ignored anyway.
21
+ */
22
+ import { chmodSync, existsSync, readFileSync } from "node:fs";
23
+ import { dirname, resolve } from "node:path";
24
+ import { fileURLToPath } from "node:url";
25
+
26
+ const here = dirname(fileURLToPath(import.meta.url));
27
+ const pkgPath = resolve(here, "..", "package.json");
28
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
29
+
30
+ const bins =
31
+ typeof pkg.bin === "string"
32
+ ? { [pkg.name]: pkg.bin }
33
+ : (pkg.bin ?? {});
34
+
35
+ let touched = 0;
36
+ for (const [name, relPath] of Object.entries(bins)) {
37
+ const target = resolve(here, "..", relPath);
38
+ if (!existsSync(target)) {
39
+ // Build not run yet / stale bin map — surface it loudly so a publish that
40
+ // would ship a missing bin fails fast instead of silently.
41
+ console.error(`chmod-bins: declared bin "${name}" not found at ${relPath}`);
42
+ process.exitCode = 1;
43
+ continue;
44
+ }
45
+ chmodSync(target, 0o755);
46
+ touched += 1;
47
+ }
48
+
49
+ console.log(`chmod-bins: made ${touched} bin(s) executable (0755)`);
@@ -0,0 +1,272 @@
1
+ import { Command } from "commander";
2
+ import {
3
+ afterEach,
4
+ beforeEach,
5
+ describe,
6
+ expect,
7
+ it,
8
+ vi,
9
+ type MockInstance,
10
+ } from "vitest";
11
+ import {
12
+ parseReposManifest,
13
+ registerOutpostsCommand,
14
+ type ReplicaSyncDependencies,
15
+ } from "./outposts.js";
16
+
17
+ const HQ_ROOT = "/home/ec2-user/hq";
18
+ const SECRET = "refresh-token-that-must-never-be-printed";
19
+
20
+ const REPOS_YAML = `repos:
21
+ - url: https://github.com/indigoai-us/hq-pro.git
22
+ visibility: private
23
+ name: hq-pro
24
+ - url: https://github.com/indigoai-us/public-widget.git
25
+ visibility: public
26
+ - url: https://github.com/indigoai-us/hq-cli.git
27
+ visibility: private
28
+ name: hq-cli
29
+ `;
30
+
31
+ type SpawnCall = {
32
+ command: string;
33
+ args: string[];
34
+ options: Parameters<ReplicaSyncDependencies["spawnSync"]>[2];
35
+ };
36
+
37
+ function ok(stdout = "") {
38
+ return {
39
+ status: 0,
40
+ stdout,
41
+ stderr: "",
42
+ error: undefined,
43
+ } as ReturnType<ReplicaSyncDependencies["spawnSync"]>;
44
+ }
45
+
46
+ function replicaDependencies(input: {
47
+ session?: { refreshToken?: string; idToken?: string } | undefined;
48
+ reposYaml?: string | null;
49
+ existing?: Set<string>;
50
+ failRescue?: boolean;
51
+ failAuthRefresh?: boolean;
52
+ } = {}): {
53
+ deps: Partial<ReplicaSyncDependencies>;
54
+ calls: SpawnCall[];
55
+ mkdirp: MockInstance;
56
+ } {
57
+ const calls: SpawnCall[] = [];
58
+ const existing = input.existing ?? new Set<string>();
59
+ const mkdirp = vi.fn();
60
+ const deps: Partial<ReplicaSyncDependencies> = {
61
+ defaultHqRoot: () => HQ_ROOT,
62
+ loadCachedTokens: () =>
63
+ "session" in input ? input.session : { refreshToken: SECRET },
64
+ readTextFile: (file: string) => {
65
+ if (file.endsWith("repos.yaml")) {
66
+ if (input.reposYaml === null) throw new Error("ENOENT");
67
+ return input.reposYaml ?? REPOS_YAML;
68
+ }
69
+ throw new Error(`unexpected readTextFile(${file})`);
70
+ },
71
+ pathExists: (p: string) => existing.has(p),
72
+ mkdirp: mkdirp as unknown as ReplicaSyncDependencies["mkdirp"],
73
+ spawnSync: ((command, args, options) => {
74
+ calls.push({ command, args, options });
75
+ if (input.failRescue && command === "hq" && args[0] === "rescue") {
76
+ return { ...ok(), status: 1 };
77
+ }
78
+ if (
79
+ input.failAuthRefresh &&
80
+ command === "hq" &&
81
+ args[0] === "auth" &&
82
+ args[1] === "refresh"
83
+ ) {
84
+ return { ...ok(), status: 1 };
85
+ }
86
+ return ok();
87
+ }) as ReplicaSyncDependencies["spawnSync"],
88
+ };
89
+ return { deps, calls, mkdirp };
90
+ }
91
+
92
+ function buildProgram(deps: Partial<ReplicaSyncDependencies>): Command {
93
+ const program = new Command();
94
+ program.name("hq").exitOverride();
95
+ registerOutpostsCommand(program, deps);
96
+ return program;
97
+ }
98
+
99
+ async function run(
100
+ deps: Partial<ReplicaSyncDependencies>,
101
+ args: string[],
102
+ ): Promise<void> {
103
+ await buildProgram(deps).parseAsync(["node", "hq", ...args]);
104
+ }
105
+
106
+ let logSpy: MockInstance<typeof console.log>;
107
+ let warnSpy: MockInstance<typeof console.warn>;
108
+ let fetchSpy: MockInstance<typeof fetch>;
109
+
110
+ beforeEach(() => {
111
+ logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
112
+ warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
113
+ fetchSpy = vi.spyOn(globalThis, "fetch");
114
+ });
115
+
116
+ afterEach(() => {
117
+ vi.restoreAllMocks();
118
+ });
119
+
120
+ function printed(): string {
121
+ return [
122
+ ...logSpy.mock.calls.map((c) => c.map(String).join(" ")),
123
+ ...warnSpy.mock.calls.map((c) => c.map(String).join(" ")),
124
+ ].join("\n");
125
+ }
126
+
127
+ function hqArgs(calls: SpawnCall[]): string[][] {
128
+ return calls.filter((c) => c.command === "hq").map((c) => c.args);
129
+ }
130
+
131
+ describe("parseReposManifest", () => {
132
+ it("resolves visibility + name, defaulting to public + URL basename", () => {
133
+ expect(parseReposManifest(REPOS_YAML)).toEqual([
134
+ { url: "https://github.com/indigoai-us/hq-pro.git", visibility: "private", name: "hq-pro" },
135
+ { url: "https://github.com/indigoai-us/public-widget.git", visibility: "public", name: "public-widget" },
136
+ { url: "https://github.com/indigoai-us/hq-cli.git", visibility: "private", name: "hq-cli" },
137
+ ]);
138
+ });
139
+
140
+ it("returns [] for junk, empty, or repo-less YAML", () => {
141
+ expect(parseReposManifest("not: valid: [")).toEqual([]);
142
+ expect(parseReposManifest("")).toEqual([]);
143
+ expect(parseReposManifest("other: 1")).toEqual([]);
144
+ });
145
+
146
+ it("skips entries with no url", () => {
147
+ expect(parseReposManifest("repos:\n - visibility: public\n - url: https://x/y.git")).toEqual([
148
+ { url: "https://x/y.git", visibility: "public", name: "y" },
149
+ ]);
150
+ });
151
+
152
+ it("rejects names that would escape repos/<visibility>/ (path traversal)", () => {
153
+ const yaml =
154
+ "repos:\n" +
155
+ " - url: https://x/evil.git\n name: ../../etc\n" +
156
+ " - url: https://x/evil2.git\n name: sub/dir\n" +
157
+ " - url: https://x/ok.git\n name: ok\n";
158
+ expect(parseReposManifest(yaml)).toEqual([
159
+ { url: "https://x/ok.git", visibility: "public", name: "ok" },
160
+ ]);
161
+ });
162
+ });
163
+
164
+ describe("hq outposts replica-sync", () => {
165
+ it("is hidden from outposts help but remains invokable", async () => {
166
+ const { deps } = replicaDependencies();
167
+ const program = buildProgram(deps);
168
+ const outposts = program.commands.find((c) => c.name() === "outposts");
169
+ expect(outposts?.helpInformation()).not.toContain("replica-sync");
170
+ await run(deps, ["outposts", "replica-sync"]);
171
+ });
172
+
173
+ it("no HQ session → replicates nothing and runs no commands", async () => {
174
+ const { deps, calls } = replicaDependencies({ session: undefined });
175
+ await run(deps, ["outposts", "replica-sync"]);
176
+ expect(calls).toEqual([]);
177
+ expect(printed()).toContain("nothing to replicate");
178
+ });
179
+
180
+ it("runs the full replica chain in order on a signed-in box", async () => {
181
+ const { deps, calls, mkdirp } = replicaDependencies();
182
+ await run(deps, ["outposts", "replica-sync"]);
183
+
184
+ const args = hqArgs(calls);
185
+ expect(args).toContainEqual(["auth", "refresh"]);
186
+ // The pull is pinned to the SAME --hq-root the command was invoked with,
187
+ // not the default root.
188
+ expect(args).toContainEqual([
189
+ "sync",
190
+ "pull",
191
+ "--personal",
192
+ "--hq-root",
193
+ HQ_ROOT,
194
+ "--on-conflict",
195
+ "keep",
196
+ ]);
197
+ expect(args).toContainEqual(["rescue", "--hq-root", HQ_ROOT, "--yes"]);
198
+ // rescue needs companies/ to pre-exist.
199
+ expect(mkdirp).toHaveBeenCalledWith(`${HQ_ROOT}/companies`);
200
+ });
201
+
202
+ it("clones only the missing repos, gh-authing via the vault for privates", async () => {
203
+ const { deps, calls } = replicaDependencies({
204
+ // hq-cli already present → should be skipped.
205
+ existing: new Set([`${HQ_ROOT}/repos/private/hq-cli/.git`]),
206
+ });
207
+ await run(deps, ["outposts", "replica-sync"]);
208
+
209
+ const clones = calls
210
+ .filter((c) => c.command === "git" && c.args[0] === "clone")
211
+ .map((c) => c.args[c.args.length - 1]);
212
+ expect(clones).toContain(`${HQ_ROOT}/repos/private/hq-pro`);
213
+ expect(clones).toContain(`${HQ_ROOT}/repos/public/public-widget`);
214
+ expect(clones).not.toContain(`${HQ_ROOT}/repos/private/hq-cli`);
215
+
216
+ // A private repo is missing → gh auth flows through the vault (token in ENV,
217
+ // never argv).
218
+ const vaultAuth = calls.find(
219
+ (c) => c.command === "hq" && c.args[0] === "secrets" && c.args.includes("GITHUB_TOKEN"),
220
+ );
221
+ expect(vaultAuth).toBeTruthy();
222
+ expect(vaultAuth?.args).toContain("--personal");
223
+ expect(vaultAuth?.args).toContain("--only");
224
+ });
225
+
226
+ it("skips the vault gh auth when every missing repo is public", async () => {
227
+ const { deps, calls } = replicaDependencies({
228
+ reposYaml: "repos:\n - url: https://github.com/x/pub.git\n visibility: public\n",
229
+ });
230
+ await run(deps, ["outposts", "replica-sync"]);
231
+ const vaultAuth = calls.find((c) => c.command === "hq" && c.args[0] === "secrets");
232
+ expect(vaultAuth).toBeUndefined();
233
+ });
234
+
235
+ it("no repos.yaml → skips the clone step without error", async () => {
236
+ const { deps, calls } = replicaDependencies({ reposYaml: null });
237
+ await run(deps, ["outposts", "replica-sync"]);
238
+ expect(calls.some((c) => c.command === "git")).toBe(false);
239
+ expect(printed()).toContain("skipping repo clone");
240
+ });
241
+
242
+ it("aborts the cycle when auth refresh fails — no pull, rescue, or clone", async () => {
243
+ const { deps, calls } = replicaDependencies({ failAuthRefresh: true });
244
+ await run(deps, ["outposts", "replica-sync"]);
245
+ const args = hqArgs(calls);
246
+ expect(args).toContainEqual(["auth", "refresh"]);
247
+ expect(args.some((a) => a[0] === "sync")).toBe(false);
248
+ expect(args.some((a) => a[0] === "rescue")).toBe(false);
249
+ expect(calls.some((c) => c.command === "git")).toBe(false);
250
+ expect(printed()).toContain("auth refresh failed");
251
+ });
252
+
253
+ it("a failed step never throws or hard-fails the unit", async () => {
254
+ const { deps } = replicaDependencies({ failRescue: true });
255
+ await expect(run(deps, ["outposts", "replica-sync"])).resolves.toBeUndefined();
256
+ expect(printed()).toContain("HQ kernel rescue did not complete cleanly");
257
+ });
258
+
259
+ it("never leaks the session token or makes hq-pro requests", async () => {
260
+ const { deps, calls } = replicaDependencies();
261
+ await run(deps, ["outposts", "replica-sync"]);
262
+ expect(JSON.stringify(calls)).not.toContain(SECRET);
263
+ expect(printed()).not.toContain(SECRET);
264
+ expect(fetchSpy).not.toHaveBeenCalled();
265
+ // The GITHUB_TOKEN never appears as a bare argv value — only the var name
266
+ // inside the bash script and the --only selector.
267
+ const bashScripts = calls
268
+ .filter((c) => c.command === "hq" && c.args.includes("bash"))
269
+ .flatMap((c) => c.args);
270
+ expect(bashScripts.some((a) => a.includes("gh auth login --with-token"))).toBe(true);
271
+ });
272
+ });