@indigoai-us/hq-cli 5.219.0 → 5.220.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.
@@ -17,7 +17,10 @@ interface WorkerLike {
17
17
  }
18
18
  export interface FlagRegistryDependencies {
19
19
  endpoint?: string;
20
- createWorker?: (filename: URL, options: WorkerOptions) => WorkerLike;
20
+ createWorker?: (filename: string | URL, options: WorkerOptions) => WorkerLike;
21
+ /** Test seam for exercising the no-source fail-open path. */
22
+ workerSource?: string;
23
+ captureWorkerFailure?: (error: Error, tags: Record<string, string>) => void;
21
24
  }
22
25
  export interface ProcessFlagCompanyContext {
23
26
  companyUid?: string;
@@ -12,8 +12,10 @@ import { readMachineCompanyBinding } from "./work-context/company.js";
12
12
  import { readSessionState } from "./work-context/state.js";
13
13
  import { workContextRoot } from "./work-context/paths.js";
14
14
  import { readCompaniesManifestMap } from "./work-context/repo-remote.js";
15
- import { resolveDefaultHqRoot } from "../utils/cognito-session.js";
15
+ import { loadCachedTokens, resolveDefaultHqRoot } from "../utils/cognito-session.js";
16
16
  import { resolveFlagRegistryEndpoint, } from "./flag-registry-endpoint.js";
17
+ import { FLAG_REGISTRY_SELF_CONTAINED_WORKER_FLAG, flagRegistryStateDir, flagRegistryWorkerModeCachePath, readCachedFlagRegistryWorkerMode, writeCachedFlagRegistryWorkerMode, } from "./flag-registry-worker-mode-cache.js";
18
+ import { FLAG_REGISTRY_WORKER_SOURCE } from "./flag-registry-worker-source.generated.js";
17
19
  export { FLAG_REGISTRY_ENDPOINT_ENV } from "./flag-registry-endpoint.js";
18
20
  const COMPANY_UID_RE = /^cmp_[A-Za-z0-9]{3,128}$/;
19
21
  const COMPANY_SLUG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
@@ -99,6 +101,75 @@ function processSnapshotReader(companyIdentifiers) {
99
101
  },
100
102
  };
101
103
  }
