@indigoai-us/hq-cli 5.14.0 → 5.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ name: CI
2
+ on:
3
+ pull_request:
4
+ branches: [main]
5
+ push:
6
+ branches: [main]
7
+
8
+ jobs:
9
+ build:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: actions/setup-node@v4
14
+ with:
15
+ node-version: 22
16
+ - run: npm ci
17
+ # generate-dsn.mjs only fatals when GITHUB_JOB=publish, so CI builds with
18
+ # an empty BUNDLED_DSN. That's intentional — the DSN is publish-only.
19
+ - run: npm run build --if-present
20
+ - run: npm run typecheck --if-present
21
+ - run: npm test --if-present
@@ -0,0 +1,86 @@
1
+ name: Publish to npm
2
+ on:
3
+ push:
4
+ tags: ["v*"]
5
+
6
+ jobs:
7
+ publish:
8
+ runs-on: ubuntu-latest
9
+ permissions:
10
+ contents: read
11
+ id-token: write
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - uses: actions/setup-node@v4
15
+ with:
16
+ # Node 24 ships npm 11.x, required for npm's trusted-publisher OIDC
17
+ # token-exchange flow. See hq-cloud's publish.yml for the full
18
+ # rationale (npm 10.x produces masked-404 failures on publish PUT).
19
+ node-version: 24
20
+ registry-url: https://registry.npmjs.org
21
+
22
+ - run: node --version && npm --version
23
+ - run: npm ci
24
+
25
+ - name: Build
26
+ run: npm run build --if-present
27
+ env:
28
+ # generate-dsn.mjs fatals when GITHUB_JOB=publish and this var is
29
+ # missing. Set this as a repo secret to bundle a Sentry DSN into the
30
+ # published binary.
31
+ HQ_CLI_PUBLISH_SENTRY_DSN: ${{ secrets.HQ_CLI_PUBLISH_SENTRY_DSN }}
32
+
33
+ # Inject Sentry debug IDs into compiled JS + map files. Must run after
34
+ # tsc and BEFORE publish so the installed binary carries the same debugId
35
+ # as the artifacts uploaded to Sentry, enabling debugId-based source-map
36
+ # resolution for globally-installed CLI users.
37
+ - name: Inject Sentry source map debug IDs
38
+ run: npx -y @sentry/cli@^2 sourcemaps inject dist/
39
+
40
+ # Pre-publish cross-package smoke test (Bug A guard).
41
+ #
42
+ # @indigoai-us/hq-cli depends on @indigoai-us/hq-cloud via a caret range
43
+ # (^5.1.0). The smoke installs a freshly-packed tarball into an isolated
44
+ # tmpdir — outside this repo — so transitive @indigoai-us/* deps resolve
45
+ # from the npm registry, NOT from a local checkout. Booting `hq --version`
46
+ # then exercises the full module-load graph. If hq-cloud removed or
47
+ # renamed a symbol hq-cli imports, this catches it BEFORE the publish PUT.
48
+ #
49
+ # Bypass: set repo-or-workflow variable WORKFLOW_ALLOW_BROKEN_PUBLISH=1.
50
+ - name: Pre-publish smoke test
51
+ if: ${{ vars.WORKFLOW_ALLOW_BROKEN_PUBLISH != '1' }}
52
+ run: bash .github/workflows/scripts/smoke-test-pkg.sh hq
53
+
54
+ - name: WARN — pre-publish smoke bypassed
55
+ if: ${{ vars.WORKFLOW_ALLOW_BROKEN_PUBLISH == '1' }}
56
+ run: |
57
+ echo "::warning::WORKFLOW_ALLOW_BROKEN_PUBLISH=1 — pre-publish cross-package smoke test was BYPASSED. This release may ship a DOA package."
58
+
59
+ # Trusted-publisher OIDC; no NODE_AUTH_TOKEN. No --provenance (private
60
+ # repo limitation since npm 2026-05-10). Pre-release versions (with '-')
61
+ # get --tag rc to keep latest dist-tag pointing at stable releases only.
62
+ - name: Publish to npm
63
+ id: publish
64
+ run: |
65
+ NAME=$(jq -r .name package.json)
66
+ VER=$(jq -r .version package.json)
67
+ if npm view "$NAME@$VER" version >/dev/null 2>&1; then
68
+ echo "$NAME@$VER already on npm — skipping"
69
+ else
70
+ if echo "$VER" | grep -q '-'; then
71
+ npm publish --access public --tag rc
72
+ else
73
+ npm publish --access public
74
+ fi
75
+ echo "published=true" >> "$GITHUB_OUTPUT"
76
+ fi
77
+
78
+ - name: Upload sourcemaps to Sentry
79
+ if: steps.publish.outputs.published == 'true'
80
+ run: |
81
+ VER=$(jq -r .version package.json)
82
+ npx -y @sentry/cli@^2 sourcemaps upload --release "hq-cli@$VER" dist/
83
+ env:
84
+ SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
85
+ SENTRY_ORG: indigo-d0
86
+ SENTRY_PROJECT: hq
@@ -0,0 +1,97 @@
1
+ #!/bin/bash
2
+ # smoke-test-pkg.sh — pre-publish cross-package import smoke test (Bug A guard).
3
+ #
4
+ # Single-package adaptation of the same script used in the legacy hq monorepo.
5
+ # The monorepo version operated on `packages/$PKG/package.json`; this version
6
+ # reads `./package.json` because each extracted repo is a single package at
7
+ # its root.
8
+ #
9
+ # Install the package in an isolated tmpdir — using a freshly-packed tarball
10
+ # when the package.json version is new, or fetching the existing name@version
11
+ # from npm when it is already published — then boot the binary with
12
+ # `--version`. The boot exercises the full module-load graph and the
13
+ # transitive `@indigoai-us/*` resolution path a customer's `npm install`
14
+ # would take. If the boot fails the script exits non-zero and the workflow
15
+ # aborts before any further publish steps run.
16
+ #
17
+ # Why we don't skip when the consumer's version is already on npm:
18
+ # @indigoai-us/hq-cli depends on @indigoai-us/hq-cloud via a caret range
19
+ # (^5.1.0). A producer bump (published from indigoai-us/hq-cloud) re-resolves
20
+ # this consumer's transitive deps on the next fresh install, so an already-
21
+ # published consumer is NOT actually "locked." If the new producer drops or
22
+ # renames a symbol the published consumer imports, `hq --version` crashes
23
+ # on every fresh install starting the moment the producer publishes.
24
+ #
25
+ # Usage:
26
+ # smoke-test-pkg.sh <bin-name>
27
+ # Example:
28
+ # smoke-test-pkg.sh hq
29
+
30
+ set -uo pipefail
31
+
32
+ BIN=${1:?usage: smoke-test-pkg.sh <bin-name>}
33
+
34
+ PKG_JSON="package.json"
35
+ if [ ! -f "$PKG_JSON" ]; then
36
+ echo "::error::smoke-test-pkg.sh: $PKG_JSON not found (cwd: $(pwd))"
37
+ exit 2
38
+ fi
39
+
40
+ NAME=$(jq -r .name "$PKG_JSON")
41
+ VER=$(jq -r .version "$PKG_JSON")
42
+
43
+ # Pick the install target. Already on npm → install by name@version so the
44
+ # smoke matches what a customer would actually receive. Not yet on npm →
45
+ # pack the local repo; the next publish step will push that exact byte-for-
46
+ # byte content. Either way, `npm install` happens OUTSIDE this repo so
47
+ # transitive @indigoai-us/* deps resolve from the registry.
48
+ INSTALL_SOURCE=""
49
+ if npm view "$NAME@$VER" version >/dev/null 2>&1; then
50
+ INSTALL_TARGET="$NAME@$VER"
51
+ INSTALL_SOURCE="npm registry (consumer version already published)"
52
+ else
53
+ TARBALL_RELATIVE=$(npm pack --silent --pack-destination /tmp)
54
+ if [ -z "$TARBALL_RELATIVE" ]; then
55
+ echo "::error::npm pack produced no tarball for $NAME"
56
+ exit 1
57
+ fi
58
+ INSTALL_TARGET="/tmp/$TARBALL_RELATIVE"
59
+ if [ ! -f "$INSTALL_TARGET" ]; then
60
+ echo "::error::expected tarball at $INSTALL_TARGET not found"
61
+ exit 1
62
+ fi
63
+ INSTALL_SOURCE="packed tarball ($TARBALL_RELATIVE)"
64
+ fi
65
+
66
+ echo "::group::smoke test: $NAME@$VER via $INSTALL_SOURCE"
67
+
68
+ SMOKE=$(mktemp -d)
69
+ pushd "$SMOKE" >/dev/null
70
+
71
+ printf '%s' '{"name":"smoke","version":"0.0.0","private":true}' > package.json
72
+
73
+ if ! npm install --no-audit --no-fund --no-package-lock "$INSTALL_TARGET" >/tmp/smoke-install.log 2>&1; then
74
+ echo "::error::pre-publish smoke install FAILED for $NAME@$VER:"
75
+ tail -50 /tmp/smoke-install.log
76
+ popd >/dev/null
77
+ echo "::endgroup::"
78
+ exit 1
79
+ fi
80
+
81
+ set +e
82
+ OUT=$("./node_modules/.bin/$BIN" --version 2>&1)
83
+ CODE=$?
84
+ set -e
85
+
86
+ popd >/dev/null
87
+
88
+ if [ "$CODE" -ne 0 ]; then
89
+ echo "::error::pre-publish smoke FAILED for $NAME@$VER (exit $CODE):"
90
+ echo "::error::$OUT"
91
+ echo "::error::Likely cause: a cross-package symbol was changed (added, removed, renamed) in another @indigoai-us/* package without keeping this consumer's import in sync. Bump this consumer (or revert the producer change) and re-tag, OR set the repo/workflow variable WORKFLOW_ALLOW_BROKEN_PUBLISH=1 to override (audited)."
92
+ echo "::endgroup::"
93
+ exit 1
94
+ fi
95
+
96
+ echo "✓ smoke ok: $NAME@$VER → $BIN --version → $OUT (via $INSTALL_SOURCE)"
97
+ echo "::endgroup::"
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 from manifest, or company dir 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, and never `"personal"` (which is auto-provisioned per-user, not
118
- * promoted via this subcommand).
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 from manifest, or company dir 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]="aacb5bf2-8362-5336-b864-43296c30a1d6")}catch(e){}}();
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]="3c9e69f3-8137-5db1-8692-e38774558f61")}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, and never `"personal"` (which is auto-provisioned per-user, not
52
- * promoted via this subcommand).
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
- throw new ProvisionError(2, `Slug "${slug}" is reserved (auto-provisioned per-user, not eligible for cloud promotion)`);
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,82 @@ 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
+ // Must be an actual directory, not a stray file. fs.existsSync returns true
168
+ // for regular files too — without this guard, auto-insert would fire on a
169
+ // file at `companies/<slug>`, provisionCompany would then create the vault
170
+ // entity and patch manifest.yaml before writeCompanyConfig's `mkdir -p .hq`
171
+ // exploded with ENOTDIR. statSync swallows the not-found case so a missing
172
+ // path is treated the same as before (no auto-insert).
173
+ let dirStat;
174
+ try {
175
+ dirStat = fs.statSync(dir);
176
+ }
177
+ catch {
178
+ return;
179
+ }
180
+ if (!dirStat.isDirectory())
181
+ return;
182
+ const raw = fs.readFileSync(mPath, "utf-8");
183
+ let parsed;
184
+ try {
185
+ parsed = yaml.load(raw);
186
+ }
187
+ catch {
188
+ return;
189
+ }
190
+ if (!parsed ||
191
+ typeof parsed !== "object" ||
192
+ !("companies" in parsed) ||
193
+ typeof parsed.companies !== "object") {
194
+ return;
195
+ }
196
+ const doc = parsed;
197
+ if (!doc.companies)
198
+ doc.companies = {};
199
+ if (doc.companies[slug] !== undefined)
200
+ return;
201
+ doc.companies[slug] = {};
202
+ const dump = yaml.dump(doc, { lineWidth: -1, noRefs: true });
203
+ const tmp = `${mPath}.tmp.${process.pid}`;
204
+ fs.writeFileSync(tmp, dump);
205
+ fs.renameSync(tmp, mPath);
206
+ }
118
207
  /**
119
208
  * Atomically patch `companies/manifest.yaml` to set `cloud_uid` + `bucket_name`
120
209
  * under the target slug. Read → mutate → temp-write → rename so concurrent
@@ -179,6 +268,18 @@ export function createDefaultVaultClient(apiUrl, accessToken) {
179
268
  Authorization: `Bearer ${accessToken}`,
180
269
  };
181
270
  return {
271
+ async listMyPersonEntities() {
272
+ const url = `${apiUrl.replace(/\/$/, "")}/entity/by-type/person`;
273
+ const res = await fetch(url, { method: "GET", headers });
274
+ if (res.status === 404)
275
+ return [];
276
+ if (!res.ok) {
277
+ const body = await safeBody(res);
278
+ throw new ProvisionError(1, `Vault GET /entity/by-type/person failed: ${res.status} ${res.statusText} — ${body}`);
279
+ }
280
+ const data = (await res.json());
281
+ return data.entities ?? [];
282
+ },
182
283
  async findCompanyBySlug(slug) {
183
284
  const url = `${apiUrl.replace(/\/$/, "")}/entity/by-slug/company/${encodeURIComponent(slug)}`;
184
285
  const res = await fetch(url, { method: "GET", headers });
@@ -258,8 +359,13 @@ async function defaultRunInitialSync(args) {
258
359
  */
