@indigoai-us/hq-cli 5.8.6 → 5.9.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/CHANGELOG.md ADDED
@@ -0,0 +1,30 @@
1
+ # Changelog
2
+
3
+ ## [5.9.0] — 2026-05-04
4
+
5
+ ### Added
6
+
7
+ - **`hq run` command** — schema-driven dev workflow. Place a `.env.schema` file in
8
+ your repo (annotated with `# @hqCompany("your-slug")` and `VARNAME=hq()` resolvers),
9
+ then run `hq run -- npm run dev` to inject all declared secrets into the child
10
+ process's environment without ever printing them to stdout/stderr. Discovers schemas
11
+ by walking up from cwd to the repo root; merges multiple schemas; respects sibling
12
+ `.env.local` files for local overrides. Supports `--check` for a dry-run summary,
13
+ `--company` to override the slug, and `--schema` to pin an explicit schema path.
14
+
15
+ - **Batch secrets endpoint** — `POST /secrets/{companyUid}/load` on the vault API
16
+ reduces N parallel single-secret fetches to chunked `ssm:GetParameters` calls
17
+ (up to 10 names per batch), cutting `hq run` latency for schemas with many vars.
18
+ Responses include both `secrets` (allowed) and `errors` (denied/not-found) per name.
19
+ Each revealed secret is audit-logged individually (same trail as `hq secrets get`).
20
+
21
+ - **`varlock` dependency** (`1.0.0`, exact pin) — used as an internal library to
22
+ parse `.env.schema` files and drive the resolver graph. The `hq()` resolver is
23
+ implemented as a varlock plugin registered at runtime; varlock is not exposed as a
24
+ public API surface.
25
+
26
+ ### Changed
27
+
28
+ - **Node minimum raised to `>=22.0.0`** — required by `varlock@1.0.0` (ESM-only,
29
+ `node>=22`). The previous minimum was unset; this makes the requirement explicit
30
+ in `engines.node`.
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerRunCommand(program: Command): void;
3
+ //# sourceMappingURL=run.d.ts.map
@@ -0,0 +1,119 @@
1
+
2
+ !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]="a1b39bbf-1f26-59e7-af70-6ecf01f2ce7e")}catch(e){}}();
3
+ import { spawn } from 'node:child_process';
4
+ import * as path from 'node:path';
5
+ import * as fs from 'node:fs';
6
+ import { internal } from 'varlock';
7
+ import { ensureCognitoToken } from '../utils/cognito-session.js';
8
+ import { vaultApiFetch, getCompanyUid } from '../utils/vault-api.js';
9
+ import { discoverSchemas } from '../run/discover-schemas.js';
10
+ import { installHqPlugin, prewarmHqSecrets } from '../run/hq-plugin.js';
11
+ export function registerRunCommand(program) {
12
+ program
13
+ .command('run')
14
+ .description('Load secrets from .env.schema and run a command with them injected')
15
+ .option('--company <slug>', 'Company slug (overrides @hqCompany in schema)')
16
+ .option('--schema <path>', 'Explicit schema path (skips walk-up discovery)')
17
+ .option('--check', 'Resolve schema and validate vars without executing the command')
18
+ .allowUnknownOption(true)
19
+ .action(async (opts) => {
20
+ try {
21
+ const dashIndex = process.argv.indexOf('--');
22
+ const childArgs = dashIndex !== -1 ? process.argv.slice(dashIndex + 1) : [];
23
+ if (!opts.check && childArgs.length === 0) {
24
+ throw new Error('no command specified. Usage: hq run [options] -- <command> [args...]');
25
+ }
26
+ let schemaPaths;
27
+ let envLocalPaths;
28
+ let schemaCompanySlug;
29
+ if (opts.schema) {
30
+ const schemaAbs = path.resolve(opts.schema);
31
+ schemaPaths = [schemaAbs];
32
+ const localPath = path.join(path.dirname(schemaAbs), '.env.local');
33
+ envLocalPaths = fs.existsSync(localPath) ? [localPath] : [];
34
+ const content = fs.readFileSync(schemaAbs, 'utf8');
35
+ const m = /^# @hqCompany\("([^"]+)"\)/m.exec(content);
36
+ schemaCompanySlug = m ? m[1] : null;
37
+ }
38
+ else {
39
+ const discovered = discoverSchemas(process.cwd());
40
+ if (discovered.conflict) {
41
+ throw new Error(`conflicting @hqCompany slugs: "${discovered.conflict.slugs[0]}" in ${discovered.conflict.paths[0]} vs "${discovered.conflict.slugs[1]}" in ${discovered.conflict.paths[1]}. Use --company <slug> to override.`);
42
+ }
43
+ if (discovered.schemaPaths.length === 0) {
44
+ throw new Error('no .env.schema found. Create one or use --schema <path>.');
45
+ }
46
+ schemaPaths = discovered.schemaPaths;
47
+ envLocalPaths = discovered.envLocalPaths;
48
+ schemaCompanySlug = discovered.companySlug;
49
+ }
50
+ const slug = opts.company ?? schemaCompanySlug;
51
+ if (!slug) {
52
+ throw new Error('company slug not set. Add # @hqCompany("slug") to your .env.schema or pass --company <slug>.');
53
+ }
54
+ const token = await ensureCognitoToken();
55
+ const uid = await getCompanyUid(token, slug);
56
+ const fetchBatch = async (companyUid, names) => {
57
+ const res = await vaultApiFetch({
58
+ token,
59
+ path: `/secrets/${encodeURIComponent(companyUid)}/load`,
60
+ method: 'POST',
61
+ body: { names },
62
+ });
63
+ if (!res.ok) {
64
+ const body = await res.json().catch(() => ({}));
65
+ throw new Error(`Failed to batch-load secrets: ${body.error ?? res.statusText}`);
66
+ }
67
+ return res.json();
68
+ };
69
+ const pluginOpts = {
70
+ companyOverride: opts.company,
71
+ resolveCompanyUid: async () => uid,
72
+ fetchBatch,
73
+ };
74
+ // LAST entry = highest precedence; .env.local files trail .env.schema files so any .env.local beats any schema regardless of depth.
75
+ const paths = [...schemaPaths, ...envLocalPaths];
76
+ let state;
77
+ const graph = await internal.loadEnvGraph({
78
+ entryFilePaths: paths,
79
+ afterInit: async (g) => { state = installHqPlugin(g, pluginOpts); },
80
+ });
81
+ await prewarmHqSecrets(graph, pluginOpts, state);
82
+ await graph.resolveEnvValues();
83
+ const schemaErrors = Object.entries(graph.configSchema)
84
+ .filter(([, item]) => item.errors?.length > 0);
85
+ if (schemaErrors.length > 0) {
86
+ const msgs = schemaErrors.flatMap(([k, item]) => item.errors.map((e) => ` ${k}: ${e.message ?? String(e)}`));
87
+ process.stderr.write(`Error: failed to resolve env vars:\n${msgs.join('\n')}\n`);
88
+ process.exit(1);
89
+ }
90
+ const resolvedEnv = graph.getResolvedEnvObject();
91
+ const varCount = Object.keys(resolvedEnv).length;
92
+ process.stderr.write(`Loaded ${varCount} env vars from .env.schema (company: ${slug})\n`);
93
+ if (opts.check) {
94
+ process.exit(0);
95
+ }
96
+ const [childCmd, ...restArgs] = childArgs;
97
+ const child = spawn(childCmd, restArgs, {
98
+ stdio: 'inherit',
99
+ env: { ...process.env, ...resolvedEnv },
100
+ });
101
+ child.on('error', (err) => {
102
+ process.stderr.write(`Error: failed to start command '${childCmd}': ${err.message}\n`);
103
+ process.exit(1);
104
+ });
105
+ child.on('close', (code, signal) => {
106
+ if (signal) {
107
+ process.kill(process.pid, signal);
108
+ }
109
+ process.exit(code ?? 1);
110
+ });
111
+ }
112
+ catch (err) {
113
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
114
+ process.exit(1);
115
+ }
116
+ });
117
+ }
118
+ //# sourceMappingURL=run.js.map
119
+ //# debugId=a1b39bbf-1f26-59e7-af70-6ecf01f2ce7e
@@ -1,12 +1,6 @@
1
1
  import { Command } from "commander";
