@indigoai-us/hq-cli 5.30.0 → 5.32.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.
@@ -34,7 +34,7 @@
34
34
  * from each pack's package.yaml; rationale lives in the layout-fix PR.)
35
35
  */
36
36
 
37
- !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]="845e5459-a7ba-53ca-a04c-dd055fabe52d")}catch(e){}}();
37
+ !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]="08d95744-5e4f-5938-8a49-4f57a9bf042f")}catch(e){}}();
38
38
  import * as fs from 'fs';
39
39
  import * as os from 'os';
40
40
  import * as path from 'path';
@@ -45,8 +45,9 @@ import chalk from 'chalk';
45
45
  import semverSatisfies from 'semver/functions/satisfies.js';
46
46
  import semverValid from 'semver/functions/valid.js';
47
47
  import semverValidRange from 'semver/ranges/valid.js';
48
+ import semverGt from 'semver/functions/gt.js';
48
49
  import { findHqRoot } from '../utils/manifest.js';
49
- function classify(source) {
50
+ export function classify(source) {
50
51
  if (source.startsWith('@'))
51
52
  return 'npm';
52
53
  if (source.startsWith('http://') ||
@@ -93,7 +94,7 @@ function expandGithubShorthand(url) {
93
94
  * Disambiguation: fragment containing '/' is a subpath (optionally with
94
95
  * '@<ref>' suffix); fragment without '/' is a ref.
95
96
  */
96
- function parseGitFragment(source) {
97
+ export function parseGitFragment(source) {
97
98
  const hashAt = source.indexOf('#');
98
99
  if (hashAt < 0)
99
100
  return { url: source };
@@ -273,6 +274,75 @@ function isNamedRef(url, ref) {
273
274
  return false;
274
275
  }
275
276
  }
277
+ /** Extract the ref (sha or named ref) recorded in a stamped git source. */
278
+ function gitRefFromSource(source) {
279
+ const { subpath, ref } = parseGitFragment(source);
280
+ // For 'url#subpath@ref' parseGitFragment returns ref; for 'url#ref' likewise.
281
+ // A bare 'url#subpath' (no @ref) has no ref.
282
+ void subpath;
283
+ return ref;
284
+ }
285
+ /**
286
+ * Probe whether a newer version of an already-installed pack is available,
287
+ * WITHOUT fetching or installing. Reuses the same git/npm primitives as the
288
+ * install path. Never throws — network/parse failures return
289
+ * `{ updateAvailable: null, error }` so callers (the menubar) stay resilient.
290
+ *
291
+ * @param source the stamped `source:` from the installed package.yaml
292
+ * @param installedVersion the installed pack's manifest `version` (npm compare)
293
+ */
294
+ export function resolveLatest(source, installedVersion) {
295
+ let transport;
296
+ try {
297
+ transport = classify(source);
298
+ }
299
+ catch (e) {
300
+ return { transport: 'local', updateAvailable: null, error: e.message };
301
+ }
302
+ if (transport === 'local') {
303
+ return { transport, updateAvailable: null, error: 'local source — re-run to re-sync' };
304
+ }
305
+ if (transport === 'npm') {
306
+ const pkg = stripVersion(source);
307
+ const current = installedVersion ?? (source.lastIndexOf('@') > 0 ? source.slice(source.lastIndexOf('@') + 1) : undefined);
308
+ try {
309
+ const latest = execFileSync('npm', ['view', pkg, 'version'], {
310
+ encoding: 'utf-8',
311
+ stdio: ['ignore', 'pipe', 'ignore'],
312
+ }).trim();
313
+ const updateAvailable = current && latest ? semverGt(latest, current) : null;
314
+ return { transport, current, latest, updateAvailable };
315
+ }
316
+ catch (e) {
317
+ return { transport, current, updateAvailable: null, error: `npm view failed: ${e.message}` };
318
+ }
319
+ }
320
+ // git
321
+ const parsed = parseGitFragment(source);
322
+ let url;
323
+ try {
324
+ url = expandGithubShorthand(parsed.url);
325
+ }
326
+ catch (e) {
327
+ return { transport, updateAvailable: null, error: e.message };
328
+ }
329
+ const current = gitRefFromSource(source);
330
+ // If install followed a named ref (branch/tag), compare that ref's tip;
331
+ // otherwise (default SHA-pin) compare the default branch HEAD.
332
+ const refArg = current && isNamedRef(url, current) ? current : 'HEAD';
333
+ try {
334
+ const out = execFileSync('git', ['ls-remote', url, refArg], {
335
+ encoding: 'utf-8',
336
+ stdio: ['ignore', 'pipe', 'ignore'],
337
+ }).trim();
338
+ const latest = out.split(/\s+/)[0] || undefined;
339
+ const updateAvailable = current && latest ? !latest.startsWith(current) && !current.startsWith(latest) : null;
340
+ return { transport, current, latest, updateAvailable };
341
+ }
342
+ catch (e) {
343
+ return { transport, current, updateAvailable: null, error: `git ls-remote failed: ${e.message}` };
344
+ }
345
+ }
276
346
  // ---------------------------------------------------------------------------
277
347
  // Manifest validation (spec §Validation, 10 checks)
278
348
  // ---------------------------------------------------------------------------
@@ -509,20 +579,26 @@ export function stampInstallSource(destDir, source) {
509
579
  * own scan.
510
580
  *
511
581
  * Exported for tests.
582
+ *
583
+ * `quiet` keeps the script's stdout off our stdout (it routes only stderr
584
+ * through, and sets HQ_SCAN_QUIET=1) so callers emitting machine-readable
585
+ * JSON — e.g. `hq packs uninstall --json` — produce clean output.
512
586
  */
513
- export function runScanPackages(hqRoot) {
587
+ export function runScanPackages(hqRoot, opts = {}) {
514
588
  const script = path.join(hqRoot, 'core', 'scripts', 'scan-packages.sh');
515
589
  if (!fs.existsSync(script)) {
516
- console.log(chalk.dim(` (core/scripts/scan-packages.sh not present — skipping auto-wire; ` +
517
- `will run on next session start)`));
590
+ if (!opts.quiet) {
591
+ console.log(chalk.dim(` (core/scripts/scan-packages.sh not present skipping auto-wire; ` +
592
+ `will run on next session start)`));
593
+ }
518
594
  return;
519
595
  }
520
596
  const r = spawnSync('bash', [script], {
521
597
  cwd: hqRoot,
522
- env: { ...process.env, HQ_ROOT: hqRoot },
523
- stdio: 'inherit',
598
+ env: { ...process.env, HQ_ROOT: hqRoot, ...(opts.quiet ? { HQ_SCAN_QUIET: '1' } : {}) },
599
+ stdio: opts.quiet ? ['ignore', 'ignore', 'inherit'] : 'inherit',
524
600
  });
525
- if (r.status !== 0) {
601
+ if (r.status !== 0 && !opts.quiet) {
526
602
  console.log(chalk.yellow(' scan-packages.sh exited non-zero; see output above.'));
527
603
  }
528
604
  }
@@ -530,7 +606,10 @@ export async function installPack(source, opts = {}) {
530
606
  const transport = classify(source);
531
607
  const hqRoot = findHqRoot();
532
608
  const hqVersion = readHqVersion(hqRoot);
533
- console.log(chalk.dim(`→ transport: ${transport}; source: ${source}`));
609
+ const say = opts.quiet
610
+ ? (...a) => console.error(...a)
611
+ : (...a) => console.log(...a);
612
+ say(chalk.dim(`-> transport: ${transport}; source: ${source}`));
534
613
  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hq-pack-'));
535
614
  try {
536
615
  let fetched;
@@ -549,18 +628,18 @@ export async function installPack(source, opts = {}) {
549
628
  if (pkg.conditional) {
550
629
  const allowed = await confirmConditional(pkg, opts.allowHooks ?? false);
551
630
  if (!allowed) {
552
- console.log(chalk.red('Install aborted (conditional predicate not approved).'));
631
+ say(chalk.red('Install aborted (conditional predicate not approved).'));
553
632
  return;
554
633
  }
555
634
  const ok = evalConditional(pkg.conditional);
556
635
  if (!ok) {
557
- console.log(chalk.yellow(`Skipping ${pkg.name}: conditional "${pkg.conditional}" returned non-zero.`));
636
+ say(chalk.yellow(`Skipping ${pkg.name}: conditional "${pkg.conditional}" returned non-zero.`));
558
637
  return;
559
638
  }
560
639
  }
561
640
  const confirmed = await confirmHooks(pkg, opts.allowHooks ?? false);
562
641
  if (!confirmed) {
563
- console.log(chalk.red('Install aborted (hooks denied).'));
642
+ say(chalk.red('Install aborted (hooks denied).'));
564
643
  return;
565
644
  }
566
645
  const destDir = installToPackages(fetched.payloadDir, pkg, hqRoot);
@@ -573,9 +652,9 @@ export async function installPack(source, opts = {}) {
573
652
  // re-runs. Stamping the literal input (not the resolved SHA/version)
574
653
  // matches the verbatim equality check in setup.sh.
575
654
  stampInstallSource(destDir, source);
576
- runScanPackages(hqRoot);
577
- console.log(chalk.green(`\n✓ Installed ${pkg.name}@${pkg.version} ${path.relative(hqRoot, destDir)}/`));
578
- console.log(chalk.dim(` Wired ${Object.values(pkg.contributes).flat().filter(Boolean).length} ` +
655
+ runScanPackages(hqRoot, { quiet: opts.quiet });
656
+ say(chalk.green(`\nOK Installed ${pkg.name}@${pkg.version} -> ${path.relative(hqRoot, destDir)}/`));
657
+ say(chalk.dim(` Wired ${Object.values(pkg.contributes).flat().filter(Boolean).length} ` +
579
658
  `contribution(s) into host-side paths.`));
580
659
  }
581
660
  finally {
@@ -583,4 +662,4 @@ export async function installPack(source, opts = {}) {
583
662
  }
584
663
  }
585
664
  //# sourceMappingURL=pack-install.js.map
586
- //# debugId=845e5459-a7ba-53ca-a04c-dd055fabe52d
665
+ //# debugId=08d95744-5e4f-5938-8a49-4f57a9bf042f
@@ -0,0 +1,22 @@
1
+ /**
2
+ * `hq packs` -- lifecycle for CONTENT packs (the ones installed via
3
+ * `hq install <source>` that live in `core/packages/hq-pack-<name>/`).
4
+ *
5
+ * hq packs list Installed packs + curated catalog, with link health.
6
+ * hq packs update Re-install the latest of an installed pack.
7
+ * hq packs uninstall Un-wire + archive a pack (clean, no dangling symlinks).
8
+ *
9
+ * Distinct from `hq packages ...` (the entitlement-gated REGISTRY system tracked
10
+ * in packages/registry.yaml). Content packs have no registry file -- the
11
+ * filesystem under core/packages/ is the source of truth. Today `hq install`
12
+ * is the only clean content-pack op; this adds the rest.
13
+ *
14
+ * Every subcommand supports `--json` for machine consumers (the HQ Sync
15
+ * menubar app). JSON is also the default when stdout is not a TTY, matching
16
+ * the `hq signals` / `hq sources` convention.
17
+ *
18
+ * Spec: knowledge/public/hq-core/package-yaml-spec.md.
19
+ */
20
+ import { Command } from 'commander';
21
+ export declare function registerPacksCommand(parent: Command): void;
22
+ //# sourceMappingURL=packs.d.ts.map
@@ -0,0 +1,406 @@
1
+ /**
2
+ * `hq packs` -- lifecycle for CONTENT packs (the ones installed via
3
+ * `hq install <source>` that live in `core/packages/hq-pack-<name>/`).
4
+ *
5
+ * hq packs list Installed packs + curated catalog, with link health.
6
+ * hq packs update Re-install the latest of an installed pack.
7
+ * hq packs uninstall Un-wire + archive a pack (clean, no dangling symlinks).
8
+ *
9
+ * Distinct from `hq packages ...` (the entitlement-gated REGISTRY system tracked
10
+ * in packages/registry.yaml). Content packs have no registry file -- the
11
+ * filesystem under core/packages/ is the source of truth. Today `hq install`
12
+ * is the only clean content-pack op; this adds the rest.
13
+ *
14
+ * Every subcommand supports `--json` for machine consumers (the HQ Sync
15
+ * menubar app). JSON is also the default when stdout is not a TTY, matching
16
+ * the `hq signals` / `hq sources` convention.
17
+ *
18
+ * Spec: knowledge/public/hq-core/package-yaml-spec.md.
19
+ */
20
+
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]="cb73c4fb-255e-5df7-accd-9b7d12362a06")}catch(e){}}();
22
+ import * as fs from 'fs';
23
+ import * as path from 'path';
24
+ import * as readline from 'readline';
25
+ import { spawnSync } from 'child_process';
26
+ import chalk from 'chalk';
27
+ import semverSatisfies from 'semver/functions/satisfies.js';
28
+ import { findHqRoot } from '../utils/manifest.js';
29
+ import { classify, resolveLatest, runScanPackages, installPack, } from './pack-install.js';
30
+ import { contributionLinks, linkStatus, listInstalledPacks, readPackManifest, unwirePack, readHqVersion, readRecommendedPackages, packagesDir, } from '../utils/pack-contributions.js';
31
+ function resolveRoot(opts) {
32
+ return opts.hqRoot ? path.resolve(opts.hqRoot) : findHqRoot();
33
+ }
34
+ /** JSON when --json or when piped (non-TTY); human-readable on a terminal. */
35
+ function wantsJson(opts) {
36
+ return opts.json === true || !process.stdout.isTTY;
37
+ }
38
+ function emitJson(value) {
39
+ process.stdout.write(JSON.stringify(value, null, 2) + '\n');
40
+ }
41
+ function safeClassify(source) {
42
+ if (!source)
43
+ return null;
44
+ try {
45
+ return classify(source);
46
+ }
47
+ catch {
48
+ return null;
49
+ }
50
+ }
51
+ function evalConditional(expr) {
52
+ return spawnSync('bash', ['-c', expr], { stdio: 'ignore' }).status === 0;
53
+ }
54
+ async function confirm(question) {
55
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
56
+ const answer = await new Promise((resolve) => {
57
+ rl.question(`${question} [y/N] `, (a) => {
58
+ rl.close();
59
+ resolve(a);
60
+ });
61
+ });
62
+ return /^(y|yes)$/i.test(answer.trim());
63
+ }
64
+ function buildInstalledView(hqRoot, hqVersion, pack, installedSources, checkUpdates) {
65
+ if (!pack.manifest) {
66
+ return {
67
+ name: pack.name,
68
+ transport: null,
69
+ hqCoreSatisfied: null,
70
+ contributes: {},
71
+ links: { live: 0, broken: 0, missing: 0, foreign: 0 },
72
+ brokenLinks: [],
73
+ inCatalog: false,
74
+ updateAvailable: null,
75
+ error: pack.error ?? 'unreadable manifest',
76
+ };
77
+ }
78
+ const m = pack.manifest;
79
+ const contributes = m.contributes ?? {};
80
+ const links = contributionLinks(hqRoot, pack.dir, contributes);
81
+ const counts = { live: 0, broken: 0, missing: 0, foreign: 0 };
82
+ const brokenLinks = [];
83
+ for (const link of links) {
84
+ const st = linkStatus(link);
85
+ counts[st]++;
86
+ if (st === 'broken')
87
+ brokenLinks.push({ key: link.key, item: link.item, dst: link.dst });
88
+ }
89
+ const contributeCounts = {};
90
+ for (const [key, items] of Object.entries(contributes)) {
91
+ if (Array.isArray(items) && items.length > 0)
92
+ contributeCounts[key] = items.length;
93
+ }
94
+ const requiresHqCore = m.requires?.hqCore;
95
+ const hqCoreSatisfied = hqVersion && requiresHqCore ? semverSatisfies(hqVersion, requiresHqCore) : null;
96
+ let updateAvailable = null;
97
+ if (checkUpdates && m.source) {
98
+ updateAvailable = resolveLatest(m.source, m.version).updateAvailable;
99
+ }
100
+ return {
101
+ name: m.name ?? pack.name,
102
+ version: m.version,
103
+ publisher: m.publisher,
104
+ source: m.source,
105
+ transport: safeClassify(m.source),
106
+ requiresHqCore,
107
+ hqCoreSatisfied,
108
+ contributes: contributeCounts,
109
+ links: counts,
110
+ brokenLinks,
111
+ inCatalog: m.source ? installedSources.has(m.source) : false,
112
+ updateAvailable,
113
+ };
114
+ }
115
+ function buildListView(hqRoot, checkUpdates, evalConditionals) {
116
+ const hqVersion = readHqVersion(hqRoot);
117
+ const packs = listInstalledPacks(hqRoot);
118
+ const catalog = readRecommendedPackages(hqRoot);
119
+ const catalogSources = new Set(catalog.map((c) => c.source));
120
+ const warnings = [];
121
+ const installed = packs.map((p) => buildInstalledView(hqRoot, hqVersion, p, catalogSources, checkUpdates));
122
+ for (const p of installed) {
123
+ if (p.error)
124
+ warnings.push(`${p.name}: ${p.error}`);
125
+ }
126
+ const installedSources = new Set(packs.map((p) => p.manifest?.source).filter((s) => !!s));
127
+ const available = catalog
128
+ .filter((c) => !installedSources.has(c.source))
129
+ .map((c) => {
130
+ let conditionalStatus = c.conditional
131
+ ? 'unevaluated'
132
+ : 'none';
133
+ if (c.conditional && evalConditionals) {
134
+ conditionalStatus = evalConditional(c.conditional) ? 'pass' : 'fail';
135
+ }
136
+ return {
137
+ source: c.source,
138
+ description: c.description,
139
+ installed: false,
140
+ conditional: c.conditional,
141
+ conditionalStatus,
142
+ };
143
+ });
144
+ return { hqRoot, hqVersion, installed, available, warnings };
145
+ }
146
+ function printListHuman(view) {
147
+ console.log(chalk.bold(`\nHQ packs (${view.hqRoot}, hqCore ${view.hqVersion ?? '?'})\n`));
148
+ if (view.installed.length === 0) {
149
+ console.log(chalk.dim(' No content packs installed.\n'));
150
+ }
151
+ else {
152
+ console.log(chalk.bold('Installed:'));
153
+ for (const p of view.installed) {
154
+ if (p.error) {
155
+ console.log(` ${chalk.red(p.name)} ${chalk.dim('(' + p.error + ')')}`);
156
+ continue;
157
+ }
158
+ const broken = p.links.broken > 0 ? chalk.red(` ! ${p.links.broken} broken link(s)`) : '';
159
+ const upd = p.updateAvailable ? chalk.yellow(' ^ update available') : '';
160
+ const hq = p.hqCoreSatisfied === false ? chalk.red(' x hqCore mismatch') : '';
161
+ console.log(` ${chalk.green(p.name)}@${p.version ?? '?'} ${chalk.dim(p.transport ?? '')}${upd}${hq}${broken}`);
162
+ }
163
+ console.log();
164
+ }
165
+ if (view.available.length > 0) {
166
+ console.log(chalk.bold('Available (curated):'));
167
+ for (const a of view.available) {
168
+ const gate = a.conditionalStatus === 'fail'
169
+ ? chalk.dim(' (gated off)')
170
+ : a.conditionalStatus === 'unevaluated' && a.conditional
171
+ ? chalk.dim(' (conditional)')
172
+ : '';
173
+ console.log(` ${chalk.cyan(a.source)}${gate}`);
174
+ if (a.description)
175
+ console.log(chalk.dim(` ${a.description}`));
176
+ }
177
+ console.log(chalk.dim('\n Install with: hq install <source>\n'));
178
+ }
179
+ }
180
+ async function runUpdate(name, opts) {
181
+ const hqRoot = resolveRoot(opts);
182
+ let packs = listInstalledPacks(hqRoot).filter((p) => p.manifest);
183
+ if (name) {
184
+ packs = packs.filter((p) => (p.manifest?.name ?? p.name) === name);
185
+ if (packs.length === 0) {
186
+ return [{ name, transport: 'unknown', updateAvailable: null, applied: false, error: 'not installed' }];
187
+ }
188
+ }
189
+ const results = [];
190
+ for (const pack of packs) {
191
+ const m = pack.manifest;
192
+ const source = m.source;
193
+ const pname = m.name ?? pack.name;
194
+ if (!source) {
195
+ results.push({ name: pname, transport: 'unknown', updateAvailable: null, applied: false, error: 'no stamped source -- cannot update' });
196
+ continue;
197
+ }
198
+ const probe = resolveLatest(source, m.version);
199
+ const base = {
200
+ name: pname,
201
+ transport: probe.transport,
202
+ current: probe.current,
203
+ latest: probe.latest,
204
+ updateAvailable: probe.updateAvailable,
205
+ applied: false,
206
+ error: probe.error,
207
+ };
208
+ if (opts.checkOnly) {
209
+ results.push(base);
210
+ continue;
211
+ }
212
+ // Apply only when there's a known update, or when explicitly named (force re-sync).
213
+ if (probe.updateAvailable === false && !name) {
214
+ results.push({ ...base, reason: 'already current' });
215
+ continue;
216
+ }
217
+ try {
218
+ // Un-wire old contributions first so a contribution dropped by the new
219
+ // version doesn't leave a dangling host symlink. installPack re-wires.
220
+ unwirePack(hqRoot, pack.dir, m.contributes ?? {});
221
+ const prevCwd = process.cwd();
222
+ if (opts.hqRoot)
223
+ process.chdir(hqRoot); // installPack resolves via findHqRoot()
224
+ try {
225
+ await installPack(source, {
226
+ allowHooks: opts.yes || opts.allowHooks,
227
+ followBranch: opts.branch,
228
+ quiet: wantsJson(opts),
229
+ });
230
+ }
231
+ finally {
232
+ if (opts.hqRoot)
233
+ process.chdir(prevCwd);
234
+ }
235
+ results.push({ ...base, applied: true });
236
+ }
237
+ catch (e) {
238
+ results.push({ ...base, applied: false, error: e.message });
239
+ }
240
+ }
241
+ return results;
242
+ }
243
+ function archiveTimestamp() {
244
+ return new Date().toISOString().replace(/[:.]/g, '-').replace('T', '_').slice(0, 19);
245
+ }
246
+ async function runUninstall(name, opts) {
247
+ const hqRoot = resolveRoot(opts);
248
+ const packDir = path.join(packagesDir(hqRoot), name);
249
+ if (!fs.existsSync(packDir)) {
250
+ throw new Error(`Pack "${name}" is not installed (no core/packages/${name}/).`);
251
+ }
252
+ const warnings = [];
253
+ const { manifest } = readPackManifest(packDir);
254
+ const contributes = manifest?.contributes ?? {};
255
+ if (!manifest) {
256
+ warnings.push('package.yaml unreadable -- host symlinks could not be computed precisely; ran a re-scan to reconcile.');
257
+ }
258
+ // 1. Un-wire only our symlinks.
259
+ const { unlinked, skipped } = unwirePack(hqRoot, packDir, contributes);
260
+ // 2. Archive (or delete) the pack dir -- BEFORE re-scan so it isn't re-wired.
261
+ let archived = null;
262
+ if (opts.archive === false) {
263
+ fs.rmSync(packDir, { recursive: true, force: true });
264
+ }
265
+ else {
266
+ const dest = path.join(packagesDir(hqRoot), '.archive', `${name}-${archiveTimestamp()}`);
267
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
268
+ fs.renameSync(packDir, dest);
269
+ archived = path.relative(hqRoot, dest);
270
+ }
271
+ // 3. Re-assert remaining packs' links (handles two packs sharing a contribution).
272
+ let rescan = 'skipped';
273
+ try {
274
+ // Quiet: uninstall emits its own summary / JSON; keep scan stdout off ours.
275
+ runScanPackages(hqRoot, { quiet: true });
276
+ rescan = 'ok';
277
+ }
278
+ catch {
279
+ rescan = 'skipped';
280
+ }
281
+ // 4. Regenerate the workers registry if this pack contributed workers.
282
+ let workersRegistryRegenerated = false;
283
+ const contributedWorkers = Array.isArray(contributes.workers) && contributes.workers.length > 0;
284
+ if (contributedWorkers) {
285
+ const gen = path.join(hqRoot, 'core', 'scripts', 'generate-workers-registry.sh');
286
+ if (fs.existsSync(gen)) {
287
+ const r = spawnSync('bash', [gen], { cwd: hqRoot, env: { ...process.env, HQ_ROOT: hqRoot }, stdio: 'ignore' });
288
+ workersRegistryRegenerated = r.status === 0;
289
+ if (!workersRegistryRegenerated)
290
+ warnings.push('generate-workers-registry.sh exited non-zero');
291
+ }
292
+ }
293
+ const sideEffectsSuggested = ['master-sync', 'qmd-reindex'];
294
+ if (contributedWorkers && !workersRegistryRegenerated)
295
+ sideEffectsSuggested.push('workers-registry');
296
+ return {
297
+ name,
298
+ archived,
299
+ unlinked,
300
+ skipped,
301
+ rescan,
302
+ workersRegistryRegenerated,
303
+ sideEffectsSuggested,
304
+ warnings,
305
+ };
306
+ }
307
+ // ---------------------------------------------------------------------------
308
+ // Registration
309
+ // ---------------------------------------------------------------------------
310
+ export function registerPacksCommand(parent) {
311
+ const packs = parent.command('packs').description('Content-pack lifecycle (install via `hq install`)');
312
+ packs
313
+ .command('list')
314
+ .alias('ls')
315
+ .description('List installed content packs and the curated catalog')
316
+ .option('--json', 'Machine-readable JSON output')
317
+ .option('--hq-root <path>', 'HQ root (default: auto-detect)')
318
+ .option('--check-updates', 'Probe each pack for available updates (network I/O)')
319
+ .option('--eval-conditionals', 'Evaluate catalog conditional predicates (runs bash)')
320
+ .action(async (opts) => {
321
+ try {
322
+ const view = buildListView(resolveRoot(opts), !!opts.checkUpdates, !!opts.evalConditionals);
323
+ if (wantsJson(opts))
324
+ emitJson(view);
325
+ else
326
+ printListHuman(view);
327
+ }
328
+ catch (e) {
329
+ console.error(chalk.red('packs list failed:'), e.message);
330
+ process.exit(1);
331
+ }
332
+ });
333
+ packs
334
+ .command('update [name]')
335
+ .description('Update an installed content pack (re-install latest)')
336
+ .option('--json', 'Machine-readable JSON output')
337
+ .option('--hq-root <path>', 'HQ root (default: auto-detect)')
338
+ .option('--check-only', 'Report availability without installing')
339
+ .option('-y, --yes', 'Non-interactive (implies --allow-hooks)')
340
+ .option('--allow-hooks', 'Install pack hooks without prompting')
341
+ .option('--branch', 'Follow the source branch instead of SHA-pinning')
342
+ .action(async (name, opts) => {
343
+ try {
344
+ const results = await runUpdate(name, opts);
345
+ if (wantsJson(opts)) {
346
+ emitJson({ checked: results, updatedCount: results.filter((r) => r.applied).length });
347
+ }
348
+ else {
349
+ for (const r of results) {
350
+ if (r.error)
351
+ console.log(` ${chalk.red(r.name)}: ${r.error}`);
352
+ else if (r.applied)
353
+ console.log(` ${chalk.green(r.name)}: updated`);
354
+ else if (r.updateAvailable)
355
+ console.log(` ${chalk.yellow(r.name)}: ${r.current ?? '?'} -> ${r.latest ?? '?'} available`);
356
+ else
357
+ console.log(` ${chalk.dim(r.name)}: ${r.reason ?? 'current'}`);
358
+ }
359
+ }
360
+ }
361
+ catch (e) {
362
+ console.error(chalk.red('packs update failed:'), e.message);
363
+ process.exit(1);
364
+ }
365
+ });
366
+ packs
367
+ .command('uninstall <name>')
368
+ .alias('remove')
369
+ .description('Un-wire and archive an installed content pack')
370
+ .option('--json', 'Machine-readable JSON output')
371
+ .option('--hq-root <path>', 'HQ root (default: auto-detect)')
372
+ .option('-y, --yes', 'Skip confirmation')
373
+ .option('--no-archive', 'Delete instead of archiving')
374
+ .action(async (name, opts) => {
375
+ try {
376
+ if (!opts.yes) {
377
+ if (!process.stdout.isTTY) {
378
+ throw new Error('Refusing to uninstall without --yes in non-interactive mode.');
379
+ }
380
+ const ok = await confirm(`Uninstall pack "${name}"? This removes its host symlinks.`);
381
+ if (!ok) {
382
+ console.log(chalk.dim('Aborted.'));
383
+ return;
384
+ }
385
+ }
386
+ const result = await runUninstall(name, opts);
387
+ if (wantsJson(opts)) {
388
+ emitJson(result);
389
+ }
390
+ else {
391
+ console.log(chalk.green(`\nOK Uninstalled ${name}`));
392
+ console.log(chalk.dim(` Unlinked ${result.unlinked.length} symlink(s); archived to ${result.archived ?? '(deleted)'}.`));
393
+ if (result.skipped.length > 0)
394
+ console.log(chalk.dim(` Left ${result.skipped.length} non-owned path(s) in place.`));
395
+ for (const w of result.warnings)
396
+ console.log(chalk.yellow(` ! ${w}`));
397
+ }
398
+ }
399
+ catch (e) {
400
+ console.error(chalk.red('packs uninstall failed:'), e.message);
401
+ process.exit(1);
402
+ }
403
+ });
404
+ }
405
+ //# sourceMappingURL=packs.js.map
406
+ //# debugId=cb73c4fb-255e-5df7-accd-9b7d12362a06
@@ -4,7 +4,7 @@
4
4
  * Graceful offline: if registry is unreachable, show cached data with a note.
5
5
  */
6
6
 
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]="2ca922cd-1115-5059-850d-3927226eb9ee")}catch(e){}}();
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]="a2386adb-d4c5-5725-b406-4246ac40425a")}catch(e){}}();
8
8
  import chalk from 'chalk';
