@indigoai-us/hq-cli 5.47.16 → 5.48.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/.github/workflows/ci.yml +16 -20
- package/.github/workflows/publish.yml +26 -101
- package/dist/commands/feedback.d.ts +2 -0
- package/dist/commands/feedback.js +24 -2
- package/dist/commands/files.d.ts +19 -0
- package/dist/commands/files.js +37 -3
- package/dist/commands/meetings.js +50 -2
- package/dist/commands/secrets.js +25 -2
- package/dist/run/hq-plugin.js +9 -2
- package/dist/sentry-dsn.generated.d.ts +1 -1
- package/dist/sentry-dsn.generated.js +1 -1
- package/dist/sentry.js +7 -3
- package/dist/utils/feedback-diagnostics.d.ts +7 -0
- package/dist/utils/feedback-diagnostics.js +4 -2
- package/dist/utils/feedback-screenshots.d.ts +23 -0
- package/dist/utils/feedback-screenshots.js +98 -0
- package/dist/utils/feedback-versions.d.ts +34 -0
- package/dist/utils/feedback-versions.js +50 -0
- package/package.json +4 -2
- package/src/commands/feedback.test.ts +44 -0
- package/src/commands/feedback.ts +46 -13
- package/src/commands/files-delete.test.ts +132 -0
- package/src/commands/files.ts +42 -1
- package/src/commands/meetings.test.ts +163 -0
- package/src/commands/meetings.ts +60 -0
- package/src/commands/secrets.test.ts +80 -0
- package/src/commands/secrets.ts +35 -0
- package/src/run/hq-plugin.test.ts +39 -0
- package/src/run/hq-plugin.ts +7 -0
- package/src/sentry.ts +5 -1
- package/src/utils/feedback-diagnostics.test.ts +11 -0
- package/src/utils/feedback-diagnostics.ts +8 -0
- package/src/utils/feedback-screenshots.test.ts +134 -0
- package/src/utils/feedback-screenshots.ts +124 -0
- package/src/utils/feedback-versions.test.ts +98 -0
- package/src/utils/feedback-versions.ts +68 -0
- package/test/helpers/vault-service-mock.ts +6 -2
package/.github/workflows/ci.yml
CHANGED
|
@@ -5,29 +5,25 @@ on:
|
|
|
5
5
|
push:
|
|
6
6
|
branches: [main]
|
|
7
7
|
|
|
8
|
+
concurrency:
|
|
9
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
10
|
+
cancel-in-progress: true
|
|
11
|
+
|
|
8
12
|
jobs:
|
|
9
13
|
build:
|
|
10
|
-
|
|
14
|
+
name: build (node ${{ matrix.node-version }})
|
|
11
15
|
strategy:
|
|
12
16
|
fail-fast: false
|
|
13
17
|
matrix:
|
|
14
18
|
# 20 is the declared engines floor (package.json engines.node >=20),
|
|
15
|
-
# 22 is the current dev/publish baseline.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
-
|
|
25
|
-
|
|
26
|
-
node-version: ${{ matrix.node-version }}
|
|
27
|
-
cache: pnpm
|
|
28
|
-
- run: pnpm install --frozen-lockfile --config.minimumReleaseAge=1440
|
|
29
|
-
# generate-dsn.mjs only fatals when GITHUB_JOB=publish, so CI builds with
|
|
30
|
-
# an empty BUNDLED_DSN. That's intentional — the DSN is publish-only.
|
|
31
|
-
- run: pnpm run build
|
|
32
|
-
- run: pnpm run typecheck
|
|
33
|
-
- run: pnpm test
|
|
19
|
+
# 22 is the current dev/publish baseline.
|
|
20
|
+
node-version: ["20", "22"]
|
|
21
|
+
uses: indigoai-us/.github/.github/workflows/_node-ci.yml@main
|
|
22
|
+
with:
|
|
23
|
+
package-manager: pnpm
|
|
24
|
+
pnpm-version: "10"
|
|
25
|
+
node-version: ${{ matrix.node-version }}
|
|
26
|
+
run-build: true
|
|
27
|
+
run-lint: false
|
|
28
|
+
run-coverage: true
|
|
29
|
+
secrets: inherit
|
|
@@ -2,110 +2,35 @@ name: Publish to npm
|
|
|
2
2
|
on:
|
|
3
3
|
push:
|
|
4
4
|
tags: ["v*"]
|
|
5
|
+
# Manual dry-run: validate the build/pack/smoke pipeline without publishing.
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
inputs:
|
|
8
|
+
dry_run:
|
|
9
|
+
description: "Dry-run (validate build/pack/smoke; npm publish --dry-run, no Sentry upload)"
|
|
10
|
+
type: boolean
|
|
11
|
+
default: true
|
|
5
12
|
|
|
6
13
|
jobs:
|
|
7
14
|
publish:
|
|
8
|
-
runs-on: ubuntu-latest
|
|
9
15
|
permissions:
|
|
10
16
|
contents: read
|
|
11
17
|
id-token: write
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
-
|
|
31
|
-
run: pnpm run build
|
|
32
|
-
env:
|
|
33
|
-
# generate-dsn.mjs fatals when GITHUB_JOB=publish and this var is
|
|
34
|
-
# missing. Set this as a repo secret to bundle a Sentry DSN into the
|
|
35
|
-
# published binary.
|
|
36
|
-
HQ_CLI_PUBLISH_SENTRY_DSN: ${{ secrets.HQ_CLI_PUBLISH_SENTRY_DSN }}
|
|
37
|
-
|
|
38
|
-
# Inject Sentry debug IDs into compiled JS + map files. Must run after
|
|
39
|
-
# tsc and BEFORE publish so the installed binary carries the same debugId
|
|
40
|
-
# as the artifacts uploaded to Sentry, enabling debugId-based source-map
|
|
41
|
-
# resolution for globally-installed CLI users.
|
|
42
|
-
- name: Inject Sentry source map debug IDs
|
|
43
|
-
run: npx -y @sentry/cli@^2 sourcemaps inject dist/
|
|
44
|
-
|
|
45
|
-
# Pre-publish cross-package smoke test (Bug A guard).
|
|
46
|
-
#
|
|
47
|
-
# @indigoai-us/hq-cli depends on @indigoai-us/hq-cloud via a caret range
|
|
48
|
-
# (^5.1.0). The smoke installs a freshly-packed tarball into an isolated
|
|
49
|
-
# tmpdir — outside this repo — so transitive @indigoai-us/* deps resolve
|
|
50
|
-
# from the npm registry, NOT from a local checkout. Booting `hq --version`
|
|
51
|
-
# then exercises the full module-load graph. If hq-cloud removed or
|
|
52
|
-
# renamed a symbol hq-cli imports, this catches it BEFORE the publish PUT.
|
|
53
|
-
#
|
|
54
|
-
# Bypass: set repo-or-workflow variable WORKFLOW_ALLOW_BROKEN_PUBLISH=1.
|
|
55
|
-
- name: Pre-publish smoke test
|
|
56
|
-
if: ${{ vars.WORKFLOW_ALLOW_BROKEN_PUBLISH != '1' }}
|
|
57
|
-
run: bash .github/workflows/scripts/smoke-test-pkg.sh hq
|
|
58
|
-
|
|
59
|
-
- name: WARN — pre-publish smoke bypassed
|
|
60
|
-
if: ${{ vars.WORKFLOW_ALLOW_BROKEN_PUBLISH == '1' }}
|
|
61
|
-
run: |
|
|
62
|
-
echo "::warning::WORKFLOW_ALLOW_BROKEN_PUBLISH=1 — pre-publish cross-package smoke test was BYPASSED. This release may ship a DOA package."
|
|
63
|
-
|
|
64
|
-
# Trusted-publisher OIDC; no NODE_AUTH_TOKEN. No --provenance (private
|
|
65
|
-
# repo limitation since npm 2026-05-10). Pre-release versions (with '-')
|
|
66
|
-
# get --tag rc to keep latest dist-tag pointing at stable releases only.
|
|
67
|
-
- name: Publish to npm
|
|
68
|
-
id: publish
|
|
69
|
-
run: |
|
|
70
|
-
NAME=$(jq -r .name package.json)
|
|
71
|
-
VER=$(jq -r .version package.json)
|
|
72
|
-
if npm view "$NAME@$VER" version >/dev/null 2>&1; then
|
|
73
|
-
echo "$NAME@$VER already on npm — skipping"
|
|
74
|
-
else
|
|
75
|
-
if echo "$VER" | grep -q '-'; then
|
|
76
|
-
npm publish --access public --tag rc
|
|
77
|
-
else
|
|
78
|
-
npm publish --access public
|
|
79
|
-
fi
|
|
80
|
-
echo "published=true" >> "$GITHUB_OUTPUT"
|
|
81
|
-
fi
|
|
82
|
-
|
|
83
|
-
# Sentry sourcemap upload runs on every tag push regardless of whether
|
|
84
|
-
# this run was the one that actually published to npm. The previous
|
|
85
|
-
# `if: steps.publish.outputs.published == 'true'` gate made re-runs
|
|
86
|
-
# useless when only the Sentry upload failed — the publish step's
|
|
87
|
-
# short-circuit (`npm view @ver already published`) zeroed out the
|
|
88
|
-
# output, so the upload step was skipped on every re-run. dist/ is
|
|
89
|
-
# rebuilt by the Build + Inject steps each run, so the upload is
|
|
90
|
-
# always operating on fresh, debug-id-stamped artifacts.
|
|
91
|
-
- name: Upload sourcemaps to Sentry
|
|
92
|
-
run: |
|
|
93
|
-
# Defang paste-time whitespace in SENTRY_AUTH_TOKEN. Sentry's
|
|
94
|
-
# auth-header parser returns HTTP 401 "Token string should not
|
|
95
|
-
# contain spaces" when the secret has a trailing \n or any
|
|
96
|
-
# embedded whitespace — observed on the v5.25.0 release after a
|
|
97
|
-
# copy-paste into the GH repo secret carried a literal \n. `tr -d`
|
|
98
|
-
# is the cheap structural guard so the workflow doesn't blow up
|
|
99
|
-
# on the next dirty paste.
|
|
100
|
-
SENTRY_AUTH_TOKEN=$(printf %s "$SENTRY_AUTH_TOKEN" | tr -d '[:space:]')
|
|
101
|
-
if [ -z "$SENTRY_AUTH_TOKEN" ]; then
|
|
102
|
-
echo "::error::SENTRY_AUTH_TOKEN secret is empty (or all whitespace) after sanitization — cannot upload sourcemaps"
|
|
103
|
-
exit 1
|
|
104
|
-
fi
|
|
105
|
-
export SENTRY_AUTH_TOKEN
|
|
106
|
-
VER=$(jq -r .version package.json)
|
|
107
|
-
npx -y @sentry/cli@^2 sourcemaps upload --release "hq-cli@$VER" dist/
|
|
108
|
-
env:
|
|
109
|
-
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
|
110
|
-
SENTRY_ORG: indigo-d0
|
|
111
|
-
SENTRY_PROJECT: hq
|
|
18
|
+
uses: indigoai-us/.github/.github/workflows/_npm-publish.yml@main
|
|
19
|
+
with:
|
|
20
|
+
package-manager: pnpm
|
|
21
|
+
pnpm-version: "10"
|
|
22
|
+
node-version: "24"
|
|
23
|
+
build-command: "pnpm run build"
|
|
24
|
+
dsn-env-name: "HQ_CLI_PUBLISH_SENTRY_DSN"
|
|
25
|
+
sentry-inject-dir: "dist/"
|
|
26
|
+
smoke-test-arg: "hq"
|
|
27
|
+
sentry-release-prefix: "hq-cli"
|
|
28
|
+
sentry-upload-paths: "dist/ node_modules/@indigoai-us/hq-cloud/dist/"
|
|
29
|
+
sentry-org: "indigo-d0"
|
|
30
|
+
sentry-project: "hq-cli"
|
|
31
|
+
# Prerelease versions (e.g. 5.x.y-rc.0) ship to the rc dist-tag, not latest.
|
|
32
|
+
prerelease-dist-tag: "rc"
|
|
33
|
+
dry-run: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run }}
|
|
34
|
+
secrets:
|
|
35
|
+
sentry-dsn: ${{ secrets.HQ_CLI_PUBLISH_SENTRY_DSN }}
|
|
36
|
+
sentry-auth-token: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
|
@@ -9,6 +9,8 @@ export interface FeedbackSubmitOptions {
|
|
|
9
9
|
body: string;
|
|
10
10
|
company?: string;
|
|
11
11
|
token: string;
|
|
12
|
+
/** S3 object keys of already-uploaded screenshots (see uploadScreenshots). */
|
|
13
|
+
screenshots?: string[];
|
|
12
14
|
}
|
|
13
15
|
export declare function readBodyFile(bodyFile: string, stdin?: NodeJS.ReadableStream): Promise<string>;
|
|
14
16
|
export declare function submitFeedback(opts: FeedbackSubmitOptions): Promise<FeedbackResult>;
|
|
@@ -1,10 +1,11 @@
|
|
|
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]="ee78c7b6-6e93-5217-8a1d-ddc51afcf9c3")}catch(e){}}();
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
6
6
|
import { vaultApiFetch } from "../utils/vault-api.js";
|
|
7
7
|
import { collectDiagnostics } from "../utils/feedback-diagnostics.js";
|
|
8
|
+
import { MAX_SCREENSHOTS, uploadScreenshots } from "../utils/feedback-screenshots.js";
|
|
8
9
|
export const BODY_MAX_BYTES = 64 * 1024;
|
|
9
10
|
export async function readBodyFile(bodyFile, stdin) {
|
|
10
11
|
if (bodyFile === "-") {
|
|
@@ -25,6 +26,16 @@ export async function readBodyFile(bodyFile, stdin) {
|
|
|
25
26
|
return fs.promises.readFile(bodyFile, "utf-8");
|
|
26
27
|
}
|
|
27
28
|
export async function submitFeedback(opts) {
|
|
29
|
+
// Validate the title locally, symmetric with the body check below. Commander's
|
|
30
|
+
// `requiredOption("--title")` only requires the flag to be PRESENT — an empty
|
|
31
|
+
// or whitespace-only value (`--title ""`, or a title the /hq-bug skill derived
|
|
32
|
+
// to nothing) passes the flag check, then the server rejects it with a 400
|
|
33
|
+
// "title (non-empty string) is required" that floods Sentry as a context-free
|
|
34
|
+
// warning (HQ-AB). Catch it here so the caller gets a clear, actionable error
|
|
35
|
+
// and the bad request never reaches the server.
|
|
36
|
+
if (opts.title.trim().length === 0) {
|
|
37
|
+
throw new Error("title must not be empty. Provide a short, non-whitespace title via --title.");
|
|
38
|
+
}
|
|
28
39
|
if (opts.body.trim().length === 0) {
|
|
29
40
|
throw new Error("body must not be empty. Provide at least one non-whitespace character.");
|
|
30
41
|
}
|
|
@@ -42,6 +53,9 @@ export async function submitFeedback(opts) {
|
|
|
42
53
|
if (opts.company) {
|
|
43
54
|
requestBody.company = opts.company;
|
|
44
55
|
}
|
|
56
|
+
if (opts.screenshots && opts.screenshots.length > 0) {
|
|
57
|
+
requestBody.screenshots = opts.screenshots;
|
|
58
|
+
}
|
|
45
59
|
const res = await vaultApiFetch({
|
|
46
60
|
token: opts.token,
|
|
47
61
|
path: "/v1/feedback",
|
|
@@ -68,16 +82,24 @@ function registerSubcommand(feedbackCmd, type) {
|
|
|
68
82
|
.requiredOption("--title <text>", "Short title for the report")
|
|
69
83
|
.requiredOption("--body-file <path>", "Path to a markdown file with the body; use - to read from stdin")
|
|
70
84
|
.option("--company <slug>", "Company slug to associate with the report")
|
|
85
|
+
.option("--screenshot <path>", `Attach a screenshot (repeatable, up to ${MAX_SCREENSHOTS}; .png/.jpg/.jpeg/.webp/.gif)`, (value, prev) => [...prev, value], [])
|
|
71
86
|
.action(async (opts) => {
|
|
72
87
|
try {
|
|
73
88
|
const token = await ensureCognitoToken({ interactive: false });
|
|
74
89
|
const body = await readBodyFile(opts.bodyFile);
|
|
90
|
+
// Validate + upload screenshots (direct-to-S3 via presigned PUT)
|
|
91
|
+
// before submitting, so the row references uploaded objects.
|
|
92
|
+
const screenshots = await uploadScreenshots({
|
|
93
|
+
paths: opts.screenshot ?? [],
|
|
94
|
+
token,
|
|
95
|
+
});
|
|
75
96
|
const result = await submitFeedback({
|
|
76
97
|
type,
|
|
77
98
|
title: opts.title,
|
|
78
99
|
body,
|
|
79
100
|
company: opts.company,
|
|
80
101
|
token,
|
|
102
|
+
screenshots,
|
|
81
103
|
});
|
|
82
104
|
console.log(`Submitted: ${result.id}`);
|
|
83
105
|
}
|
|
@@ -95,4 +117,4 @@ export function registerFeedbackCommand(program) {
|
|
|
95
117
|
registerSubcommand(feedbackCmd, "feature");
|
|
96
118
|
}
|
|
97
119
|
//# sourceMappingURL=feedback.js.map
|
|
98
|
-
//# debugId=
|
|
120
|
+
//# debugId=ee78c7b6-6e93-5217-8a1d-ddc51afcf9c3
|
package/dist/commands/files.d.ts
CHANGED
|
@@ -68,6 +68,25 @@ interface RunFilesDeleteParams {
|
|
|
68
68
|
yes: boolean;
|
|
69
69
|
companySlug: string | undefined;
|
|
70
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* The vault bucket is already company-scoped, so a delete prefix must be
|
|
73
|
+
* BUCKET-RELATIVE (e.g. `projects/foo/*`). A caller who pastes an HQ *local*
|
|
74
|
+
* tree path (`companies/<slug>/projects/foo`) over-prefixes it; the server then
|
|
75
|
+
* rejects it with INVALID_PREFIX_COMPANIES_SCOPED (HTTP 400), the source of the
|
|
76
|
+
* recurring Sentry warning HQ-8F. Strip a redundant leading `companies/<slug>/`
|
|
77
|
+
* so the local-looking path is normalized to the bucket-relative key the vault
|
|
78
|
+
* actually stores. Returns the stripped slug for a one-line notice, or null when
|
|
79
|
+
* there was nothing to strip. Pure → unit-testable.
|
|
80
|
+
*
|
|
81
|
+
* This runs UPSTREAM of the server's exact-vs-glob branch, so it covers both
|
|
82
|
+
* the glob spelling (`companies/<slug>/projects/foo/*` → `validatePrefix`,
|
|
83
|
+
* HQ-8F) and the EXACT-key spelling (`companies/<slug>/notes/foo.md` →
|
|
84
|
+
* `validateObjectKey`, HQ-CA) with the same normalization.
|
|
85
|
+
*/
|
|
86
|
+
export declare function stripRedundantCompanyScope(prefix: string): {
|
|
87
|
+
prefix: string;
|
|
88
|
+
strippedSlug: string;
|
|
89
|
+
} | null;
|
|
71
90
|
export declare function runFilesDelete(params: RunFilesDeleteParams, deps?: {
|
|
72
91
|
confirm?: ConfirmFn;
|
|
73
92
|
}): Promise<void>;
|
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]="bc564dd4-d8c1-5d09-ad49-3bcd9f7328a6")}catch(e){}}();
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import open from "open";
|
|
5
5
|
import * as readline from "node:readline";
|
|
@@ -566,13 +566,47 @@ function printKeyPreview(resp) {
|
|
|
566
566
|
console.log(chalk.dim(` … and ${resp.matched - resp.keys.length} more`));
|
|
567
567
|
}
|
|
568
568
|
}
|
|
569
|
+
/**
|
|
570
|
+
* The vault bucket is already company-scoped, so a delete prefix must be
|
|
571
|
+
* BUCKET-RELATIVE (e.g. `projects/foo/*`). A caller who pastes an HQ *local*
|
|
572
|
+
* tree path (`companies/<slug>/projects/foo`) over-prefixes it; the server then
|
|
573
|
+
* rejects it with INVALID_PREFIX_COMPANIES_SCOPED (HTTP 400), the source of the
|
|
574
|
+
* recurring Sentry warning HQ-8F. Strip a redundant leading `companies/<slug>/`
|
|
575
|
+
* so the local-looking path is normalized to the bucket-relative key the vault
|
|
576
|
+
* actually stores. Returns the stripped slug for a one-line notice, or null when
|
|
577
|
+
* there was nothing to strip. Pure → unit-testable.
|
|
578
|
+
*
|
|
579
|
+
* This runs UPSTREAM of the server's exact-vs-glob branch, so it covers both
|
|
580
|
+
* the glob spelling (`companies/<slug>/projects/foo/*` → `validatePrefix`,
|
|
581
|
+
* HQ-8F) and the EXACT-key spelling (`companies/<slug>/notes/foo.md` →
|
|
582
|
+
* `validateObjectKey`, HQ-CA) with the same normalization.
|
|
583
|
+
*/
|
|
584
|
+
export function stripRedundantCompanyScope(prefix) {
|
|
585
|
+
const m = /^companies\/([^/]+)(?:\/(.*))?$/.exec(prefix);
|
|
586
|
+
if (!m)
|
|
587
|
+
return null;
|
|
588
|
+
return { prefix: m[2] ?? "", strippedSlug: m[1] };
|
|
589
|
+
}
|
|
569
590
|
export async function runFilesDelete(params, deps = {}) {
|
|
570
591
|
const confirm = deps.confirm ?? realConfirm;
|
|
592
|
+
// The vault is already company-scoped — a `companies/<slug>/` prefix is the HQ
|
|
593
|
+
// LOCAL tree layout, not a vault key, and the server 400s it (HQ-8F). Strip it
|
|
594
|
+
// here so a pasted local path is gracefully normalized to bucket-relative
|
|
595
|
+
// BEFORE the dry-run/preview (so the operator still sees the exact keys and
|
|
596
|
+
// confirms the right target). If stripping empties the prefix, the root-reject
|
|
597
|
+
// below catches it with a clear message.
|
|
598
|
+
const scope = stripRedundantCompanyScope(params.prefix);
|
|
599
|
+
if (scope) {
|
|
600
|
+
console.error(chalk.yellow(`Note: stripped redundant 'companies/${scope.strippedSlug}/' — the vault ` +
|
|
601
|
+
`is already company-scoped; using bucket-relative ` +
|
|
602
|
+
`'${scope.prefix || "(root)"}'.`));
|
|
603
|
+
}
|
|
604
|
+
const rawPrefix = scope ? scope.prefix : params.prefix;
|
|
571
605
|
// Normalize exactly as the share/unshare/acl paths do (trailing `/` → `/*`),
|
|
572
606
|
// then reject the root/empty prefix CLIENT-side so a typo never reaches the
|
|
573
607
|
// server as a vault-wide delete. The server enforces this too (defense in
|
|
574
608
|
// depth), but failing fast here is clearer and avoids a wasted round-trip.
|
|
575
|
-
const normalized = normalizeFilePrefix(
|
|
609
|
+
const normalized = normalizeFilePrefix(rawPrefix);
|
|
576
610
|
if (normalized === "" || normalized === "*" || normalized === "/*") {
|
|
577
611
|
console.error(chalk.red("Refusing to delete the vault root. Pass a bounded prefix (e.g. 'projects/foo/' or 'projects/foo/*') or an exact key."));
|
|
578
612
|
process.exit(1);
|
|
@@ -648,4 +682,4 @@ export async function runFilesDelete(params, deps = {}) {
|
|
|
648
682
|
}
|
|
649
683
|
}
|
|
650
684
|
//# sourceMappingURL=files.js.map
|
|
651
|
-
//# debugId=
|
|
685
|
+
//# debugId=bc564dd4-d8c1-5d09-ad49-3bcd9f7328a6
|
|
@@ -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]="f1986062-db39-5448-a8fa-3bb6be32c58b")}catch(e){}}();
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
5
5
|
import { vaultApiFetch, getCompanyUid } from "../utils/vault-api.js";
|
|
@@ -207,6 +207,54 @@ export function registerMeetingsCommand(program) {
|
|
|
207
207
|
process.exit(1);
|
|
208
208
|
}
|
|
209
209
|
});
|
|
210
|
+
// ── hq meetings set-company <id> ──────────────────────────────────
|
|
211
|
+
meetings
|
|
212
|
+
.command("set-company <meetingId>")
|
|
213
|
+
.description("Set or change the company a meeting is attributed to (use 'unknown' to clear)")
|
|
214
|
+
.option("--company <slug>", "Target company slug, or 'unknown'/'personal' to clear attribution")
|
|
215
|
+
.option("--no-series", "Apply only to this occurrence, not the whole recurring series")
|
|
216
|
+
.action(async (rawId, cmdOpts) => {
|
|
217
|
+
try {
|
|
218
|
+
const token = await ensureCognitoToken();
|
|
219
|
+
// `--company` is shared with the parent `meetings` command, so commander
|
|
220
|
+
// may bind it to either level depending on position — accept both.
|
|
221
|
+
const target = cmdOpts.company ?? meetings.opts().company;
|
|
222
|
+
if (!target) {
|
|
223
|
+
console.error(chalk.red("Error:"), "a target company is required: --company <slug> (or 'unknown'/'personal' to clear)");
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
226
|
+
// The bot record is person-keyed, so no company scope is needed to
|
|
227
|
+
// resolve the meeting id; pass an empty scope.
|
|
228
|
+
const meetingId = await resolveShortId(token, rawId, {});
|
|
229
|
+
const lc = target.toLowerCase();
|
|
230
|
+
const companyId = lc === "unknown" || lc === "personal"
|
|
231
|
+
? "unknown"
|
|
232
|
+
: await getCompanyUid(token, target);
|
|
233
|
+
const applyToSeries = cmdOpts.series !== false;
|
|
234
|
+
const res = await vaultApiFetch({
|
|
235
|
+
token,
|
|
236
|
+
method: "POST",
|
|
237
|
+
path: `/v1/meetings/${encodeURIComponent(meetingId)}/company`,
|
|
238
|
+
body: { companyId, applyToSeries },
|
|
239
|
+
});
|
|
240
|
+
if (!res.ok)
|
|
241
|
+
await handleApiError(res);
|
|
242
|
+
const data = (await res.json());
|
|
243
|
+
if (meetings.opts().json) {
|
|
244
|
+
console.log(JSON.stringify(data, null, 2));
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const where = companyId === "unknown" ? "unattributed (personal)" : companyId;
|
|
248
|
+
console.log(chalk.green(`\n✓ Meeting ${chalk.cyan(meetingId)} attributed to ${chalk.bold(where)}.`));
|
|
249
|
+
if (data && data.appliedToSeries)
|
|
250
|
+
console.log(chalk.dim(" Future occurrences of this recurring series will inherit this attribution."));
|
|
251
|
+
console.log();
|
|
252
|
+
}
|
|
253
|
+
catch (err) {
|
|
254
|
+
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
255
|
+
process.exit(1);
|
|
256
|
+
}
|
|
257
|
+
});
|
|
210
258
|
// ── hq meetings search <query> ─────────────────────────────────────
|
|
211
259
|
meetings
|
|
212
260
|
.command("search <query>")
|
|
@@ -371,4 +419,4 @@ export function registerMeetingsCommand(program) {
|
|
|
371
419
|
});
|
|
372
420
|
}
|
|
373
421
|
//# sourceMappingURL=meetings.js.map
|
|
374
|
-
//# debugId=
|
|
422
|
+
//# debugId=f1986062-db39-5448-a8fa-3bb6be32c58b
|
package/dist/commands/secrets.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]="3049e431-c5db-5430-90fe-82866c35e95b")}catch(e){}}();
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import * as readline from "node:readline";
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
@@ -251,6 +251,11 @@ export async function loadRevealedSecrets(token, companyUid, keys, usage) {
|
|
|
251
251
|
if (!res.ok) {
|
|
252
252
|
const body = (await res.json().catch(() => ({})));
|
|
253
253
|
const message = extractApiMessage(body, res.statusText);
|
|
254
|
+
// High-security ("nuclear") refusal surfaced at the batch level (rather
|
|
255
|
+
// than per-name): point the caller at the proxy and never leak plaintext.
|
|
256
|
+
if (body.code === "high_security_denied" || body.highSecurity === true) {
|
|
257
|
+
throw new Error("A requested secret is high-security and cannot be injected locally — it can only be used through the HQ secret proxy, which keeps the plaintext server-side.");
|
|
258
|
+
}
|
|
254
259
|
if (res.status >= 400 &&
|
|
255
260
|
res.status < 500 &&
|
|
256
261
|
typeof body.code === "string") {
|
|
@@ -280,6 +285,15 @@ export async function loadRevealedSecrets(token, companyUid, keys, usage) {
|
|
|
280
285
|
if (resolved.has(key))
|
|
281
286
|
continue;
|
|
282
287
|
const err = errorsByName.get(key);
|
|
288
|
+
// High-security ("nuclear") secret: the server refuses to vend it on the
|
|
289
|
+
// local-injection (batch-load) path — per-name code `high_security_denied`,
|
|
290
|
+
// no plaintext returned. Every caller of loadRevealedSecrets injects or
|
|
291
|
+
// prints the plaintext locally (`secrets get --reveal`, `secrets exec`,
|
|
292
|
+
// `secrets env`), so a high-security secret can NEVER be used here. Surface
|
|
293
|
+
// a clear, actionable error pointing at the proxy instead of a raw failure.
|
|
294
|
+
if (err?.code === "high_security_denied") {
|
|
295
|
+
throw new Error(`Secret '${key}' is high-security and cannot be injected locally — it can only be used through the HQ secret proxy, which keeps the plaintext server-side.`);
|
|
296
|
+
}
|
|
283
297
|
const reason = err?.code === "not_found"
|
|
284
298
|
? "Secret not found"
|
|
285
299
|
: err?.code === "forbidden"
|
|
@@ -364,6 +378,15 @@ export function registerSecretsCommand(program) {
|
|
|
364
378
|
});
|
|
365
379
|
if (!res.ok) {
|
|
366
380
|
const body = (await res.json().catch(() => ({})));
|
|
381
|
+
// High-security ("nuclear") secret: the server refuses to reveal it on
|
|
382
|
+
// the local-injection path (403, no plaintext). Surface a clear,
|
|
383
|
+
// actionable error pointing the user at the proxy rather than a raw
|
|
384
|
+
// 4xx — the value can ONLY be used through the server-side proxy.
|
|
385
|
+
if (res.status === 403 && body.highSecurity === true) {
|
|
386
|
+
console.error(chalk.red(`Secret '${name}' is high-security and cannot be revealed locally.`));
|
|
387
|
+
console.error(chalk.dim(" It can only be used through the HQ secret proxy, which keeps the plaintext server-side."));
|
|
388
|
+
process.exit(1);
|
|
389
|
+
}
|
|
367
390
|
console.error(chalk.red(`Failed to get secret: ${extractApiMessage(body, res.statusText)}`));
|
|
368
391
|
process.exit(1);
|
|
369
392
|
}
|
|
@@ -1125,4 +1148,4 @@ export function registerSecretsCommand(program) {
|
|
|
1125
1148
|
});
|
|
1126
1149
|
}
|
|
1127
1150
|
//# sourceMappingURL=secrets.js.map
|
|
1128
|
-
//# debugId=
|
|
1151
|
+
//# debugId=3049e431-c5db-5430-90fe-82866c35e95b
|
package/dist/run/hq-plugin.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]="20ece834-f9b5-5295-8620-87eb4f13ba7c")}catch(e){}}();
|
|
3
3
|
import { ResolutionError } from 'varlock/plugin-lib';
|
|
4
4
|
import { DEFAULT_SECRETS_CACHE_TTL_MS, readCache, writeCache, } from '../utils/secrets-cache.js';
|
|
5
5
|
function normalizeCacheTtlMs(cacheTtlMs) {
|
|
@@ -59,6 +59,13 @@ export function installHqPlugin(graph /* EnvGraph */, opts) {
|
|
|
59
59
|
if (err.code === 'not_found') {
|
|
60
60
|
throw new ResolutionError(`Secret "${secretName}" does not exist in company`);
|
|
61
61
|
}
|
|
62
|
+
// High-security ("nuclear") secret: the server refuses to vend it on
|
|
63
|
+
// the local-injection path. It can ONLY be used through the
|
|
64
|
+
// server-side proxy, so `hq run` (which injects plaintext into the
|
|
65
|
+
// child env) can never load it. Surface a clear, actionable error.
|
|
66
|
+
if (err.code === 'high_security_denied') {
|
|
67
|
+
throw new ResolutionError(`Secret "${secretName}" is high-security and cannot be injected locally — it can only be used via the HQ secret proxy (POST /secrets/{companyUid}/proxy/{path}), which keeps the plaintext server-side. Remove it from this schema's locally-injected vars.`);
|
|
68
|
+
}
|
|
62
69
|
throw new ResolutionError(`Failed to load secret "${secretName}": ${err.message ?? err.code}`);
|
|
63
70
|
}
|
|
64
71
|
// Sentinel-check style throughout: `readCache` returns `string | null`
|
|
@@ -158,4 +165,4 @@ export async function prewarmHqSecrets(graph /* EnvGraph */, opts, state) {
|
|
|
158
165
|
state.uid = uid;
|
|
159
166
|
}
|
|
160
167
|
//# sourceMappingURL=hq-plugin.js.map
|
|
161
|
-
//# debugId=
|
|
168
|
+
//# debugId=20ece834-f9b5-5295-8620-87eb4f13ba7c
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const BUNDLED_DSN = "https://
|
|
1
|
+
export declare const BUNDLED_DSN = "https://467061dc39fb64644f5640a82d8949cf@o4507292345892864.ingest.us.sentry.io/4511597516226560";
|
|
2
2
|
//# sourceMappingURL=sentry-dsn.generated.d.ts.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
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]="55f2362e-5cf7-5dac-b6a3-ef0f9ccde645")}catch(e){}}();
|
|
3
|
-
export const BUNDLED_DSN = "https://
|
|
3
|
+
export const BUNDLED_DSN = "https://467061dc39fb64644f5640a82d8949cf@o4507292345892864.ingest.us.sentry.io/4511597516226560";
|
|
4
4
|
//# sourceMappingURL=sentry-dsn.generated.js.map
|
|
5
5
|
//# debugId=55f2362e-5cf7-5dac-b6a3-ef0f9ccde645
|
package/dist/sentry.js
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
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]="80ca2b2d-c649-5e88-9bc6-69a20fa4aa51")}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
6
|
import { beforeBreadcrumb } from "./utils/breadcrumb-buffer.js";
|
|
7
|
+
import { CLI_VERSION } from "./cli-version.js";
|
|
7
8
|
export function initSentry() {
|
|
8
9
|
const dsn = BUNDLED_DSN || process.env.SENTRY_DSN;
|
|
9
10
|
if (!dsn)
|
|
10
11
|
return;
|
|
11
12
|
Sentry.init({
|
|
12
13
|
dsn,
|
|
13
|
-
|
|
14
|
+
// CLI_VERSION reads package.json at runtime; npm_package_version is only
|
|
15
|
+
// set under `npm run`, so an installed `hq` binary would always report
|
|
16
|
+
// 0.0.0 and never match the uploaded source maps.
|
|
17
|
+
release: `hq-cli@${CLI_VERSION}`,
|
|
14
18
|
environment: process.env.HQ_CLI_ENV ?? "production",
|
|
15
19
|
initialScope: {
|
|
16
20
|
tags: { repo: "hq-cli" },
|
|
@@ -21,4 +25,4 @@ export function initSentry() {
|
|
|
21
25
|
}
|
|
22
26
|
export { Sentry };
|
|
23
27
|
//# sourceMappingURL=sentry.js.map
|
|
24
|
-
//# debugId=
|
|
28
|
+
//# debugId=80ca2b2d-c649-5e88-9bc6-69a20fa4aa51
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type VersionInfo } from "./feedback-versions.js";
|
|
1
2
|
export interface GitContext {
|
|
2
3
|
branch: string | null;
|
|
3
4
|
head: string | null;
|
|
@@ -6,6 +7,12 @@ export interface GitContext {
|
|
|
6
7
|
}
|
|
7
8
|
export interface DiagnosticsBlob {
|
|
8
9
|
cliVersion: string;
|
|
10
|
+
/**
|
|
11
|
+
* The hq-cli, hq-core, and hq-sync versions from the submitter's
|
|
12
|
+
* environment. `cliVersion` above is retained for back-compat; new
|
|
13
|
+
* consumers should read `versions` (which carries core + sync too).
|
|
14
|
+
*/
|
|
15
|
+
versions: VersionInfo;
|
|
9
16
|
nodeVersion: string;
|
|
10
17
|
os: {
|
|
11
18
|
platform: string;
|
|
@@ -1,9 +1,10 @@
|
|
|
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]="ae4c924b-fe3d-558c-8b44-e1bac1f5a73e")}catch(e){}}();
|
|
3
3
|
import * as os from "os";
|
|
4
4
|
import { execFileSync } from "child_process";
|
|
5
5
|
import { getRecentBreadcrumbs } from "./breadcrumb-buffer.js";
|
|
6
6
|
import { CLI_VERSION } from "../cli-version.js";
|
|
7
|
+
import { collectVersions } from "./feedback-versions.js";
|
|
7
8
|
const SECRET_FLAGS = new Set([
|
|
8
9
|
"--token",
|
|
9
10
|
"--secret",
|
|
@@ -79,6 +80,7 @@ function collectGitContext() {
|
|
|
79
80
|
export function collectDiagnostics() {
|
|
80
81
|
return {
|
|
81
82
|
cliVersion: CLI_VERSION,
|
|
83
|
+
versions: collectVersions(),
|
|
82
84
|
nodeVersion: process.version,
|
|
83
85
|
os: {
|
|
84
86
|
platform: os.platform(),
|
|
@@ -92,4 +94,4 @@ export function collectDiagnostics() {
|
|
|
92
94
|
};
|
|
93
95
|
}
|
|
94
96
|
//# sourceMappingURL=feedback-diagnostics.js.map
|
|
95
|
-
//# debugId=
|
|
97
|
+
//# debugId=ae4c924b-fe3d-558c-8b44-e1bac1f5a73e
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export declare const MAX_SCREENSHOTS = 5;
|
|
2
|
+
export declare const MAX_SCREENSHOT_BYTES: number;
|
|
3
|
+
export declare function contentTypeForPath(filePath: string): string;
|
|
4
|
+
export interface ScreenshotInput {
|
|
5
|
+
path: string;
|
|
6
|
+
contentType: string;
|
|
7
|
+
bytes: Buffer;
|
|
8
|
+
}
|
|
9
|
+
/** Validate + read the given screenshot paths (count, type, existence, size). */
|
|
10
|
+
export declare function loadScreenshots(paths: string[]): ScreenshotInput[];
|
|
11
|
+
/**
|
|
12
|
+
* Validate the screenshot paths, request presigned PUT URLs from the feedback
|
|
13
|
+
* endpoint, upload each image direct to S3, and return the object keys to
|
|
14
|
+
* attach to the feedback submission. Returns [] for no screenshots.
|
|
15
|
+
*
|
|
16
|
+
* `fetchImpl` is injectable for tests; defaults to the global fetch.
|
|
17
|
+
*/
|
|
18
|
+
export declare function uploadScreenshots(opts: {
|
|
19
|
+
paths: string[];
|
|
20
|
+
token: string;
|
|
21
|
+
fetchImpl?: typeof fetch;
|
|
22
|
+
}): Promise<string[]>;
|
|
23
|
+
//# sourceMappingURL=feedback-screenshots.d.ts.map
|