2
- export interface VaultApiOptions {
3
- token: string;
4
- path: string;
5
- method?: string;
6
- body?: Record<string, unknown>;
7
- query?: Record<string, string>;
8
- }
9
- export declare function vaultApiFetch(opts: VaultApiOptions): Promise<Response>;
10
- export declare function getCompanyUid(token: string, companySlug: string | undefined): Promise<string>;
2
+ import { vaultApiFetch, getCompanyUid } from "../utils/vault-api.js";
3
+ export type { VaultApiOptions } from "../utils/vault-api.js";
4
+ export { vaultApiFetch, getCompanyUid };
11
5
  export declare function registerSecretsCommand(program: Command): void;
12
6
  //# sourceMappingURL=secrets.d.ts.map
@@ -1,11 +1,13 @@
1
1
 
2
- !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]="70f0907f-bfcb-5d2e-958a-8813d3bbaf96")}catch(e){}}();
2
+ !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]="0239c476-98b8-53c0-9458-1c82dc0d26a9")}catch(e){}}();
3
3
  import chalk from "chalk";
4
4
  import * as readline from "node:readline";
5
5
  import { spawn } from "node:child_process";
6
- import { ensureCognitoToken, DEFAULT_VAULT_API_URL, } from "../utils/cognito-session.js";
6
+ import { ensureCognitoToken } from "../utils/cognito-session.js";
7
7
  import { readCache, writeCache, removeCacheEntry, clearAllCache, } from "../utils/secrets-cache.js";