259
360
  export async function provisionCompany(options) {
260
361
  const log = options.log ?? ((msg) => process.stderr.write(`[hq cloud provision] ${msg}\n`));
261
- // Step 1+2+3: validate slug, manifest, dir
362
+ // Step 1+2+3: validate slug, auto-heal a folder-only manifest, then validate
363
+ // the resulting manifest + dir. The auto-heal is provision-specific: it
364
+ // inserts an empty entry when companies/<slug>/ exists on disk but the slug
365
+ // is missing from manifest.yaml. validateManifestAndDir stays strict so
366
+ // other callers (cloud-demote) keep their typo-catching guard.
262
367
  validateSlug(options.slug);
368
+ ensureManifestEntryForProvision(options.hqRoot, options.slug);
263
369
  validateManifestAndDir(options.hqRoot, options.slug);
264
370
  log(`validated slug=${options.slug}`);
265
371
  // Step 4: auth — defer to injected resolver (default: ensureCognitoToken)
@@ -270,6 +376,23 @@ export async function provisionCompany(options) {
270
376
  // Step 5: GET-then-POST for idempotency
271
377
  const vaultClient = options.vaultClient ??
272
378
  createDefaultVaultClient(options.vaultApiUrl, accessToken);
379
+ // Pre-flight: caller MUST have a registered person entity. Without one,
380
+ // the initial-sync step at the end of this flow (STS /sts/vend) returns
381
+ // 403 "no person entity" and leaves the operator with a half-built cloud
382
+ // company — vault entity created, S3 bucket provisioned, manifest patched,
383
+ // .hq/config.json written, but nothing actually syncing. This was the
384
+ // primary failure mode in the 2026-05-14 setup-session deep dive
385
+ // (Joey Muller / sum-digital).
386
+ //
387
+ // Failing here, BEFORE any cloud-side resource creation, leaves the operator
388
+ // with no cleanup work — they fix the underlying onboarding gap (run
389
+ // `hq onboard` / sign in with the correct federated identity) and re-run
390
+ // `hq cloud provision company <slug>` cleanly.
391
+ const persons = await vaultClient.listMyPersonEntities();
392
+ if (persons.length === 0) {
393
+ 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.)');
394
+ }
395
+ log(`pre-flight ok — caller has ${persons.length} person entity(ies)`);
273
396
  let entity = await vaultClient.findCompanyBySlug(options.slug);
274
397
  let createdEntity = false;
275
398
  if (entity) {
@@ -426,4 +549,4 @@ export function registerCloudProvisionCommands(program) {
426
549
  });
427
550
  }
428
551
  //# sourceMappingURL=cloud-provision.js.map
429
- //# debugId=aacb5bf2-8362-5336-b864-43296c30a1d6
552
+ //# debugId=3c9e69f3-8137-5db1-8692-e38774558f61
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ export declare function registerMeetingsCommand(program: Command): void;
3
+ //# sourceMappingURL=meetings.d.ts.map