9
9
  import { resolveDefaultHqRoot } from '../utils/cognito-session.js';
10
10
  import { readRegistry } from '../utils/registry.js';
@@ -15,9 +15,10 @@ export function registerPackageListCommand(parent) {
15
15
  .command('list')
16
16
  .alias('ls')
17
17
  .description('List installed and available packages')
18
- .action(async () => {
18
+ .option('--json', 'Machine-readable JSON output')
19
+ .action(async (opts) => {
19
20
  try {
20
- await listPackages();
21
+ await listPackages(opts.json === true || !process.stdout.isTTY);
21
22
  }
22
23
  catch (error) {
23
24
  console.error(chalk.red('List failed:'), error instanceof Error ? error.message : 'Unknown error');
@@ -25,7 +26,32 @@ export function registerPackageListCommand(parent) {
25
26
  }
26
27
  });
27
28
  }
28
- async function listPackages() {
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
+ }
29
55
  const hqRoot = resolveDefaultHqRoot({ onMissing: 'throw' });
30
56
  const installed = readRegistry(hqRoot);
31
57
  // Print installed packages
@@ -70,4 +96,4 @@ async function listPackages() {
70
96
  }
71
97
  }
72
98
  //# sourceMappingURL=pkg-list.js.map
73
- //# debugId=2ca922cd-1115-5059-850d-3927226eb9ee
99
+ //# debugId=a2386adb-d4c5-5725-b406-4246ac40425a