8
8
  import { SECRET_NAME_PATTERN, GROUP_ID_PATTERN } from "./_patterns.js";
9
+ import { vaultApiFetch, getCompanyUid } from "../utils/vault-api.js";
10
+ export { vaultApiFetch, getCompanyUid };
9
11
  function shellSingleQuote(value) {
10
12
  return "'" + value.replace(/'/g, "'\\''") + "'";
11
13
  }
@@ -19,59 +21,6 @@ function buildSecretNamePath(companyUid, name) {
19
21
  .join("/");
20
22
  return `/secrets/${encodeURIComponent(companyUid)}/name/${encodedName}`;
21
23
  }
22
- export async function vaultApiFetch(opts) {
23
- const url = new URL(opts.path, DEFAULT_VAULT_API_URL);
24
- if (opts.query) {
25
- for (const [k, v] of Object.entries(opts.query)) {
26
- url.searchParams.set(k, v);
27
- }
28
- }
29
- return fetch(url.toString(), {
30
- method: opts.method ?? "GET",
31
- headers: {
32
- Authorization: `Bearer ${opts.token}`,
33
- "Content-Type": "application/json",
34
- },
35
- body: opts.body ? JSON.stringify(opts.body) : undefined,
36
- });
37
- }
38
- async function resolveCompanyUid(token, slug) {
39
- const res = await vaultApiFetch({
40
- token,
41
- path: `/entity/by-slug/company/${encodeURIComponent(slug)}`,
42
- });
43
- if (!res.ok) {
44
- const body = await res.json().catch(() => ({}));
45
- throw new Error(`Failed to resolve company slug '${slug}': ${body.error ?? res.statusText}`);
46
- }
47
- const data = (await res.json());
48
- return data.entity.uid;
49
- }
50
- async function resolveCompanyFromMemberships(token) {
51
- const res = await vaultApiFetch({
52
- token,
53
- path: "/membership/me",
54
- });
55
- if (!res.ok) {
56
- throw new Error("Failed to fetch memberships — run `hq login` and try again");
57
- }
58
- const data = (await res.json());
59
- const active = data.memberships.filter((m) => m.status === "active");
60
- if (active.length === 0) {
61
- throw new Error("No active company memberships found. Use --company <slug> to specify.");
62
- }
63
- if (active.length === 1) {
64
- return active[0].companyUid;
65
- }
66
- const uids = active.map((m) => m.companyUid).join(", ");
67
- throw new Error(`Multiple companies found (${uids}). Use --company <slug> to specify which one.`);
68
- }
69
- export async function getCompanyUid(token, companySlug) {
70
- if (companySlug) {
71
- return resolveCompanyUid(token, companySlug);
72
- }
73
- return resolveCompanyFromMemberships(token);
74
- }
75
24
  function parseDuration(input) {
76
25
  const match = input.match(/^(\d+)(m|h|d)$/);
77
26
  if (!match)
@@ -756,4 +705,4 @@ export function registerSecretsCommand(program) {
756
705
  });
757
706
  }
758
707
  //# sourceMappingURL=secrets.js.map
