@indigoai-us/hq-cli 5.60.0 → 5.61.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.
@@ -36,6 +36,12 @@
36
36
  import { type KeyObject } from 'node:crypto';
37
37
  import { type SecretResolver } from './mcp-registration.js';
38
38
  import type { PackManifest } from '../types.js';
39
+ export interface ResolveLatestOptions {
40
+ forceRefresh?: boolean;
41
+ now?: number;
42
+ cacheTtlMs?: number;
43
+ fetchImpl?: typeof fetch;
44
+ }
39
45
  export type Transport = 'npm' | 'git' | 'local' | 'marketplace';
40
46
  /** Prefix that routes a source through the HQ marketplace transport (US-006). */
41
47
  export declare const MARKETPLACE_PREFIX = "marketplace:";
@@ -181,7 +187,7 @@ export interface LatestResult {
181
187
  * @param source the stamped `source:` from the installed package.yaml
182
188
  * @param installedVersion the installed pack's manifest `version` (npm compare)
183
189
  */
184
- export declare function resolveLatest(source: string, installedVersion?: string): LatestResult;
190
+ export declare function resolveLatest(source: string, installedVersion?: string, opts?: ResolveLatestOptions): Promise<LatestResult>;
185
191
  /**
186
192
  * Async marketplace update probe (US-006): resolve the slug's latest approved
187
193
  * listing version and compare it to the installed version. Reuses the same
@@ -34,7 +34,7 @@
34
34
  * from each pack's package.yaml; rationale lives in the layout-fix PR.)
35
35
  */
36
36
 
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]="0ec07d3a-ee86-51fd-adbd-2608c620ae99")}catch(e){}}();
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]="a90272ca-94a1-5905-85f8-af12a01cb58a")}catch(e){}}();
38
38
  import * as fs from 'fs';
39
39
  import * as os from 'os';
40
40
  import * as path from 'path';
@@ -54,6 +54,58 @@ import { safeExtractTarball } from './safe-extract.js';
54
54
  import { vaultApiFetchPublic } from '../utils/vault-api.js';
55
55
  import { redactSecrets, SECRET_REDACTION, registerMcpServers, McpManifestError, } from './mcp-registration.js';
56
56
  import { readCache, listSecretCacheScopes } from '../utils/secrets-cache.js';
57
+ const PACK_UPDATE_CACHE_TTL_MS = 12 * 60 * 60 * 1000;
58
+ const PACK_UPDATE_FETCH_TIMEOUT_MS = 3_000;
59
+ const gitLsRemoteMemo = new Map();
60
+ function packUpdateCachePath() {
61
+ return path.join(os.homedir(), '.hq', 'pack-update-cache.json');
62
+ }
63
+ function readPackUpdateCache() {
64
+ try {
65
+ const parsed = JSON.parse(fs.readFileSync(packUpdateCachePath(), 'utf-8'));
66
+ if (!parsed || typeof parsed !== 'object' || !parsed.entries || typeof parsed.entries !== 'object') {
67
+ return { entries: {} };
68
+ }
69
+ return { entries: parsed.entries };
70
+ }
71
+ catch {
72
+ return { entries: {} };
73
+ }
74
+ }
75
+ function writePackUpdateCache(cache) {
76
+ try {
77
+ const file = packUpdateCachePath();
78
+ fs.mkdirSync(path.dirname(file), { recursive: true });
79
+ fs.writeFileSync(file, JSON.stringify(cache));
80
+ }
81
+ catch {
82
+ // best-effort; update checks must never fail because the cache is unwritable
83
+ }
84
+ }
85
+ function cachedLatest(cacheKey, opts) {
86
+ if (opts.forceRefresh)
87
+ return undefined;
88
+ const entry = readPackUpdateCache().entries[cacheKey];
89
+ if (!entry || typeof entry.latest !== 'string' || typeof entry.fetchedAt !== 'number')
90
+ return undefined;
91
+ const now = opts.now ?? Date.now();
92
+ const ttl = opts.cacheTtlMs ?? PACK_UPDATE_CACHE_TTL_MS;
93
+ return now - entry.fetchedAt <= ttl ? entry.latest : undefined;
94
+ }
95
+ function storeCachedLatest(cacheKey, latest, opts) {
96
+ const cache = readPackUpdateCache();
97
+ cache.entries[cacheKey] = { latest, fetchedAt: opts.now ?? Date.now() };
98
+ writePackUpdateCache(cache);
99
+ }
100
+ async function latestWithDiskCache(cacheKey, opts, refresh) {
101
+ const cached = cachedLatest(cacheKey, opts);
102
+ if (cached)
103
+ return cached;
104
+ const latest = await refresh();
105
+ if (latest)
106
+ storeCachedLatest(cacheKey, latest, opts);
107
+ return latest;
108
+ }
57
109
  /** Prefix that routes a source through the HQ marketplace transport (US-006). */
