@indigoai-us/hq-cli 5.31.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.
@@ -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
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * HQ CLI - Module management, package management, and cloud sync for HQ
4
4
  */
5
5
 
6
- !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]="d7693093-4011-58fa-b5be-805b5f9f5421")}catch(e){}}();
6
+ !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]="89e1a66a-6a45-56a3-847c-2f0beba1429b")}catch(e){}}();
7
7
  import { Command } from "commander";
8
8
  import { initSentry, Sentry } from "./sentry.js";
9
9
  import { registerAddCommand } from "./commands/add.js";
@@ -23,11 +23,13 @@ import { registerPackageInstallCommand } from "./commands/pkg-install.js";
23
23
  import { registerPackageRemoveCommand } from "./commands/pkg-remove.js";
24
24
  import { registerPackageUpdateCommand } from "./commands/pkg-update.js";
25
25
  import { registerPackageListCommand } from "./commands/pkg-list.js";
26
+ import { registerPacksCommand } from "./commands/packs.js";
26
27
  import { registerTeamSyncCommand } from "./commands/team-sync.js";
27
28
  import { registerAuthCommands } from "./commands/auth.js";
28
29
  import { registerSecretsCommand } from "./commands/secrets.js";
29
30
  import { registerRunCommand } from "./commands/run.js";
30
31
  import { registerGroupsCommand } from "./commands/groups.js";
32
+ import { registerGroupGrantsCommand } from "./commands/group-grants.js";
31
33
  import { registerFilesCommand } from "./commands/files.js";
32
34
  import { registerFilesBrowseCommands } from "./commands/files-browse.js";
33
35
  import { registerMembersCommand } from "./commands/members.js";
@@ -72,6 +74,11 @@ registerPackageInstallCommand(packagesCmd);
72
74
  registerPackageRemoveCommand(packagesCmd);
73
75
  registerPackageUpdateCommand(packagesCmd);
74
76
  registerPackageListCommand(packagesCmd);
77
+ // Content-pack lifecycle (core/packages/hq-pack-*). Distinct from the registry
78
+ // `packages` system above. Available as both `hq packages packs …` (grouped)
79
+ // and `hq packs …` (top-level convenience).
80
+ registerPacksCommand(packagesCmd);
81
+ registerPacksCommand(program);
75
82
  // Top-level shortcuts for package commands
76
83
  // "hq install <slug>" = "hq packages install <slug>"
77
84
  // "hq remove <slug>" = "hq packages remove <slug>"
@@ -104,6 +111,9 @@ registerSecretsCommand(program);
104
111
  registerRunCommand(program);
105
112
  // Groups management (subcommand group — hq groups create|delete|add|remove|list|members)
106
113
  registerGroupsCommand(program);
114
+ // Cross-company group grants (subcommand group —
115
+ // hq group-grants grant|revoke|outbound|inbound)
116
+ registerGroupGrantsCommand(program);
107
117
  // Files ACL management (subcommand group — hq files share|unshare|acl)
108
118
  // `registerFilesCommand` returns the `files` group so we can attach the
109
119
  // browse-vs-sync subcommands (`hq files browse`/`cat`) onto the same group.
@@ -148,4 +158,4 @@ registerSignalsCommand(program);
148
158
  }
149
159
  })();
150
160
  //# sourceMappingURL=index.js.map
