@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.
- package/dist/bin/hq-auth-refresh.js +1 -4
- package/dist/cli-version.js +1 -4
- package/dist/commands/__fixtures__/make-tar.js +1 -4
- package/dist/commands/_patterns.js +1 -4
- package/dist/commands/add.js +1 -4
- package/dist/commands/agents.js +1 -4
- package/dist/commands/api-keys.js +1 -4
- package/dist/commands/auth.js +1 -4
- package/dist/commands/billing.js +1 -4
- package/dist/commands/channels.js +1 -4
- package/dist/commands/cloud-demote.js +1 -4
- package/dist/commands/cloud-provision.js +1 -4
- package/dist/commands/cloud.js +1 -4
- package/dist/commands/company.js +1 -4
- package/dist/commands/creators.js +1 -4
- package/dist/commands/crm.js +1 -4
- package/dist/commands/db-migrate.js +1 -4
- package/dist/commands/db-provision.js +1 -4
- package/dist/commands/db-sql.js +1 -4
- package/dist/commands/db-status.js +1 -4
- package/dist/commands/db.js +1 -4
- package/dist/commands/dm.js +1 -4
- package/dist/commands/feedback.js +1 -4
- package/dist/commands/files-browse.js +1 -4
- package/dist/commands/files.js +1 -4
- package/dist/commands/group-grants.js +1 -4
- package/dist/commands/groups.js +1 -4
- package/dist/commands/integrations.js +1 -4
- package/dist/commands/list.js +1 -4
- package/dist/commands/login.js +1 -4
- package/dist/commands/logout.js +1 -4
- package/dist/commands/mcp-registration.js +1 -4
- package/dist/commands/mcp-status.js +1 -4
- package/dist/commands/meetings.js +1 -4
- package/dist/commands/members.js +1 -4
- package/dist/commands/onboard-identity-guard.js +1 -4
- package/dist/commands/onboard-join.js +1 -4
- package/dist/commands/onboard-warning.js +1 -4
- package/dist/commands/onboard.js +1 -4
- package/dist/commands/outposts.d.ts +23 -1
- package/dist/commands/outposts.js +172 -8
- package/dist/commands/pack-install.js +1 -4
- package/dist/commands/packs.js +1 -4
- package/dist/commands/people.js +1 -4
- package/dist/commands/pkg-install.js +1 -4
- package/dist/commands/pkg-list.js +1 -4
- package/dist/commands/pkg-remove.js +1 -4
- package/dist/commands/pkg-update.js +1 -4
- package/dist/commands/publish.js +1 -4
- package/dist/commands/reindex.js +1 -4
- package/dist/commands/rescue.js +1 -4
- package/dist/commands/run.js +1 -4
- package/dist/commands/safe-extract.js +1 -4
- package/dist/commands/secrets-scope.js +1 -4
- package/dist/commands/secrets.js +1 -4
- package/dist/commands/signals.js +1 -4
- package/dist/commands/skill.js +1 -4
- package/dist/commands/sources.js +1 -4
- package/dist/commands/sync-mode.js +1 -4
- package/dist/commands/sync-narrow.js +1 -4
- package/dist/commands/sync.js +1 -4
- package/dist/commands/team-sync.js +1 -4
- package/dist/commands/update.js +1 -4
- package/dist/commands/whoami.js +1 -4
- package/dist/commands/workers.js +1 -4
- package/dist/index.js +1 -4
- package/dist/lib/db/control-plane.js +1 -4
- package/dist/lib/db/local.js +1 -4
- package/dist/lib/db/migrate.js +1 -4
- package/dist/lib/db/paths.js +1 -4
- package/dist/lib/db/remote-engine.js +1 -4
- package/dist/lib/db/remote-sql.js +1 -4
- package/dist/lib/db/sql.js +1 -4
- package/dist/lib/local-tree-diff.js +1 -4
- package/dist/lib/narrow-hint-banner.js +1 -4
- package/dist/main.js +1 -4
- package/dist/node-preflight.js +1 -4
- package/dist/run/discover-schemas.js +1 -4
- package/dist/run/hq-plugin.js +1 -4
- package/dist/sentry-before-send.js +1 -4
- package/dist/sentry-dsn.generated.js +1 -4
- package/dist/sentry.js +1 -4
- package/dist/strategies/link.js +1 -4
- package/dist/strategies/merge.js +1 -4
- package/dist/types.js +1 -4
- package/dist/utils/billing-gate.js +1 -4
- package/dist/utils/breadcrumb-buffer.js +1 -4
- package/dist/utils/cli-telemetry.js +1 -4
- package/dist/utils/cognito-session.js +1 -4
- package/dist/utils/contribution-table.js +1 -4
- package/dist/utils/environmental-error.js +1 -4
- package/dist/utils/epipe.js +1 -4
- package/dist/utils/feedback-diagnostics.js +1 -4
- package/dist/utils/feedback-screenshots.js +1 -4
- package/dist/utils/feedback-versions.js +1 -4
- package/dist/utils/git.js +1 -4
- package/dist/utils/integrity.js +1 -4
- package/dist/utils/intercepted-process-exit.js +1 -4
- package/dist/utils/login-provider.js +1 -4
- package/dist/utils/manifest.js +1 -4
- package/dist/utils/pack-contributions.js +1 -4
- package/dist/utils/people.js +1 -4
- package/dist/utils/registry-client.js +1 -4
- package/dist/utils/registry.js +1 -4
- package/dist/utils/sandbox-runner-client.js +1 -4
- package/dist/utils/secrets-cache.js +1 -4
- package/dist/utils/sentry-identity.js +1 -4
- package/dist/utils/vault-api.js +1 -4
- package/dist/utils/version-check.js +1 -4
- package/dist/utils/version-gate.js +1 -4
- package/package.json +3 -2
- package/scripts/chmod-bins.mjs +49 -0
- package/src/commands/outposts-replica-sync.test.ts +272 -0
- package/src/commands/outposts.ts +258 -5
- 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
|
package/dist/utils/vault-api.js
CHANGED
|
@@ -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.
|
|
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
|
+
});
|