@indigoai-us/hq-cli 5.14.0 → 5.14.1
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/CHANGELOG.md +19 -0
- package/dist/commands/cloud-provision.d.ts +40 -3
- package/dist/commands/cloud-provision.js +118 -8
- package/dist/commands/pack-install.d.ts +31 -3
- package/dist/commands/pack-install.js +43 -31
- package/dist/utils/cognito-session.d.ts +25 -0
- package/dist/utils/cognito-session.js +50 -3
- package/package.json +1 -1
- package/src/commands/cloud-provision.test.ts +275 -0
- package/src/commands/cloud-provision.ts +131 -9
- package/src/commands/pack-install.test.ts +148 -0
- package/src/commands/pack-install.ts +42 -44
- package/src/utils/cognito-session.test.ts +112 -0
- package/src/utils/cognito-session.ts +46 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.14.1] — 2026-05-14
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **`hq install <pack>` now writes to `core/packages/<name>/`** under the
|
|
10
|
+
v12 HQ layout, not top-level `packages/<name>/`. Pre-v12 behavior left
|
|
11
|
+
an orphan tree alongside the canonical `core/packages/` shipped by the
|
|
12
|
+
template (`hq-core` / `hq-core-staging`).
|
|
13
|
+
- **Post-install `scan-packages.sh` is now resolved at
|
|
14
|
+
`core/scripts/scan-packages.sh`** instead of top-level `scripts/`.
|
|
15
|
+
- **Stopped writing pack entries to `modules/modules.yaml`.** Packs are
|
|
16
|
+
tracked by filesystem presence under v12 (no separate registry file).
|
|
17
|
+
`hq update <pack>` will re-resolve source from each pack's
|
|
18
|
+
`package.yaml` or prompt; this is intentional. The legacy
|
|
19
|
+
knowledge-modules feature (`strategy: 'git-clone'`) is untouched.
|
|
20
|
+
|
|
21
|
+
(Surfaced during end-to-end testing of the hq-installer staging-channel
|
|
22
|
+
toggle — see indigoai-us/hq-installer#61.)
|
|
23
|
+
|
|
5
24
|
## [5.14.0] — 2026-05-14
|
|
6
25
|
|
|
7
26
|
### Changed
|
|
@@ -20,7 +20,10 @@
|
|
|
20
20
|
* Exit codes:
|
|
21
21
|
* 0 — success (and `initial_sync.ok=true`)
|
|
22
22
|
* 1 — vault auth/network/API error (no entity provisioned)
|
|
23
|
-
* 2 — invalid slug, company missing
|
|
23
|
+
* 2 — invalid slug, company dir missing, or company is status=archived.
|
|
24
|
+
* Folder-only states (companies/<slug>/ exists on disk but no manifest
|
|
25
|
+
* entry) self-heal via ensureManifestEntryForProvision before
|
|
26
|
+
* validation runs, so a missing manifest entry alone is NOT an exit-2.
|
|
24
27
|
* 3 — sync failure after entity provisioned (cloud_uid in JSON;
|
|
25
28
|
* `initial_sync.ok=false`). Manifest + config may have been written.
|
|
26
29
|
*/
|
|
@@ -99,6 +102,15 @@ interface InitialSyncArgs {
|
|
|
99
102
|
}
|
|
100
103
|
/** Vault HTTP client interface — minimal surface for entity ops. */
|
|
101
104
|
export interface VaultClient {
|
|
105
|
+
/**
|
|
106
|
+
* List every person entity visible to the caller (scoped server-side by the
|
|
107
|
+
* caller's Cognito identity). Used by `provisionCompany` as a pre-flight
|
|
108
|
+
* to verify the caller has been onboarded before mutating any cloud-side
|
|
109
|
+
* resources — the initial-sync step at the end of provision needs to vend
|
|
110
|
+
* STS credentials, which the server only does for callers with a person
|
|
111
|
+
* entity.
|
|
112
|
+
*/
|
|
113
|
+
listMyPersonEntities(): Promise<VaultEntity[]>;
|
|
102
114
|
findCompanyBySlug(slug: string): Promise<VaultEntity | null>;
|
|
103
115
|
createCompanyEntity(input: {
|
|
104
116
|
slug: string;
|
|
@@ -114,8 +126,10 @@ export declare class ProvisionError extends Error {
|
|
|
114
126
|
}
|
|
115
127
|
/**
|
|
116
128
|
* Validate a company slug per the contract: alphanumeric / dot / dash / underscore,
|
|
117
|
-
* non-empty,
|
|
118
|
-
*
|
|
129
|
+
* non-empty, never `"."` or `".."` (path-traversal — would resolve to
|
|
130
|
+
* `companies/` itself or `<hqRoot>` and write outside the intended company
|
|
131
|
+
* directory), and never `"personal"` (auto-provisioned per-user, not promoted
|
|
132
|
+
* via this subcommand).
|
|
119
133
|
*
|
|
120
134
|
* Throws ProvisionError with code=2 on failure.
|
|
121
135
|
*/
|
|
@@ -136,11 +150,34 @@ export declare function companyConfigPath(hqRoot: string, slug: string): string;
|
|
|
136
150
|
* - company is `status: archived`
|
|
137
151
|
* - `companies/<slug>/` does not exist
|
|
138
152
|
*
|
|
153
|
+
* Strict on purpose — callers that want the "auto-insert a missing entry when
|
|
154
|
+
* the local folder exists" affordance (cloud-provision's fault-tolerant flow)
|
|
155
|
+
* MUST call `ensureManifestEntryForProvision` first. cloud-demote and other
|
|
156
|
+
* sensitive callers depend on this strict failure for typo-catching.
|
|
157
|
+
*
|
|
139
158
|
* Returns the parsed manifest (so the caller can re-use it for the patch step).
|
|
140
159
|
*/
|
|
141
160
|
export declare function validateManifestAndDir(hqRoot: string, slug: string): {
|
|
142
161
|
manifest: ManifestDoc;
|
|
143
162
|
};
|
|
163
|
+
/**
|
|
164
|
+
* Provision-only pre-step. If `companies/<slug>/` exists on disk but the slug
|
|
165
|
+
* is absent from `manifest.yaml`, atomically insert an empty entry
|
|
166
|
+
* (`<slug>: {}`) so the downstream `validateManifestAndDir` + `patchManifest`
|
|
167
|
+
* flow has something to operate on. Heals the common folder-only state
|
|
168
|
+
* (manual mkdir, or older tools that didn't patch manifest.yaml) that hq-sync's
|
|
169
|
+
* Connect button used to dead-end on with exit 2.
|
|
170
|
+
*
|
|
171
|
+
* No-op in every other case:
|
|
172
|
+
* - manifest file missing / malformed (validation will surface)
|
|
173
|
+
* - slug already present (any value, including null — preserved as-is)
|
|
174
|
+
* - `companies/<slug>/` does not exist (validation will surface as a typo)
|
|
175
|
+
*
|
|
176
|
+
* This affordance is provision-specific. `cloud-demote` and other strict
|
|
177
|
+
* callers MUST NOT call this — their typo-catching contract depends on
|
|
178
|
+
* `validateManifestAndDir` throwing on missing-slug.
|
|
179
|
+
*/
|
|
180
|
+
export declare function ensureManifestEntryForProvision(hqRoot: string, slug: string): void;
|
|
144
181
|
/**
|
|
145
182
|
* Top-level manifest shape we touch. We preserve all unknown fields — only
|
|
146
183
|
* `cloud_uid` and `bucket_name` under the target slug are mutated.
|
|
@@ -20,12 +20,15 @@
|
|
|
20
20
|
* Exit codes:
|
|
21
21
|
* 0 — success (and `initial_sync.ok=true`)
|
|
22
22
|
* 1 — vault auth/network/API error (no entity provisioned)
|
|
23
|
-
* 2 — invalid slug, company missing
|
|
23
|
+
* 2 — invalid slug, company dir missing, or company is status=archived.
|
|
24
|
+
* Folder-only states (companies/<slug>/ exists on disk but no manifest
|
|
25
|
+
* entry) self-heal via ensureManifestEntryForProvision before
|
|
26
|
+
* validation runs, so a missing manifest entry alone is NOT an exit-2.
|
|
24
27
|
* 3 — sync failure after entity provisioned (cloud_uid in JSON;
|
|
25
28
|
* `initial_sync.ok=false`). Manifest + config may have been written.
|
|
26
29
|
*/
|
|
27
30
|
|
|
28
|
-
!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]="
|
|
31
|
+
!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]="5573abae-ee71-5106-94e7-3485e3e0fbb2")}catch(e){}}();
|
|
29
32
|
import chalk from "chalk";
|
|
30
33
|
import * as fs from "node:fs";
|
|
31
34
|
import * as path from "node:path";
|
|
@@ -45,11 +48,13 @@ export class ProvisionError extends Error {
|
|
|
45
48
|
}
|
|
46
49
|
// ── Validation ───────────────────────────────────────────────────────────────
|
|
47
50
|
const SLUG_REGEX = /^[A-Za-z0-9._-]+$/;
|
|
48
|
-
const FORBIDDEN_SLUGS = new Set(["personal"]);
|
|
51
|
+
const FORBIDDEN_SLUGS = new Set(["personal", ".", ".."]);
|
|
49
52
|
/**
|
|
50
53
|
* Validate a company slug per the contract: alphanumeric / dot / dash / underscore,
|
|
51
|
-
* non-empty,
|
|
52
|
-
*
|
|
54
|
+
* non-empty, never `"."` or `".."` (path-traversal — would resolve to
|
|
55
|
+
* `companies/` itself or `<hqRoot>` and write outside the intended company
|
|
56
|
+
* directory), and never `"personal"` (auto-provisioned per-user, not promoted
|
|
57
|
+
* via this subcommand).
|
|
53
58
|
*
|
|
54
59
|
* Throws ProvisionError with code=2 on failure.
|
|
55
60
|
*/
|
|
@@ -61,7 +66,10 @@ export function validateSlug(slug) {
|
|
|
61
66
|
throw new ProvisionError(2, `Invalid slug "${slug}" — must match ${SLUG_REGEX.source}`);
|
|
62
67
|
}
|
|
63
68
|
if (FORBIDDEN_SLUGS.has(slug)) {
|
|
64
|
-
|
|
69
|
+
const reason = slug === "personal"
|
|
70
|
+
? "auto-provisioned per-user, not eligible for cloud promotion"
|
|
71
|
+
: "path-traversal slug (would resolve to a directory outside the intended company folder)";
|
|
72
|
+
throw new ProvisionError(2, `Slug "${slug}" is reserved (${reason})`);
|
|
65
73
|
}
|
|
66
74
|
}
|
|
67
75
|
/** Path to the top-level companies manifest file. */
|
|
@@ -86,6 +94,11 @@ export function companyConfigPath(hqRoot, slug) {
|
|
|
86
94
|
* - company is `status: archived`
|
|
87
95
|
* - `companies/<slug>/` does not exist
|
|
88
96
|
*
|
|
97
|
+
* Strict on purpose — callers that want the "auto-insert a missing entry when
|
|
98
|
+
* the local folder exists" affordance (cloud-provision's fault-tolerant flow)
|
|
99
|
+
* MUST call `ensureManifestEntryForProvision` first. cloud-demote and other
|
|
100
|
+
* sensitive callers depend on this strict failure for typo-catching.
|
|
101
|
+
*
|
|
89
102
|
* Returns the parsed manifest (so the caller can re-use it for the patch step).
|
|
90
103
|
*/
|
|
91
104
|
export function validateManifestAndDir(hqRoot, slug) {
|
|
@@ -115,6 +128,69 @@ export function validateManifestAndDir(hqRoot, slug) {
|
|
|
115
128
|
}
|
|
116
129
|
return { manifest };
|
|
117
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* Provision-only pre-step. If `companies/<slug>/` exists on disk but the slug
|
|
133
|
+
* is absent from `manifest.yaml`, atomically insert an empty entry
|
|
134
|
+
* (`<slug>: {}`) so the downstream `validateManifestAndDir` + `patchManifest`
|
|
135
|
+
* flow has something to operate on. Heals the common folder-only state
|
|
136
|
+
* (manual mkdir, or older tools that didn't patch manifest.yaml) that hq-sync's
|
|
137
|
+
* Connect button used to dead-end on with exit 2.
|
|
138
|
+
*
|
|
139
|
+
* No-op in every other case:
|
|
140
|
+
* - manifest file missing / malformed (validation will surface)
|
|
141
|
+
* - slug already present (any value, including null — preserved as-is)
|
|
142
|
+
* - `companies/<slug>/` does not exist (validation will surface as a typo)
|
|
143
|
+
*
|
|
144
|
+
* This affordance is provision-specific. `cloud-demote` and other strict
|
|
145
|
+
* callers MUST NOT call this — their typo-catching contract depends on
|
|
146
|
+
* `validateManifestAndDir` throwing on missing-slug.
|
|
147
|
+
*/
|
|
148
|
+
export function ensureManifestEntryForProvision(hqRoot, slug) {
|
|
149
|
+
const mPath = manifestPath(hqRoot);
|
|
150
|
+
if (!fs.existsSync(mPath))
|
|
151
|
+
return;
|
|
152
|
+
// Defense-in-depth against path-traversal slugs (`.`, `..`, anything that
|
|
153
|
+
// resolves outside companies/). validateSlug rejects these upstream, but
|
|
154
|
+
// this helper is exported and might be called directly by future code —
|
|
155
|
+
// never auto-insert based on a path that isn't a literal direct child of
|
|
156
|
+
// `companies/`. fs.realpathSync would also catch symlink-escapes, but the
|
|
157
|
+
// directory may legitimately not exist yet on disk; path.resolve gives us
|
|
158
|
+
// the canonical lexical form without a stat call.
|
|
159
|
+
const companiesDir = path.resolve(hqRoot, "companies");
|
|
160
|
+
const expected = path.resolve(companiesDir, slug);
|
|
161
|
+
const expectedParent = path.dirname(expected);
|
|
162
|
+
if (expectedParent !== companiesDir)
|
|
163
|
+
return;
|
|
164
|
+
if (path.basename(expected) !== slug)
|
|
165
|
+
return;
|
|
166
|
+
const dir = companyDirPath(hqRoot, slug);
|
|
167
|
+
if (!fs.existsSync(dir))
|
|
168
|
+
return;
|
|
169
|
+
const raw = fs.readFileSync(mPath, "utf-8");
|
|
170
|
+
let parsed;
|
|
171
|
+
try {
|
|
172
|
+
parsed = yaml.load(raw);
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (!parsed ||
|
|
178
|
+
typeof parsed !== "object" ||
|
|
179
|
+
!("companies" in parsed) ||
|
|
180
|
+
typeof parsed.companies !== "object") {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const doc = parsed;
|
|
184
|
+
if (!doc.companies)
|
|
185
|
+
doc.companies = {};
|
|
186
|
+
if (doc.companies[slug] !== undefined)
|
|
187
|
+
return;
|
|
188
|
+
doc.companies[slug] = {};
|
|
189
|
+
const dump = yaml.dump(doc, { lineWidth: -1, noRefs: true });
|
|
190
|
+
const tmp = `${mPath}.tmp.${process.pid}`;
|
|
191
|
+
fs.writeFileSync(tmp, dump);
|
|
192
|
+
fs.renameSync(tmp, mPath);
|
|
193
|
+
}
|
|
118
194
|
/**
|
|
119
195
|
* Atomically patch `companies/manifest.yaml` to set `cloud_uid` + `bucket_name`
|
|
120
196
|
* under the target slug. Read → mutate → temp-write → rename so concurrent
|
|
@@ -179,6 +255,18 @@ export function createDefaultVaultClient(apiUrl, accessToken) {
|
|
|
179
255
|
Authorization: `Bearer ${accessToken}`,
|
|
180
256
|
};
|
|
181
257
|
return {
|
|
258
|
+
async listMyPersonEntities() {
|
|
259
|
+
const url = `${apiUrl.replace(/\/$/, "")}/entity/by-type/person`;
|
|
260
|
+
const res = await fetch(url, { method: "GET", headers });
|
|
261
|
+
if (res.status === 404)
|
|
262
|
+
return [];
|
|
263
|
+
if (!res.ok) {
|
|
264
|
+
const body = await safeBody(res);
|
|
265
|
+
throw new ProvisionError(1, `Vault GET /entity/by-type/person failed: ${res.status} ${res.statusText} — ${body}`);
|
|
266
|
+
}
|
|
267
|
+
const data = (await res.json());
|
|
268
|
+
return data.entities ?? [];
|
|
269
|
+
},
|
|
182
270
|
async findCompanyBySlug(slug) {
|
|
183
271
|
const url = `${apiUrl.replace(/\/$/, "")}/entity/by-slug/company/${encodeURIComponent(slug)}`;
|
|
184
272
|
const res = await fetch(url, { method: "GET", headers });
|
|
@@ -258,8 +346,13 @@ async function defaultRunInitialSync(args) {
|
|
|
258
346
|
*/
|
|
259
347
|
export async function provisionCompany(options) {
|
|
260
348
|
const log = options.log ?? ((msg) => process.stderr.write(`[hq cloud provision] ${msg}\n`));
|
|
261
|
-
// Step 1+2+3: validate slug, manifest,
|
|
349
|
+
// Step 1+2+3: validate slug, auto-heal a folder-only manifest, then validate
|
|
350
|
+
// the resulting manifest + dir. The auto-heal is provision-specific: it
|
|
351
|
+
// inserts an empty entry when companies/<slug>/ exists on disk but the slug
|
|
352
|
+
// is missing from manifest.yaml. validateManifestAndDir stays strict so
|
|
353
|
+
// other callers (cloud-demote) keep their typo-catching guard.
|
|
262
354
|
validateSlug(options.slug);
|
|
355
|
+
ensureManifestEntryForProvision(options.hqRoot, options.slug);
|
|
263
356
|
validateManifestAndDir(options.hqRoot, options.slug);
|
|
264
357
|
log(`validated slug=${options.slug}`);
|
|
265
358
|
// Step 4: auth — defer to injected resolver (default: ensureCognitoToken)
|
|
@@ -270,6 +363,23 @@ export async function provisionCompany(options) {
|
|
|
270
363
|
// Step 5: GET-then-POST for idempotency
|
|
271
364
|
const vaultClient = options.vaultClient ??
|
|
272
365
|
createDefaultVaultClient(options.vaultApiUrl, accessToken);
|
|
366
|
+
// Pre-flight: caller MUST have a registered person entity. Without one,
|
|
367
|
+
// the initial-sync step at the end of this flow (STS /sts/vend) returns
|
|
368
|
+
// 403 "no person entity" and leaves the operator with a half-built cloud
|
|
369
|
+
// company — vault entity created, S3 bucket provisioned, manifest patched,
|
|
370
|
+
// .hq/config.json written, but nothing actually syncing. This was the
|
|
371
|
+
// primary failure mode in the 2026-05-14 setup-session deep dive
|
|
372
|
+
// (Joey Muller / sum-digital).
|
|
373
|
+
//
|
|
374
|
+
// Failing here, BEFORE any cloud-side resource creation, leaves the operator
|
|
375
|
+
// with no cleanup work — they fix the underlying onboarding gap (run
|
|
376
|
+
// `hq onboard` / sign in with the correct federated identity) and re-run
|
|
377
|
+
// `hq cloud provision company <slug>` cleanly.
|
|
378
|
+
const persons = await vaultClient.listMyPersonEntities();
|
|
379
|
+
if (persons.length === 0) {
|
|
380
|
+
throw new ProvisionError(2, 'No person entity found for this Cognito identity. Run `hq onboard` first to create your HQ identity, then re-run `hq cloud provision company`. (Provision was halted before any cloud-side resources were created.)');
|
|
381
|
+
}
|
|
382
|
+
log(`pre-flight ok — caller has ${persons.length} person entity(ies)`);
|
|
273
383
|
let entity = await vaultClient.findCompanyBySlug(options.slug);
|
|
274
384
|
let createdEntity = false;
|
|
275
385
|
if (entity) {
|
|
@@ -426,4 +536,4 @@ export function registerCloudProvisionCommands(program) {
|
|
|
426
536
|
});
|
|
427
537
|
}
|
|
428
538
|
//# sourceMappingURL=cloud-provision.js.map
|
|
429
|
-
//# debugId=
|
|
539
|
+
//# debugId=5573abae-ee71-5106-94e7-3485e3e0fbb2
|
|
@@ -26,16 +26,44 @@
|
|
|
26
26
|
* 3. Parse + validate package.yaml (10 checks from spec)
|
|
27
27
|
* 4. Evaluate `conditional` predicate — skip if exits non-zero
|
|
28
28
|
* 5. Confirm hooks if `contributes.hooks` non-empty (unless --allow-hooks)
|
|
29
|
-
* 6. Move into packages/{name}/
|
|
30
|
-
* 7.
|
|
31
|
-
*
|
|
29
|
+
* 6. Move into core/packages/{name}/
|
|
30
|
+
* 7. Run core/scripts/scan-packages.sh to wire contributions into host paths
|
|
31
|
+
*
|
|
32
|
+
* Installed packs are tracked by filesystem presence — there's no separate
|
|
33
|
+
* registry file under the v12 layout. (`hq update <pack>` re-resolves source
|
|
34
|
+
* from each pack's package.yaml; rationale lives in the layout-fix PR.)
|
|
32
35
|
*/
|
|
36
|
+
import type { PackManifest } from '../types.js';
|
|
33
37
|
/**
|
|
34
38
|
* sourceMatchesPackPattern — exported for the dispatcher in pkg-install.ts
|
|
35
39
|
* so it can decide whether to route to the new content-pack handler or fall
|
|
36
40
|
* back to the legacy registry flow.
|
|
37
41
|
*/
|
|
38
42
|
export declare function sourceMatchesPackPattern(source: string): boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Install the fetched payload to `<hqRoot>/core/packages/<pkg.name>/` (HQ
|
|
45
|
+
* v12+ layout). The HQ template (`hq-core` / `hq-core-staging`) ships
|
|
46
|
+
* `core/packages/` as the canonical pack root; writing to top-level
|
|
47
|
+
* `packages/` would leave an orphan tree alongside the real one.
|
|
48
|
+
*
|
|
49
|
+
* Re-installs replace the existing destination so stale contributions don't
|
|
50
|
+
* linger (the post-install `scan-packages.sh` would otherwise wire them
|
|
51
|
+
* back into host paths).
|
|
52
|
+
*
|
|
53
|
+
* Exported for tests in pack-install.test.ts — see that file for the
|
|
54
|
+
* contract this function pins.
|
|
55
|
+
*/
|
|
56
|
+
export declare function installToPackages(payloadDir: string, pkg: PackManifest, hqRoot: string): string;
|
|
57
|
+
/**
|
|
58
|
+
* Run `<hqRoot>/core/scripts/scan-packages.sh` to wire the newly installed
|
|
59
|
+
* pack's contributions into the host paths (skills, hooks, policies, etc.).
|
|
60
|
+
* Skipping with a dim warning if the script is missing keeps fresh HQs (or
|
|
61
|
+
* older templates) usable — the next session start picks them up via its
|
|
62
|
+
* own scan.
|
|
63
|
+
*
|
|
64
|
+
* Exported for tests.
|
|
65
|
+
*/
|
|
66
|
+
export declare function runScanPackages(hqRoot: string): void;
|
|
39
67
|
export interface InstallPackOptions {
|
|
40
68
|
allowHooks?: boolean;
|
|
41
69
|
followBranch?: boolean;
|
|
@@ -26,12 +26,15 @@
|
|
|
26
26
|
* 3. Parse + validate package.yaml (10 checks from spec)
|
|
27
27
|
* 4. Evaluate `conditional` predicate — skip if exits non-zero
|
|
28
28
|
* 5. Confirm hooks if `contributes.hooks` non-empty (unless --allow-hooks)
|
|
29
|
-
* 6. Move into packages/{name}/
|
|
30
|
-
* 7.
|
|
31
|
-
*
|
|
29
|
+
* 6. Move into core/packages/{name}/
|
|
30
|
+
* 7. Run core/scripts/scan-packages.sh to wire contributions into host paths
|
|
31
|
+
*
|
|
32
|
+
* Installed packs are tracked by filesystem presence — there's no separate
|
|
33
|
+
* registry file under the v12 layout. (`hq update <pack>` re-resolves source
|
|
34
|
+
* from each pack's package.yaml; rationale lives in the layout-fix PR.)
|
|
32
35
|
*/
|
|
33
36
|
|
|
34
|
-
!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]="
|
|
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]="5dea7752-f10c-553c-a419-cf05441b3c13")}catch(e){}}();
|
|
35
38
|
import * as fs from 'fs';
|
|
36
39
|
import * as os from 'os';
|
|
37
40
|
import * as path from 'path';
|
|
@@ -42,7 +45,7 @@ import chalk from 'chalk';
|
|
|
42
45
|
import semverSatisfies from 'semver/functions/satisfies.js';
|
|
43
46
|
import semverValid from 'semver/functions/valid.js';
|
|
44
47
|
import semverValidRange from 'semver/ranges/valid.js';
|
|
45
|
-
import { findHqRoot
|
|
48
|
+
import { findHqRoot } from '../utils/manifest.js';
|
|
46
49
|
function classify(source) {
|
|
47
50
|
if (source.startsWith('@'))
|
|
48
51
|
return 'npm';
|
|
@@ -415,10 +418,23 @@ function evalConditional(expr) {
|
|
|
415
418
|
return r.status === 0;
|
|
416
419
|
}
|
|
417
420
|
// ---------------------------------------------------------------------------
|
|
418
|
-
// Move into packages/ +
|
|
421
|
+
// Move into core/packages/ + run core/scripts/scan-packages.sh
|
|
419
422
|
// ---------------------------------------------------------------------------
|
|
420
|
-
|
|
421
|
-
|
|
423
|
+
/**
|
|
424
|
+
* Install the fetched payload to `<hqRoot>/core/packages/<pkg.name>/` (HQ
|
|
425
|
+
* v12+ layout). The HQ template (`hq-core` / `hq-core-staging`) ships
|
|
426
|
+
* `core/packages/` as the canonical pack root; writing to top-level
|
|
427
|
+
* `packages/` would leave an orphan tree alongside the real one.
|
|
428
|
+
*
|
|
429
|
+
* Re-installs replace the existing destination so stale contributions don't
|
|
430
|
+
* linger (the post-install `scan-packages.sh` would otherwise wire them
|
|
431
|
+
* back into host paths).
|
|
432
|
+
*
|
|
433
|
+
* Exported for tests in pack-install.test.ts — see that file for the
|
|
434
|
+
* contract this function pins.
|
|
435
|
+
*/
|
|
436
|
+
export function installToPackages(payloadDir, pkg, hqRoot) {
|
|
437
|
+
const packagesDir = path.join(hqRoot, 'core', 'packages');
|
|
422
438
|
fs.mkdirSync(packagesDir, { recursive: true });
|
|
423
439
|
const destDir = path.join(packagesDir, pkg.name);
|
|
424
440
|
if (fs.existsSync(destDir)) {
|
|
@@ -430,28 +446,19 @@ function installToPackages(payloadDir, pkg, hqRoot) {
|
|
|
430
446
|
execFileSync('rsync', ['-a', srcSlashed, destSlashed], { stdio: 'inherit' });
|
|
431
447
|
return destDir;
|
|
432
448
|
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
access: pkg.access === 'public' ? 'public' : undefined,
|
|
445
|
-
};
|
|
446
|
-
if (fetched.resolvedSha)
|
|
447
|
-
entry.resolved_sha = fetched.resolvedSha;
|
|
448
|
-
manifest.modules.push(entry);
|
|
449
|
-
writeManifest(hqRoot, manifest);
|
|
450
|
-
}
|
|
451
|
-
function runScanPackages(hqRoot) {
|
|
452
|
-
const script = path.join(hqRoot, 'scripts', 'scan-packages.sh');
|
|
449
|
+
/**
|
|
450
|
+
* Run `<hqRoot>/core/scripts/scan-packages.sh` to wire the newly installed
|
|
451
|
+
* pack's contributions into the host paths (skills, hooks, policies, etc.).
|
|
452
|
+
* Skipping with a dim warning if the script is missing keeps fresh HQs (or
|
|
453
|
+
* older templates) usable — the next session start picks them up via its
|
|
454
|
+
* own scan.
|
|
455
|
+
*
|
|
456
|
+
* Exported for tests.
|
|
457
|
+
*/
|
|
458
|
+
export function runScanPackages(hqRoot) {
|
|
459
|
+
const script = path.join(hqRoot, 'core', 'scripts', 'scan-packages.sh');
|
|
453
460
|
if (!fs.existsSync(script)) {
|
|
454
|
-
console.log(chalk.dim(` (scripts/scan-packages.sh not present — skipping auto-wire; ` +
|
|
461
|
+
console.log(chalk.dim(` (core/scripts/scan-packages.sh not present — skipping auto-wire; ` +
|
|
455
462
|
`will run on next session start)`));
|
|
456
463
|
return;
|
|
457
464
|
}
|
|
@@ -502,7 +509,12 @@ export async function installPack(source, opts = {}) {
|
|
|
502
509
|
return;
|
|
503
510
|
}
|
|
504
511
|
const destDir = installToPackages(fetched.payloadDir, pkg, hqRoot);
|
|
505
|
-
|
|
512
|
+
// Under the v12 HQ layout, packs live at `core/packages/<name>/` and are
|
|
513
|
+
// tracked by filesystem presence alone — no `modules.yaml` write. That
|
|
514
|
+
// removes the side effect that created a top-level `modules/` directory
|
|
515
|
+
// alongside the canonical `core/`. `hq update <pack>` will re-resolve a
|
|
516
|
+
// pack's source from its on-disk package.yaml or prompt for it, but that
|
|
517
|
+
// tradeoff is intentional — see the layout-fix PR for rationale.
|
|
506
518
|
runScanPackages(hqRoot);
|
|
507
519
|
console.log(chalk.green(`\n✓ Installed ${pkg.name}@${pkg.version} → ${path.relative(hqRoot, destDir)}/`));
|
|
508
520
|
console.log(chalk.dim(` Wired ${Object.values(pkg.contributes).flat().filter(Boolean).length} ` +
|
|
@@ -513,4 +525,4 @@ export async function installPack(source, opts = {}) {
|
|
|
513
525
|
}
|
|
514
526
|
}
|
|
515
527
|
//# sourceMappingURL=pack-install.js.map
|
|
516
|
-
//# debugId=
|
|
528
|
+
//# debugId=5dea7752-f10c-553c-a419-cf05441b3c13
|
|
@@ -21,6 +21,31 @@
|
|
|
21
21
|
import { type CognitoAuthConfig, type VaultServiceConfig } from "@indigoai-us/hq-cloud";
|
|
22
22
|
export declare const DEFAULT_COGNITO: CognitoAuthConfig;
|
|
23
23
|
export declare const DEFAULT_VAULT_API_URL: string;
|
|
24
|
+
/**
|
|
25
|
+
* Resolve the default HQ tree root for cloud-aware subcommands.
|
|
26
|
+
*
|
|
27
|
+
* Priority order:
|
|
28
|
+
* 1. `$HQ_ROOT` env var (explicit user override)
|
|
29
|
+
* 2. Walk up from `process.cwd()` to the nearest dir containing BOTH a
|
|
30
|
+
* `core.yaml` file AND a `companies/` directory (root-unique marker
|
|
31
|
+
* pair — see note below).
|
|
32
|
+
* 3. Fall back to `~/hq` (the historical default)
|
|
33
|
+
*
|
|
34
|
+
* Why both markers?
|
|
35
|
+
* The HQ root has `core.yaml` AND a sibling `companies/` directory. The
|
|
36
|
+
* synced `core/` subtree (which is itself part of the root's personal-vault
|
|
37
|
+
* scope) ALSO contains a `core.yaml` (the template's version-source-of-
|
|
38
|
+
* truth), but does NOT contain `companies/`. Single-marker `core.yaml`
|
|
39
|
+
* detection would stop at `<hqRoot>/core/` when the CLI is launched from
|
|
40
|
+
* somewhere inside that subtree, and downstream `companies/` lookups would
|
|
41
|
+
* silently miss the real content. Requiring `companies/` as well guarantees
|
|
42
|
+
* we resolve to the actual HQ root (Codex P2 on hq#146).
|
|
43
|
+
*
|
|
44
|
+
* Evaluated once at module load — commander.js `.option()` callers pin the
|
|
45
|
+
* value at registration time, which matches the user's actual cwd at process
|
|
46
|
+
* start. Re-importable as a function for tests and command-time resolution.
|
|
47
|
+
*/
|
|
48
|
+
export declare function resolveDefaultHqRoot(): string;
|
|
24
49
|
export declare const DEFAULT_HQ_ROOT: string;
|
|
25
50
|
/**
|
|
26
51
|
* Return a non-expired Cognito access token, refreshing or browser-logging-in
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
* HQ_VAULT_API_URL — vault-service API Gateway URL
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
!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]="
|
|
22
|
+
!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]="6a8ac157-0903-513d-b4c5-45fb532b8026")}catch(e){}}();
|
|
23
|
+
import * as fs from "fs";
|
|
23
24
|
import * as os from "os";
|
|
24
25
|
import * as path from "path";
|
|
25
26
|
import chalk from "chalk";
|
|
@@ -41,7 +42,53 @@ export const DEFAULT_COGNITO = {
|
|
|
41
42
|
: "Google",
|
|
42
43
|
};
|
|
43
44
|
export const DEFAULT_VAULT_API_URL = process.env.HQ_VAULT_API_URL ?? "https://hqapi.getindigo.ai";
|
|
44
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Resolve the default HQ tree root for cloud-aware subcommands.
|
|
47
|
+
*
|
|
48
|
+
* Priority order:
|
|
49
|
+
* 1. `$HQ_ROOT` env var (explicit user override)
|
|
50
|
+
* 2. Walk up from `process.cwd()` to the nearest dir containing BOTH a
|
|
51
|
+
* `core.yaml` file AND a `companies/` directory (root-unique marker
|
|
52
|
+
* pair — see note below).
|
|
53
|
+
* 3. Fall back to `~/hq` (the historical default)
|
|
54
|
+
*
|
|
55
|
+
* Why both markers?
|
|
56
|
+
* The HQ root has `core.yaml` AND a sibling `companies/` directory. The
|
|
57
|
+
* synced `core/` subtree (which is itself part of the root's personal-vault
|
|
58
|
+
* scope) ALSO contains a `core.yaml` (the template's version-source-of-
|
|
59
|
+
* truth), but does NOT contain `companies/`. Single-marker `core.yaml`
|
|
60
|
+
* detection would stop at `<hqRoot>/core/` when the CLI is launched from
|
|
61
|
+
* somewhere inside that subtree, and downstream `companies/` lookups would
|
|
62
|
+
* silently miss the real content. Requiring `companies/` as well guarantees
|
|
63
|
+
* we resolve to the actual HQ root (Codex P2 on hq#146).
|
|
64
|
+
*
|
|
65
|
+
* Evaluated once at module load — commander.js `.option()` callers pin the
|
|
66
|
+
* value at registration time, which matches the user's actual cwd at process
|
|
67
|
+
* start. Re-importable as a function for tests and command-time resolution.
|
|
68
|
+
*/
|
|
69
|
+
export function resolveDefaultHqRoot() {
|
|
70
|
+
if (process.env.HQ_ROOT)
|
|
71
|
+
return path.resolve(process.env.HQ_ROOT);
|
|
72
|
+
let cur = path.resolve(process.cwd());
|
|
73
|
+
while (cur !== path.dirname(cur)) {
|
|
74
|
+
if (isHqRoot(cur))
|
|
75
|
+
return cur;
|
|
76
|
+
cur = path.dirname(cur);
|
|
77
|
+
}
|
|
78
|
+
return path.join(os.homedir(), "hq");
|
|
79
|
+
}
|
|
80
|
+
/** True iff `dir` looks like an HQ root (has core.yaml + companies/ dir). */
|
|
81
|
+
function isHqRoot(dir) {
|
|
82
|
+
if (!fs.existsSync(path.join(dir, "core.yaml")))
|
|
83
|
+
return false;
|
|
84
|
+
try {
|
|
85
|
+
return fs.statSync(path.join(dir, "companies")).isDirectory();
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
export const DEFAULT_HQ_ROOT = resolveDefaultHqRoot();
|
|
45
92
|
/**
|
|
46
93
|
* Return a non-expired Cognito access token, refreshing or browser-logging-in
|
|
47
94
|
* as needed. Cache lives at ~/.hq/cognito-tokens.json.
|
|
@@ -108,4 +155,4 @@ export async function refreshCachedSession() {
|
|
|
108
155
|
}
|
|
109
156
|
}
|
|
110
157
|
//# sourceMappingURL=cognito-session.js.map
|
|
111
|
-
//# debugId=
|
|
158
|
+
//# debugId=6a8ac157-0903-513d-b4c5-45fb532b8026
|