759
- //# debugId=70f0907f-bfcb-5d2e-958a-8813d3bbaf96
708
+ //# debugId=0239c476-98b8-53c0-9458-1c82dc0d26a9
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]="79222b07-0f20-582f-962f-6bc13b8c1c88")}catch(e){}}();
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]="ad8b9496-9cb2-5e12-ae08-5b29ef77c209")}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,6 +23,7 @@ import { registerPackageListCommand } from "./commands/pkg-list.js";
23
23
  import { registerTeamSyncCommand } from "./commands/team-sync.js";
24
24
  import { registerAuthCommands } from "./commands/auth.js";
25
25
  import { registerSecretsCommand } from "./commands/secrets.js";
26
+ import { registerRunCommand } from "./commands/run.js";
26
27
  import { registerGroupsCommand } from "./commands/groups.js";
27
28
  import { registerFilesCommand } from "./commands/files.js";
28
29
  initSentry();
@@ -72,6 +73,8 @@ registerWhoamiCommand(program);
72
73
  registerAuthCommands(program);
73
74
  // Secrets management (subcommand group — hq secrets set|get|list|delete|exec|generate-link|cache)
74
75
  registerSecretsCommand(program);
76
+ // Schema-driven dev runner — hq run [options] -- <cmd>
77
+ registerRunCommand(program);
75
78
  // Groups management (subcommand group — hq groups create|delete|add|remove|list|members)
76
79
  registerGroupsCommand(program);
77
80
  // Files ACL management (subcommand group — hq files share|unshare|acl)
@@ -91,4 +94,4 @@ registerOnboardCommand(program);
91
94
  }
92
95
  })();
93
96
  //# sourceMappingURL=index.js.map
