@indigoai-us/hq-cli 5.3.0 → 5.4.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 (141) hide show
  1. package/dist/bin/hq-auth-refresh.d.ts +13 -0
  2. package/dist/bin/hq-auth-refresh.d.ts.map +1 -0
  3. package/dist/bin/hq-auth-refresh.js +27 -0
  4. package/dist/bin/hq-auth-refresh.js.map +1 -0
  5. package/dist/commands/auth.d.ts +17 -13
  6. package/dist/commands/auth.d.ts.map +1 -1
  7. package/dist/commands/auth.js +83 -238
  8. package/dist/commands/auth.js.map +1 -1
  9. package/dist/commands/cloud.d.ts +3 -12
  10. package/dist/commands/cloud.d.ts.map +1 -1
  11. package/dist/commands/cloud.js +55 -212
  12. package/dist/commands/cloud.js.map +1 -1
  13. package/dist/commands/login.d.ts +6 -0
  14. package/dist/commands/login.d.ts.map +1 -0
  15. package/dist/commands/login.js +25 -0
  16. package/dist/commands/login.js.map +1 -0
  17. package/dist/commands/logout.d.ts +6 -0
  18. package/dist/commands/logout.d.ts.map +1 -0
  19. package/dist/commands/logout.js +21 -0
  20. package/dist/commands/logout.js.map +1 -0
  21. package/dist/commands/onboard.d.ts +23 -0
  22. package/dist/commands/onboard.d.ts.map +1 -0
  23. package/dist/commands/onboard.js +142 -0
  24. package/dist/commands/onboard.js.map +1 -0
  25. package/dist/commands/pkg-install.d.ts +17 -0
  26. package/dist/commands/pkg-install.d.ts.map +1 -0
  27. package/dist/commands/pkg-install.js +144 -0
  28. package/dist/commands/pkg-install.js.map +1 -0
  29. package/dist/commands/pkg-list.d.ts +8 -0
  30. package/dist/commands/pkg-list.d.ts.map +1 -0
  31. package/dist/commands/pkg-list.js +70 -0
  32. package/dist/commands/pkg-list.js.map +1 -0
  33. package/dist/commands/pkg-remove.d.ts +10 -0
  34. package/dist/commands/pkg-remove.d.ts.map +1 -0
  35. package/dist/commands/pkg-remove.js +53 -0
  36. package/dist/commands/pkg-remove.js.map +1 -0
  37. package/dist/commands/pkg-update.d.ts +9 -0
  38. package/dist/commands/pkg-update.d.ts.map +1 -0
  39. package/dist/commands/pkg-update.js +124 -0
  40. package/dist/commands/pkg-update.js.map +1 -0
  41. package/dist/commands/team-sync.d.ts +14 -0
  42. package/dist/commands/team-sync.d.ts.map +1 -0
  43. package/dist/commands/team-sync.js +422 -0
  44. package/dist/commands/team-sync.js.map +1 -0
  45. package/dist/commands/whoami.d.ts +6 -0
  46. package/dist/commands/whoami.d.ts.map +1 -0
  47. package/dist/commands/whoami.js +29 -0
  48. package/dist/commands/whoami.js.map +1 -0
  49. package/dist/index.d.ts +1 -1
  50. package/dist/index.js +33 -10
  51. package/dist/index.js.map +1 -1
  52. package/dist/strategies/merge.js +2 -2
  53. package/dist/strategies/merge.js.map +1 -1
  54. package/dist/utils/auth.d.ts +27 -0
  55. package/dist/utils/auth.d.ts.map +1 -0
  56. package/dist/utils/auth.js +155 -0
  57. package/dist/utils/auth.js.map +1 -0
  58. package/dist/utils/cognito-session.d.ts +35 -0
  59. package/dist/utils/cognito-session.d.ts.map +1 -0
  60. package/dist/utils/cognito-session.js +72 -0
  61. package/dist/utils/cognito-session.js.map +1 -0
  62. package/dist/utils/git.js +1 -1
  63. package/dist/utils/git.js.map +1 -1
  64. package/dist/utils/hq-root.d.ts +10 -0
  65. package/dist/utils/hq-root.d.ts.map +1 -0
  66. package/dist/utils/hq-root.js +22 -0
  67. package/dist/utils/hq-root.js.map +1 -0
  68. package/dist/utils/integrity.d.ts +14 -0
  69. package/dist/utils/integrity.d.ts.map +1 -0
  70. package/dist/utils/integrity.js +40 -0
  71. package/dist/utils/integrity.js.map +1 -0
  72. package/dist/utils/manifest.d.ts.map +1 -1
  73. package/dist/utils/manifest.js +2 -5
  74. package/dist/utils/manifest.js.map +1 -1
  75. package/dist/utils/registry-client.d.ts +65 -0
  76. package/dist/utils/registry-client.d.ts.map +1 -0
  77. package/dist/utils/registry-client.js +102 -0
  78. package/dist/utils/registry-client.js.map +1 -0
  79. package/dist/utils/registry.d.ts +33 -0
  80. package/dist/utils/registry.d.ts.map +1 -0
  81. package/dist/utils/registry.js +58 -0
  82. package/dist/utils/registry.js.map +1 -0
  83. package/dist/utils/token-store.d.ts +28 -0
  84. package/dist/utils/token-store.d.ts.map +1 -0
  85. package/dist/utils/token-store.js +68 -0
  86. package/dist/utils/token-store.js.map +1 -0
  87. package/package.json +35 -45
  88. package/src/bin/hq-auth-refresh.ts +32 -0
  89. package/src/commands/add.ts +74 -0
  90. package/src/commands/auth.ts +153 -0
  91. package/src/commands/cloud.ts +125 -0
  92. package/src/commands/list.ts +66 -0
  93. package/src/commands/login.ts +29 -0
  94. package/src/commands/logout.ts +25 -0
  95. package/src/commands/pkg-install.ts +181 -0
  96. package/src/commands/pkg-list.ts +98 -0
  97. package/src/commands/pkg-remove.ts +71 -0
  98. package/src/commands/pkg-update.ts +189 -0
  99. package/src/commands/sync.ts +149 -0
  100. package/src/commands/team-sync.ts +629 -0
  101. package/src/commands/update.ts +71 -0
  102. package/src/commands/whoami.ts +38 -0
  103. package/src/index.ts +72 -0
  104. package/src/schemas/hq-package.schema.json +137 -0
  105. package/src/strategies/link.ts +62 -0
  106. package/src/strategies/merge.ts +142 -0
  107. package/src/types.ts +47 -0
  108. package/src/utils/auth.ts +193 -0
  109. package/src/utils/cognito-session.ts +94 -0
  110. package/src/utils/git.ts +74 -0
  111. package/src/utils/hq-root.ts +27 -0
  112. package/src/utils/integrity.ts +54 -0
  113. package/src/utils/manifest.ts +109 -0
  114. package/src/utils/registry-client.ts +204 -0
  115. package/src/utils/registry.ts +89 -0
  116. package/src/utils/token-store.ts +83 -0
  117. package/tsconfig.json +8 -0
  118. package/dist/commands/cloud-setup.d.ts +0 -19
  119. package/dist/commands/cloud-setup.d.ts.map +0 -1
  120. package/dist/commands/cloud-setup.js +0 -206
  121. package/dist/commands/cloud-setup.js.map +0 -1
  122. package/dist/commands/initial-upload.d.ts +0 -67
  123. package/dist/commands/initial-upload.d.ts.map +0 -1
  124. package/dist/commands/initial-upload.js +0 -205
  125. package/dist/commands/initial-upload.js.map +0 -1
  126. package/dist/sync-worker.d.ts +0 -11
  127. package/dist/sync-worker.d.ts.map +0 -1
  128. package/dist/sync-worker.js +0 -77
  129. package/dist/sync-worker.js.map +0 -1
  130. package/dist/utils/api-client.d.ts +0 -26
  131. package/dist/utils/api-client.d.ts.map +0 -1
  132. package/dist/utils/api-client.js +0 -87
  133. package/dist/utils/api-client.js.map +0 -1
  134. package/dist/utils/credentials.d.ts +0 -44
  135. package/dist/utils/credentials.d.ts.map +0 -1
  136. package/dist/utils/credentials.js +0 -101
  137. package/dist/utils/credentials.js.map +0 -1
  138. package/dist/utils/sync.d.ts +0 -125
  139. package/dist/utils/sync.d.ts.map +0 -1
  140. package/dist/utils/sync.js +0 -291
  141. package/dist/utils/sync.js.map +0 -1