104
+ function personSubjectForWorkerMode(idToken) {
105
+ if (typeof idToken !== "string")
106
+ return null;
107
+ const payload = idToken.split(".")[1];
108
+ if (!payload)
109
+ return null;
110
+ try {
111
+ const subject = JSON.parse(Buffer.from(payload, "base64url").toString("utf8")).sub;
112
+ return typeof subject === "string" && subject.trim() ? subject : null;
113
+ }
114
+ catch {
115
+ Sentry.captureMessage("Could not scope cached flag-registry worker mode because the cached ID token payload is invalid", { level: "warning", tags: { component: "flag-registry-worker-mode-cache" } });
116
+ return null;
117
+ }
118
+ }
119
+ /** Scope a persisted rollout value to the same identity that fetched it. */
120
+ function workerModeCacheScopes(company) {
121
+ const tokens = loadCachedTokens();
122
+ const personSubject = tokens ? personSubjectForWorkerMode(tokens.idToken) : null;
123
+ if (tokens && !personSubject)
124
+ return null;
125
+ return [
126
+ ...(company.companyUid ? [`company:${company.companyUid}`] : []),
127
+ ...(company.companySlug ? [`company-slug:${company.companySlug}`] : []),
128
+ ...(personSubject ? [`person:${personSubject}`] : []),
129
+ ];
130
+ }
131
+ function selfContainedWorkerEnabled(company) {
132
+ const localOverride = readLocalOverride(FLAG_REGISTRY_SELF_CONTAINED_WORKER_FLAG, process.env);
133
+ if (localOverride !== null)
134
+ return localOverride;
135
+ const scopes = workerModeCacheScopes(company);
136
+ if (!scopes)
137
+ return false;
138
+ const filePath = flagRegistryWorkerModeCachePath(flagRegistryStateDir(), scopes);
139
+ try {
140
+ return readCachedFlagRegistryWorkerMode(filePath)?.value ?? false;
141
+ }
142
+ catch (error) {
143
+ Sentry.captureException(error, {
144
+ tags: { component: "flag-registry-worker-mode-cache", worker: "flag-registry" },
145
+ });
146
+ return false;
147
+ }
148
+ }
149
+ function cacheWorkerModeFromSnapshot(snapshot, company) {
150
+ // A developer's local hq-flags override is process-local and must not become
151
+ // the value selected by a future process.
152
+ if (readLocalOverride(FLAG_REGISTRY_SELF_CONTAINED_WORKER_FLAG, process.env) !== null)
153
+ return;
154
+ const scopes = workerModeCacheScopes(company);
155
+ if (!scopes)
156
+ return;
157
+ const decision = resolveFlag({
158
+ flagKey: FLAG_REGISTRY_SELF_CONTAINED_WORKER_FLAG,
159
+ env: {},
160
+ snapshot,
161
+ companyIdentifiers: [company.companyUid, company.companySlug].filter((value) => Boolean(value)),
162
+ lookup: { fallback: false },
163
+ });
164
+ try {
165
+ writeCachedFlagRegistryWorkerMode(flagRegistryWorkerModeCachePath(flagRegistryStateDir(), scopes), { version: snapshot.version, value: decision.value });
166
+ }
167
+ catch (error) {
168
+ Sentry.captureException(error, {
169
+ tags: { component: "flag-registry-worker-mode-cache", worker: "flag-registry" },
170
+ });
171
+ }
172
+ }
102
173
  /**
103
174
  * Begin one registry refresh without awaiting it or retaining the CLI process.
104
175
  *
@@ -117,23 +188,43 @@ export function kickFlagRegistryReadiness(dependencies = {}) {
117
188
  const companyIdentifiers = [companyUid, company.companySlug].filter((value) => Boolean(value));
118
189
  processReader = processSnapshotReader(companyIdentifiers);
119
190
  try {
191
+ let workerSource = new URL("./flag-registry-worker.js", import.meta.url);
192
+ let workerOptions = { workerData: {
193
+ endpoint,
194
+ ...(companyUid ? { companyUid } : {}),
195
+ ...(company.companySlug ? { companyIdentifiers: [company.companySlug] } : {}),
196
+ } };
197
+ if (selfContainedWorkerEnabled(company)) {
198
+ // The worker source is generated and embedded in this parent module at
199
+ // build time. It is already resident before company, identity, and cache
200
+ // reads can yield to an updater replacing this package tree.
201
+ const embeddedSource = dependencies.workerSource ?? FLAG_REGISTRY_WORKER_SOURCE;
202
+ if (embeddedSource.length === 0) {
203
+ (dependencies.captureWorkerFailure ?? ((error, tags) => {
204
+ Sentry.captureException(error, { tags });
205
+ }))(new Error("Self-contained flag-registry worker source is unavailable"), {
206
+ component: "flag-registry-worker-startup",
207
+ worker: "flag-registry",
208
+ flag_worker_failure: "embedded_source_unavailable",
209
+ });
210
+ return processReader;
211
+ }
212
+ workerSource = embeddedSource;
213
+ workerOptions = { ...workerOptions, eval: true };
214
+ }
120
215
  // The SDK's child-process integration captures every Worker error unless
121
216
  // its process-wide captureWorkerErrors option is disabled. Scope only this
122
217
  // worker so the reporting boundary can classify its startup errors without
123
218
  // changing capture for unrelated workers.
124
219
  const worker = Sentry.withScope((scope) => {
125
220
  scope.setTag("worker", "flag-registry");
126
- return (dependencies.createWorker ?? ((filename, options) => new Worker(filename, options)))(new URL("./flag-registry-worker.js", import.meta.url), {
127
- workerData: {
128
- endpoint,
129
- ...(companyUid ? { companyUid } : {}),
130
- ...(company.companySlug ? { companyIdentifiers: [company.companySlug] } : {}),
131
- },
132
- });
221
+ return (dependencies.createWorker ?? ((filename, options) => new Worker(filename, options)))(workerSource, workerOptions);
133
222
  });
134
223
  worker.on("message", (value) => {
135
- if (isFlagSnapshot(value))
224
+ if (isFlagSnapshot(value)) {
136
225
  heldSnapshot = value;
226
+ cacheWorkerModeFromSnapshot(value, company);
227
+ }
137
228
  });
138
229
  // A failed background refresh must stay invisible to an offline CLI.
139
230
  worker.on("error", () => { });
@@ -16,7 +16,15 @@ export declare function assertQuestionBodySecretSafe(body: QuestionBody): void;
16
16
  export declare function questionRecordRefusalCode(err: unknown): "secret_text_refused" | "question_company_unavailable" | null;
17
17
  export declare function parseQuestionBodyStore(value: unknown): QuestionBodyStore;
18
18
  export declare function bodyStoreForLane(lane: Pick<LaneRecord, "company" | "mesh_company">, resolvedCompany?: LaneCompanyRef): QuestionBodyStore;
19
- export declare function resolveQuestionCompany(lane: Pick<LaneRecord, "lane_id" | "company" | "mesh_company">, activeCompany?: LaneCompanyRef | null): LaneCompanyRef;
19
+ export declare function resolveQuestionCompany(lane: Pick<LaneRecord, "lane_id" | "company" | "mesh_company" | "project_id">, activeCompany?: LaneCompanyRef | null, hqRoot?: string): LaneCompanyRef;
20
+ /**
21
+ * Resolve project ownership only when exactly one manifest company contains
22
+ * the project's directory. Multiple matches are unresolved so the caller can
23
+ * continue to an explicit active-company choice without picking an owner.
24
+ */
25
+ export declare function resolveProjectQuestionCompany(hqRoot: string, lane: Pick<LaneRecord, "lane_id" | "project_id">): LaneCompanyRef | null;
26
+ /** Resolve the lane/project company before lazily consulting active HQ scope. */
27
+ export declare function resolveQuestionCompanyWithFallback(hqRoot: string, lane: Pick<LaneRecord, "lane_id" | "company" | "mesh_company" | "project_id">, resolveActiveCompany?: () => Promise<LaneCompanyRef | null>): Promise<LaneCompanyRef>;
20
28
  export declare function questionBodyRef(questionId: string): string;