58
110
  export const MARKETPLACE_PREFIX = 'marketplace:';
59
111
  export function classify(source) {
@@ -489,13 +541,33 @@ function rsyncDir(src, dest) {
489
541
  */
490
542
  function isNamedRef(url, ref) {
491
543
  try {
492
- const out = execFileSync('git', ['ls-remote', '--heads', '--tags', url, ref], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
544
+ const out = gitLsRemote(['--heads', '--tags', url, ref]);
493
545
  return out.trim().length > 0;
494
546
  }
495
547
  catch {
496
548
  return false;
497
549
  }
498
550
  }
551
+ function gitLsRemote(args) {
552
+ const key = args.join('\0');
553
+ const cached = gitLsRemoteMemo.get(key);
554
+ if (cached !== undefined)
555
+ return cached;
556
+ const out = execFileSync('git', ['ls-remote', ...args], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
557
+ gitLsRemoteMemo.set(key, out);
558
+ return out;
559
+ }
560
+ async function fetchLatestNpmVersion(pkg, opts) {
561
+ const fetchImpl = opts.fetchImpl ?? fetch;
562
+ const res = await fetchImpl(`https://registry.npmjs.org/${encodeURIComponent(pkg)}/latest`, {
563
+ headers: { Accept: 'application/json' },
564
+ signal: AbortSignal.timeout(PACK_UPDATE_FETCH_TIMEOUT_MS),
565
+ });
566
+ if (!res.ok)
567
+ throw new Error(`registry returned ${res.status}`);
568
+ const body = (await res.json());
569
+ return typeof body.version === 'string' ? body.version : undefined;
570
+ }
499
571
  /** Extract the ref (sha or named ref) recorded in a stamped git source. */
500
572
  function gitRefFromSource(source) {
501
573
  const { subpath, ref } = parseGitFragment(source);
@@ -504,6 +576,9 @@ function gitRefFromSource(source) {
504
576
  void subpath;
505
577
  return ref;
506
578
  }
579
+ function isFullGitSha(ref) {
580
+ return /^[0-9a-f]{40}$/i.test(ref);
581
+ }
507
582
  /**
508
583
  * Probe whether a newer version of an already-installed pack is available,
509
584
  * WITHOUT fetching or installing. Reuses the same git/npm primitives as the
@@ -513,7 +588,7 @@ function gitRefFromSource(source) {
513
588
  * @param source the stamped `source:` from the installed package.yaml
514
589
  * @param installedVersion the installed pack's manifest `version` (npm compare)
515
590
  */
516
- export function resolveLatest(source, installedVersion) {
591
+ export async function resolveLatest(source, installedVersion, opts = {}) {
517
592
  let transport;
518
593
  try {
519
594
  transport = classify(source);
@@ -542,15 +617,12 @@ export function resolveLatest(source, installedVersion) {
542
617
  const pkg = stripVersion(source);
543
618
  const current = installedVersion ?? (source.lastIndexOf('@') > 0 ? source.slice(source.lastIndexOf('@') + 1) : undefined);
544
619
  try {
545
- const latest = execFileSync('npm', ['view', pkg, 'version'], {
546
- encoding: 'utf-8',
547
- stdio: ['ignore', 'pipe', 'ignore'],
548
- }).trim();
620
+ const latest = await latestWithDiskCache(`npm:${pkg}`, opts, () => fetchLatestNpmVersion(pkg, opts));
549
621
  const updateAvailable = current && latest ? semverGt(latest, current) : null;
550
622
  return { transport, current, latest, updateAvailable };
551
623
  }
552
624
  catch (e) {
553
- return { transport, current, updateAvailable: null, error: `npm view failed: ${e.message}` };
625
+ return { transport, current, updateAvailable: null, error: `npm registry check failed: ${e.message}` };
554
626
  }
555
627
  }
556
628
  // git
@@ -565,13 +637,12 @@ export function resolveLatest(source, installedVersion) {
565
637
  const current = gitRefFromSource(source);
566
638
  // If install followed a named ref (branch/tag), compare that ref's tip;
567
639
  // otherwise (default SHA-pin) compare the default branch HEAD.
568
- const refArg = current && isNamedRef(url, current) ? current : 'HEAD';
640
+ const refArg = current && !isFullGitSha(current) && isNamedRef(url, current) ? current : 'HEAD';
569
641
  try {
570
- const out = execFileSync('git', ['ls-remote', url, refArg], {
571
- encoding: 'utf-8',
572
- stdio: ['ignore', 'pipe', 'ignore'],
573
- }).trim();
574
- const latest = out.split(/\s+/)[0] || undefined;
642
+ const latest = await latestWithDiskCache(`git:${url}#${refArg}`, opts, () => {
643
+ const out = gitLsRemote([url, refArg]).trim();
644
+ return out.split(/\s+/)[0] || undefined;
645
+ });
575
646
  const updateAvailable = current && latest ? !latest.startsWith(current) && !current.startsWith(latest) : null;
576
647
  return { transport, current, latest, updateAvailable };
577
648
  }
@@ -1629,4 +1700,4 @@ export async function installPack(source, opts = {}) {
1629
1700
  }
1630
1701
  }
1631
1702
  //# sourceMappingURL=pack-install.js.map
1632
- //# debugId=0ec07d3a-ee86-51fd-adbd-2608c620ae99
1703
+ //# debugId=a90272ca-94a1-5905-85f8-af12a01cb58a
@@ -18,6 +18,7 @@
18
18
  * Spec: knowledge/public/hq-core/package-yaml-spec.md.
19
19
  */
20
20
  import { Command } from 'commander';
21
+ import { type LatestResult } from './pack-install.js';
21
22
  import { type InstalledPack, type LinkStatus } from '../utils/pack-contributions.js';
22
23
  import type { PackContributeKey } from '../types.js';
23
24
  interface InstalledPackView {
@@ -48,7 +49,7 @@ interface InstalledPackView {
48
49
  };
49
50
  error?: string;
50
51
  }
51
- export declare function buildInstalledView(hqRoot: string, hqVersion: string | null, pack: InstalledPack, installedSources: Set<string>, checkUpdates: boolean): InstalledPackView;
52
+ export declare function buildInstalledView(hqRoot: string, hqVersion: string | null, pack: InstalledPack, installedSources: Set<string>, checkUpdates: boolean, latestProbe?: LatestResult): InstalledPackView;
52
53
  export declare function registerPacksCommand(parent: Command): void;
53
54
  export {};
54
55
  //# sourceMappingURL=packs.d.ts.map
@@ -18,7 +18,7 @@
18
18
  * Spec: knowledge/public/hq-core/package-yaml-spec.md.
19
19
  */
20
20
 
21
- !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]="69746edd-f9d8-528e-8902-de799c58bd8a")}catch(e){}}();
21
+ !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]="f8ee3de6-a097-5e0f-8afa-63381f29032d")}catch(e){}}();
22
22
  import * as fs from 'fs';
