@indigoai-us/hq-cli 5.10.1 → 5.12.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 +48 -0
- package/dist/cli-version.d.ts +2 -0
- package/dist/cli-version.js +5 -0
- package/dist/commands/cloud.js +42 -3
- package/dist/commands/feedback.d.ts +16 -0
- package/dist/commands/feedback.js +98 -0
- package/dist/commands/secrets.d.ts +2 -2
- package/dist/commands/secrets.js +33 -25
- package/dist/index.js +12 -3
- package/dist/sentry.js +4 -2
- package/dist/utils/breadcrumb-buffer.d.ts +4 -0
- package/dist/utils/breadcrumb-buffer.js +18 -0
- package/dist/utils/feedback-diagnostics.d.ts +22 -0
- package/dist/utils/feedback-diagnostics.js +95 -0
- package/dist/utils/vault-api.d.ts +5 -0
- package/dist/utils/vault-api.js +55 -4
- package/package.json +2 -1
- package/src/cli-version.ts +1 -0
- package/src/commands/cloud.ts +40 -0
- package/src/commands/feedback.test.ts +369 -0
- package/src/commands/feedback.ts +136 -0
- package/src/commands/secrets.ts +86 -23
- package/src/index.ts +11 -1
- package/src/sentry.ts +2 -0
- package/src/utils/breadcrumb-buffer.ts +18 -0
- package/src/utils/feedback-diagnostics.test.ts +172 -0
- package/src/utils/feedback-diagnostics.ts +115 -0
- package/src/utils/vault-api.test.ts +147 -0
- package/src/utils/vault-api.ts +63 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,53 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [5.12.0] — 2026-05-08
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **`hq feedback` command group** — two subcommands `hq feedback bug` and
|
|
8
|
+
`hq feedback feature` let any logged-in user submit structured feedback without
|
|
9
|
+
leaving the terminal. Submissions are JWT-authenticated via the existing Cognito
|
|
10
|
+
session (no additional login step).
|
|
11
|
+
|
|
12
|
+
- **`--body-file <path>` flag** — pass a pre-written markdown file as the feedback
|
|
13
|
+
body. Multi-line input without shell quoting headaches. Use `--body-file -` to read
|
|
14
|
+
from stdin (useful for piped workflows). Body input is file-based by design (no
|
|
15
|
+
inline string flag).
|
|
16
|
+
|
|
17
|
+
- **`--company <slug>` flag (optional)** — attaches a company context to the submission.
|
|
18
|
+
Resolved automatically from `core/scripts/hq-session.sh get company_slug` inside the
|
|
19
|
+
`/feedback` skill; the flag is omitted when the slug is empty.
|
|
20
|
+
|
|
21
|
+
- **Diagnostics blob** — every submission includes a structured diagnostics payload
|
|
22
|
+
(`cliVersion`, `nodeVersion`, `os.platform`, `os.release`, `command` argv array,
|
|
23
|
+
`cwd`, `recentSentryBreadcrumbs`). Purely plain CLI/process context — no GHQ-OS-aware
|
|
24
|
+
identifiers.
|
|
25
|
+
|
|
26
|
+
- **Slack relay** — the vault-service backend best-effort posts a formatted card to
|
|
27
|
+
`#hq-feedback` after writing the DynamoDB row. Relay is fire-and-forget; Slack
|
|
28
|
+
failures do not affect the HTTP response.
|
|
29
|
+
|
|
30
|
+
### Example
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# Interactive: title prompted if not supplied
|
|
34
|
+
hq feedback bug --title "search broken on mobile"
|
|
35
|
+
|
|
36
|
+
# Scripted: body from a pre-written file
|
|
37
|
+
hq feedback feature --title "dark mode" --body-file /tmp/feature-body.md
|
|
38
|
+
|
|
39
|
+
# Piped: read body from stdin
|
|
40
|
+
echo "Steps to reproduce: ..." | hq feedback bug --title "crash on login" --body-file -
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## [5.11.0] — 2026-05-07
|
|
44
|
+
|
|
45
|
+
### Added
|
|
46
|
+
|
|
47
|
+
- **`--personal` flag for `hq secrets`** — pin a secret to the per-user store rather
|
|
48
|
+
than the company store. See commit `b5ec9bd` ("release: hq-cli@5.11.0 — --personal
|
|
49
|
+
flag for secrets") for details.
|
|
50
|
+
|
|
3
51
|
## [5.10.0] — 2026-05-04
|
|
4
52
|
|
|
5
53
|
### Added
|
|
@@ -0,0 +1,5 @@
|
|
|
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]="f65738b2-fa67-5b68-a0c8-c73fba12c6c5")}catch(e){}}();
|
|
3
|
+
export const CLI_VERSION = "5.12.0";
|
|
4
|
+
//# sourceMappingURL=cli-version.js.map
|
|
5
|
+
//# debugId=f65738b2-fa67-5b68-a0c8-c73fba12c6c5
|
package/dist/commands/cloud.js
CHANGED
|
@@ -13,11 +13,11 @@
|
|
|
13
13
|
* hq sync status — show local journal summary
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
!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]="
|
|
16
|
+
!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]="213f074e-13c2-5b95-9519-813adf9adfa9")}catch(e){}}();
|
|
17
17
|
import chalk from "chalk";
|
|
18
18
|
import * as fs from "fs";
|
|
19
19
|
import * as path from "path";
|
|
20
|
-
import { share, sync, readJournal, getJournalPath, } from "@indigoai-us/hq-cloud";
|
|
20
|
+
import { share, sync, readJournal, getJournalPath, loadCachedTokens, } from "@indigoai-us/hq-cloud";
|
|
21
21
|
import { DEFAULT_HQ_ROOT, ensureCognitoToken, buildVaultConfig, } from "../utils/cognito-session.js";
|
|
22
22
|
export function registerCloudCommands(program) {
|
|
23
23
|
program
|
|
@@ -85,6 +85,13 @@ export function registerCloudCommands(program) {
|
|
|
85
85
|
const onEvent = jsonMode
|
|
86
86
|
? (event) => emitJson(event)
|
|
87
87
|
: undefined;
|
|
88
|
+
// Stamp every uploaded object's S3 user metadata with the syncing
|
|
89
|
+
// user's Cognito identity (`Metadata['created-by']`). The hq-console
|
|
90
|
+
// vault UI's CREATED BY column reads this back via HEAD; without it,
|
|
91
|
+
// every row renders `—`. Resolved best-effort from the cached
|
|
92
|
+
// idToken — pre-vended `--creds-from-stdin` paths still get author
|
|
93
|
+
// attribution as long as the caller is logged in locally.
|
|
94
|
+
const author = resolveUploadAuthorFromCache();
|
|
88
95
|
const result = await share({
|
|
89
96
|
paths: targetPaths,
|
|
90
97
|
company: options.company,
|
|
@@ -94,6 +101,7 @@ export function registerCloudCommands(program) {
|
|
|
94
101
|
entityContext,
|
|
95
102
|
hqRoot: options.hqRoot,
|
|
96
103
|
onEvent,
|
|
104
|
+
...(author ? { author } : {}),
|
|
97
105
|
});
|
|
98
106
|
if (jsonMode) {
|
|
99
107
|
// Synthetic terminal event so subprocess consumers can read final
|
|
@@ -226,5 +234,36 @@ async function readAllStdin() {
|
|
|
226
234
|
}
|
|
227
235
|
return Buffer.concat(chunks).toString("utf8");
|
|
228
236
|
}
|
|
237
|
+
/**
|
|
238
|
+
* Resolve the syncing user's `UploadAuthor` (sub + email) from the cached
|
|
239
|
+
* Cognito idToken. Returns `undefined` when no tokens are cached or the
|
|
240
|
+
* token is missing the required claims — share() then skips the metadata
|
|
241
|
+
* stamp gracefully (not an error).
|
|
242
|
+
*
|
|
243
|
+
* We deliberately decode the JWT here instead of verifying it: Cognito
|
|
244
|
+
* already verified at issuance, and we only use the public claims to
|
|
245
|
+
* label the upload's S3 user metadata (no auth decision rides on it).
|
|
246
|
+
*/
|
|
247
|
+
function resolveUploadAuthorFromCache() {
|
|
248
|
+
const tokens = loadCachedTokens();
|
|
249
|
+
if (!tokens?.idToken)
|
|
250
|
+
return undefined;
|
|
251
|
+
const parts = tokens.idToken.split(".");
|
|
252
|
+
if (parts.length !== 3)
|
|
253
|
+
return undefined;
|
|
254
|
+
try {
|
|
255
|
+
const payload = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
256
|
+
const padded = payload + "=".repeat((4 - (payload.length % 4)) % 4);
|
|
257
|
+
const json = Buffer.from(padded, "base64").toString("utf-8");
|
|
258
|
+
const claims = JSON.parse(json);
|
|
259
|
+
if (claims.sub && claims.email) {
|
|
260
|
+
return { userSub: claims.sub, email: claims.email };
|
|
261
|
+
}
|
|
262
|
+
return undefined;
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
return undefined;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
229
268
|
//# sourceMappingURL=cloud.js.map
|
|
230
|
-
//# debugId=
|
|
269
|
+
//# debugId=213f074e-13c2-5b95-9519-813adf9adfa9
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
export declare const BODY_MAX_BYTES: number;
|
|
3
|
+
export interface FeedbackResult {
|
|
4
|
+
id: string;
|
|
5
|
+
}
|
|
6
|
+
export interface FeedbackSubmitOptions {
|
|
7
|
+
type: "bug" | "feature";
|
|
8
|
+
title: string;
|
|
9
|
+
body: string;
|
|
10
|
+
company?: string;
|
|
11
|
+
token: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function readBodyFile(bodyFile: string, stdin?: NodeJS.ReadableStream): Promise<string>;
|
|
14
|
+
export declare function submitFeedback(opts: FeedbackSubmitOptions): Promise<FeedbackResult>;
|
|
15
|
+
export declare function registerFeedbackCommand(program: Command): void;
|
|
16
|
+
//# sourceMappingURL=feedback.d.ts.map
|
|
@@ -0,0 +1,98 @@
|
|
|
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]="3e8d95ee-bfb4-5ca3-8177-7bc3ca618bd4")}catch(e){}}();
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
6
|
+
import { vaultApiFetch } from "../utils/vault-api.js";
|
|
7
|
+
import { collectDiagnostics } from "../utils/feedback-diagnostics.js";
|
|
8
|
+
export const BODY_MAX_BYTES = 64 * 1024;
|
|
9
|
+
export async function readBodyFile(bodyFile, stdin) {
|
|
10
|
+
if (bodyFile === "-") {
|
|
11
|
+
const stream = stdin ?? process.stdin;
|
|
12
|
+
if (stream.isTTY) {
|
|
13
|
+
throw new Error("--body-file - requires piped stdin (got interactive terminal).");
|
|
14
|
+
}
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
let data = "";
|
|
17
|
+
stream.setEncoding("utf8");
|
|
18
|
+
stream.on("data", (chunk) => {
|
|
19
|
+
data += chunk;
|
|
20
|
+
});
|
|
21
|
+
stream.on("end", () => resolve(data));
|
|
22
|
+
stream.on("error", reject);
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
return fs.promises.readFile(bodyFile, "utf-8");
|
|
26
|
+
}
|
|
27
|
+
export async function submitFeedback(opts) {
|
|
28
|
+
if (opts.body.trim().length === 0) {
|
|
29
|
+
throw new Error("body must not be empty. Provide at least one non-whitespace character.");
|
|
30
|
+
}
|
|
31
|
+
const bodyBytes = Buffer.byteLength(opts.body, "utf8");
|
|
32
|
+
if (bodyBytes > BODY_MAX_BYTES) {
|
|
33
|
+
throw new Error(`Body exceeds 64 KiB limit (${bodyBytes} bytes). Reduce the body size before submitting.`);
|
|
34
|
+
}
|
|
35
|
+
const diagnostics = collectDiagnostics();
|
|
36
|
+
const requestBody = {
|
|
37
|
+
type: opts.type,
|
|
38
|
+
title: opts.title,
|
|
39
|
+
body: opts.body,
|
|
40
|
+
diagnostics,
|
|
41
|
+
};
|
|
42
|
+
if (opts.company) {
|
|
43
|
+
requestBody.company = opts.company;
|
|
44
|
+
}
|
|
45
|
+
const res = await vaultApiFetch({
|
|
46
|
+
token: opts.token,
|
|
47
|
+
path: "/v1/feedback",
|
|
48
|
+
method: "POST",
|
|
49
|
+
body: requestBody,
|
|
50
|
+
});
|
|
51
|
+
if (!res.ok) {
|
|
52
|
+
const data = await res.json().catch(() => ({}));
|
|
53
|
+
const errMsg = data &&
|
|
54
|
+
typeof data === "object" &&
|
|
55
|
+
!Array.isArray(data) &&
|
|
56
|
+
typeof data.error === "string"
|
|
57
|
+
? data.error
|
|
58
|
+
: res.statusText;
|
|
59
|
+
throw new Error(`Failed to submit feedback: ${errMsg}`);
|
|
60
|
+
}
|
|
61
|
+
const data = (await res.json());
|
|
62
|
+
return { id: data.id };
|
|
63
|
+
}
|
|
64
|
+
function registerSubcommand(feedbackCmd, type) {
|
|
65
|
+
feedbackCmd
|
|
66
|
+
.command(type)
|
|
67
|
+
.description(type === "bug" ? "Report a bug" : "Request a feature")
|
|
68
|
+
.requiredOption("--title <text>", "Short title for the report")
|
|
69
|
+
.requiredOption("--body-file <path>", "Path to a markdown file with the body; use - to read from stdin")
|
|
70
|
+
.option("--company <slug>", "Company slug to associate with the report")
|
|
71
|
+
.action(async (opts) => {
|
|
72
|
+
try {
|
|
73
|
+
const token = await ensureCognitoToken({ interactive: false });
|
|
74
|
+
const body = await readBodyFile(opts.bodyFile);
|
|
75
|
+
const result = await submitFeedback({
|
|
76
|
+
type,
|
|
77
|
+
title: opts.title,
|
|
78
|
+
body,
|
|
79
|
+
company: opts.company,
|
|
80
|
+
token,
|
|
81
|
+
});
|
|
82
|
+
console.log(`Submitted: ${result.id}`);
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
export function registerFeedbackCommand(program) {
|
|
91
|
+
const feedbackCmd = program
|
|
92
|
+
.command("feedback")
|
|
93
|
+
.description("Submit a bug report or feature request to HQ");
|
|
94
|
+
registerSubcommand(feedbackCmd, "bug");
|
|
95
|
+
registerSubcommand(feedbackCmd, "feature");
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=feedback.js.map
|
|
98
|
+
//# debugId=3e8d95ee-bfb4-5ca3-8177-7bc3ca618bd4
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
-
import { vaultApiFetch, getCompanyUid } from "../utils/vault-api.js";
|
|
2
|
+
import { vaultApiFetch, getCompanyUid, getEntityUid } from "../utils/vault-api.js";
|
|
3
3
|
export type { VaultApiOptions } from "../utils/vault-api.js";
|
|
4
|
-
export { vaultApiFetch, getCompanyUid };
|
|
4
|
+
export { vaultApiFetch, getCompanyUid, getEntityUid };
|
|
5
5
|
export declare function registerSecretsCommand(program: Command): void;
|
|
6
6
|
//# sourceMappingURL=secrets.d.ts.map
|
package/dist/commands/secrets.js
CHANGED
|
@@ -1,13 +1,26 @@
|
|
|
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]="217ceadb-01cd-5778-9cc0-6f6d895d8d67")}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
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
|
+
import { vaultApiFetch, getCompanyUid, getEntityUid, } from "../utils/vault-api.js";
|
|
10
|
+
export { vaultApiFetch, getCompanyUid, getEntityUid };
|
|
11
|
+
function scopeOpts(opts) {
|
|
12
|
+
if (opts.personal && opts.company) {
|
|
13
|
+
console.error(chalk.red("Error: --personal cannot be combined with --company."));
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
return { personal: !!opts.personal, companySlug: opts.company };
|
|
17
|
+
}
|
|
18
|
+
function rejectIfPersonal(opts, action) {
|
|
19
|
+
if (opts.personal) {
|
|
20
|
+
console.error(chalk.red(`Error: ${action} is not supported with --personal.`));
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
11
24
|
function shellSingleQuote(value) {
|
|
12
25
|
return "'" + value.replace(/'/g, "'\\''") + "'";
|
|
13
26
|
}
|
|
@@ -115,7 +128,8 @@ export function registerSecretsCommand(program) {
|
|
|
115
128
|
const secrets = program
|
|
116
129
|
.command("secrets")
|
|
117
130
|
.description("Manage secrets in HQ vault (SSM Parameter Store)")
|
|
118
|
-
.option("--company <slug>", "Company slug (resolves to companyUid)")
|
|
131
|
+
.option("--company <slug>", "Company slug (resolves to companyUid)")
|
|
132
|
+
.option("--personal", "Operate on the caller's personal vault (no sharing)");
|
|
119
133
|
secrets
|
|
120
134
|
.command("set <name>")
|
|
121
135
|
.description("Create or update a secret")
|
|
@@ -150,8 +164,7 @@ export function registerSecretsCommand(program) {
|
|
|
150
164
|
process.exit(1);
|
|
151
165
|
}
|
|
152
166
|
const token = await ensureCognitoToken();
|
|
153
|
-
const
|
|
154
|
-
const companyUid = await getCompanyUid(token, companySlug);
|
|
167
|
+
const companyUid = await getEntityUid(token, scopeOpts(secrets.opts()));
|
|
155
168
|
const res = await vaultApiFetch({
|
|
156
169
|
token,
|
|
157
170
|
path: `/secrets/${encodeURIComponent(companyUid)}`,
|
|
@@ -178,8 +191,7 @@ export function registerSecretsCommand(program) {
|
|
|
178
191
|
.action(async (name, opts) => {
|
|
179
192
|
try {
|
|
180
193
|
const token = await ensureCognitoToken();
|
|
181
|
-
const
|
|
182
|
-
const companyUid = await getCompanyUid(token, companySlug);
|
|
194
|
+
const companyUid = await getEntityUid(token, scopeOpts(secrets.opts()));
|
|
183
195
|
const query = {};
|
|
184
196
|
if (opts.reveal) {
|
|
185
197
|
query.reveal = "true";
|
|
@@ -233,8 +245,7 @@ export function registerSecretsCommand(program) {
|
|
|
233
245
|
normalizedPrefix = normalized;
|
|
234
246
|
}
|
|
235
247
|
const token = await ensureCognitoToken();
|
|
236
|
-
const
|
|
237
|
-
const companyUid = await getCompanyUid(token, companySlug);
|
|
248
|
+
const companyUid = await getEntityUid(token, scopeOpts(secrets.opts()));
|
|
238
249
|
const query = {};
|
|
239
250
|
if (normalizedPrefix) {
|
|
240
251
|
query.prefix = normalizedPrefix;
|
|
@@ -298,8 +309,7 @@ export function registerSecretsCommand(program) {
|
|
|
298
309
|
}
|
|
299
310
|
}
|
|
300
311
|
const token = await ensureCognitoToken();
|
|
301
|
-
const
|
|
302
|
-
const companyUid = await getCompanyUid(token, companySlug);
|
|
312
|
+
const companyUid = await getEntityUid(token, scopeOpts(secrets.opts()));
|
|
303
313
|
const res = await vaultApiFetch({
|
|
304
314
|
token,
|
|
305
315
|
path: buildSecretNamePath(companyUid, name),
|
|
@@ -350,8 +360,7 @@ export function registerSecretsCommand(program) {
|
|
|
350
360
|
}
|
|
351
361
|
}
|
|
352
362
|
const token = await ensureCognitoToken();
|
|
353
|
-
const
|
|
354
|
-
const companyUid = await getCompanyUid(token, companySlug);
|
|
363
|
+
const companyUid = await getEntityUid(token, scopeOpts(secrets.opts()));
|
|
355
364
|
const revealed = await Promise.all(keys.map(async (key) => {
|
|
356
365
|
const cached = readCache(companyUid, key);
|
|
357
366
|
if (cached !== null) {
|
|
@@ -420,8 +429,7 @@ export function registerSecretsCommand(program) {
|
|
|
420
429
|
}
|
|
421
430
|
}
|
|
422
431
|
const token = await ensureCognitoToken();
|
|
423
|
-
const
|
|
424
|
-
const companyUid = await getCompanyUid(token, companySlug);
|
|
432
|
+
const companyUid = await getEntityUid(token, scopeOpts(secrets.opts()));
|
|
425
433
|
const revealed = await Promise.all(keys.map(async (key) => {
|
|
426
434
|
const cached = readCache(companyUid, key);
|
|
427
435
|
if (cached !== null) {
|
|
@@ -459,6 +467,7 @@ export function registerSecretsCommand(program) {
|
|
|
459
467
|
.option("--expires <duration>", "Token expiry duration (e.g. 24h, 2d, 30m)", "24h")
|
|
460
468
|
.action(async (name, opts) => {
|
|
461
469
|
try {
|
|
470
|
+
rejectIfPersonal(secrets.opts(), "generate-link");
|
|
462
471
|
if (!SECRET_NAME_PATTERN.test(name)) {
|
|
463
472
|
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)`));
|
|
464
473
|
process.exit(1);
|
|
@@ -474,8 +483,7 @@ export function registerSecretsCommand(program) {
|
|
|
474
483
|
process.exit(1);
|
|
475
484
|
}
|
|
476
485
|
const token = await ensureCognitoToken();
|
|
477
|
-
const
|
|
478
|
-
const companyUid = await getCompanyUid(token, companySlug);
|
|
486
|
+
const companyUid = await getEntityUid(token, scopeOpts(secrets.opts()));
|
|
479
487
|
const res = await vaultApiFetch({
|
|
480
488
|
token,
|
|
481
489
|
path: buildSecretNamePath(companyUid, name),
|
|
@@ -507,6 +515,7 @@ export function registerSecretsCommand(program) {
|
|
|
507
515
|
.requiredOption("--permission <level>", "Permission level: read | write | admin")
|
|
508
516
|
.action(async (path, opts) => {
|
|
509
517
|
try {
|
|
518
|
+
rejectIfPersonal(secrets.opts(), "share");
|
|
510
519
|
if (!SECRET_NAME_PATTERN.test(path)) {
|
|
511
520
|
console.error(chalk.red(`Invalid secret path '${path}': must match ^[A-Z][A-Z0-9_]*(/[A-Z][A-Z0-9_]+)*$ (e.g. MY_KEY or PROD/DB_PASSWORD)`));
|
|
512
521
|
process.exit(1);
|
|
@@ -523,8 +532,7 @@ export function registerSecretsCommand(program) {
|
|
|
523
532
|
const granteeType = isEmail ? "email" : "group";
|
|
524
533
|
const granteeId = opts.with;
|
|
525
534
|
const token = await ensureCognitoToken();
|
|
526
|
-
const
|
|
527
|
-
const companyUid = await getCompanyUid(token, companySlug);
|
|
535
|
+
const companyUid = await getEntityUid(token, scopeOpts(secrets.opts()));
|
|
528
536
|
const res = await vaultApiFetch({
|
|
529
537
|
token,
|
|
530
538
|
path: `/secrets/${encodeURIComponent(companyUid)}/acl/grant`,
|
|
@@ -566,6 +574,7 @@ export function registerSecretsCommand(program) {
|
|
|
566
574
|
.requiredOption("--from <principal>", "Email address or group id to remove")
|
|
567
575
|
.action(async (path, opts) => {
|
|
568
576
|
try {
|
|
577
|
+
rejectIfPersonal(secrets.opts(), "unshare");
|
|
569
578
|
if (!SECRET_NAME_PATTERN.test(path)) {
|
|
570
579
|
console.error(chalk.red(`Invalid secret path '${path}': must match ^[A-Z][A-Z0-9_]*(/[A-Z][A-Z0-9_]+)*$ (e.g. MY_KEY or PROD/DB_PASSWORD)`));
|
|
571
580
|
process.exit(1);
|
|
@@ -578,8 +587,7 @@ export function registerSecretsCommand(program) {
|
|
|
578
587
|
const granteeType = isEmailFrom ? "email" : "group";
|
|
579
588
|
const granteeId = opts.from;
|
|
580
589
|
const token = await ensureCognitoToken();
|
|
581
|
-
const
|
|
582
|
-
const companyUid = await getCompanyUid(token, companySlug);
|
|
590
|
+
const companyUid = await getEntityUid(token, scopeOpts(secrets.opts()));
|
|
583
591
|
const res = await vaultApiFetch({
|
|
584
592
|
token,
|
|
585
593
|
path: `/secrets/${encodeURIComponent(companyUid)}/acl/revoke`,
|
|
@@ -618,13 +626,13 @@ export function registerSecretsCommand(program) {
|
|
|
618
626
|
.description("Show the ACL (access control list) for a secret path")
|
|
619
627
|
.action(async (path) => {
|
|
620
628
|
try {
|
|
629
|
+
rejectIfPersonal(secrets.opts(), "acl");
|
|
621
630
|
if (!SECRET_NAME_PATTERN.test(path)) {
|
|
622
631
|
console.error(chalk.red(`Invalid secret path '${path}': must match ^[A-Z][A-Z0-9_]*(/[A-Z][A-Z0-9_]+)*$ (e.g. MY_KEY or PROD/DB_PASSWORD)`));
|
|
623
632
|
process.exit(1);
|
|
624
633
|
}
|
|
625
634
|
const token = await ensureCognitoToken();
|
|
626
|
-
const
|
|
627
|
-
const companyUid = await getCompanyUid(token, companySlug);
|
|
635
|
+
const companyUid = await getEntityUid(token, scopeOpts(secrets.opts()));
|
|
628
636
|
const secretPath = path;
|
|
629
637
|
const res = await vaultApiFetch({
|
|
630
638
|
token,
|
|
@@ -705,4 +713,4 @@ export function registerSecretsCommand(program) {
|
|
|
705
713
|
});
|
|
706
714
|
}
|
|
707
715
|
//# sourceMappingURL=secrets.js.map
|
|
708
|
-
//# debugId=
|
|
716
|
+
//# debugId=217ceadb-01cd-5778-9cc0-6f6d895d8d67
|
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]="65033669-3ba8-5aac-8a98-49827f627ff9")}catch(e){}}();
|
|
7
7
|
import { Command } from "commander";
|
|
8
8
|
import { initSentry, Sentry } from "./sentry.js";
|
|
9
9
|
import { registerAddCommand } from "./commands/add.js";
|
|
@@ -28,12 +28,14 @@ import { registerRunCommand } from "./commands/run.js";
|
|
|
28
28
|
import { registerGroupsCommand } from "./commands/groups.js";
|
|
29
29
|
import { registerFilesCommand } from "./commands/files.js";
|
|
30
30
|
import { registerMembersCommand } from "./commands/members.js";
|
|
31
|
+
import { registerFeedbackCommand } from "./commands/feedback.js";
|
|
32
|
+
import { sanitizeArgv } from "./utils/feedback-diagnostics.js";
|
|
31
33
|
initSentry();
|
|
32
34
|
const program = new Command();
|
|
33
35
|
program
|
|
34
36
|
.name("hq")
|
|
35
37
|
.description("HQ management CLI — modules, packages, and cloud sync")
|
|
36
|
-
.version("5.
|
|
38
|
+
.version("5.12.0");
|
|
37
39
|
// Module management subcommand group
|
|
38
40
|
const modulesCmd = program
|
|
39
41
|
.command("modules")
|
|
@@ -86,8 +88,15 @@ registerFilesCommand(program);
|
|
|
86
88
|
registerMembersCommand(program);
|
|
87
89
|
// Onboarding (top-level — Cognito + vault-service provisioning)
|
|
88
90
|
registerOnboardCommand(program);
|
|
91
|
+
// Feedback (subcommand group — hq feedback bug|feature)
|
|
92
|
+
registerFeedbackCommand(program);
|
|
89
93
|
(async () => {
|
|
90
94
|
try {
|
|
95
|
+
Sentry.addBreadcrumb({
|
|
96
|
+
category: "command",
|
|
97
|
+
message: sanitizeArgv(process.argv.slice(2)).join(" "),
|
|
98
|
+
level: "info",
|
|
99
|
+
});
|
|
91
100
|
await program.parseAsync();
|
|
92
101
|
}
|
|
93
102
|
catch (err) {
|
|
@@ -99,4 +108,4 @@ registerOnboardCommand(program);
|
|
|
99
108
|
}
|
|
100
109
|
})();
|
|
101
110
|
//# sourceMappingURL=index.js.map
|
|
102
|
-
//# debugId=
|
|
111
|
+
//# debugId=65033669-3ba8-5aac-8a98-49827f627ff9
|
package/dist/sentry.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
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]="508ac88d-5683-5d1f-9857-80a8a74e03e2")}catch(e){}}();
|
|
3
3
|
import * as Sentry from "@sentry/node";
|
|
4
4
|
import { BUNDLED_DSN } from "./sentry-dsn.generated.js";
|
|
5
5
|
import { beforeSend } from "./sentry-before-send.js";
|
|
6
|
+
import { beforeBreadcrumb } from "./utils/breadcrumb-buffer.js";
|
|
6
7
|
export function initSentry() {
|
|
7
8
|
const dsn = BUNDLED_DSN || process.env.SENTRY_DSN;
|
|
8
9
|
if (!dsn)
|
|
@@ -15,8 +16,9 @@ export function initSentry() {
|
|
|
15
16
|
tags: { repo: "hq-cli" },
|
|
16
17
|
},
|
|
17
18
|
beforeSend,
|
|
19
|
+
beforeBreadcrumb,
|
|
18
20
|
});
|
|
19
21
|
}
|
|
20
22
|
export { Sentry };
|
|
21
23
|
//# sourceMappingURL=sentry.js.map
|
|
22
|
-
//# debugId=
|
|
24
|
+
//# debugId=508ac88d-5683-5d1f-9857-80a8a74e03e2
|
|
@@ -0,0 +1,18 @@
|
|
|
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]="6a54e8f0-10de-59c2-82d9-22de94d9e6f2")}catch(e){}}();
|
|
3
|
+
const BUFFER_SIZE = 20;
|
|
4
|
+
const _buffer = [];
|
|
5
|
+
// Sentry beforeBreadcrumb hook: records every breadcrumb in a ring buffer
|
|
6
|
+
// and returns it unchanged so Sentry still processes it normally.
|
|
7
|
+
export function beforeBreadcrumb(breadcrumb) {
|
|
8
|
+
_buffer.push(breadcrumb);
|
|
9
|
+
if (_buffer.length > BUFFER_SIZE) {
|
|
10
|
+
_buffer.shift();
|
|
11
|
+
}
|
|
12
|
+
return breadcrumb;
|
|
13
|
+
}
|
|
14
|
+
export function getRecentBreadcrumbs() {
|
|
15
|
+
return [..._buffer];
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=breadcrumb-buffer.js.map
|
|
18
|
+
//# debugId=6a54e8f0-10de-59c2-82d9-22de94d9e6f2
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface GitContext {
|
|
2
|
+
branch: string | null;
|
|
3
|
+
head: string | null;
|
|
4
|
+
dirty: boolean;
|
|
5
|
+
remoteUrl: string | null;
|
|
6
|
+
}
|
|
7
|
+
export interface DiagnosticsBlob {
|
|
8
|
+
cliVersion: string;
|
|
9
|
+
nodeVersion: string;
|
|
10
|
+
os: {
|
|
11
|
+
platform: string;
|
|
12
|
+
release: string;
|
|
13
|
+
arch: string;
|
|
14
|
+
};
|
|
15
|
+
command: string[];
|
|
16
|
+
cwd: string;
|
|
17
|
+
git: GitContext;
|
|
18
|
+
recentSentryBreadcrumbs: unknown[];
|
|
19
|
+
}
|
|
20
|
+
export declare function sanitizeArgv(argv: string[]): string[];
|
|
21
|
+
export declare function collectDiagnostics(): DiagnosticsBlob;
|
|
22
|
+
//# sourceMappingURL=feedback-diagnostics.d.ts.map
|
|
@@ -0,0 +1,95 @@
|
|
|
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]="ee39c63b-ddaa-553d-b5db-77633d323d88")}catch(e){}}();
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
import { execFileSync } from "child_process";
|
|
5
|
+
import { getRecentBreadcrumbs } from "./breadcrumb-buffer.js";
|
|
6
|
+
import { CLI_VERSION } from "../cli-version.js";
|
|
7
|
+
const SECRET_FLAGS = new Set([
|
|
8
|
+
"--token",
|
|
9
|
+
"--secret",
|
|
10
|
+
"--password",
|
|
11
|
+
"--key",
|
|
12
|
+
"--api-key",
|
|
13
|
+
"--access-token",
|
|
14
|
+
"--auth-token",
|
|
15
|
+
]);
|
|
16
|
+
export function sanitizeArgv(argv) {
|
|
17
|
+
const result = [];
|
|
18
|
+
for (let i = 0; i < argv.length; i++) {
|
|
19
|
+
const arg = argv[i];
|
|
20
|
+
if (arg.startsWith("--") && arg.includes("=")) {
|
|
21
|
+
const eqIdx = arg.indexOf("=");
|
|
22
|
+
const flag = arg.slice(0, eqIdx);
|
|
23
|
+
if (SECRET_FLAGS.has(flag)) {
|
|
24
|
+
result.push(`${flag}=***`);
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
result.push(arg);
|
|
29
|
+
if (SECRET_FLAGS.has(arg) && i + 1 < argv.length) {
|
|
30
|
+
result.push("***");
|
|
31
|
+
i++;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
function sanitizeRemoteUrl(url) {
|
|
37
|
+
return url.replace(/https?:\/\/[^@]+@/, "https://***@");
|
|
38
|
+
}
|
|
39
|
+
function runGit(args) {
|
|
40
|
+
return execFileSync("git", args, {
|
|
41
|
+
encoding: "utf-8",
|
|
42
|
+
timeout: 2000,
|
|
43
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
44
|
+
}).trim();
|
|
45
|
+
}
|
|
46
|
+
function collectGitContext() {
|
|
47
|
+
let branch = null;
|
|
48
|
+
let head = null;
|
|
49
|
+
let dirty = false;
|
|
50
|
+
let remoteUrl = null;
|
|
51
|
+
try {
|
|
52
|
+
branch = runGit(["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return { branch: null, head: null, dirty: false, remoteUrl: null };
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
head = runGit(["rev-parse", "--short", "HEAD"]);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// best-effort
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
const statusOut = runGit(["status", "--porcelain"]);
|
|
65
|
+
dirty = statusOut.length > 0;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// best-effort
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
const raw = runGit(["remote", "get-url", "origin"]);
|
|
72
|
+
remoteUrl = sanitizeRemoteUrl(raw);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// no origin remote
|
|
76
|
+
}
|
|
77
|
+
return { branch, head, dirty, remoteUrl };
|
|
78
|
+
}
|
|
79
|
+
export function collectDiagnostics() {
|
|
80
|
+
return {
|
|
81
|
+
cliVersion: CLI_VERSION,
|
|
82
|
+
nodeVersion: process.version,
|
|
83
|
+
os: {
|
|
84
|
+
platform: os.platform(),
|
|
85
|
+
release: os.release(),
|
|
86
|
+
arch: os.arch(),
|
|
87
|
+
},
|
|
88
|
+
command: sanitizeArgv(process.argv.slice(2)),
|
|
89
|
+
cwd: process.cwd(),
|
|
90
|
+
git: collectGitContext(),
|
|
91
|
+
recentSentryBreadcrumbs: getRecentBreadcrumbs(),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=feedback-diagnostics.js.map
|
|
95
|
+
//# debugId=ee39c63b-ddaa-553d-b5db-77633d323d88
|
|
@@ -7,4 +7,9 @@ export interface VaultApiOptions {
|
|
|
7
7
|
}
|
|
8
8
|
export declare function vaultApiFetch(opts: VaultApiOptions): Promise<Response>;
|
|
9
9
|
export declare function getCompanyUid(token: string, companySlug: string | undefined): Promise<string>;
|
|
10
|
+
export declare function resolveCallerPersonUid(token: string): Promise<string>;
|
|
11
|
+
export declare function getEntityUid(token: string, opts: {
|
|
12
|
+
personal?: boolean;
|
|
13
|
+
companySlug?: string;
|
|
14
|
+
}): Promise<string>;
|
|
10
15
|
//# sourceMappingURL=vault-api.d.ts.map
|