151
- //# debugId=d7693093-4011-58fa-b5be-805b5f9f5421
161
+ //# debugId=89e1a66a-6a45-56a3-847c-2f0beba1429b
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Content-pack contribution helpers -- the single source of truth (in TS) for
3
+ * the `contributes.* -> host-path` symlink mapping that `hq install` wires via
4
+ * `core/scripts/scan-packages.sh`.
5
+ *
6
+ * `pack-install.ts` only INSTALLS content packs (into `core/packages/<name>/`,
7
+ * tracked by filesystem presence -- there is no registry file). The list /
8
+ * update / uninstall lifecycle in `commands/packs.ts` needs to reason about the
9
+ * SAME mapping so it can report link health and cleanly un-wire a pack without
10
+ * leaving dangling symlinks. That mapping is duplicated today in two places:
11
+ *
12
+ * - core/scripts/scan-packages.sh (bash `case`, the wiring authority)
13
+ * - pack-install.ts validateManifest's `subpaths` record (payload validation)
14
+ *
15
+ * This module re-encodes it once for TS callers. A parity test
16
+ * (`pack-contributions.test.ts`) asserts it matches scan-packages.sh's `case`
17
+ * arms so the three copies cannot drift.
18
+ */
19
+ import type { PackManifest, PackContributeKey } from '../types.js';
20
+ /** A single symlink a pack contributes: dst (host path) -> src (inside pack). */
21
+ export interface WiredLink {
22
+ key: PackContributeKey;
23
+ item: string;
24
+ src: string;
25
+ dst: string;
26
+ }
27
+ export type LinkStatus = 'live' | 'broken' | 'missing' | 'foreign';
28
+ /**
29
+ * Every symlink a pack's `contributes` block declares. Empty subfields and
30
+ * non-array values are ignored, mirroring scan-packages.sh.
31
+ */
32
+ export declare function contributionLinks(hqRoot: string, packDir: string, contributes: Partial<Record<PackContributeKey, string[]>>): WiredLink[];
33
+ /** Classify a host path against the link that should own it. */
34
+ export declare function linkStatus(link: WiredLink): LinkStatus;
35
+ /** A content pack's manifest plus the install-time stamped source. */
36
+ export interface InstalledPackManifest extends PackManifest {
37
+ source?: string;
38
+ }
39
+ export interface InstalledPack {
40
+ name: string;
41
+ dir: string;
42
+ manifest: InstalledPackManifest | null;
43
+ error?: string;
44
+ }
45
+ /** Absolute path to `<hqRoot>/core/packages`. */
46
+ export declare function packagesDir(hqRoot: string): string;
47
+ /** Read and shallowly validate a pack's package.yaml. */
48
+ export declare function readPackManifest(packDir: string): {
49
+ manifest: InstalledPackManifest | null;
50
+ error?: string;
51
+ };
52
+ /**
53
+ * Walk `core/packages/<name>/package.yaml`. Skips the `.archive` dir, the bundled
54
+ * `README.md`, and any non-directory entry. Filesystem presence is the source
55
+ * of truth for installed content packs.
56
+ */
57
+ export declare function listInstalledPacks(hqRoot: string): InstalledPack[];
58
+ export interface UnwireResult {
59
+ unlinked: Array<{
60
+ key: PackContributeKey;
61
+ item: string;
62
+ dst: string;
63
+ }>;
64
+ skipped: Array<{
65
+ key: PackContributeKey;
66
+ item: string;
67
+ dst: string;
68
+ reason: 'foreign' | 'missing';
69
+ }>;
70
+ }
71
+ /**
72
+ * Remove only the host symlinks that resolve into THIS pack's directory
73
+ * (status `live` or `broken`). Foreign links and real files are left in place
74
+ * -- same collision philosophy as scan-packages.sh. This is what prevents an
75
+ * uninstall from leaving dangling symlinks behind.
76
+ */
77
+ export declare function unwirePack(hqRoot: string, packDir: string, contributes: Partial<Record<PackContributeKey, string[]>>): UnwireResult;
78
+ export declare function readHqVersion(hqRoot: string): string | null;
79
+ export interface CatalogEntry {
80
+ source: string;
81
+ description?: string;
82
+ conditional?: string;
83
+ auto_install?: boolean;
84
+ }
85
+ /** Read `recommended_packages` from core.yaml (the curated content-pack catalog). */
86
+ export declare function readRecommendedPackages(hqRoot: string): CatalogEntry[];
87
+ //# sourceMappingURL=pack-contributions.d.ts.map