21
29
  export declare function sha256Text(text: string): string;
22
30
  /** Tenant-store path for a question body (spec §6.2 / §10). Never the personal vault for company prose. */
@@ -8,6 +8,7 @@ import * as fs from "node:fs";
8
8
  import * as path from "node:path";
9
9
  import { LanesError } from "./errors.js";
10
10
  import { assertNoSecretText } from "./text-scan.js";
11
+ import { readCompaniesManifestMap } from "../work-context/repo-remote.js";
11
12
  import { appendCappedHistory, createQuestion, retryUpdateQuestion, } from "./store.js";
12
13
  import { QUESTION_BODY_STORES, } from "./types.js";
13
14
  export function assertQuestionBodySecretSafe(body) {
@@ -39,13 +40,91 @@ export function parseQuestionBodyStore(value) {
39
40
  export function bodyStoreForLane(lane, resolvedCompany) {
40
41
  return lane.company || lane.mesh_company || resolvedCompany ? "company" : "personal";
41
42
  }
42
- export function resolveQuestionCompany(lane, activeCompany) {
43
- const company = lane.company ?? lane.mesh_company ?? activeCompany;
43
+ export function resolveQuestionCompany(lane, activeCompany, hqRoot) {
44
+ const laneCompany = lane.company ?? lane.mesh_company;
45
+ if (laneCompany !== null && laneCompany !== undefined) {
46
+ return requireQuestionCompany(laneCompany, lane.lane_id, "lane");
47
+ }
48
+ if (hqRoot) {
49
+ const projectCompany = resolveProjectQuestionCompany(hqRoot, lane);
50
+ if (projectCompany)
51
+ return projectCompany;
52
+ }
53
+ const company = activeCompany;
44
54
  if (!company || !company.slug.trim() || !company.uid.trim()) {
45
- throw new LanesError("question_company_unavailable", `Cannot raise a question for lane ${lane.lane_id}: no lane, project, or active company could be resolved`, { lane_id: lane.lane_id });
55
+ throw new LanesError("question_company_unavailable", `Cannot raise a question for lane ${lane.lane_id}: no lane, project, or active company could be resolved`, { lane_id: lane.lane_id, resolution: "active_company_required" });
56
+ }
57
+ return company;
58
+ }
59
+ function requireQuestionCompany(company, laneId, source) {
60
+ if (!company.slug.trim() || !company.uid.trim()) {
61
+ throw new LanesError("question_company_unavailable", `Cannot resolve the ${source} company for lane ${laneId}: its slug or uid is missing`, { lane_id: laneId, resolution: `${source}_company_invalid` });
46
62
  }
47
63
  return company;
48
64
  }
65
+ /**
66
+ * Resolve project ownership only when exactly one manifest company contains
67
+ * the project's directory. Multiple matches are unresolved so the caller can
68
+ * continue to an explicit active-company choice without picking an owner.
69
+ */
70
+ export function resolveProjectQuestionCompany(hqRoot, lane) {
71
+ const projectId = lane.project_id?.trim();
72
+ if (!projectId)
73
+ return null;
74
+ if (projectId === "." || projectId === ".." || /[\\/\0]/.test(projectId)) {
75
+ throw new LanesError("question_company_unavailable", `Cannot resolve the project company for lane ${lane.lane_id}: its project id is invalid`, { lane_id: lane.lane_id, resolution: "project_id_invalid" });
76
+ }
77
+ const manifestPath = path.join(hqRoot, "companies", "manifest.yaml");
78
+ if (!fs.existsSync(manifestPath))
79
+ return null;
80
+ const companies = readCompaniesManifestMap(hqRoot);
81
+ if (!companies) {
82
+ throw new LanesError("question_company_unavailable", `Cannot resolve the project company for lane ${lane.lane_id}: companies/manifest.yaml is unreadable or malformed`, { lane_id: lane.lane_id, resolution: "company_manifest_unavailable" });
83
+ }
84
+ const matches = [];
85
+ for (const [slug, entry] of Object.entries(companies)) {
86
+ if (!entry || typeof entry !== "object" || !slug || /[\\/\0]/.test(slug))
87
+ continue;
88
+ const projectDir = path.join(hqRoot, "companies", slug, "projects", projectId);
89
+ let isDirectory = false;
90
+ try {
91
+ isDirectory = fs.statSync(projectDir).isDirectory();
92
+ }
93
+ catch (err) {
94
+ if (err.code !== "ENOENT" && err.code !== "ENOTDIR") {
95
+ throw new LanesError("question_company_unavailable", `Cannot verify project ownership for lane ${lane.lane_id}`, { lane_id: lane.lane_id, resolution: "project_directory_unavailable" });
96
+ }
97
+ }
98
+ if (!isDirectory)
99
+ continue;
100
+ const uid = typeof entry.cloud_uid === "string" ? entry.cloud_uid.trim() : "";
101
+ matches.push({ slug, uid });
102
+ }
103
+ if (matches.length > 1)
104
+ return null;
105
+ const match = matches[0];
106
+ if (!match)
107
+ return null;
108
+ if (!match.uid) {
109
+ throw new LanesError("question_company_unavailable", `Cannot resolve the project company for lane ${lane.lane_id}: its manifest company has no cloud uid`, { lane_id: lane.lane_id, resolution: "project_company_uid_missing" });
110
+ }
111
+ return requireQuestionCompany({ slug: match.slug, uid: match.uid }, lane.lane_id, "project");
112
+ }
113
+ /** Resolve the lane/project company before lazily consulting active HQ scope. */
114
+ export async function resolveQuestionCompanyWithFallback(hqRoot, lane, resolveActiveCompany) {
115
+ try {
116
+ return resolveQuestionCompany(lane, null, hqRoot);
117
+ }
118
+ catch (err) {
119
+ if (!(err instanceof LanesError) ||
120
+ err.error !== "question_company_unavailable" ||
121
+ err.details.resolution !== "active_company_required" ||
122
+ !resolveActiveCompany) {
123
+ throw err;
124
+ }
125
+ return resolveQuestionCompany(lane, await resolveActiveCompany(), hqRoot);
126
+ }
127
+ }
49
128
  export function questionBodyRef(questionId) {
50
129
  return path.posix.join("lanes", "questions", `${questionId}.json`);
51
130
  }
@@ -82,7 +161,7 @@ export function writeQuestionBody(hqRoot, question, body) {
82
161
  */
83
162
  export function raiseLaneQuestion(input) {
84
163
  assertQuestionBodySecretSafe(input.body);
85
- const company = resolveQuestionCompany(input.lane, input.activeCompany);
164
+ const company = resolveQuestionCompany(input.lane, input.activeCompany, input.hqRoot);
86
165
  const store = bodyStoreForLane(input.lane, company);
87
166
  let questionId = input.questionId;
88
167
  if (!questionId) {
@@ -282,7 +361,7 @@ export function raiseEnvelopeQuestions(hqRoot, lane, envelope, envelopeRef, at,
282
361
  for (const item of presentItems) {
283
362
  assertQuestionBodySecretSafe(envelopeQuestionBody(item));
284
363
  }
285
- resolveQuestionCompany(lane, activeCompany);
364
+ resolveQuestionCompany(lane, activeCompany, hqRoot);
286
365
  const raised = [];
287
366
  for (let i = 0; i < items.length; i += 1) {
288
367
  const item = items[i];
@@ -24,7 +24,7 @@ import { readLanesRegistryFlag } from "./registry-flag.js";
24
24
  import { LEAD_LANE_KEEP_ALIVE_FLAG, LEAD_LANE_KEEP_ALIVE_INTERRUPTED_RESUME_TEXT, LEAD_LANE_KEEP_ALIVE_MAX_RESUMES, LEAD_LANE_KEEP_ALIVE_RESUME_TEXT, keepAliveExclusion, loadRetiredLaneIds, leadLaneKeepAliveBackoffMs, } from "./keep-alive.js";
25
25
  import { providerSupportsSessionResume } from "./delivery.js";
26
26
  import { applyDefaultQuotaHold, readQuotaSignal, recordAccountQuota } from "./quota.js";
27
- import { answeredQuestionEntries, questionRecordRefusalCode, raiseEnvelopeQuestions, } from "./questions.js";
27
+ import { answeredQuestionEntries, questionRecordRefusalCode, raiseEnvelopeQuestions, resolveQuestionCompanyWithFallback, } from "./questions.js";
28
28
  import { SILENCE_MS, classifyLaneExit, isPidAlive, missingExpectedEnvelopeFile, readLatestEnvelopeFile, readLatestEnvelopeRecord, watcherEventsPath, watcherOutLogPath, writeWatcherDecision, } from "./watcher.js";
29
29
  import { createLaneMesh, emitLaneDecisionEvents, emitLaneTerminalStateEvent, laneMeshCompany, } from "./mesh-events.js";
30
30
  import { laneStories, storyStatusForEnvelope } from "./stories.js";
@@ -1054,10 +1054,8 @@ async function heartbeatAndDeadHosts(hqRoot, lanes, summary, deps, now) {
1054
1054
  latest &&
1055
1055
  (latest.envelope.questions?.length ?? 0) > 0) {
1056
1056
  const questionLane = readLane(hqRoot, lane.lane_id);
1057
- const activeCompany = questionLane.company || questionLane.mesh_company
1058
- ? undefined
1059
- : await deps.resolveQuestionActiveCompany?.();
1060
1057
  try {
1058
+ const activeCompany = await resolveQuestionCompanyWithFallback(hqRoot, questionLane, deps.resolveQuestionActiveCompany);
1061
1059
  raiseEnvelopeQuestions(hqRoot, questionLane, latest.envelope, latest.ref, nowIso, undefined, activeCompany);
1062
1060
  }
1063
1061
  catch (err) {
@@ -34,7 +34,7 @@
34
34
  import * as fs from "node:fs";
35
35
  import * as path from "node:path";
36
36
  import { LanesError } from "./errors.js";
37
- import { questionRecordRefusalCode, raiseEnvelopeQuestions } from "./questions.js";
37
+ import { questionRecordRefusalCode, raiseEnvelopeQuestions, resolveQuestionCompanyWithFallback, } from "./questions.js";
38
38
  import { isTerminalLaneState, ENVELOPE_INVALID_REASON, parseLaneEnvelope, STORY_STATUSES, } from "./types.js";
39
39
  import { laneStories, storyStatusForEnvelope } from "./stories.js";
40
40
  import { readLane, retryUpdateLane } from "./store.js";
@@ -1070,12 +1070,18 @@ export async function runWatcherLoop(hqRoot, laneId, deps = {}) {
1070
1070
  }
1071
1071
  }
1072
1072
  const terminalLane = readLane(hqRoot, laneId);
1073
- const activeQuestionCompany = envelope?.decision === "ask" &&
1074
- (envelope.questions?.length ?? 0) > 0 &&
1075
- terminalLane.company === null &&
1076
- terminalLane.mesh_company === undefined
1077
- ? await deps.resolveQuestionActiveCompany?.()
1078
- : undefined;
1073
+ let activeQuestionCompany;
1074
+ if (envelope?.decision === "ask" && (envelope.questions?.length ?? 0) > 0) {
1075
+ try {
1076
+ activeQuestionCompany = await resolveQuestionCompanyWithFallback(hqRoot, terminalLane, deps.resolveQuestionActiveCompany);
1077
+ }
1078
+ catch (err) {
1079
+ if (questionRecordRefusalCode(err) !== "question_company_unavailable")
1080
+ throw err;
1081
+ // applyTerminalToLaneWithResult repeats the synchronous lane and
1082
+ // project checks, then terminalizes this ask with the named refusal.
1083
+ }
1084
+ }
1079
1085
  const terminalWrite = applyTerminalToLaneWithResult(hqRoot, laneId, classification, {
1080
1086
  envelope,
1081
1087
  envelopeRef: latest?.ref,
package/dist/sentry.js CHANGED
@@ -26,6 +26,9 @@ export function epipeAwareBeforeSend(event, hint) {
26
26
  return null;
27
27
  event.level = "warning";
28
28
  event.tags = { ...event.tags, worker: "flag-registry" };
29
+ const missingFile = flagRegistryWorkerMissingFileTags(hint?.originalException, event);
30
+ if (missingFile)
31
+ event.tags = { ...event.tags, ...missingFile };
29
32
  }
30
33
  if (!flagRegistryWorkerEvent && isEpipe(hint?.originalException))
31
34
  return null;
@@ -123,6 +126,44 @@ function isExpectedFlagRegistryWorkerStartupFailure(error) {
123
126
  return missingModule.replaceAll("\\", "/").endsWith("/flag-registry-worker.js") ||
124
127
  missingModule === "flag-registry-worker.js";
125
128
  }
129
+ function flagRegistryWorkerMissingFileTags(error, event) {
130
+ if (typeof error !== "object" || error === null)
131
+ return null;
132
+ const details = error;
133
+ if (details.code !== "ENOENT" || !["open", "lstat"].includes(String(details.syscall))) {
134
+ return null;
135
+ }
136
+ let missingPath = typeof details.path === "string" ? details.path : "";
137
+ if (!missingPath && typeof details.message === "string") {
138
+ missingPath = /\b(?:open|lstat)\s+['"]([^'"]+)['"]/.exec(details.message)?.[1] ?? "";
139
+ }
140
+ const basename = missingPath.replaceAll("\\", "/").split("/").at(-1) ?? "";
141
+ if (!basename)
142
+ return null;
143
+ const normalizedPath = missingPath.replaceAll("\\", "/");
144
+ const role = basename === "flag-registry-worker.js"
145
+ ? "worker_entry"
146
+ : normalizedPath.includes("/node_modules/")
147
+ ? "dependency"
148
+ : "other";
149
+ const eventText = [
150
+ event.message,
151
+ ...(event.exception?.values?.map((value) => value.value) ?? []),
152
+ ].filter((value) => typeof value === "string").join("\n");
153
+ const pathVisibility = eventText.includes(missingPath)
154
+ ? "full_path"
155
+ : eventText.includes(basename)
156
+ ? "basename_only"
157
+ : "omitted";
158
+ return {
159
+ flag_worker_load_failure: "1",
160
+ flag_worker_missing_basename: basename,
161
+ flag_worker_missing_file_role: role,
162
+ flag_worker_path_in_event: pathVisibility,
163
+ // The HQ beforeSend scrubber redacts credentials but does not strip paths.
164
+ flag_worker_path_scrubbed_by_hq_cli: "no",
165
+ };
166
+ }
126
167
  /**
127
168
  * Tests substitute only the transport so the real SDK lifecycle can be
128
169
  * exercised without an ingest request. Production callers pass nothing.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.219.0",
3
+ "version": "5.220.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -22,12 +22,14 @@
22
22
  "scripts/mesh-daemon-if-stale.mjs"
23
23
  ],
24
24
  "scripts": {
25
- "build": "node scripts/generate-dsn.mjs && tsc && node scripts/generate-command-catalog.mjs --check && node scripts/copy-schemas.mjs && node scripts/chmod-bins.mjs",
25
+ "build": "node scripts/generate-dsn.mjs && node scripts/build-flag-registry-worker.mjs && tsc && node scripts/generate-command-catalog.mjs --check && node scripts/copy-schemas.mjs && node scripts/chmod-bins.mjs",
26
26
  "prepublishOnly": "npm run build",
27
+ "pretypecheck": "node scripts/generate-dsn.mjs && node scripts/build-flag-registry-worker.mjs",
27
28
  "typecheck": "tsc --noEmit",
28
29
  "gen:scan-golden": "node scripts/generate-scan-packages-table.mjs > src/utils/__fixtures__/scan-packages.generated-block.sh",
29
30
  "gen:command-catalog": "tsc && node scripts/generate-command-catalog.mjs",
30
31
  "lint": "eslint .",
32
+ "pretest": "node scripts/generate-dsn.mjs && node scripts/build-flag-registry-worker.mjs",
31
33
  "test": "vitest run",
32
34
  "test:db": "vitest run src/lib/db test/commands/db.test.ts test/commands/db-tenant-isolation.test.ts",
33
35
  "test:e2e": "vitest run --config vitest.e2e.config.ts",
@@ -66,6 +68,7 @@
66
68
  "@vitest/coverage-v8": "4.1.6",
67
69
  "aedes": "1.1.2",
68
70
  "aws-sdk-client-mock": "^4.1.0",
71
+ "esbuild": "0.27.0",
69
72
  "eslint": "^10.5.0",
70
73
  "husky": "^9.1.7",
71
74
  "lint-staged": "^15.5.2",