@indigoai-us/hq-cli 5.68.0 → 5.68.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/dist/commands/outposts.d.ts +14 -0
- package/dist/commands/outposts.js +22 -4
- package/dist/commands/packs.d.ts +4 -4
- package/dist/commands/packs.js +11 -9
- package/dist/commands/pkg-list.d.ts +3 -2
- package/dist/commands/pkg-list.js +7 -92
- package/package.json +1 -1
- package/src/commands/outposts.test.ts +18 -3
- package/src/commands/outposts.ts +24 -2
- package/src/commands/packs.ts +11 -8
- package/src/commands/pkg-list.test.ts +79 -0
- package/src/commands/pkg-list.ts +5 -121
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.68.1]
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **`hq outposts exec` now runs from the box's HQ folder by default.** The
|
|
10
|
+
command was sent with no working directory, so cwd depended on the transport:
|
|
11
|
+
SSM boxes (root, no `$HOME`) ran from `/usr/bin` and Lightsail/SSH boxes ran
|
|
12
|
+
from the login home. `exec -- pwd` printed `/usr/bin` on EC2, and relative
|
|
13
|
+
paths resolved against the wrong place. The command is now wrapped to `cd`
|
|
14
|
+
into the box's HQ checkout first (`$HOME/hq`, falling back to ec2-user's home
|
|
15
|
+
for the SSM/root path), with a `|| true` so exec still runs when no HQ folder
|
|
16
|
+
is present. Applies to both the SSM and SSH-fallback paths.
|
|
17
|
+
|
|
5
18
|
## [5.62.1]
|
|
6
19
|
|
|
7
20
|
### Added
|
|
@@ -94,6 +94,20 @@ export interface OutpostExecResult {
|
|
|
94
94
|
* `step` distinguishes not-ready / platform-unsupported / timeout / ssm.
|
|
95
95
|
*/
|
|
96
96
|
export declare function execOutpost(token: string, command: string, outpostId?: string): Promise<OutpostExecResult>;
|
|
97
|
+
/**
|
|
98
|
+
* Prefix that best-effort `cd`s into the box's HQ checkout before running the
|
|
99
|
+
* caller's command. `exec` runs over two transports with two different default
|
|
100
|
+
* working directories — SSM runs as root with no `$HOME` (cwd `/usr/bin`) and
|
|
101
|
+
* SSH lands in the login user's home — so without this, `hq outposts exec -- pwd`
|
|
102
|
+
* printed an unhelpful, transport-dependent directory. We resolve the HQ folder
|
|
103
|
+
* in-shell: `$HOME/hq` for the login/SSH user, falling back to ec2-user's home
|
|
104
|
+
* for the SSM/root path. The trailing `|| true` keeps the command running from
|
|
105
|
+
* the default directory when no HQ checkout is present, so exec never fails
|
|
106
|
+
* merely because the box has no HQ folder.
|
|
107
|
+
*/
|
|
108
|
+
export declare const REMOTE_HQ_DIR_PREFIX = "cd \"$HOME/hq\" 2>/dev/null || cd ~ec2-user/hq 2>/dev/null || true";
|
|
109
|
+
/** Wrap `command` so it runs from the box's HQ folder (see REMOTE_HQ_DIR_PREFIX). */
|
|
110
|
+
export declare function withRemoteHqDir(command: string): string;
|
|
97
111
|
/** SSH connection details vended by `POST /outpost/ssh-access`. */
|
|
98
112
|
export interface OutpostSshAccess {
|
|
99
113
|
outpostId: string;
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* and status routes that exist. Renaming an Outpost is not a backend capability.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
!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]="
|
|
24
|
+
!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]="c4ce2b27-be72-5025-a4e7-2d4725c67c26")}catch(e){}}();
|
|
25
25
|
import chalk from "chalk";
|
|
26
26
|
import { spawnSync } from "node:child_process";
|
|
27
27
|
import * as fs from "node:fs";
|
|
@@ -138,6 +138,22 @@ export async function execOutpost(token, command, outpostId) {
|
|
|
138
138
|
query: outpostId ? { outpostId } : undefined,
|
|
139
139
|
});
|
|
140
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* Prefix that best-effort `cd`s into the box's HQ checkout before running the
|
|
143
|
+
* caller's command. `exec` runs over two transports with two different default
|
|
144
|
+
* working directories — SSM runs as root with no `$HOME` (cwd `/usr/bin`) and
|
|
145
|
+
* SSH lands in the login user's home — so without this, `hq outposts exec -- pwd`
|
|
146
|
+
* printed an unhelpful, transport-dependent directory. We resolve the HQ folder
|
|
147
|
+
* in-shell: `$HOME/hq` for the login/SSH user, falling back to ec2-user's home
|
|
148
|
+
* for the SSM/root path. The trailing `|| true` keeps the command running from
|
|
149
|
+
* the default directory when no HQ checkout is present, so exec never fails
|
|
150
|
+
* merely because the box has no HQ folder.
|
|
151
|
+
*/
|
|
152
|
+
export const REMOTE_HQ_DIR_PREFIX = 'cd "$HOME/hq" 2>/dev/null || cd ~ec2-user/hq 2>/dev/null || true';
|
|
153
|
+
/** Wrap `command` so it runs from the box's HQ folder (see REMOTE_HQ_DIR_PREFIX). */
|
|
154
|
+
export function withRemoteHqDir(command) {
|
|
155
|
+
return `${REMOTE_HQ_DIR_PREFIX}; ${command}`;
|
|
156
|
+
}
|
|
141
157
|
/**
|
|
142
158
|
* Fetch SSH connection info + key for the caller's Outpost and open the caller's
|
|
143
159
|
* IP on the box's SSH port. Used to reach a Lightsail box (no SSM). Throws
|
|
@@ -365,8 +381,10 @@ export function registerOutpostsCommand(program) {
|
|
|
365
381
|
process.exit(1);
|
|
366
382
|
}
|
|
367
383
|
const token = await ensureCognitoToken();
|
|
384
|
+
// Run from the box's HQ folder by default (works over both SSM and SSH).
|
|
385
|
+
const remoteCommand = withRemoteHqDir(command);
|
|
368
386
|
try {
|
|
369
|
-
const result = await execOutpost(token,
|
|
387
|
+
const result = await execOutpost(token, remoteCommand, opts.id);
|
|
370
388
|
if (opts.json) {
|
|
371
389
|
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
372
390
|
}
|
|
@@ -392,7 +410,7 @@ export function registerOutpostsCommand(program) {
|
|
|
392
410
|
if (err instanceof OutpostHttpError &&
|
|
393
411
|
err.step === "platform-unsupported") {
|
|
394
412
|
const access = await getOutpostSshAccess(token, opts.id);
|
|
395
|
-
const ssh = execViaSsh(access,
|
|
413
|
+
const ssh = execViaSsh(access, remoteCommand);
|
|
396
414
|
if (ssh.error) {
|
|
397
415
|
console.error(chalk.red(`Could not run the command over SSH: ${ssh.error}`));
|
|
398
416
|
process.exit(1);
|
|
@@ -496,4 +514,4 @@ export function registerOutpostsCommand(program) {
|
|
|
496
514
|
});
|
|
497
515
|
}
|
|
498
516
|
//# sourceMappingURL=outposts.js.map
|
|
499
|
-
//# debugId=
|
|
517
|
+
//# debugId=c4ce2b27-be72-5025-a4e7-2d4725c67c26
|
package/dist/commands/packs.d.ts
CHANGED
|
@@ -6,10 +6,9 @@
|
|
|
6
6
|
* hq packs update Re-install the latest of an installed pack.
|
|
7
7
|
* hq packs uninstall Un-wire + archive a pack (clean, no dangling symlinks).
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* is the only clean content-pack op; this adds the rest.
|
|
9
|
+
* Also exposed through `hq packages list`, because package installs use this
|
|
10
|
+
* content-pack system. Content packs have no registry file -- the filesystem
|
|
11
|
+
* under core/packages/ is the source of truth.
|
|
13
12
|
*
|
|
14
13
|
* Every subcommand supports `--json` for machine consumers (the HQ Sync
|
|
15
14
|
* menubar app). JSON is also the default when stdout is not a TTY, matching
|
|
@@ -50,6 +49,7 @@ interface InstalledPackView {
|
|
|
50
49
|
error?: string;
|
|
51
50
|
}
|
|
52
51
|
export declare function buildInstalledView(hqRoot: string, hqVersion: string | null, pack: InstalledPack, installedSources: Set<string>, checkUpdates: boolean, latestProbe?: LatestResult): InstalledPackView;
|
|
52
|
+
export declare function registerPacksListCommand(parent: Command): void;
|
|
53
53
|
export declare function registerPacksCommand(parent: Command): void;
|
|
54
54
|
export {};
|
|
55
55
|
//# sourceMappingURL=packs.d.ts.map
|
package/dist/commands/packs.js
CHANGED
|
@@ -6,10 +6,9 @@
|
|
|
6
6
|
* hq packs update Re-install the latest of an installed pack.
|
|
7
7
|
* hq packs uninstall Un-wire + archive a pack (clean, no dangling symlinks).
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* is the only clean content-pack op; this adds the rest.
|
|
9
|
+
* Also exposed through `hq packages list`, because package installs use this
|
|
10
|
+
* content-pack system. Content packs have no registry file -- the filesystem
|
|
11
|
+
* under core/packages/ is the source of truth.
|
|
13
12
|
*
|
|
14
13
|
* Every subcommand supports `--json` for machine consumers (the HQ Sync
|
|
15
14
|
* menubar app). JSON is also the default when stdout is not a TTY, matching
|
|
@@ -18,7 +17,7 @@
|
|
|
18
17
|
* Spec: knowledge/public/hq-core/package-yaml-spec.md.
|
|
19
18
|
*/
|
|
20
19
|
|
|
21
|
-
!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]="
|
|
20
|
+
!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]="b743f774-8d9b-594b-9ce4-e7f5a44faca0")}catch(e){}}();
|
|
22
21
|
import * as fs from 'fs';
|
|
23
22
|
import * as path from 'path';
|
|
24
23
|
import * as readline from 'readline';
|
|
@@ -368,9 +367,8 @@ async function runUninstall(name, opts) {
|
|
|
368
367
|
// ---------------------------------------------------------------------------
|
|
369
368
|
// Registration
|
|
370
369
|
// ---------------------------------------------------------------------------
|
|
371
|
-
export function
|
|
372
|
-
|
|
373
|
-
packs
|
|
370
|
+
export function registerPacksListCommand(parent) {
|
|
371
|
+
parent
|
|
374
372
|
.command('list')
|
|
375
373
|
.alias('ls')
|
|
376
374
|
.description('List installed content packs and the curated catalog')
|
|
@@ -392,6 +390,10 @@ export function registerPacksCommand(parent) {
|
|
|
392
390
|
process.exit(1);
|
|
393
391
|
}
|
|
394
392
|
});
|
|
393
|
+
}
|
|
394
|
+
export function registerPacksCommand(parent) {
|
|
395
|
+
const packs = parent.command('packs').description('Content-pack lifecycle (install via `hq install`)');
|
|
396
|
+
registerPacksListCommand(packs);
|
|
395
397
|
packs
|
|
396
398
|
.command('update [name]')
|
|
397
399
|
.description('Update an installed content pack (re-install latest)')
|
|
@@ -468,4 +470,4 @@ export function registerPacksCommand(parent) {
|
|
|
468
470
|
});
|
|
469
471
|
}
|
|
470
472
|
//# sourceMappingURL=packs.js.map
|
|
471
|
-
//# debugId=
|
|
473
|
+
//# debugId=b743f774-8d9b-594b-9ce4-e7f5a44faca0
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* hq packages list —
|
|
2
|
+
* hq packages list — list filesystem-backed content packs.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Package installs use the content-pack installer, so this command delegates
|
|
5
|
+
* to the same list view as `hq packs list`.
|
|
5
6
|
*/
|
|
6
7
|
import { Command } from 'commander';
|
|
7
8
|
export declare function registerPackageListCommand(parent: Command): void;
|
|
@@ -1,99 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* hq packages list —
|
|
2
|
+
* hq packages list — list filesystem-backed content packs.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Package installs use the content-pack installer, so this command delegates
|
|
5
|
+
* to the same list view as `hq packs list`.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
|
-
!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]="
|
|
8
|
-
import
|
|
9
|
-
import { resolveDefaultHqRoot } from '../utils/cognito-session.js';
|
|
10
|
-
import { readRegistry } from '../utils/registry.js';
|
|
11
|
-
import { loadCachedTokens, isExpiring } from '@indigoai-us/hq-cloud';
|
|
12
|
-
import { getRegistryUrl, RegistryClient, } from '../utils/registry-client.js';
|
|
8
|
+
!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]="96eb060c-c1d0-5874-918f-e818f92c6242")}catch(e){}}();
|
|
9
|
+
import { registerPacksListCommand } from './packs.js';
|
|
13
10
|
export function registerPackageListCommand(parent) {
|
|
14
|
-
parent
|
|
15
|
-
.command('list')
|
|
16
|
-
.alias('ls')
|
|
17
|
-
.description('List installed and available packages')
|
|
18
|
-
.option('--json', 'Machine-readable JSON output')
|
|
19
|
-
.action(async (opts) => {
|
|
20
|
-
try {
|
|
21
|
-
await listPackages(opts.json === true || !process.stdout.isTTY);
|
|
22
|
-
}
|
|
23
|
-
catch (error) {
|
|
24
|
-
console.error(chalk.red('List failed:'), error instanceof Error ? error.message : 'Unknown error');
|
|
25
|
-
process.exit(1);
|
|
26
|
-
}
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
async function gatherRegistryPackages() {
|
|
30
|
-
const hqRoot = resolveDefaultHqRoot({ onMissing: 'throw' });
|
|
31
|
-
const installed = readRegistry(hqRoot);
|
|
32
|
-
let entitlements = [];
|
|
33
|
-
let offline = false;
|
|
34
|
-
try {
|
|
35
|
-
const cached = loadCachedTokens();
|
|
36
|
-
if (cached && !isExpiring(cached, 120)) {
|
|
37
|
-
const client = new RegistryClient(getRegistryUrl(), cached.accessToken);
|
|
38
|
-
const result = await client.getMyEntitlements();
|
|
39
|
-
entitlements = result.entitlements;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
catch {
|
|
43
|
-
offline = true;
|
|
44
|
-
}
|
|
45
|
-
const installedSlugs = new Set(installed.map((p) => p.slug));
|
|
46
|
-
const available = entitlements.filter((e) => !installedSlugs.has(e.slug));
|
|
47
|
-
return { installed, available, offline };
|
|
48
|
-
}
|
|
49
|
-
async function listPackages(json) {
|
|
50
|
-
if (json) {
|
|
51
|
-
const { installed, available, offline } = await gatherRegistryPackages();
|
|
52
|
-
process.stdout.write(JSON.stringify({ installed, available, offline }, null, 2) + '\n');
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
const hqRoot = resolveDefaultHqRoot({ onMissing: 'throw' });
|
|
56
|
-
const installed = readRegistry(hqRoot);
|
|
57
|
-
// Print installed packages
|
|
58
|
-
if (installed.length > 0) {
|
|
59
|
-
console.log(chalk.bold('Installed packages:\n'));
|
|
60
|
-
for (const pkg of installed) {
|
|
61
|
-
const scope = pkg.scope ? chalk.dim(` (${pkg.scope})`) : '';
|
|
62
|
-
console.log(` ${chalk.green(pkg.slug)}@${pkg.version}${scope}`);
|
|
63
|
-
console.log(chalk.dim(` Installed: ${pkg.installed_at.slice(0, 10)} Updated: ${pkg.updated_at.slice(0, 10)}`));
|
|
64
|
-
}
|
|
65
|
-
console.log();
|
|
66
|
-
}
|
|
67
|
-
else {
|
|
68
|
-
console.log(chalk.dim('No packages installed.\n'));
|
|
69
|
-
}
|
|
70
|
-
// Try to show available (but not installed) packages from entitlements
|
|
71
|
-
let entitlements = [];
|
|
72
|
-
let offline = false;
|
|
73
|
-
try {
|
|
74
|
-
const cached = loadCachedTokens();
|
|
75
|
-
if (cached && !isExpiring(cached, 120)) {
|
|
76
|
-
const registryUrl = getRegistryUrl();
|
|
77
|
-
const client = new RegistryClient(registryUrl, cached.accessToken);
|
|
78
|
-
const result = await client.getMyEntitlements();
|
|
79
|
-
entitlements = result.entitlements;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
catch {
|
|
83
|
-
offline = true;
|
|
84
|
-
}
|
|
85
|
-
const installedSlugs = new Set(installed.map((p) => p.slug));
|
|
86
|
-
const available = entitlements.filter((e) => !installedSlugs.has(e.slug));
|
|
87
|
-
if (available.length > 0) {
|
|
88
|
-
console.log(chalk.bold('Available (not installed):\n'));
|
|
89
|
-
for (const ent of available) {
|
|
90
|
-
console.log(` ${chalk.cyan(ent.slug)} ${chalk.dim(`[${ent.tier}]`)} ${chalk.yellow('available')}`);
|
|
91
|
-
}
|
|
92
|
-
console.log(chalk.dim('\nRun "hq packages install <slug>" to install.\n'));
|
|
93
|
-
}
|
|
94
|
-
if (offline) {
|
|
95
|
-
console.log(chalk.yellow('Note: Could not reach registry — showing cached data only.'));
|
|
96
|
-
}
|
|
11
|
+
registerPacksListCommand(parent);
|
|
97
12
|
}
|
|
98
13
|
//# sourceMappingURL=pkg-list.js.map
|
|
99
|
-
//# debugId=
|
|
14
|
+
//# debugId=96eb060c-c1d0-5874-918f-e818f92c6242
|
package/package.json
CHANGED
|
@@ -52,7 +52,7 @@ vi.mock("node:child_process", async (importOriginal) => {
|
|
|
52
52
|
|
|
53
53
|
import { spawnSync } from "node:child_process";
|
|
54
54
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
55
|
-
import { registerOutpostsCommand } from "./outposts.js";
|
|
55
|
+
import { registerOutpostsCommand, withRemoteHqDir } from "./outposts.js";
|
|
56
56
|
|
|
57
57
|
const mockSpawnSync = vi.mocked(spawnSync);
|
|
58
58
|
|
|
@@ -262,6 +262,18 @@ describe("hq outposts exec", () => {
|
|
|
262
262
|
process.exitCode = undefined;
|
|
263
263
|
});
|
|
264
264
|
|
|
265
|
+
it("wraps the command to run from the box's HQ folder, then runs it", () => {
|
|
266
|
+
const wrapped = withRemoteHqDir("pwd");
|
|
267
|
+
// cd into $HOME/hq (SSH/login user) …
|
|
268
|
+
expect(wrapped).toContain('cd "$HOME/hq"');
|
|
269
|
+
// … falling back to ec2-user's home for the SSM/root path (no $HOME) …
|
|
270
|
+
expect(wrapped).toContain("cd ~ec2-user/hq");
|
|
271
|
+
// … never failing merely because the box has no HQ folder …
|
|
272
|
+
expect(wrapped).toContain("|| true");
|
|
273
|
+
// … and the caller's command still runs after the cd.
|
|
274
|
+
expect(wrapped.endsWith("; pwd")).toBe(true);
|
|
275
|
+
});
|
|
276
|
+
|
|
265
277
|
it("POSTs /outpost/exec with the joined command and streams stdout", async () => {
|
|
266
278
|
const stdoutSpy = vi
|
|
267
279
|
.spyOn(process.stdout, "write")
|
|
@@ -285,7 +297,10 @@ describe("hq outposts exec", () => {
|
|
|
285
297
|
const [url, init] = fetchSpy.mock.calls[0];
|
|
286
298
|
expect(String(url)).toContain("/outpost/exec");
|
|
287
299
|
expect(init?.method).toBe("POST");
|
|
288
|
-
|
|
300
|
+
// The command is wrapped so it runs from the box's HQ folder.
|
|
301
|
+
expect(JSON.parse(init?.body as string)).toEqual({
|
|
302
|
+
command: withRemoteHqDir("echo hello world"),
|
|
303
|
+
});
|
|
289
304
|
const printed = stdoutSpy.mock.calls.map((c) => String(c[0])).join("");
|
|
290
305
|
expect(printed).toContain("hello world\n");
|
|
291
306
|
expect(process.exitCode).toBe(0);
|
|
@@ -416,7 +431,7 @@ describe("hq outposts exec — Lightsail SSH fallback", () => {
|
|
|
416
431
|
const args = mockSpawnSync.mock.calls[0][1] as string[];
|
|
417
432
|
expect(mockSpawnSync.mock.calls[0][0]).toBe("ssh");
|
|
418
433
|
expect(args).toContain("ec2-user@52.2.2.2");
|
|
419
|
-
expect(args[args.length - 1]).toBe("uname -a");
|
|
434
|
+
expect(args[args.length - 1]).toBe(withRemoteHqDir("uname -a"));
|
|
420
435
|
expect(args).toContain("BatchMode=yes");
|
|
421
436
|
const printed = stdoutSpy.mock.calls.map((c) => String(c[0])).join("");
|
|
422
437
|
expect(printed).toContain("lightsail-out\n");
|
package/src/commands/outposts.ts
CHANGED
|
@@ -227,6 +227,25 @@ export async function execOutpost(
|
|
|
227
227
|
});
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
+
/**
|
|
231
|
+
* Prefix that best-effort `cd`s into the box's HQ checkout before running the
|
|
232
|
+
* caller's command. `exec` runs over two transports with two different default
|
|
233
|
+
* working directories — SSM runs as root with no `$HOME` (cwd `/usr/bin`) and
|
|
234
|
+
* SSH lands in the login user's home — so without this, `hq outposts exec -- pwd`
|
|
235
|
+
* printed an unhelpful, transport-dependent directory. We resolve the HQ folder
|
|
236
|
+
* in-shell: `$HOME/hq` for the login/SSH user, falling back to ec2-user's home
|
|
237
|
+
* for the SSM/root path. The trailing `|| true` keeps the command running from
|
|
238
|
+
* the default directory when no HQ checkout is present, so exec never fails
|
|
239
|
+
* merely because the box has no HQ folder.
|
|
240
|
+
*/
|
|
241
|
+
export const REMOTE_HQ_DIR_PREFIX =
|
|
242
|
+
'cd "$HOME/hq" 2>/dev/null || cd ~ec2-user/hq 2>/dev/null || true';
|
|
243
|
+
|
|
244
|
+
/** Wrap `command` so it runs from the box's HQ folder (see REMOTE_HQ_DIR_PREFIX). */
|
|
245
|
+
export function withRemoteHqDir(command: string): string {
|
|
246
|
+
return `${REMOTE_HQ_DIR_PREFIX}; ${command}`;
|
|
247
|
+
}
|
|
248
|
+
|
|
230
249
|
/** SSH connection details vended by `POST /outpost/ssh-access`. */
|
|
231
250
|
export interface OutpostSshAccess {
|
|
232
251
|
outpostId: string;
|
|
@@ -519,8 +538,11 @@ export function registerOutpostsCommand(program: Command): void {
|
|
|
519
538
|
}
|
|
520
539
|
const token = await ensureCognitoToken();
|
|
521
540
|
|
|
541
|
+
// Run from the box's HQ folder by default (works over both SSM and SSH).
|
|
542
|
+
const remoteCommand = withRemoteHqDir(command);
|
|
543
|
+
|
|
522
544
|
try {
|
|
523
|
-
const result = await execOutpost(token,
|
|
545
|
+
const result = await execOutpost(token, remoteCommand, opts.id);
|
|
524
546
|
if (opts.json) {
|
|
525
547
|
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
526
548
|
} else {
|
|
@@ -546,7 +568,7 @@ export function registerOutpostsCommand(program: Command): void {
|
|
|
546
568
|
err.step === "platform-unsupported"
|
|
547
569
|
) {
|
|
548
570
|
const access = await getOutpostSshAccess(token, opts.id);
|
|
549
|
-
const ssh = execViaSsh(access,
|
|
571
|
+
const ssh = execViaSsh(access, remoteCommand);
|
|
550
572
|
if (ssh.error) {
|
|
551
573
|
console.error(chalk.red(`Could not run the command over SSH: ${ssh.error}`));
|
|
552
574
|
process.exit(1);
|
package/src/commands/packs.ts
CHANGED
|
@@ -6,10 +6,9 @@
|
|
|
6
6
|
* hq packs update Re-install the latest of an installed pack.
|
|
7
7
|
* hq packs uninstall Un-wire + archive a pack (clean, no dangling symlinks).
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* is the only clean content-pack op; this adds the rest.
|
|
9
|
+
* Also exposed through `hq packages list`, because package installs use this
|
|
10
|
+
* content-pack system. Content packs have no registry file -- the filesystem
|
|
11
|
+
* under core/packages/ is the source of truth.
|
|
13
12
|
*
|
|
14
13
|
* Every subcommand supports `--json` for machine consumers (the HQ Sync
|
|
15
14
|
* menubar app). JSON is also the default when stdout is not a TTY, matching
|
|
@@ -521,10 +520,8 @@ async function runUninstall(name: string, opts: UninstallOpts): Promise<Uninstal
|
|
|
521
520
|
// Registration
|
|
522
521
|
// ---------------------------------------------------------------------------
|
|
523
522
|
|
|
524
|
-
export function
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
packs
|
|
523
|
+
export function registerPacksListCommand(parent: Command): void {
|
|
524
|
+
parent
|
|
528
525
|
.command('list')
|
|
529
526
|
.alias('ls')
|
|
530
527
|
.description('List installed content packs and the curated catalog')
|
|
@@ -550,6 +547,12 @@ export function registerPacksCommand(parent: Command): void {
|
|
|
550
547
|
}
|
|
551
548
|
},
|
|
552
549
|
);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
export function registerPacksCommand(parent: Command): void {
|
|
553
|
+
const packs = parent.command('packs').description('Content-pack lifecycle (install via `hq install`)');
|
|
554
|
+
|
|
555
|
+
registerPacksListCommand(packs);
|
|
553
556
|
|
|
554
557
|
packs
|
|
555
558
|
.command('update [name]')
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// @vitest-environment node
|
|
2
|
+
/**
|
|
3
|
+
* Regression: `hq packages list` must use the same filesystem-backed pack
|
|
4
|
+
* discovery as `hq packs list`, matching the live package install path.
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as os from 'os';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
10
|
+
import { Command } from 'commander';
|
|
11
|
+
|
|
12
|
+
vi.mock('./pack-install.js', () => ({
|
|
13
|
+
classify: vi.fn(() => 'local'),
|
|
14
|
+
resolveLatest: vi.fn(),
|
|
15
|
+
resolveLatestMarketplace: vi.fn(),
|
|
16
|
+
runScanPackages: vi.fn(),
|
|
17
|
+
installPack: vi.fn(),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
import { registerPackageListCommand } from './pkg-list.js';
|
|
21
|
+
import { registerPacksCommand } from './packs.js';
|
|
22
|
+
|
|
23
|
+
describe('hq packages list content-pack discovery', () => {
|
|
24
|
+
let hqRoot: string;
|
|
25
|
+
let previousCwd: string;
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'hq-pkg-list-'));
|
|
29
|
+
previousCwd = process.cwd();
|
|
30
|
+
fs.mkdirSync(path.join(hqRoot, '.claude'));
|
|
31
|
+
fs.mkdirSync(path.join(hqRoot, 'companies'));
|
|
32
|
+
fs.mkdirSync(path.join(hqRoot, 'core', 'packages', 'hq-pack-fixture'), {
|
|
33
|
+
recursive: true,
|
|
34
|
+
});
|
|
35
|
+
fs.writeFileSync(path.join(hqRoot, 'core', 'core.yaml'), 'hqVersion: 14.0.0\n');
|
|
36
|
+
fs.writeFileSync(
|
|
37
|
+
path.join(hqRoot, 'core', 'packages', 'hq-pack-fixture', 'package.yaml'),
|
|
38
|
+
'name: hq-pack-fixture\nversion: 1.2.3\n',
|
|
39
|
+
);
|
|
40
|
+
process.chdir(hqRoot);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
process.chdir(previousCwd);
|
|
45
|
+
fs.rmSync(hqRoot, { recursive: true, force: true });
|
|
46
|
+
vi.restoreAllMocks();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
async function captureJson(register: (program: Command) => void, args: string[]) {
|
|
50
|
+
const stdout = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
51
|
+
const program = new Command();
|
|
52
|
+
program.exitOverride();
|
|
53
|
+
register(program);
|
|
54
|
+
await program.parseAsync(args, { from: 'user' });
|
|
55
|
+
const output = stdout.mock.calls.map((call) => String(call[0])).join('');
|
|
56
|
+
stdout.mockRestore();
|
|
57
|
+
return JSON.parse(output) as {
|
|
58
|
+
installed: Array<{ name: string; version?: string }>;
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
it('reports a pack installed under core/packages through both list commands', async () => {
|
|
63
|
+
const packagesView = await captureJson(
|
|
64
|
+
(program) => {
|
|
65
|
+
const packages = program.command('packages');
|
|
66
|
+
registerPackageListCommand(packages);
|
|
67
|
+
},
|
|
68
|
+
['packages', 'list', '--json'],
|
|
69
|
+
);
|
|
70
|
+
const packsView = await captureJson(registerPacksCommand, ['packs', 'list', '--json']);
|
|
71
|
+
|
|
72
|
+
expect(packagesView.installed).toContainEqual(
|
|
73
|
+
expect.objectContaining({ name: 'hq-pack-fixture', version: '1.2.3' }),
|
|
74
|
+
);
|
|
75
|
+
expect(packsView.installed).toContainEqual(
|
|
76
|
+
expect.objectContaining({ name: 'hq-pack-fixture', version: '1.2.3' }),
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
});
|
package/src/commands/pkg-list.ts
CHANGED
|
@@ -1,129 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* hq packages list —
|
|
2
|
+
* hq packages list — list filesystem-backed content packs.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Package installs use the content-pack installer, so this command delegates
|
|
5
|
+
* to the same list view as `hq packs list`.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import { Command } from 'commander';
|
|
8
|
-
import
|
|
9
|
-
import { resolveDefaultHqRoot } from '../utils/cognito-session.js';
|
|
10
|
-
import { readRegistry } from '../utils/registry.js';
|
|
11
|
-
import { loadCachedTokens, isExpiring } from '@indigoai-us/hq-cloud';
|
|
12
|
-
import {
|
|
13
|
-
getRegistryUrl,
|
|
14
|
-
RegistryClient,
|
|
15
|
-
type EntitlementEntry,
|
|
16
|
-
} from '../utils/registry-client.js';
|
|
9
|
+
import { registerPacksListCommand } from './packs.js';
|
|
17
10
|
|
|
18
11
|
export function registerPackageListCommand(parent: Command): void {
|
|
19
|
-
parent
|
|
20
|
-
.command('list')
|
|
21
|
-
.alias('ls')
|
|
22
|
-
.description('List installed and available packages')
|
|
23
|
-
.option('--json', 'Machine-readable JSON output')
|
|
24
|
-
.action(async (opts: { json?: boolean }) => {
|
|
25
|
-
try {
|
|
26
|
-
await listPackages(opts.json === true || !process.stdout.isTTY);
|
|
27
|
-
} catch (error) {
|
|
28
|
-
console.error(
|
|
29
|
-
chalk.red('List failed:'),
|
|
30
|
-
error instanceof Error ? error.message : 'Unknown error'
|
|
31
|
-
);
|
|
32
|
-
process.exit(1);
|
|
33
|
-
}
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async function gatherRegistryPackages(): Promise<{
|
|
38
|
-
installed: ReturnType<typeof readRegistry>;
|
|
39
|
-
available: EntitlementEntry[];
|
|
40
|
-
offline: boolean;
|
|
41
|
-
}> {
|
|
42
|
-
const hqRoot = resolveDefaultHqRoot({ onMissing: 'throw' });
|
|
43
|
-
const installed = readRegistry(hqRoot);
|
|
44
|
-
let entitlements: EntitlementEntry[] = [];
|
|
45
|
-
let offline = false;
|
|
46
|
-
try {
|
|
47
|
-
const cached = loadCachedTokens();
|
|
48
|
-
if (cached && !isExpiring(cached, 120)) {
|
|
49
|
-
const client = new RegistryClient(getRegistryUrl(), cached.accessToken);
|
|
50
|
-
const result = await client.getMyEntitlements();
|
|
51
|
-
entitlements = result.entitlements;
|
|
52
|
-
}
|
|
53
|
-
} catch {
|
|
54
|
-
offline = true;
|
|
55
|
-
}
|
|
56
|
-
const installedSlugs = new Set(installed.map((p) => p.slug));
|
|
57
|
-
const available = entitlements.filter((e) => !installedSlugs.has(e.slug));
|
|
58
|
-
return { installed, available, offline };
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
async function listPackages(json: boolean): Promise<void> {
|
|
62
|
-
if (json) {
|
|
63
|
-
const { installed, available, offline } = await gatherRegistryPackages();
|
|
64
|
-
process.stdout.write(JSON.stringify({ installed, available, offline }, null, 2) + '\n');
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const hqRoot = resolveDefaultHqRoot({ onMissing: 'throw' });
|
|
69
|
-
const installed = readRegistry(hqRoot);
|
|
70
|
-
|
|
71
|
-
// Print installed packages
|
|
72
|
-
if (installed.length > 0) {
|
|
73
|
-
console.log(chalk.bold('Installed packages:\n'));
|
|
74
|
-
for (const pkg of installed) {
|
|
75
|
-
const scope = pkg.scope ? chalk.dim(` (${pkg.scope})`) : '';
|
|
76
|
-
console.log(` ${chalk.green(pkg.slug)}@${pkg.version}${scope}`);
|
|
77
|
-
console.log(
|
|
78
|
-
chalk.dim(
|
|
79
|
-
` Installed: ${pkg.installed_at.slice(0, 10)} Updated: ${pkg.updated_at.slice(0, 10)}`
|
|
80
|
-
)
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
console.log();
|
|
84
|
-
} else {
|
|
85
|
-
console.log(chalk.dim('No packages installed.\n'));
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Try to show available (but not installed) packages from entitlements
|
|
89
|
-
let entitlements: EntitlementEntry[] = [];
|
|
90
|
-
let offline = false;
|
|
91
|
-
|
|
92
|
-
try {
|
|
93
|
-
const cached = loadCachedTokens();
|
|
94
|
-
if (cached && !isExpiring(cached, 120)) {
|
|
95
|
-
const registryUrl = getRegistryUrl();
|
|
96
|
-
const client = new RegistryClient(
|
|
97
|
-
registryUrl,
|
|
98
|
-
cached.accessToken
|
|
99
|
-
);
|
|
100
|
-
const result = await client.getMyEntitlements();
|
|
101
|
-
entitlements = result.entitlements;
|
|
102
|
-
}
|
|
103
|
-
} catch {
|
|
104
|
-
offline = true;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const installedSlugs = new Set(installed.map((p) => p.slug));
|
|
108
|
-
const available = entitlements.filter((e) => !installedSlugs.has(e.slug));
|
|
109
|
-
|
|
110
|
-
if (available.length > 0) {
|
|
111
|
-
console.log(chalk.bold('Available (not installed):\n'));
|
|
112
|
-
for (const ent of available) {
|
|
113
|
-
console.log(
|
|
114
|
-
` ${chalk.cyan(ent.slug)} ${chalk.dim(`[${ent.tier}]`)} ${chalk.yellow('available')}`
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
|
-
console.log(
|
|
118
|
-
chalk.dim('\nRun "hq packages install <slug>" to install.\n')
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (offline) {
|
|
123
|
-
console.log(
|
|
124
|
-
chalk.yellow(
|
|
125
|
-
'Note: Could not reach registry — showing cached data only.'
|
|
126
|
-
)
|
|
127
|
-
);
|
|
128
|
-
}
|
|
12
|
+
registerPacksListCommand(parent);
|
|
129
13
|
}
|