@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.
- package/dist/commands/group-grants.d.ts +76 -0
- package/dist/commands/group-grants.js +296 -0
- package/dist/commands/pack-install.d.ts +46 -1
- package/dist/commands/pack-install.js +96 -17
- package/dist/commands/packs.d.ts +22 -0
- package/dist/commands/packs.js +406 -0
- package/dist/commands/pkg-list.js +31 -5
- package/dist/index.js +12 -2
- package/dist/utils/pack-contributions.d.ts +87 -0
- package/dist/utils/pack-contributions.js +239 -0
- package/package.json +1 -1
- package/src/commands/group-grants.test.ts +291 -0
- package/src/commands/group-grants.ts +452 -0
- package/src/commands/pack-install.ts +129 -21
- package/src/commands/packs.ts +524 -0
- package/src/commands/pkg-list.ts +34 -3
- package/src/index.ts +12 -0
- package/src/utils/pack-contributions.test.ts +208 -0
- package/src/utils/pack-contributions.ts +303 -0
|
@@ -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]="
|
|
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
|
-
.
|
|
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
|
|
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=
|
|
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]="
|
|
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=
|
|
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
|