23
23
  import * as path from 'path';
24
24
  import * as readline from 'readline';
@@ -61,7 +61,7 @@ async function confirm(question) {
61
61
  });
62
62
  return /^(y|yes)$/i.test(answer.trim());
63
63
  }
64
- export function buildInstalledView(hqRoot, hqVersion, pack, installedSources, checkUpdates) {
64
+ export function buildInstalledView(hqRoot, hqVersion, pack, installedSources, checkUpdates, latestProbe) {
65
65
  if (!pack.manifest) {
66
66
  return {
67
67
  name: pack.name,
@@ -95,7 +95,7 @@ export function buildInstalledView(hqRoot, hqVersion, pack, installedSources, ch
95
95
  const hqCoreSatisfied = hqVersion && requiresHqCore ? semverSatisfies(hqVersion, requiresHqCore) : null;
96
96
  let updateAvailable = null;
97
97
  if (checkUpdates && m.source) {
98
- updateAvailable = resolveLatest(m.source, m.version).updateAvailable;
98
+ updateAvailable = latestProbe?.updateAvailable ?? null;
99
99
  }
100
100
  // US-005 — surface the pack's `initialization` block so the HQ Sync
101
101
  // "Installed" panel can render its get-started affordance. `readPackManifest`
@@ -129,13 +129,16 @@ export function buildInstalledView(hqRoot, hqVersion, pack, installedSources, ch
129
129
  ...(initialization ? { initialization } : {}),
130
130
  };
131
131
  }
132
- function buildListView(hqRoot, checkUpdates, evalConditionals) {
132
+ async function buildListView(hqRoot, checkUpdates, evalConditionals, refreshUpdates) {
133
133
  const hqVersion = readHqVersion(hqRoot);
134
134
  const packs = listInstalledPacks(hqRoot);
135
135
  const catalog = readRecommendedPackages(hqRoot);
136
136
  const catalogSources = new Set(catalog.map((c) => c.source));
137
137
  const warnings = [];
138
- const installed = packs.map((p) => buildInstalledView(hqRoot, hqVersion, p, catalogSources, checkUpdates));
138
+ const latestProbes = await Promise.all(packs.map((p) => checkUpdates && p.manifest?.source
139
+ ? resolveLatest(p.manifest.source, p.manifest.version, { forceRefresh: refreshUpdates })
140
+ : Promise.resolve(undefined)));
141
+ const installed = packs.map((p, i) => buildInstalledView(hqRoot, hqVersion, p, catalogSources, checkUpdates, latestProbes[i]));
139
142
  for (const p of installed) {
140
143
  if (p.error)
141
144
  warnings.push(`${p.name}: ${p.error}`);
@@ -217,7 +220,7 @@ async function runUpdate(name, opts) {
217
220
  // other transport keeps the existing synchronous probe unchanged.
218
221
  const probe = safeClassify(source) === 'marketplace'
219
222
  ? await resolveLatestMarketplace(source, m.version)
220
- : resolveLatest(source, m.version);
223
+ : await resolveLatest(source, m.version, { forceRefresh: true });
221
224
  const base = {
222
225
  name: pname,
223
226
  transport: probe.transport,
@@ -374,10 +377,11 @@ export function registerPacksCommand(parent) {
374
377
  .option('--json', 'Machine-readable JSON output')
375
378
  .option('--hq-root <path>', 'HQ root (default: auto-detect)')
376
379
  .option('--check-updates', 'Probe each pack for available updates (network I/O)')
380
+ .option('--refresh', 'Bypass cached update probes')
377
381
  .option('--eval-conditionals', 'Evaluate catalog conditional predicates (runs bash)')
378
382
  .action(async (opts) => {
379
383
  try {
380
- const view = buildListView(resolveRoot(opts), !!opts.checkUpdates, !!opts.evalConditionals);
384
+ const view = await buildListView(resolveRoot(opts), !!opts.checkUpdates, !!opts.evalConditionals, !!opts.refresh);
381
385
  if (wantsJson(opts))
382
386
  emitJson(view);
383
387
  else
@@ -394,6 +398,7 @@ export function registerPacksCommand(parent) {
394
398
  .option('--json', 'Machine-readable JSON output')
395
399
  .option('--hq-root <path>', 'HQ root (default: auto-detect)')
396
400
  .option('--check-only', 'Report availability without installing')
401
+ .option('--refresh', 'Bypass cached update probes (update refreshes by default)')
397
402
  .option('-y, --yes', 'Non-interactive (implies --allow-hooks and --allow-mcp)')
398
403
  .option('--allow-hooks', 'Install pack hooks without prompting')
399
404
  .option('--allow-mcp', 'Register pack MCP servers without prompting')
@@ -463,4 +468,4 @@ export function registerPacksCommand(parent) {
463
468
  });
464
469
  }
465
470
  //# sourceMappingURL=packs.js.map
466
- //# debugId=69746edd-f9d8-528e-8902-de799c58bd8a
471
+ //# debugId=f8ee3de6-a097-5e0f-8afa-63381f29032d
@@ -36,6 +36,11 @@ export interface SecretLoadResponse {
36
36
  message?: string;
37
37
  }>;
38
38
  }
39
+ export interface SecretInjectionRecipe {
40
+ header: string;
41
+ scheme: "raw" | "bearer";
42
+ extraHeaders?: Record<string, string>;
43
+ }
39
44
  export declare function scrubSandboxOutput(text: string, secretNames?: string[]): string;
40
45
  export declare function loadRevealedSecrets(token: string, companyUid: string, keys: string[], usage?: SecretUsage): Promise<Map<string, string>>;
41
46
  export declare function registerSecretsCommand(program: Command): void;
@@ -1,5 +1,5 @@
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]="3b78d543-70d6-52e2-b19f-fa2650534e97")}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]="ac2acbfe-d6f8-52af-9e1a-38b43474a6d5")}catch(e){}}();
3
3
  import chalk from "chalk";
4
4
  import * as readline from "node:readline";
5
5
  import { spawn } from "node:child_process";
@@ -153,6 +153,74 @@ function describeSecretAclPrincipal(principal) {
153
153
  ? "@all (entire company)"
154
154
  : principal.granteeId;
155
155
  }
156
+ // Mirrors hq-pro's server-authoritative KNOWN_DESTINATION_REGISTRY
157
+ // (src/vault-service/handlers/destination-registry.ts) — a KNOWN host's
158
+ // --auth-style is optional because the server resolves the recipe itself.
159
+ // This client-side copy exists purely so an UNKNOWN host with no
160
+ // --auth-style can be rejected immediately with a clear, actionable message
161
+ // instead of a round trip; the server remains the authoritative validator
162
+ // (this list drifting stale merely means one extra CLI round trip, not a
163
+ // security gap — the server still 400s an unrecognized host with no recipe).
164
+ const KNOWN_DESTINATION_HOSTS = new Set([
165
+ "api.anthropic.com",
166
+ "api.openai.com",
167
+ "api.stripe.com",
168
+ ]);
169
+ // Parses `--auth-style` into the InjectionRecipe shape the server expects.
170
+ // Returns `null` (with a printed error) for an unrecognized value.
171
+ function parseAuthStyle(authStyle) {
172
+ if (authStyle === "bearer") {
173
+ return { header: "authorization", scheme: "bearer" };
174
+ }
175
+ if (authStyle === "x-api-key") {
176
+ return { header: "x-api-key", scheme: "raw" };
177
+ }
178
+ const headerMatch = authStyle.match(/^header:(.+)$/);
179
+ if (headerMatch) {
180
+ const headerName = headerMatch[1].trim();
181
+ if (!headerName) {
182
+ console.error(chalk.red(`Invalid --auth-style 'header:': must name a header, e.g. header:X-Custom-Key`));
183
+ return null;
184
+ }
185
+ return { header: headerName, scheme: "raw" };
186
+ }
187
+ console.error(chalk.red(`Invalid --auth-style '${authStyle}': must be one of bearer, x-api-key, or header:NAME`));
188
+ return null;
189
+ }
190
+ // Validates `--destination` is a bare HTTPS scheme+host URL (no path, query,
191
+ // port). Mirrors hq-pro's `validateDestinations` server-side check
192
+ // (src/vault-service/handlers/secrets.ts) so a malformed URL is caught
193
+ // locally with an actionable message rather than a round trip — the server
194
+ // re-validates and remains authoritative.
195
+ function parseDestinationUrl(raw) {
196
+ let parsed;
197
+ try {
198
+ parsed = new URL(raw);
199
+ }
200
+ catch {
201
+ console.error(chalk.red(`Invalid --destination '${raw}': must be a valid URL`));
202
+ return { ok: false };
203
+ }
204
+ if (parsed.protocol !== "https:") {
205
+ console.error(chalk.red(`Invalid --destination '${raw}': must use https://`));
206
+ return { ok: false };
207
+ }
208
+ if (!parsed.hostname) {
209
+ console.error(chalk.red(`Invalid --destination '${raw}': missing hostname`));
210
+ return { ok: false };
211
+ }
212
+ if ((parsed.pathname !== "" && parsed.pathname !== "/") ||
213
+ parsed.search !== "" ||
214
+ parsed.hash !== "") {
215
+ console.error(chalk.red(`Invalid --destination '${raw}': must be a bare scheme+host URL with no path, query, or fragment (e.g. https://api.openai.com)`));
216
+ return { ok: false };
217
+ }
218
+ if (parsed.port !== "") {
219
+ console.error(chalk.red(`Invalid --destination '${raw}': must not specify a port`));
220
+ return { ok: false };
221
+ }
222
+ return { ok: true, url: `https://${parsed.hostname}`, hostname: parsed.hostname };
223
+ }
156
224
  function normalizeSecretTier(tier) {
157
225
  return tier === "sensitive" || tier === "nuclear" ? tier : "standard";
158
226
  }
@@ -378,12 +446,56 @@ export function registerSecretsCommand(program) {
378
446
  .command("set <name>")
379
447
  .description("Create or update a secret")
380
448
  .option("--from-stdin", "Read secret value from piped stdin")
449
+ .option("--high-security", "Mark the secret high-security: it can never be revealed or injected locally, only used through the HQ secret proxy (requires --destination)")
450
+ .option("--destination <https-url>", "Approved scheme+host HTTPS URL the proxy may forward this secret to (e.g. https://api.openai.com); required with --high-security")
451
+ .option("--auth-style <style>", "How the proxy attaches the key upstream: bearer | x-api-key | header:NAME. Optional for known destinations (auto-resolved server-side); required for unknown ones")
381
452
  .action(async (name, opts) => {
382
453
  try {
383
454
  if (!SECRET_NAME_PATTERN.test(name)) {
384
455
  console.error(chalk.red(`Invalid secret name '${name}': must match ^[A-Z][A-Z0-9_]*(/[A-Z][A-Z0-9_]+)*$ (e.g. MY_API_KEY or DEV/MY_KEY)`));
385
456
  process.exit(1);
386
457
  }
458
+ // secrets-proxy-per-secret-destination US-006: --high-security marks
459
+ // the secret so it can only ever be used through the server-side
460
+ // proxy (never revealed/injected locally — that refusal is the
461
+ // pre-existing consumption-side behavior in `get`/`exec`/`env` above,
462
+ // unchanged by this story). It REQUIRES a --destination: the proxy
463
+ // (hq-pro US-002) fails closed with no destination configured, so
464
+ // catching the missing pin here is a clear, immediate CLI error
465
+ // rather than a deferred proxy-time failure.
466
+ let destinations;
467
+ let injection;
468
+ if (opts.highSecurity) {
469
+ if (!opts.destination) {
470
+ console.error(chalk.red("Error: --high-security requires --destination <https-url> (e.g. --destination https://api.openai.com)."));
471
+ process.exit(1);
472
+ }
473
+ const destResult = parseDestinationUrl(opts.destination);
474
+ if (!destResult.ok) {
475
+ process.exit(1);
476
+ }
477
+ destinations = [destResult.url];
478
+ if (opts.authStyle) {
479
+ const recipe = parseAuthStyle(opts.authStyle);
480
+ if (!recipe) {
481
+ process.exit(1);
482
+ }
483
+ injection = recipe;
484
+ }
485
+ else if (!KNOWN_DESTINATION_HOSTS.has(destResult.hostname)) {
486
+ // Unknown host + no explicit recipe: the server would reject this
487
+ // 400 anyway (US-004 registry lookup only, never guesses) — fail
488
+ // fast locally with an actionable message instead of a round trip.
489
+ console.error(chalk.red(`Error: unknown destination host '${destResult.hostname}' — provide --auth-style <bearer|x-api-key|header:NAME> (known hosts auto-resolve: ${[...KNOWN_DESTINATION_HOSTS].join(", ")}).`));
490
+ process.exit(1);
491
+ }
492
+ // Known host + no --auth-style: leave `injection` undefined so the
493
+ // server (US-004) auto-resolves the recipe from its registry.
494
+ }
495
+ else if (opts.destination || opts.authStyle) {
496
+ console.error(chalk.red("Error: --destination/--auth-style require --high-security."));
497
+ process.exit(1);
498
+ }
387
499
  let value;
388
500
  if (opts.fromStdin) {
389
501
  if (process.stdin.isTTY) {
@@ -419,15 +531,27 @@ export function registerSecretsCommand(program) {
419
531
  token,
420
532
  path: `/secrets/${encodeURIComponent(companyUid)}`,
421
533
  method: "POST",
422
- body: { name, value },
534
+ body: {
535
+ name,
536
+ value,
537
+ // Only present when --high-security was passed — an ordinary
538
+ // `set` with no flags sends exactly `{ name, value }`, byte-for-
539
+ // byte unchanged from before this story.
540
+ ...(opts.highSecurity ? { highSecurity: true } : {}),
541
+ ...(destinations ? { destinations } : {}),
542
+ ...(injection ? { injection } : {}),
543
+ },
423
544
  });
424
545
  if (!res.ok) {
425
- const body = await res.json().catch(() => ({}));
426
- console.error(chalk.red(`Failed to set secret: ${body.error ?? res.statusText}`));
546
+ const body = (await res.json().catch(() => ({})));
547
+ console.error(chalk.red(`Failed to set secret: ${extractApiMessage(body, res.statusText)}`));
427
548
  process.exit(1);
428
549
  }
429
550
  removeCacheEntry(companyUid, name);
430
551
  console.log(chalk.green(formatSecretSaved(name, scopeLabel)));
552
+ if (opts.highSecurity) {
553
+ console.log(chalk.dim(` High-security: destination pinned to ${destinations?.[0]}. This value can never be revealed or injected locally — only used through the HQ secret proxy.`));
554
+ }
431
555
  }
432
556
  catch (err) {
433
557
  console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
@@ -1258,4 +1382,4 @@ export function registerSecretsCommand(program) {
1258
1382
  });
1259
1383
  }
1260
1384
  //# sourceMappingURL=secrets.js.map
1261
- //# debugId=3b78d543-70d6-52e2-b19f-fa2650534e97
1385
+ //# debugId=ac2acbfe-d6f8-52af-9e1a-38b43474a6d5
package/dist/index.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * HQ CLI - Module management, package management, and cloud sync for HQ
4
- */
5
2
  import "./node-preflight.js";
3
+ declare function isVersionRequest(argv: readonly string[]): boolean;
4
+ export declare const __test__: {
5
+ isVersionRequest: typeof isVersionRequest;
6
+ };
7
+ export {};
6
8
  //# sourceMappingURL=index.d.ts.map