@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.
- package/dist/commands/files-browse.js +40 -28
- 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/files-browse.test.ts +49 -0
- package/src/commands/files-browse.ts +38 -26
- 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
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
|
|
@@ -0,0 +1,239 @@
|
|
|
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
|
+
|
|
20
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="f5492359-fdf3-5e85-9a81-699c4cd68f6a")}catch(e){}}();
|
|
21
|
+
import * as fs from 'fs';
|
|
22
|
+
import * as path from 'path';
|
|
23
|
+
import * as yaml from 'js-yaml';
|
|
24
|
+
/**
|
|
25
|
+
* Map one contributes entry to its source/host paths. MUST stay in lockstep
|
|
26
|
+
* with scan-packages.sh:wire_one_package and pack-install.ts:validateManifest.
|
|
27
|
+
*/
|
|
28
|
+
function linkFor(hqRoot, packDir, key, item) {
|
|
29
|
+
let srcRel;
|
|
30
|
+
let dstRel;
|
|
31
|
+
switch (key) {
|
|
32
|
+
case 'workers':
|
|
33
|
+
srcRel = path.join('workers', item);
|
|
34
|
+
dstRel = path.join('core', 'workers', 'public', item);
|
|
35
|
+
break;
|
|
36
|
+
case 'knowledge':
|
|
37
|
+
srcRel = path.join('knowledge', item);
|
|
38
|
+
dstRel = path.join('core', 'knowledge', 'public', item);
|
|
39
|
+
break;
|
|
40
|
+
case 'skills':
|
|
41
|
+
srcRel = path.join('skills', item);
|
|
42
|
+
dstRel = path.join('.claude', 'skills', item);
|
|
43
|
+
break;
|
|
44
|
+
case 'commands':
|
|
45
|
+
srcRel = path.join('commands', `${item}.md`);
|
|
46
|
+
dstRel = path.join('.claude', 'commands', `${item}.md`);
|
|
47
|
+
break;
|
|
48
|
+
case 'hooks':
|
|
49
|
+
srcRel = path.join('hooks', `${item}.sh`);
|
|
50
|
+
dstRel = path.join('.claude', 'hooks', `${item}.sh`);
|
|
51
|
+
break;
|
|
52
|
+
case 'policies':
|
|
53
|
+
srcRel = path.join('policies', `${item}.md`);
|
|
54
|
+
dstRel = path.join('core', 'policies', `${item}.md`);
|
|
55
|
+
break;
|
|
56
|
+
case 'scripts':
|
|
57
|
+
srcRel = path.join('scripts', item);
|
|
58
|
+
dstRel = path.join('core', 'scripts', item);
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
key,
|
|
63
|
+
item,
|
|
64
|
+
src: path.join(packDir, srcRel),
|
|
65
|
+
dst: path.join(hqRoot, dstRel),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Every symlink a pack's `contributes` block declares. Empty subfields and
|
|
70
|
+
* non-array values are ignored, mirroring scan-packages.sh.
|
|
71
|
+
*/
|
|
72
|
+
export function contributionLinks(hqRoot, packDir, contributes) {
|
|
73
|
+
const links = [];
|
|
74
|
+
for (const [key, items] of Object.entries(contributes)) {
|
|
75
|
+
if (!Array.isArray(items))
|
|
76
|
+
continue;
|
|
77
|
+
for (const item of items) {
|
|
78
|
+
if (typeof item !== 'string' || item.length === 0)
|
|
79
|
+
continue;
|
|
80
|
+
links.push(linkFor(hqRoot, packDir, key, item));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return links;
|
|
84
|
+
}
|
|
85
|
+
/** Classify a host path against the link that should own it. */
|
|
86
|
+
export function linkStatus(link) {
|
|
87
|
+
let st;
|
|
88
|
+
try {
|
|
89
|
+
st = fs.lstatSync(link.dst);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return 'missing';
|
|
93
|
+
}
|
|
94
|
+
if (!st.isSymbolicLink()) {
|
|
95
|
+
return 'foreign'; // a real file/dir occupies the slot -- not ours
|
|
96
|
+
}
|
|
97
|
+
let target;
|
|
98
|
+
try {
|
|
99
|
+
target = fs.readlinkSync(link.dst);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return 'foreign';
|
|
103
|
+
}
|
|
104
|
+
// scan-packages.sh writes the symlink target as the absolute `src` path, so a
|
|
105
|
+
// direct compare is correct. Resolve both to be robust to trailing slashes.
|
|
106
|
+
const resolvedTarget = path.resolve(path.dirname(link.dst), target);
|
|
107
|
+
if (path.resolve(link.src) !== resolvedTarget) {
|
|
108
|
+
return 'foreign'; // points at another pack / somewhere else
|
|
109
|
+
}
|
|
110
|
+
return fs.existsSync(link.src) ? 'live' : 'broken';
|
|
111
|
+
}
|
|
112
|
+
/** Absolute path to `<hqRoot>/core/packages`. */
|
|
113
|
+
export function packagesDir(hqRoot) {
|
|
114
|
+
return path.join(hqRoot, 'core', 'packages');
|
|
115
|
+
}
|
|
116
|
+
/** Read and shallowly validate a pack's package.yaml. */
|
|
117
|
+
export function readPackManifest(packDir) {
|
|
118
|
+
const manifestPath = path.join(packDir, 'package.yaml');
|
|
119
|
+
if (!fs.existsSync(manifestPath)) {
|
|
120
|
+
return { manifest: null, error: 'package.yaml missing' };
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
const parsed = yaml.load(fs.readFileSync(manifestPath, 'utf-8'));
|
|
124
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
125
|
+
return { manifest: null, error: 'package.yaml is not a mapping' };
|
|
126
|
+
}
|
|
127
|
+
return { manifest: parsed };
|
|
128
|
+
}
|
|
129
|
+
catch (e) {
|
|
130
|
+
return { manifest: null, error: `package.yaml invalid: ${e.message}` };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Walk `core/packages/<name>/package.yaml`. Skips the `.archive` dir, the bundled
|
|
135
|
+
* `README.md`, and any non-directory entry. Filesystem presence is the source
|
|
136
|
+
* of truth for installed content packs.
|
|
137
|
+
*/
|
|
138
|
+
export function listInstalledPacks(hqRoot) {
|
|
139
|
+
const dir = packagesDir(hqRoot);
|
|
140
|
+
if (!fs.existsSync(dir))
|
|
141
|
+
return [];
|
|
142
|
+
const out = [];
|
|
143
|
+
for (const name of fs.readdirSync(dir).sort()) {
|
|
144
|
+
if (name.startsWith('.'))
|
|
145
|
+
continue; // .archive, .DS_Store, etc.
|
|
146
|
+
const packDir = path.join(dir, name);
|
|
147
|
+
let st;
|
|
148
|
+
try {
|
|
149
|
+
st = fs.statSync(packDir);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (!st.isDirectory())
|
|
155
|
+
continue; // README.md and friends
|
|
156
|
+
const { manifest, error } = readPackManifest(packDir);
|
|
157
|
+
if (!manifest && !fs.existsSync(path.join(packDir, 'package.yaml'))) {
|
|
158
|
+
continue; // a plain dir that isn't a pack -- ignore silently
|
|
159
|
+
}
|
|
160
|
+
out.push({ name, dir: packDir, manifest, error });
|
|
161
|
+
}
|
|
162
|
+
return out;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Remove only the host symlinks that resolve into THIS pack's directory
|
|
166
|
+
* (status `live` or `broken`). Foreign links and real files are left in place
|
|
167
|
+
* -- same collision philosophy as scan-packages.sh. This is what prevents an
|
|
168
|
+
* uninstall from leaving dangling symlinks behind.
|
|
169
|
+
*/
|
|
170
|
+
export function unwirePack(hqRoot, packDir, contributes) {
|
|
171
|
+
const result = { unlinked: [], skipped: [] };
|
|
172
|
+
for (const link of contributionLinks(hqRoot, packDir, contributes)) {
|
|
173
|
+
const status = linkStatus(link);
|
|
174
|
+
if (status === 'live' || status === 'broken') {
|
|
175
|
+
try {
|
|
176
|
+
fs.unlinkSync(link.dst);
|
|
177
|
+
result.unlinked.push({ key: link.key, item: link.item, dst: link.dst });
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
result.skipped.push({
|
|
181
|
+
key: link.key,
|
|
182
|
+
item: link.item,
|
|
183
|
+
dst: link.dst,
|
|
184
|
+
reason: 'foreign',
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
result.skipped.push({
|
|
190
|
+
key: link.key,
|
|
191
|
+
item: link.item,
|
|
192
|
+
dst: link.dst,
|
|
193
|
+
reason: status === 'missing' ? 'missing' : 'foreign',
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return result;
|
|
198
|
+
}
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
// Host introspection: hqVersion + recommended_packages catalog
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
/** Locate core.yaml -- v14+ nests it under `core/`, older layouts at the root. */
|
|
203
|
+
function coreYamlPath(hqRoot) {
|
|
204
|
+
const nested = path.join(hqRoot, 'core', 'core.yaml');
|
|
205
|
+
if (fs.existsSync(nested))
|
|
206
|
+
return nested;
|
|
207
|
+
const flat = path.join(hqRoot, 'core.yaml');
|
|
208
|
+
if (fs.existsSync(flat))
|
|
209
|
+
return flat;
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
export function readHqVersion(hqRoot) {
|
|
213
|
+
const p = coreYamlPath(hqRoot);
|
|
214
|
+
if (!p)
|
|
215
|
+
return null;
|
|
216
|
+
try {
|
|
217
|
+
const c = yaml.load(fs.readFileSync(p, 'utf-8'));
|
|
218
|
+
return c?.hqVersion ?? null;
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
/** Read `recommended_packages` from core.yaml (the curated content-pack catalog). */
|
|
225
|
+
export function readRecommendedPackages(hqRoot) {
|
|
226
|
+
const p = coreYamlPath(hqRoot);
|
|
227
|
+
if (!p)
|
|
228
|
+
return [];
|
|
229
|
+
try {
|
|
230
|
+
const c = yaml.load(fs.readFileSync(p, 'utf-8'));
|
|
231
|
+
const list = c?.recommended_packages;
|
|
232
|
+
return Array.isArray(list) ? list : [];
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
return [];
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
//# sourceMappingURL=pack-contributions.js.map
|
|
239
|
+
//# debugId=f5492359-fdf3-5e85-9a81-699c4cd68f6a
|
package/package.json
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
23
|
+
import { Command } from "commander";
|
|
23
24
|
import * as fs from "node:fs";
|
|
24
25
|
import * as os from "node:os";
|
|
25
26
|
import * as path from "node:path";
|
|
@@ -1034,3 +1035,51 @@ describe("runGet", () => {
|
|
|
1034
1035
|
).rejects.toThrow(/No objects under/);
|
|
1035
1036
|
});
|
|
1036
1037
|
});
|
|
1038
|
+
|
|
1039
|
+
// ── --company parent-binding (regression) ───────────────────────────────────
|
|
1040
|
+
//
|
|
1041
|
+
// The `files` parent group declares `--company`, so commander binds the flag
|
|
1042
|
+
// to the PARENT command, not the subcommand — `subcommand.opts().company` is
|
|
1043
|
+
// empty. The browse/cat/search/get/shared-with-me actions therefore resolve
|
|
1044
|
+
// it via `command.optsWithGlobals().company`. This guards that contract so a
|
|
1045
|
+
// future refactor doesn't silently regress `hq files <sub> ... --company X`
|
|
1046
|
+
// back to the swallowed-flag bug.
|
|
1047
|
+
|
|
1048
|
+
describe("--company parent binding (optsWithGlobals)", () => {
|
|
1049
|
+
it("a subcommand sees --company declared on the parent group", () => {
|
|
1050
|
+
const program = new Command();
|
|
1051
|
+
const files = program
|
|
1052
|
+
.command("files")
|
|
1053
|
+
.option("--company <slug>", "Company slug");
|
|
1054
|
+
let seen: string | undefined = "UNSET";
|
|
1055
|
+
files
|
|
1056
|
+
.command("search <query>")
|
|
1057
|
+
.option("--personal")
|
|
1058
|
+
.action((_query: string, _options: unknown, command: Command) => {
|
|
1059
|
+
seen = command.optsWithGlobals().company as string | undefined;
|
|
1060
|
+
});
|
|
1061
|
+
program.parse(
|
|
1062
|
+
["files", "search", "README", "--company", "indigo"],
|
|
1063
|
+
{ from: "user" } as never,
|
|
1064
|
+
);
|
|
1065
|
+
expect(seen).toBe("indigo");
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
it("documents the bug: subcommand-local opts.company is empty for a parent-bound flag", () => {
|
|
1069
|
+
const program = new Command();
|
|
1070
|
+
const files = program
|
|
1071
|
+
.command("files")
|
|
1072
|
+
.option("--company <slug>", "Company slug");
|
|
1073
|
+
let local: string | undefined = "UNSET";
|
|
1074
|
+
files
|
|
1075
|
+
.command("search <query>")
|
|
1076
|
+
.action((_query: string, options: { company?: string }) => {
|
|
1077
|
+
local = options.company;
|
|
1078
|
+
});
|
|
1079
|
+
program.parse(
|
|
1080
|
+
["files", "search", "README", "--company", "indigo"],
|
|
1081
|
+
{ from: "user" } as never,
|
|
1082
|
+
);
|
|
1083
|
+
expect(local).toBeUndefined();
|
|
1084
|
+
});
|
|
1085
|
+
});
|
|
@@ -936,9 +936,12 @@ export function registerFilesBrowseCommands(filesCmd: Command): void {
|
|
|
936
936
|
`Local HQ tree root (default: ${DEFAULT_HQ_ROOT})`,
|
|
937
937
|
DEFAULT_HQ_ROOT,
|
|
938
938
|
)
|
|
939
|
-
.action(async (pathArg: string | undefined, options: FilesBrowseCliOptions) => {
|
|
939
|
+
.action(async (pathArg: string | undefined, options: FilesBrowseCliOptions, command: Command) => {
|
|
940
940
|
try {
|
|
941
|
-
|
|
941
|
+
// `--company` is declared on the parent `files` group, so commander
|
|
942
|
+
// binds it there — read merged opts to see it from the subcommand.
|
|
943
|
+
const company = command.optsWithGlobals().company as string | undefined;
|
|
944
|
+
if (options.personal && company) {
|
|
942
945
|
throw new Error(
|
|
943
946
|
"--personal and --company are mutually exclusive. Pick one.",
|
|
944
947
|
);
|
|
@@ -981,13 +984,13 @@ export function registerFilesBrowseCommands(filesCmd: Command): void {
|
|
|
981
984
|
}
|
|
982
985
|
|
|
983
986
|
// Resolve slug — CLI flag wins, otherwise parse from path arg.
|
|
984
|
-
const slug =
|
|
987
|
+
const slug = company ?? parseCompanySlugFromPath(pathArg);
|
|
985
988
|
|
|
986
989
|
// If the user passed `--company` AND the path doesn't begin with
|
|
987
990
|
// companies/<that-slug>/, refuse — we'd otherwise vend creds for
|
|
988
991
|
// one company and list keys from another tree, which never makes
|
|
989
992
|
// sense (defense in depth against operator typos).
|
|
990
|
-
if (
|
|
993
|
+
if (company !== undefined) {
|
|
991
994
|
const fromPath = (() => {
|
|
992
995
|
try {
|
|
993
996
|
return parseCompanySlugFromPath(pathArg);
|
|
@@ -995,9 +998,9 @@ export function registerFilesBrowseCommands(filesCmd: Command): void {
|
|
|
995
998
|
return undefined;
|
|
996
999
|
}
|
|
997
1000
|
})();
|
|
998
|
-
if (fromPath && fromPath !==
|
|
1001
|
+
if (fromPath && fromPath !== company) {
|
|
999
1002
|
throw new Error(
|
|
1000
|
-
`--company '${
|
|
1003
|
+
`--company '${company}' disagrees with path slug '${fromPath}'.`,
|
|
1001
1004
|
);
|
|
1002
1005
|
}
|
|
1003
1006
|
}
|
|
@@ -1060,9 +1063,11 @@ export function registerFilesBrowseCommands(filesCmd: Command): void {
|
|
|
1060
1063
|
`Local HQ tree root (default: ${DEFAULT_HQ_ROOT})`,
|
|
1061
1064
|
DEFAULT_HQ_ROOT,
|
|
1062
1065
|
)
|
|
1063
|
-
.action(async (keyArg: string, options: FilesCatCliOptions) => {
|
|
1066
|
+
.action(async (keyArg: string, options: FilesCatCliOptions, command: Command) => {
|
|
1064
1067
|
try {
|
|
1065
|
-
|
|
1068
|
+
// `--company` is bound on the parent `files` group — read merged opts.
|
|
1069
|
+
const company = command.optsWithGlobals().company as string | undefined;
|
|
1070
|
+
if (options.personal && company) {
|
|
1066
1071
|
throw new Error(
|
|
1067
1072
|
"--personal and --company are mutually exclusive. Pick one.",
|
|
1068
1073
|
);
|
|
@@ -1099,8 +1104,8 @@ export function registerFilesBrowseCommands(filesCmd: Command): void {
|
|
|
1099
1104
|
return;
|
|
1100
1105
|
}
|
|
1101
1106
|
|
|
1102
|
-
const slug =
|
|
1103
|
-
if (
|
|
1107
|
+
const slug = company ?? parseCompanySlugFromPath(keyArg);
|
|
1108
|
+
if (company !== undefined) {
|
|
1104
1109
|
const fromPath = (() => {
|
|
1105
1110
|
try {
|
|
1106
1111
|
return parseCompanySlugFromPath(keyArg);
|
|
@@ -1108,9 +1113,9 @@ export function registerFilesBrowseCommands(filesCmd: Command): void {
|
|
|
1108
1113
|
return undefined;
|
|
1109
1114
|
}
|
|
1110
1115
|
})();
|
|
1111
|
-
if (fromPath && fromPath !==
|
|
1116
|
+
if (fromPath && fromPath !== company) {
|
|
1112
1117
|
throw new Error(
|
|
1113
|
-
`--company '${
|
|
1118
|
+
`--company '${company}' disagrees with path slug '${fromPath}'.`,
|
|
1114
1119
|
);
|
|
1115
1120
|
}
|
|
1116
1121
|
}
|
|
@@ -1150,23 +1155,25 @@ export function registerFilesBrowseCommands(filesCmd: Command): void {
|
|
|
1150
1155
|
"--company <slug>",
|
|
1151
1156
|
"Scope to a single company (defaults to a cross-company roll-up).",
|
|
1152
1157
|
)
|
|
1153
|
-
.action(async (options: { company?: string }) => {
|
|
1158
|
+
.action(async (options: { company?: string }, command: Command) => {
|
|
1154
1159
|
try {
|
|
1160
|
+
// `--company` is bound on the parent `files` group — read merged opts.
|
|
1161
|
+
const company = command.optsWithGlobals().company as string | undefined;
|
|
1155
1162
|
const accessToken = await ensureCognitoToken();
|
|
1156
1163
|
const vaultConfig = buildVaultConfig(accessToken);
|
|
1157
1164
|
const client = new VaultClient(vaultConfig);
|
|
1158
1165
|
|
|
1159
1166
|
let companyUid: string | undefined;
|
|
1160
|
-
if (
|
|
1167
|
+
if (company) {
|
|
1161
1168
|
// Confirm membership + resolve UID, same early-failure pattern as
|
|
1162
1169
|
// browse/cat. Roll-up mode skips this and fans out internally.
|
|
1163
|
-
companyUid = await getCompanyUid(accessToken,
|
|
1170
|
+
companyUid = await getCompanyUid(accessToken, company);
|
|
1164
1171
|
}
|
|
1165
1172
|
|
|
1166
1173
|
const rows = await runSharedWithMe({
|
|
1167
1174
|
vaultClient: client,
|
|
1168
1175
|
companyUid,
|
|
1169
|
-
companySlug:
|
|
1176
|
+
companySlug: company,
|
|
1170
1177
|
});
|
|
1171
1178
|
|
|
1172
1179
|
console.log(formatSharedWithMeTable(rows));
|
|
@@ -1189,9 +1196,12 @@ export function registerFilesBrowseCommands(filesCmd: Command): void {
|
|
|
1189
1196
|
"--personal",
|
|
1190
1197
|
"Search the caller's canonical personal vault. Mutually exclusive with --company.",
|
|
1191
1198
|
)
|
|
1192
|
-
.action(async (query: string, options: FilesSearchCliOptions) => {
|
|
1199
|
+
.action(async (query: string, options: FilesSearchCliOptions, command: Command) => {
|
|
1193
1200
|
try {
|
|
1194
|
-
|
|
1201
|
+
// `--company` is declared on the parent `files` group too, so commander
|
|
1202
|
+
// binds it there; read the merged (global+local) opts to see it.
|
|
1203
|
+
const company = command.optsWithGlobals().company as string | undefined;
|
|
1204
|
+
if (options.personal && company) {
|
|
1195
1205
|
throw new Error(
|
|
1196
1206
|
"--personal and --company are mutually exclusive. Pick one.",
|
|
1197
1207
|
);
|
|
@@ -1219,16 +1229,16 @@ export function registerFilesBrowseCommands(filesCmd: Command): void {
|
|
|
1219
1229
|
return;
|
|
1220
1230
|
}
|
|
1221
1231
|
|
|
1222
|
-
if (!
|
|
1232
|
+
if (!company) {
|
|
1223
1233
|
throw new Error(
|
|
1224
1234
|
"search: --company <slug> is required (or --personal to search your personal vault).",
|
|
1225
1235
|
);
|
|
1226
1236
|
}
|
|
1227
|
-
await getCompanyUid(accessToken,
|
|
1237
|
+
await getCompanyUid(accessToken, company);
|
|
1228
1238
|
|
|
1229
1239
|
const rows = await runSearch({
|
|
1230
1240
|
query,
|
|
1231
|
-
companySlug:
|
|
1241
|
+
companySlug: company,
|
|
1232
1242
|
vaultClient: client,
|
|
1233
1243
|
s3Factory: defaultS3Factory,
|
|
1234
1244
|
region: DEFAULT_COGNITO.region,
|
|
@@ -1261,13 +1271,15 @@ export function registerFilesBrowseCommands(filesCmd: Command): void {
|
|
|
1261
1271
|
`Local HQ tree root (default: ${DEFAULT_HQ_ROOT})`,
|
|
1262
1272
|
DEFAULT_HQ_ROOT,
|
|
1263
1273
|
)
|
|
1264
|
-
.action(async (pathArg: string, options: FilesGetCliOptions) => {
|
|
1274
|
+
.action(async (pathArg: string, options: FilesGetCliOptions, command: Command) => {
|
|
1265
1275
|
try {
|
|
1266
1276
|
const accessToken = await ensureCognitoToken();
|
|
1267
1277
|
const client = new VaultClient(buildVaultConfig(accessToken));
|
|
1268
1278
|
|
|
1269
|
-
|
|
1270
|
-
|
|
1279
|
+
// `--company` is bound on the parent `files` group — read merged opts.
|
|
1280
|
+
const companyOpt = command.optsWithGlobals().company as string | undefined;
|
|
1281
|
+
const slug = companyOpt ?? parseCompanySlugFromPath(pathArg);
|
|
1282
|
+
if (companyOpt !== undefined) {
|
|
1271
1283
|
const fromPath = (() => {
|
|
1272
1284
|
try {
|
|
1273
1285
|
return parseCompanySlugFromPath(pathArg);
|
|
@@ -1275,9 +1287,9 @@ export function registerFilesBrowseCommands(filesCmd: Command): void {
|
|
|
1275
1287
|
return undefined;
|
|
1276
1288
|
}
|
|
1277
1289
|
})();
|
|
1278
|
-
if (fromPath && fromPath !==
|
|
1290
|
+
if (fromPath && fromPath !== companyOpt) {
|
|
1279
1291
|
throw new Error(
|
|
1280
|
-
`--company '${
|
|
1292
|
+
`--company '${companyOpt}' disagrees with path slug '${fromPath}'.`,
|
|
1281
1293
|
);
|
|
1282
1294
|
}
|
|
1283
1295
|
}
|