@indigoai-us/hq-cli 5.8.5 → 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 +30 -0
- package/dist/commands/files.js +94 -48
- package/dist/commands/run.d.ts +3 -0
- package/dist/commands/run.js +119 -0
- package/dist/commands/secrets.d.ts +3 -9
- package/dist/commands/secrets.js +5 -56
- package/dist/index.js +6 -3
- package/dist/run/discover-schemas.d.ts +12 -0
- package/dist/run/discover-schemas.js +64 -0
- package/dist/run/hq-plugin.d.ts +26 -0
- package/dist/run/hq-plugin.js +144 -0
- package/dist/utils/vault-api.d.ts +10 -0
- package/dist/utils/vault-api.js +58 -0
- package/package.json +7 -3
- package/src/commands/files.ts +110 -50
- package/src/commands/run.env-local.test.ts +84 -0
- package/src/commands/run.ts +137 -0
- package/src/commands/secrets.ts +4 -88
- package/src/index.ts +5 -1
- package/src/run/__fixtures__/discover-schemas/example.env.schema +4 -0
- package/src/run/discover-schemas.test.ts +153 -0
- package/src/run/discover-schemas.ts +79 -0
- package/src/run/hq-plugin.test.ts +125 -0
- package/src/run/hq-plugin.ts +174 -0
- package/src/run/varlock-shape.test.ts +57 -0
- package/src/utils/vault-api.ts +80 -0
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`.
|
package/dist/commands/files.js
CHANGED
|
@@ -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]="
|
|
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]="a754383e-f129-5052-a8d8-59ea590b4f3d")}catch(e){}}();
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
5
5
|
import { vaultApiFetch, getCompanyUid } from "./secrets.js";
|
|
@@ -148,22 +148,36 @@ export function registerFilesCommand(program) {
|
|
|
148
148
|
const token = await ensureCognitoToken();
|
|
149
149
|
const companySlug = files.opts().company;
|
|
150
150
|
const companyUid = await getCompanyUid(token, companySlug);
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
151
|
+
// Fetch the prefix's own ACL row (creator, open flag, effective
|
|
152
|
+
// permission) and the inherited/descendant tree in parallel so the
|
|
153
|
+
// user sees every grant that affects this prefix in one shot.
|
|
154
|
+
const [aclRes, treeRes] = await Promise.all([
|
|
155
|
+
vaultApiFetch({
|
|
156
|
+
token,
|
|
157
|
+
path: `/files/${encodeURIComponent(companyUid)}/acl`,
|
|
158
|
+
query: { prefix: canonicalPrefix },
|
|
159
|
+
}),
|
|
160
|
+
vaultApiFetch({
|
|
161
|
+
token,
|
|
162
|
+
path: `/files/${encodeURIComponent(companyUid)}/acl/tree`,
|
|
163
|
+
query: { prefix: canonicalPrefix },
|
|
164
|
+
}),
|
|
165
|
+
]);
|
|
166
|
+
async function readErrorBody(res) {
|
|
167
|
+
return (await res.json().catch(() => ({})));
|
|
168
|
+
}
|
|
169
|
+
// Auth/server failures from either call are treated identically — bail
|
|
170
|
+
// out with a single message rather than printing a half-rendered view.
|
|
171
|
+
for (const res of [aclRes, treeRes]) {
|
|
172
|
+
if (res.ok || res.status === 404)
|
|
173
|
+
continue;
|
|
174
|
+
const body = await readErrorBody(res);
|
|
158
175
|
if (res.status === 401) {
|
|
159
176
|
console.error(chalk.red("Not authenticated — please run `hq login`"));
|
|
160
177
|
}
|
|
161
178
|
else if (res.status === 403) {
|
|
162
179
|
console.error(chalk.red("Not authorized to view this file prefix's ACL"));
|
|
163
180
|
}
|
|
164
|
-
else if (res.status === 404) {
|
|
165
|
-
console.error(chalk.red(`No ACL record exists for '${canonicalPrefix}'`));
|
|
166
|
-
}
|
|
167
181
|
else if (res.status >= 500) {
|
|
168
182
|
console.error(chalk.red(`Server error: ${body.error ?? res.statusText}`));
|
|
169
183
|
}
|
|
@@ -172,46 +186,78 @@ export function registerFilesCommand(program) {
|
|
|
172
186
|
}
|
|
173
187
|
process.exit(1);
|
|
174
188
|
}
|
|
175
|
-
const
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
189
|
+
const acl = aclRes.ok ? (await aclRes.json()).acl : null;
|
|
190
|
+
const tree = treeRes.ok ? (await treeRes.json()) : null;
|
|
191
|
+
// No own row AND nothing inherited or granted below — original
|
|
192
|
+
// "no ACL record" exit path.
|
|
193
|
+
if (!acl && (!tree || (tree.inherited.length === 0 && tree.children.length === 0))) {
|
|
194
|
+
console.error(chalk.red(`No ACL record exists for '${canonicalPrefix}'`));
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
const aclPrefix = acl?.path ?? acl?.prefix ?? tree?.prefix ?? canonicalPrefix;
|
|
198
|
+
const aclStatus = acl?.open ? "open" : "restricted";
|
|
179
199
|
console.log(chalk.green(`ACL for ${aclPrefix} (${aclStatus})`));
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
200
|
+
if (acl) {
|
|
201
|
+
console.log(`Creator: ${acl.creatorUid}`);
|
|
202
|
+
if (acl.effectivePermission) {
|
|
203
|
+
console.log(`Your effective permission: ${acl.effectivePermission}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
console.log(chalk.gray("No direct ACL row — access flows from the inherited/descendant grants below."));
|
|
208
|
+
}
|
|
209
|
+
function printEntryTable(rows, showSource) {
|
|
210
|
+
const TYPE_W = Math.max(4, ...rows.map((e) => e.granteeType.length));
|
|
211
|
+
const GRANTEE_W = Math.max(7, ...rows.map((e) => e.granteeId.length));
|
|
212
|
+
const PERM_W = Math.max(10, ...rows.map((e) => e.permission.length));
|
|
213
|
+
const BY_W = Math.max(10, ...rows.map((e) => e.grantedBy.length));
|
|
214
|
+
const SRC_W = showSource
|
|
215
|
+
? Math.max(6, ...rows.map((e) => (e.sourcePrefix ?? "").length))
|
|
216
|
+
: 0;
|
|
217
|
+
const headerCols = [
|
|
218
|
+
"TYPE".padEnd(TYPE_W),
|
|
219
|
+
"GRANTEE".padEnd(GRANTEE_W),
|
|
220
|
+
"PERMISSION".padEnd(PERM_W),
|
|
221
|
+
"GRANTED_BY".padEnd(BY_W),
|
|
222
|
+
"GRANTED_AT",
|
|
223
|
+
];
|
|
224
|
+
if (showSource)
|
|
225
|
+
headerCols.splice(4, 0, "SOURCE".padEnd(SRC_W));
|
|
226
|
+
console.log(chalk.bold(headerCols.join(" ")));
|
|
227
|
+
for (const e of rows) {
|
|
228
|
+
const grantedAt = e.grantedAt.slice(0, 10);
|
|
229
|
+
const cols = [
|
|
230
|
+
e.granteeType.padEnd(TYPE_W),
|
|
231
|
+
e.granteeId.padEnd(GRANTEE_W),
|
|
232
|
+
e.permission.padEnd(PERM_W),
|
|
233
|
+
e.grantedBy.padEnd(BY_W),
|
|
234
|
+
grantedAt,
|
|
235
|
+
];
|
|
236
|
+
if (showSource)
|
|
237
|
+
cols.splice(4, 0, (e.sourcePrefix ?? "").padEnd(SRC_W));
|
|
238
|
+
console.log(cols.join(" "));
|
|
239
|
+
}
|
|
183
240
|
}
|
|
184
|
-
|
|
185
|
-
|
|
241
|
+
const directEntries = acl?.entries ?? tree?.direct ?? [];
|
|
242
|
+
if (directEntries.length === 0) {
|
|
243
|
+
if (acl?.open) {
|
|
186
244
|
console.log(chalk.gray("Open ACL — all active members have read access."));
|
|
187
245
|
}
|
|
188
|
-
else {
|
|
189
|
-
console.log(chalk.gray("No explicit grants — only creator has access."));
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
"GRANTED_AT",
|
|
204
|
-
].join(" ");
|
|
205
|
-
console.log(chalk.bold(tableHeader));
|
|
206
|
-
for (const e of acl.entries) {
|
|
207
|
-
const grantedAt = e.grantedAt.slice(0, 10);
|
|
208
|
-
console.log([
|
|
209
|
-
e.granteeType.padEnd(TYPE_W),
|
|
210
|
-
e.granteeId.padEnd(GRANTEE_W),
|
|
211
|
-
e.permission.padEnd(PERM_W),
|
|
212
|
-
e.grantedBy.padEnd(BY_W),
|
|
213
|
-
grantedAt,
|
|
214
|
-
].join(" "));
|
|
246
|
+
else if (acl) {
|
|
247
|
+
console.log(chalk.gray("No explicit grants on this prefix — only creator has access."));
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
console.log("\nDirect entries (granted on this prefix):");
|
|
252
|
+
printEntryTable(directEntries, false);
|
|
253
|
+
}
|
|
254
|
+
if (tree && tree.inherited.length > 0) {
|
|
255
|
+
console.log("\nInherited (granted on an ancestor prefix):");
|
|
256
|
+
printEntryTable(tree.inherited, true);
|
|
257
|
+
}
|
|
258
|
+
if (tree && tree.children.length > 0) {
|
|
259
|
+
console.log("\nGranted on descendant prefixes (do not affect this prefix's access):");
|
|
260
|
+
printEntryTable(tree.children, true);
|
|
215
261
|
}
|
|
216
262
|
}
|
|
217
263
|
catch (err) {
|
|
@@ -221,4 +267,4 @@ export function registerFilesCommand(program) {
|
|
|
221
267
|
});
|
|
222
268
|
}
|
|
223
269
|
//# sourceMappingURL=files.js.map
|
|
224
|
-
//# debugId=
|
|
270
|
+
//# debugId=a754383e-f129-5052-a8d8-59ea590b4f3d
|
|
@@ -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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
package/dist/commands/secrets.js
CHANGED
|
@@ -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]="
|
|
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
|
|
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=
|
|
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]="
|
|
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();
|
|
@@ -30,7 +31,7 @@ const program = new Command();
|
|
|
30
31
|
program
|
|
31
32
|
.name("hq")
|
|
32
33
|
.description("HQ management CLI — modules, packages, and cloud sync")
|
|
33
|
-
.version("5.8.
|
|
34
|
+
.version("5.8.6");
|
|
34
35
|
// Module management subcommand group
|
|
35
36
|
const modulesCmd = program
|
|
36
37
|
.command("modules")
|
|
@@ -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=
|
|
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
|