@@ -0,0 +1,66 @@
1
+ /**
2
+ * hq modules list command (US-005)
3
+ */
4
+
5
+ import * as fs from 'fs';
6
+ import * as path from 'path';
7
+ import { Command } from 'commander';
8
+ import { findHqRoot, readManifest, readLock, getModulesDir } from '../utils/manifest.js';
9
+ import { isRepo, getCurrentCommit, isBehindRemote } from '../utils/git.js';
10
+
11
+ export function registerListCommand(program: Command): void {
12
+ program
13
+ .command('list')
14
+ .alias('ls')
15
+ .description('List all modules and their status')
16
+ .action(async () => {
17
+ try {
18
+ const hqRoot = findHqRoot();
19
+ const manifest = readManifest(hqRoot);
20
+
21
+ if (!manifest || manifest.modules.length === 0) {
22
+ console.log('No modules in manifest. Use "hq modules add" to add modules.');
23
+ return;
24
+ }
25
+
26
+ const modulesDir = getModulesDir(hqRoot);
27
+ const lock = readLock(hqRoot);
28
+
29
+ console.log('Modules:\n');
30
+
31
+ for (const module of manifest.modules) {
32
+ const moduleDir = path.join(modulesDir, module.name);
33
+ const installed = await isRepo(moduleDir);
34
+
35
+ console.log(` ${module.name}`);
36
+ console.log(` Repo: ${module.repo}`);
37
+ console.log(` Branch: ${module.branch || 'main'}`);
38
+ console.log(` Strategy: ${module.strategy}`);
39
+ console.log(` Paths: ${module.paths.map(p => `${p.src} -> ${p.dest}`).join(', ')}`);
40
+
41
+ if (installed) {
42
+ const commit = await getCurrentCommit(moduleDir);
43
+ const shortCommit = commit.slice(0, 7);
44
+ const lockedCommit = lock?.locked[module.name];
45
+ const isLocked = lockedCommit === commit;
46
+
47
+ console.log(` Status: ✓ installed @ ${shortCommit}${isLocked ? ' (locked)' : ''}`);
48
+
49
+ // Check if behind upstream
50
+ const { behind, commits } = await isBehindRemote(moduleDir);
51
+ if (behind) {
52
+ console.log(` Updates: ${commits} commit(s) behind remote`);
53
+ }
54
+ } else {
55
+ console.log(` Status: ✗ not installed`);
56
+ }
57
+
58
+ console.log();
59
+ }
60
+
61
+ } catch (error) {
62
+ console.error('Error:', error instanceof Error ? error.message : error);
63
+ process.exit(1);
64
+ }
65
+ });
66
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * hq login — opens browser for Clerk auth, caches token (US-004)
3
+ */
4
+
5
+ import { Command } from 'commander';
6
+ import chalk from 'chalk';
7
+ import { startAuthFlow } from '../utils/auth.js';
8
+ import { saveToken } from '../utils/token-store.js';
9
+ import { getRegistryUrl } from '../utils/registry-client.js';
10
+
11
+ export function registerLoginCommand(program: Command): void {
12
+ program
13
+ .command('login')
14
+ .description('Authenticate with the HQ package registry')
15
+ .action(async () => {
16
+ try {
17
+ const registryUrl = getRegistryUrl();
18
+ const token = await startAuthFlow(registryUrl);
19
+ await saveToken(token);
20
+ console.log(chalk.green(`Logged in as ${token.email}`));
21
+ } catch (error) {
22
+ console.error(
23
+ chalk.red('Login failed:'),
24
+ error instanceof Error ? error.message : 'Unknown error'
25
+ );
26
+ process.exit(1);
27
+ }
28
+ });
29
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * hq logout — clears cached session (US-004)
3
+ */
4
+
5
+ import { Command } from 'commander';
6
+ import chalk from 'chalk';
7
+ import { clearToken } from '../utils/token-store.js';
8
+
9
+ export function registerLogoutCommand(program: Command): void {
10
+ program
11
+ .command('logout')
12
+ .description('Log out and clear cached credentials')
13
+ .action(async () => {
14
+ try {
15
+ await clearToken();
16
+ console.log(chalk.green('Logged out successfully'));
17
+ } catch (error) {
18
+ console.error(
19
+ chalk.red('Logout failed:'),
20
+ error instanceof Error ? error.message : 'Unknown error'
21
+ );
22
+ process.exit(1);
23
+ }
24
+ });
25
+ }
@@ -0,0 +1,181 @@
1
+ /**
2
+ * hq packages install <slug> — download, verify, and extract a package (US-005)
3
+ *
4
+ * Flow:
5
+ * 1. Check auth (prompt login if needed)
6
+ * 2. Check entitlement via API
7
+ * 3. Download tarball via presigned URL
8
+ * 4. Verify SHA256 hash
9
+ * 5. Verify RSA signature (if provided)
10
+ * 6. Extract to packages/installed/<slug>/
11
+ * 7. Validate extracted package.yaml slug matches
12
+ * 8. Update packages/registry.yaml
13
+ * 9. Print next-step message
14
+ */
15
+
16
+ import * as fs from 'fs';
17
+ import * as os from 'os';
18
+ import * as path from 'path';
19
+ import * as yaml from 'js-yaml';
20
+ import { execSync } from 'child_process';
21
+ import { Command } from 'commander';
22
+ import chalk from 'chalk';
23
+ import { getAuthToken } from '../utils/auth.js';
24
+ import { findHqRoot } from '../utils/hq-root.js';
25
+ import {
26
+ getRegistryUrl,
27
+ RegistryClient,
28
+ } from '../utils/registry-client.js';
29
+ import { verifySha256, verifyRsaSignature } from '../utils/integrity.js';
30
+ import { addToRegistry } from '../utils/registry.js';
31
+
32
+ export function registerPackageInstallCommand(parent: Command): void {
33
+ parent
34
+ .command('install <slug>')
35
+ .description('Install a package from the registry')
36
+ .option('--company <co>', 'Scope the package to a specific company')
37
+ .action(async (slug: string, opts: { company?: string }) => {
38
+ try {
39
+ await installPackage(slug, opts.company);
40
+ } catch (error) {
41
+ console.error(
42
+ chalk.red('Install failed:'),
43
+ error instanceof Error ? error.message : 'Unknown error'
44
+ );
45
+ process.exit(1);
46
+ }
47
+ });
48
+ }
49
+
50
+ async function installPackage(
51
+ slug: string,
52
+ company?: string
53
+ ): Promise<void> {
54
+ // 1. Auth
55
+ const token = await getAuthToken();
56
+ const registryUrl = getRegistryUrl();
57
+ const client = new RegistryClient(registryUrl, token.clerk_session_token);
58
+
59
+ // 2. Check entitlement
60
+ console.log(chalk.dim(`Checking entitlement for ${slug}...`));
61
+ const entitlement = await client.checkEntitlement(slug);
62
+ if (!entitlement.entitled) {
63
+ throw new Error(
64
+ `You are not entitled to package "${slug}". Visit the registry to purchase or request access.`
65
+ );
66
+ }
67
+
68
+ // 3. Get download URL
69
+ console.log(chalk.dim('Fetching download URL...'));
70
+ const download = await client.getDownloadUrl(slug);
71
+
72
+ // 4. Download to temp file
73
+ const tmpDir = os.tmpdir();
74
+ const tmpFile = path.resolve(tmpDir, `hq-pkg-${slug}-${Date.now()}.tar.gz`);
75
+
76
+ try {
77
+ console.log(chalk.dim('Downloading package...'));
78
+ const response = await fetch(download.url, {
79
+ signal: AbortSignal.timeout(120_000),
80
+ });
81
+
82
+ if (!response.ok) {
83
+ throw new Error(`Download failed (${response.status})`);
84
+ }
85
+
86
+ const buffer = Buffer.from(await response.arrayBuffer());
87
+ fs.writeFileSync(tmpFile, buffer);
88
+
89
+ // 5. Verify SHA256
90
+ console.log(chalk.dim('Verifying integrity...'));
91
+ const hashValid = await verifySha256(tmpFile, download.sha256);
92
+ if (!hashValid) {
93
+ throw new Error(
94
+ 'SHA256 hash mismatch — the downloaded file may be corrupted or tampered with.'
95
+ );
96
+ }
97
+
98
+ // 6. Verify RSA signature (if provided)
99
+ if (download.signature) {
100
+ const sigValid = verifyRsaSignature(download.sha256, download.signature);
101
+ if (!sigValid) {
102
+ throw new Error(
103
+ 'RSA signature verification failed — the package may have been tampered with or the public key is missing/invalid. Aborting install.'
104
+ );
105
+ }
106
+ }
107
+
108
+ // 7. Extract
109
+ const hqRoot = findHqRoot();
110
+ const installDir = path.resolve(hqRoot, 'packages', 'installed', slug);
111
+
112
+ // Clean existing installation
113
+ if (fs.existsSync(installDir)) {
114
+ fs.rmSync(installDir, { recursive: true, force: true });
115
+ }
116
+ fs.mkdirSync(installDir, { recursive: true });
117
+
118
+ execSync(`tar -xzf "${tmpFile}" -C "${installDir}"`, {
119
+ stdio: 'pipe',
120
+ });
121
+
122
+ // 8. Validate package.yaml slug
123
+ const packageYamlPath = path.resolve(installDir, 'package.yaml');
124
+ if (fs.existsSync(packageYamlPath)) {
125
+ const pkgContent = fs.readFileSync(packageYamlPath, 'utf-8');
126
+ const pkgMeta = yaml.load(pkgContent) as { slug?: string } | null;
127
+ if (pkgMeta?.slug && pkgMeta.slug !== slug) {
128
+ // Mismatch — clean up and abort
129
+ fs.rmSync(installDir, { recursive: true, force: true });
130
+ throw new Error(
131
+ `Package slug mismatch: expected "${slug}", got "${pkgMeta.slug}"`
132
+ );
133
+ }
134
+ }
135
+
136
+ // 9. Get package info for registry entry
137
+ let version = 'unknown';
138
+ let name = slug;
139
+ try {
140
+ const pkgInfo = await client.getPackage(slug);
141
+ version = pkgInfo.package.latest_version;
142
+ name = pkgInfo.package.name;
143
+ } catch {
144
+ // Best-effort — fall back to package.yaml if present
145
+ const packageYaml = path.resolve(installDir, 'package.yaml');
146
+ if (fs.existsSync(packageYaml)) {
147
+ const content = fs.readFileSync(packageYaml, 'utf-8');
148
+ const meta = yaml.load(content) as {
149
+ version?: string;
150
+ name?: string;
151
+ } | null;
152
+ if (meta?.version) version = meta.version;
153
+ if (meta?.name) name = meta.name;
154
+ }
155
+ }
156
+
157
+ // 10. Update registry.yaml
158
+ const now = new Date().toISOString();
159
+ addToRegistry(hqRoot, {
160
+ name,
161
+ slug,
162
+ version,
163
+ source: registryUrl,
164
+ scope: company,
165
+ installed_at: now,
166
+ updated_at: now,
167
+ });
168
+
169
+ console.log(chalk.green(`\nInstalled ${slug}@${version} to packages/installed/${slug}/`));
170
+ console.log(
171
+ chalk.cyan(
172
+ 'Run /package-install ' + slug + ' in Claude to merge into your HQ.'
173
+ )
174
+ );
175
+ } finally {
176
+ // Clean up temp file — never leave partial downloads
177
+ if (fs.existsSync(tmpFile)) {
178
+ fs.unlinkSync(tmpFile);
179
+ }
180
+ }
181
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * hq packages list — show installed packages and available entitlements (US-005)
3
+ *
4
+ * Graceful offline: if registry is unreachable, show cached data with a note.
5
+ */
6
+
7
+ import { Command } from 'commander';
8
+ import chalk from 'chalk';
9
+ import { findHqRoot } from '../utils/hq-root.js';
10
+ import { readRegistry } from '../utils/registry.js';
11
+ import { loadToken, isTokenExpired } from '../utils/token-store.js';
12
+ import {
13
+ getRegistryUrl,
14
+ RegistryClient,
15
+ type EntitlementEntry,
16
+ } from '../utils/registry-client.js';
17
+
18
+ export function registerPackageListCommand(parent: Command): void {
19
+ parent
20
+ .command('list')
21
+ .alias('ls')
22
+ .description('List installed and available packages')
23
+ .action(async () => {
24
+ try {
25
+ await listPackages();
26
+ } catch (error) {
27
+ console.error(
28
+ chalk.red('List failed:'),
29
+ error instanceof Error ? error.message : 'Unknown error'
30
+ );
31
+ process.exit(1);
32
+ }
33
+ });
34
+ }
35
+
36
+ async function listPackages(): Promise<void> {
37
+ const hqRoot = findHqRoot();
38
+ const installed = readRegistry(hqRoot);
39
+
40
+ // Print installed packages
41
+ if (installed.length > 0) {
42
+ console.log(chalk.bold('Installed packages:\n'));
43
+ for (const pkg of installed) {
44
+ const scope = pkg.scope ? chalk.dim(` (${pkg.scope})`) : '';
45
+ console.log(` ${chalk.green(pkg.slug)}@${pkg.version}${scope}`);
46
+ console.log(
47
+ chalk.dim(
48
+ ` Installed: ${pkg.installed_at.slice(0, 10)} Updated: ${pkg.updated_at.slice(0, 10)}`
49
+ )
50
+ );
51
+ }
52
+ console.log();
53
+ } else {
54
+ console.log(chalk.dim('No packages installed.\n'));
55
+ }
56
+
57
+ // Try to show available (but not installed) packages from entitlements
58
+ let entitlements: EntitlementEntry[] = [];
59
+ let offline = false;
60
+
61
+ try {
62
+ const token = await loadToken();
63
+ if (token && !isTokenExpired(token)) {
64
+ const registryUrl = getRegistryUrl();
65
+ const client = new RegistryClient(
66
+ registryUrl,
67
+ token.clerk_session_token
68
+ );
69
+ const result = await client.getMyEntitlements();
70
+ entitlements = result.entitlements;
71
+ }
72
+ } catch {
73
+ offline = true;
74
+ }
75
+
76
+ const installedSlugs = new Set(installed.map((p) => p.slug));
77
+ const available = entitlements.filter((e) => !installedSlugs.has(e.slug));
78
+
79
+ if (available.length > 0) {
80
+ console.log(chalk.bold('Available (not installed):\n'));
81
+ for (const ent of available) {
82
+ console.log(
83
+ ` ${chalk.cyan(ent.slug)} ${chalk.dim(`[${ent.tier}]`)} ${chalk.yellow('available')}`
84
+ );
85
+ }
86
+ console.log(
87
+ chalk.dim('\nRun "hq packages install <slug>" to install.\n')
88
+ );
89
+ }
90
+
91
+ if (offline) {
92
+ console.log(
93
+ chalk.yellow(
94
+ 'Note: Could not reach registry — showing cached data only.'
95
+ )
96
+ );
97
+ }
98
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * hq packages remove <slug> — archive and unregister a package (US-005)
3
+ *
4
+ * 1. Archive packages/installed/<slug>/ to packages/.archive/<slug>-<timestamp>/
5
+ * 2. Remove entry from packages/registry.yaml
6
+ * 3. Print next-step message
7
+ */
8
+
9
+ import * as fs from 'fs';
10
+ import * as path from 'path';
11
+ import { Command } from 'commander';
12
+ import chalk from 'chalk';
13
+ import { findHqRoot } from '../utils/hq-root.js';
14
+ import { removeFromRegistry, readRegistry } from '../utils/registry.js';
15
+
16
+ export function registerPackageRemoveCommand(parent: Command): void {
17
+ parent
18
+ .command('remove <slug>')
19
+ .description('Remove an installed package (archives it first)')
20
+ .action(async (slug: string) => {
21
+ try {
22
+ await removePackage(slug);
23
+ } catch (error) {
24
+ console.error(
25
+ chalk.red('Remove failed:'),
26
+ error instanceof Error ? error.message : 'Unknown error'
27
+ );
28
+ process.exit(1);
29
+ }
30
+ });
31
+ }
32
+
33
+ async function removePackage(slug: string): Promise<void> {
34
+ const hqRoot = findHqRoot();
35
+ const installDir = path.resolve(hqRoot, 'packages', 'installed', slug);
36
+
37
+ // Verify it is actually installed
38
+ const entries = readRegistry(hqRoot);
39
+ const entry = entries.find((e) => e.slug === slug);
40
+ if (!entry && !fs.existsSync(installDir)) {
41
+ throw new Error(`Package "${slug}" is not installed.`);
42
+ }
43
+
44
+ // 1. Archive
45
+ if (fs.existsSync(installDir)) {
46
+ const timestamp = new Date()
47
+ .toISOString()
48
+ .replace(/[:.]/g, '-')
49
+ .replace('T', '_')
50
+ .slice(0, 19);
51
+ const archiveDir = path.resolve(
52
+ hqRoot,
53
+ 'packages',
54
+ '.archive',
55
+ `${slug}-${timestamp}`
56
+ );
57
+ fs.mkdirSync(path.dirname(archiveDir), { recursive: true });
58
+ fs.renameSync(installDir, archiveDir);
59
+ console.log(chalk.dim(`Archived to packages/.archive/${slug}-${timestamp}/`));
60
+ }
61
+
62
+ // 2. Remove from registry.yaml
63
+ removeFromRegistry(hqRoot, slug);
64
+
65
+ console.log(chalk.green(`\nRemoved ${slug}.`));
66
+ console.log(
67
+ chalk.cyan(
68
+ 'Run /package-remove ' + slug + ' in Claude to clean up merged content.'
69
+ )
70
+ );
71
+ }
@@ -0,0 +1,189 @@
1
+ /**
2
+ * hq packages update [slug] — check for and apply package updates (US-005)
3
+ *
4
+ * If no slug: check all installed packages for updates.
5
+ * If slug given: update only that package.
6
+ */
7
+
8
+ import * as fs from 'fs';
9
+ import * as os from 'os';
10
+ import * as path from 'path';
11
+ import * as yaml from 'js-yaml';
12
+ import { execSync } from 'child_process';
13
+ import { Command } from 'commander';
14
+ import chalk from 'chalk';
15
+ import { getAuthToken } from '../utils/auth.js';
16
+ import { findHqRoot } from '../utils/hq-root.js';
17
+ import {
18
+ getRegistryUrl,
19
+ RegistryClient,
20
+ } from '../utils/registry-client.js';
21
+ import { verifySha256, verifyRsaSignature } from '../utils/integrity.js';
22
+ import {
23
+ readRegistry,
24
+ addToRegistry,
25
+ type RegistryEntry,
26
+ } from '../utils/registry.js';
27
+
28
+ export function registerPackageUpdateCommand(parent: Command): void {
29
+ parent
30
+ .command('update [slug]')
31
+ .description('Check for and apply package updates')
32
+ .action(async (slug?: string) => {
33
+ try {
34
+ await updatePackages(slug);
35
+ } catch (error) {
36
+ console.error(
37
+ chalk.red('Update failed:'),
38
+ error instanceof Error ? error.message : 'Unknown error'
39
+ );
40
+ process.exit(1);
41
+ }
42
+ });
43
+ }
44
+
45
+ async function updatePackages(slug?: string): Promise<void> {
46
+ const hqRoot = findHqRoot();
47
+ const entries = readRegistry(hqRoot);
48
+
49
+ if (entries.length === 0) {
50
+ console.log('No packages installed. Use "hq packages install <slug>" to install one.');
51
+ return;
52
+ }
53
+
54
+ const token = await getAuthToken();
55
+ const registryUrl = getRegistryUrl();
56
+ const client = new RegistryClient(registryUrl, token.clerk_session_token);
57
+
58
+ const toCheck = slug
59
+ ? entries.filter((e) => e.slug === slug)
60
+ : entries;
61
+
62
+ if (slug && toCheck.length === 0) {
63
+ throw new Error(`Package "${slug}" is not installed.`);
64
+ }
65
+
66
+ let updatedCount = 0;
67
+
68
+ for (const entry of toCheck) {
69
+ try {
70
+ const pkgInfo = await client.getPackage(entry.slug);
71
+ const latestVersion = pkgInfo.package.latest_version;
72
+
73
+ if (latestVersion === entry.version) {
74
+ console.log(chalk.dim(`${entry.slug}@${entry.version} — up to date`));
75
+ continue;
76
+ }
77
+
78
+ console.log(
79
+ chalk.yellow(
80
+ `${entry.slug}: ${entry.version} → ${latestVersion}`
81
+ )
82
+ );
83
+
84
+ // Download new version
85
+ const download = await client.getDownloadUrl(
86
+ entry.slug,
87
+ latestVersion
88
+ );
89
+
90
+ const tmpFile = path.resolve(
91
+ os.tmpdir(),
92
+ `hq-pkg-${entry.slug}-${Date.now()}.tar.gz`
93
+ );
94
+
95
+ try {
96
+ const response = await fetch(download.url, {
97
+ signal: AbortSignal.timeout(120_000),
98
+ });
99
+
100
+ if (!response.ok) {
101
+ throw new Error(`Download failed (${response.status})`);
102
+ }
103
+
104
+ const buffer = Buffer.from(await response.arrayBuffer());
105
+ fs.writeFileSync(tmpFile, buffer);
106
+
107
+ // Verify SHA256
108
+ const hashValid = await verifySha256(tmpFile, download.sha256);
109
+ if (!hashValid) {
110
+ throw new Error('SHA256 hash mismatch');
111
+ }
112
+
113
+ // Verify RSA signature (if provided)
114
+ if (download.signature) {
115
+ const sigValid = verifyRsaSignature(
116
+ download.sha256,
117
+ download.signature
118
+ );
119
+ if (!sigValid) {
120
+ console.warn(
121
+ chalk.yellow(
122
+ ` Warning: RSA signature verification failed for ${entry.slug}. Proceeding with SHA256-only.`
123
+ )
124
+ );
125
+ }
126
+ }
127
+
128
+ // Extract over existing
129
+ const installDir = path.resolve(
130
+ hqRoot,
131
+ 'packages',
132
+ 'installed',
133
+ entry.slug
134
+ );
135
+
136
+ if (fs.existsSync(installDir)) {
137
+ fs.rmSync(installDir, { recursive: true, force: true });
138
+ }
139
+ fs.mkdirSync(installDir, { recursive: true });
140
+
141
+ execSync(`tar -xzf "${tmpFile}" -C "${installDir}"`, {
142
+ stdio: 'pipe',
143
+ });
144
+
145
+ // Validate package.yaml slug
146
+ const packageYamlPath = path.resolve(installDir, 'package.yaml');
147
+ if (fs.existsSync(packageYamlPath)) {
148
+ const pkgContent = fs.readFileSync(packageYamlPath, 'utf-8');
149
+ const pkgMeta = yaml.load(pkgContent) as {
150
+ slug?: string;
151
+ } | null;
152
+ if (pkgMeta?.slug && pkgMeta.slug !== entry.slug) {
153
+ fs.rmSync(installDir, { recursive: true, force: true });
154
+ throw new Error(
155
+ `Package slug mismatch: expected "${entry.slug}", got "${pkgMeta.slug}"`
156
+ );
157
+ }
158
+ }
159
+
160
+ // Update registry entry — preserve scope
161
+ const now = new Date().toISOString();
162
+ const updated: RegistryEntry = {
163
+ ...entry,
164
+ version: latestVersion,
165
+ updated_at: now,
166
+ };
167
+ addToRegistry(hqRoot, updated);
168
+
169
+ console.log(
170
+ chalk.green(` Updated ${entry.slug} to ${latestVersion}`)
171
+ );
172
+ updatedCount++;
173
+ } finally {
174
+ if (fs.existsSync(tmpFile)) {
175
+ fs.unlinkSync(tmpFile);
176
+ }
177
+ }
178
+ } catch (error) {
179
+ console.error(
180
+ chalk.red(` Error updating ${entry.slug}:`),
181
+ error instanceof Error ? error.message : 'Unknown error'
182
+ );
183
+ }
184
+ }
185
+
186
+ console.log(
187
+ `\n${updatedCount} package(s) updated${updatedCount > 0 ? '.' : ' — everything is current.'}`
188
+ );
189
+ }