94
- //# debugId=79222b07-0f20-582f-962f-6bc13b8c1c88
97
+ //# debugId=ad8b9496-9cb2-5e12-ae08-5b29ef77c209
@@ -0,0 +1,12 @@
1
+ export interface SchemaConflict {
2
+ paths: [string, string];
3
+ slugs: [string, string];
4
+ }
5
+ export interface DiscoverSchemasResult {
6
+ schemaPaths: string[];
7
+ envLocalPaths: string[];
8
+ companySlug: string | null;
9
+ conflict: SchemaConflict | null;
10
+ }
11
+ export declare function discoverSchemas(cwd: string): DiscoverSchemasResult;
12
+ //# sourceMappingURL=discover-schemas.d.ts.map
@@ -0,0 +1,64 @@
1
+
2
+ !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]="17c9937b-a219-5b2d-a120-0acbf96f945a")}catch(e){}}();
3
+ import * as fs from 'node:fs';
4
+ import * as path from 'node:path';
5
+ const SLUG_RE = /^# @hqCompany\("([^"]+)"\)/m;
6
+ function parseSlug(filePath) {
7
+ try {
8
+ const content = fs.readFileSync(filePath, 'utf8');
9
+ const m = SLUG_RE.exec(content);
10
+ return m ? m[1] : null;
11
+ }
12
+ catch {
13
+ return null;
14
+ }
15
+ }
16
+ export function discoverSchemas(cwd) {
17
+ const schemaPaths = [];
18
+ const envLocalPaths = [];
19
+ let dir = path.resolve(cwd);
20
+ while (true) {
21
+ const schemaPath = path.join(dir, '.env.schema');
22
+ if (fs.existsSync(schemaPath)) {
23
+ schemaPaths.push(schemaPath);
24
+ const localPath = path.join(dir, '.env.local');
25
+ if (fs.existsSync(localPath)) {
26
+ envLocalPaths.push(localPath);
27
+ }
28
+ }
29
+ const isGitRoot = fs.existsSync(path.join(dir, '.git'));
30
+ if (isGitRoot)
31
+ break;
32
+ const parent = path.dirname(dir);
33
+ if (parent === dir)
34
+ break; // filesystem root
35
+ dir = parent;
36
+ }
37
+ // cwd-closest is pushed last during walk-up, so reversing puts it last.
38
+ // (During walk-up cwd is checked first → pushed first → reversed → ends up last.)
39
+ schemaPaths.reverse();
40
+ envLocalPaths.reverse();
41
+ let companySlug = null;
42
+ let companySlugPath = null;
43
+ let conflict = null;
44
+ for (const schemaPath of schemaPaths) {
45
+ const slug = parseSlug(schemaPath);
46
+ if (slug == null)
47
+ continue;
48
+ if (companySlug == null) {
49
+ companySlug = slug;
50
+ companySlugPath = schemaPath;
51
+ }
52
+ else if (companySlug !== slug) {
53
+ conflict = {
54
+ paths: [companySlugPath, schemaPath],
55
+ slugs: [companySlug, slug],
56
+ };
57
+ companySlug = null;
58
+ break;
59
+ }
60
+ }
61
+ return { schemaPaths, envLocalPaths, companySlug, conflict };
62
+ }
63
+ //# sourceMappingURL=discover-schemas.js.map
64
+ //# debugId=17c9937b-a219-5b2d-a120-0acbf96f945a
@@ -0,0 +1,26 @@
1
+ export interface InstallHqPluginOpts {
2
+ companyOverride?: string;
3
+ resolveCompanyUid: (slug: string) => Promise<string>;
4
+ fetchBatch: (uid: string, names: string[]) => Promise<{
5
+ secrets: Array<{
6
+ name: string;
7
+ value: string;
8
+ }>;
9
+ errors: Array<{
10
+ name: string;
11
+ code: string;
12
+ message?: string;
13
+ }>;
14
+ }>;
15
+ }
16
+ export interface PluginState {
17
+ schemaCompanySlug: string | null;
18
+ uid: string | null;
19
+ errorsByName: Map<string, {
20
+ code: string;
21
+ message?: string;
22
+ }>;
23
+ }
24
+ export declare function installHqPlugin(graph: any, opts: InstallHqPluginOpts): PluginState;
25
+ export declare function prewarmHqSecrets(graph: any, opts: InstallHqPluginOpts, state: PluginState): Promise<void>;
26
+ //# sourceMappingURL=hq-plugin.d.ts.map
@@ -0,0 +1,144 @@
1
+
2
+ !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]="01fee323-1f63-5411-bfc9-ff49e241f712")}catch(e){}}();
3
+ import { ResolutionError } from 'varlock/plugin-lib';
4
+ import { readCache, writeCache } from '../utils/secrets-cache.js';
5
+ export function installHqPlugin(graph /* EnvGraph */, opts) {
6
+ const pluginState = {
7
+ schemaCompanySlug: null,
8
+ uid: null,
9
+ errorsByName: new Map(),
10
+ };
11
+ // varlock@1.0.0's plugin-lib.js omits the Resolver export (d.ts/JS mismatch);
12
+ // extract it at runtime from any already-registered built-in resolver's prototype.
13
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
14
+ let RuntimeResolver;
15
+ try {
16
+ const fns = graph.registeredResolverFunctions;
17
+ const first = Object.values(fns)[0];
18
+ if (first == null)
19
+ throw new Error('registeredResolverFunctions is empty');
20
+ const proto = Object.getPrototypeOf(first);
21
+ if (proto == null || typeof proto.prototype?.process !== 'function') {
22
+ throw new Error('prototype has no process method');
23
+ }
24
+ RuntimeResolver = proto;
25
+ }
26
+ catch (e) {
27
+ throw new Error('varlock Resolver base class extraction failed — the varlock@1.0.0 d.ts/JS mismatch ' +
28
+ 'may have been resolved; switch to `import { Resolver } from "varlock/plugin-lib"`. ' +
29
+ `Underlying: ${e instanceof Error ? e.message : String(e)}`);
30
+ }
31
+ // HqResolver is declared INSIDE installHqPlugin so its static def.resolve
32
+ // closes over `pluginState`. Module-scope declaration is forbidden — resolve()
33
+ // would hit `ReferenceError: pluginState is not defined`.
34
+ class HqResolver extends RuntimeResolver {
35
+ static def = {
36
+ name: 'hq',
37
+ impliesSensitive: true,
38
+ argsSchema: { type: 'array', arrayMaxLength: 1 },
39
+ resolve: async function () {
40
+ // Cache-only read. `pluginState` is captured by this inner-class closure;
41
+ // `prewarmHqSecrets(graph, opts, state)` populates `state.uid` and
42
+ // `state.errorsByName` before `graph.resolveEnvValues()` calls us.
43
+ const explicit = this.arrArgs?.[0]?.staticValue;
44
+ const secretName = (typeof explicit === 'string' && explicit) ? explicit : this._ownerKey;
45
+ if (!secretName) {
46
+ throw new ResolutionError('hq() resolver could not determine secret name (missing owner key)');
47
+ }
48
+ const err = pluginState.errorsByName.get(secretName);
49
+ if (err) {
50
+ if (err.code === 'forbidden') {
51
+ throw new ResolutionError(`No read permission for secret "${secretName}" — ask an admin to share it via \`hq secrets share ${secretName} --with <you> --permission read\``);
52
+ }
53
+ if (err.code === 'not_found') {
54
+ throw new ResolutionError(`Secret "${secretName}" does not exist in company`);
55
+ }
56
+ throw new ResolutionError(`Failed to load secret "${secretName}": ${err.message ?? err.code}`);
57
+ }
58
+ // Sentinel-check style throughout: `readCache` returns `string | null`
59
+ // (verified at `hq/packages/hq-cli/src/utils/secrets-cache.ts:45`); `pluginState.uid`
60
+ // is `string | null` per `PluginState`. Use `== null` (covers null AND undefined defensively)
61
+ // for both — do not mix in truthy checks like `if (!x)`, which would silently swallow a
62
+ // legitimate empty-string value if the contract ever loosened.
63
+ if (pluginState.uid == null) {
64
+ throw new ResolutionError('Internal error: prewarmHqSecrets was not called before resolveEnvValues');
65
+ }
66
+ const cached = readCache(pluginState.uid, secretName); // string | null
67
+ if (cached == null) {
68
+ throw new ResolutionError(`Internal error: pre-warm did not populate cache for "${secretName}"`);
69
+ }
70
+ return cached;
71
+ },
72
+ };
73
+ // Captured during process(parent); used by resolve() to fall back to the var key.
74
+ _ownerKey;
75
+ process(parent) {
76
+ super.process(parent);
77
+ if (parent != null && typeof parent.key === 'string') {
78
+ this._ownerKey = parent.key;
79
+ }
80
+ }
81
+ }
82
+ // varlock@1.0.0's env-spec parser rejects dots in decorator names ([a-zA-Z0-9_] only),
83
+ // so `@hq.company` is not valid syntax. We register as `@hqCompany` (camelCase) instead.
84
+ // Schema files must use `# @hqCompany("slug")` followed by a blank line so the parser
85
+ // treats it as a file-level root decorator rather than an item decorator for the next var.
86
+ graph.registerRootDecorator({
87
+ name: 'hqCompany',
88
+ isFunction: true,
89
+ process: (decoratorValue) => {
90
+ const slug = decoratorValue.arrArgs?.[0]?.staticValue;
91
+ return typeof slug === 'string' ? slug : null;
92
+ },
93
+ execute: (slug) => {
94
+ if (slug)
95
+ pluginState.schemaCompanySlug = slug;
96
+ },
97
+ });
98
+ graph.registerResolver(HqResolver);
99
+ // Returned so `prewarmHqSecrets(graph, opts, state)` can read schemaCompanySlug
100
+ // and write `uid` + `errorsByName`. The state is held by the closure; the returned handle
101
+ // is purely for the prewarm helper.
102
+ return pluginState;
103
+ }
104
+ export async function prewarmHqSecrets(graph /* EnvGraph */, opts, state) {
105
+ const queue = [];
106
+ for (const [key, item] of Object.entries(graph.configSchema)) {
107
+ const resolver = item.valueResolver;
108
+ if (resolver?.def?.name === 'hq') {
109
+ const explicit = resolver.arrArgs?.[0]?.staticValue;
110
+ const secretName = (typeof explicit === 'string' && explicit) ? explicit : key;
111
+ queue.push({ key, secretName });
112
+ }
113
+ }
114
+ if (queue.length === 0) {
115
+ return;
116
+ }
117
+ let slug;
118
+ if (state.schemaCompanySlug) {
119
+ slug = state.schemaCompanySlug;
120
+ }
121
+ else if (opts.companyOverride) {
122
+ slug = opts.companyOverride;
123
+ }
124
+ else {
125
+ throw new Error('@hqCompany("...") not declared and --company not passed');
126
+ }
127
+ const uid = await opts.resolveCompanyUid(slug);
128
+ const uniqueNames = [...new Set(queue.map((q) => q.secretName))];
129
+ if (uniqueNames.length > 100) {
130
+ throw new Error(`hq run supports at most 100 hq() resolvers per schema; got ${uniqueNames.length}`);
131
+ }
132
+ const result = await opts.fetchBatch(uid, uniqueNames);
133
+ for (const s of result.secrets) {
134
+ writeCache(uid, s.name, s.value);
135
+ }
136
+ const errorsByName = new Map();
137
+ for (const e of result.errors) {
138
+ errorsByName.set(e.name, { code: e.code, message: e.message });
139
+ }
140
+ state.errorsByName = errorsByName;
141
+ state.uid = uid;
142
+ }
143
+ //# sourceMappingURL=hq-plugin.js.map
144
+ //# debugId=01fee323-1f63-5411-bfc9-ff49e241f712
@@ -0,0 +1,10 @@
1
+ export interface VaultApiOptions {
2
+ token: string;
3
+ path: string;
4
+ method?: string;
5
+ body?: Record<string, unknown>;
6
+ query?: Record<string, string>;
7
+ }
8
+ export declare function vaultApiFetch(opts: VaultApiOptions): Promise<Response>;
9
+ export declare function getCompanyUid(token: string, companySlug: string | undefined): Promise<string>;
10
+ //# sourceMappingURL=vault-api.d.ts.map
@@ -0,0 +1,58 @@
1
+
2
+ !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]="4e482805-d129-5563-a77b-97b80410154b")}catch(e){}}();
3
+ import { DEFAULT_VAULT_API_URL } from './cognito-session.js';
4
+ export async function vaultApiFetch(opts) {
5
+ const url = new URL(opts.path, DEFAULT_VAULT_API_URL);
6
+ if (opts.query) {
7
+ for (const [k, v] of Object.entries(opts.query)) {
8
+ url.searchParams.set(k, v);
9
+ }
10
+ }
11
+ return fetch(url.toString(), {
12
+ method: opts.method ?? 'GET',
13
+ headers: {
14
+ Authorization: `Bearer ${opts.token}`,
15
+ 'Content-Type': 'application/json',
16
+ },
17
+ body: opts.body ? JSON.stringify(opts.body) : undefined,
18
+ });
19
+ }
20
+ async function resolveCompanyUid(token, slug) {
21
+ const res = await vaultApiFetch({
22
+ token,
23
+ path: `/entity/by-slug/company/${encodeURIComponent(slug)}`,
24
+ });
25
+ if (!res.ok) {
26
+ const body = await res.json().catch(() => ({}));
27
+ throw new Error(`Failed to resolve company slug '${slug}': ${body.error ?? res.statusText}`);
28
+ }
29
+ const data = (await res.json());
30
+ return data.entity.uid;
31
+ }
32
+ async function resolveCompanyFromMemberships(token) {
33
+ const res = await vaultApiFetch({
34
+ token,
35
+ path: '/membership/me',
36
+ });
37
+ if (!res.ok) {
38
+ throw new Error("Failed to fetch memberships — run `hq login` and try again");
39
+ }
40
+ const data = (await res.json());
41
+ const active = data.memberships.filter((m) => m.status === 'active');
42
+ if (active.length === 0) {
43
+ throw new Error('No active company memberships found. Use --company <slug> to specify.');
44
+ }
45
+ if (active.length === 1) {
46
+ return active[0].companyUid;
47
+ }
48
+ const uids = active.map((m) => m.companyUid).join(', ');
49
+ throw new Error(`Multiple companies found (${uids}). Use --company <slug> to specify which one.`);
50
+ }
51
+ export async function getCompanyUid(token, companySlug) {
52
+ if (companySlug) {
53
+ return resolveCompanyUid(token, companySlug);
54
+ }
55
+ return resolveCompanyFromMemberships(token);
56
+ }
57
+ //# sourceMappingURL=vault-api.js.map
58
+ //# debugId=4e482805-d129-5563-a77b-97b80410154b
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.8.6",
3
+ "version": "5.9.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -21,7 +21,8 @@
21
21
  "commander": "^12.1.0",
22
22
  "js-yaml": "^4.1.0",
23
23
  "simple-git": "^3.27.0",
24
- "semver": "^7.6.3"
24
+ "semver": "^7.6.3",
25
+ "varlock": "1.0.0"
25
26
  },
26
27
  "devDependencies": {
27
28
  "@types/js-yaml": "^4.0.9",
@@ -42,5 +43,8 @@
42
43
  "cloud"
43
44
  ],
44
45
  "license": "MIT",
45
- "type": "module"
46
+ "type": "module",
47
+ "engines": {
48
+ "node": ">=22.0.0"
49
+ }
46
50
  }