@indigoai-us/hq-cli 5.47.17 → 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/meetings.js +50 -2
- package/dist/sentry.js +7 -3
- package/package.json +4 -2
- package/src/commands/meetings.test.ts +163 -0
- package/src/commands/meetings.ts +60 -0
- package/src/sentry.ts +5 -1
- 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/ node_modules/@indigoai-us/hq-cloud/dist/
|
|
108
|
-
env:
|
|
109
|
-
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
|
110
|
-
SENTRY_ORG: indigo-d0
|
|
111
|
-
SENTRY_PROJECT: hq-cli
|
|
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 }}
|
|
@@ -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/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
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@indigoai-us/hq-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.48.0",
|
|
4
4
|
"description": "HQ by Indigo management CLI — modules and cloud sync",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -11,11 +11,12 @@
|
|
|
11
11
|
"build": "node scripts/generate-dsn.mjs && tsc",
|
|
12
12
|
"typecheck": "tsc --noEmit",
|
|
13
13
|
"test": "vitest run",
|
|
14
|
+
"coverage": "vitest run --coverage --coverage.reporter=text-summary --coverage.reporter=json-summary",
|
|
14
15
|
"vitest": "vitest",
|
|
15
16
|
"clean": "rm -rf dist"
|
|
16
17
|
},
|
|
17
18
|
"dependencies": {
|
|
18
|
-
"@indigoai-us/hq-cloud": "^6.11.
|
|
19
|
+
"@indigoai-us/hq-cloud": "^6.11.14",
|
|
19
20
|
"@indigoai-us/hq-onboarding": "^0.1.0",
|
|
20
21
|
"@sentry/node": "^10.49.0",
|
|
21
22
|
"chalk": "^5.3.0",
|
|
@@ -31,6 +32,7 @@
|
|
|
31
32
|
"@types/js-yaml": "^4.0.9",
|
|
32
33
|
"@types/node": "^22.0.0",
|
|
33
34
|
"@types/semver": "^7.5.8",
|
|
35
|
+
"@vitest/coverage-v8": "4.1.6",
|
|
34
36
|
"typescript": "^5.7.0",
|
|
35
37
|
"vitest": "^4.1.2"
|
|
36
38
|
},
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import {
|
|
3
|
+
afterEach,
|
|
4
|
+
beforeEach,
|
|
5
|
+
describe,
|
|
6
|
+
expect,
|
|
7
|
+
it,
|
|
8
|
+
vi,
|
|
9
|
+
type MockInstance,
|
|
10
|
+
} from "vitest";
|
|
11
|
+
|
|
12
|
+
vi.mock("../utils/cognito-session.js", async (importOriginal) => {
|
|
13
|
+
const original = (await importOriginal()) as Record<string, unknown>;
|
|
14
|
+
return {
|
|
15
|
+
...original,
|
|
16
|
+
ensureCognitoToken: vi.fn(async () => "test-token"),
|
|
17
|
+
};
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
vi.mock("../utils/vault-api.js", async (importOriginal) => {
|
|
21
|
+
const original = (await importOriginal()) as Record<string, unknown>;
|
|
22
|
+
return {
|
|
23
|
+
...original,
|
|
24
|
+
getCompanyUid: vi.fn(async (_token: string, slug: string) => `cmp_${slug}`),
|
|
25
|
+
vaultApiFetch: vi.fn(async () =>
|
|
26
|
+
new Response(JSON.stringify({ appliedToSeries: true }), {
|
|
27
|
+
status: 200,
|
|
28
|
+
headers: { "Content-Type": "application/json" },
|
|
29
|
+
}),
|
|
30
|
+
),
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
import { registerMeetingsCommand } from "./meetings.js";
|
|
35
|
+
import { getCompanyUid, vaultApiFetch } from "../utils/vault-api.js";
|
|
36
|
+
|
|
37
|
+
let logSpy: MockInstance<typeof console.log>;
|
|
38
|
+
let errSpy: MockInstance<typeof console.error>;
|
|
39
|
+
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
vi.clearAllMocks();
|
|
42
|
+
logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined);
|
|
43
|
+
errSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
afterEach(() => {
|
|
47
|
+
vi.restoreAllMocks();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
function buildProgram(): Command {
|
|
51
|
+
const program = new Command();
|
|
52
|
+
program.exitOverride();
|
|
53
|
+
program.configureOutput({
|
|
54
|
+
writeOut: () => undefined,
|
|
55
|
+
writeErr: () => undefined,
|
|
56
|
+
});
|
|
57
|
+
registerMeetingsCommand(program);
|
|
58
|
+
return program;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function jsonRes(body: unknown, status = 200): Response {
|
|
62
|
+
return new Response(JSON.stringify(body), {
|
|
63
|
+
status,
|
|
64
|
+
headers: { "Content-Type": "application/json" },
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
describe("meetings set-company", () => {
|
|
69
|
+
it("POSTs the resolved company id and applies to the recurring series by default", async () => {
|
|
70
|
+
vi.mocked(vaultApiFetch).mockResolvedValueOnce(
|
|
71
|
+
jsonRes({ appliedToSeries: true }),
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const program = buildProgram();
|
|
75
|
+
await program.parseAsync([
|
|
76
|
+
"node",
|
|
77
|
+
"hq",
|
|
78
|
+
"meetings",
|
|
79
|
+
"set-company",
|
|
80
|
+
"meeting-123456",
|
|
81
|
+
"--company",
|
|
82
|
+
"acme",
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
expect(getCompanyUid).toHaveBeenCalledWith("test-token", "acme");
|
|
86
|
+
expect(vaultApiFetch).toHaveBeenCalledWith({
|
|
87
|
+
token: "test-token",
|
|
88
|
+
method: "POST",
|
|
89
|
+
path: "/v1/meetings/meeting-123456/company",
|
|
90
|
+
body: { companyId: "cmp_acme", applyToSeries: true },
|
|
91
|
+
});
|
|
92
|
+
expect(errSpy).not.toHaveBeenCalled();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("sends unknown without resolving a target company", async () => {
|
|
96
|
+
vi.mocked(vaultApiFetch).mockResolvedValueOnce(
|
|
97
|
+
jsonRes({ appliedToSeries: true }),
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const program = buildProgram();
|
|
101
|
+
await program.parseAsync([
|
|
102
|
+
"node",
|
|
103
|
+
"hq",
|
|
104
|
+
"meetings",
|
|
105
|
+
"set-company",
|
|
106
|
+
"meeting-123456",
|
|
107
|
+
"--company",
|
|
108
|
+
"unknown",
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
expect(getCompanyUid).not.toHaveBeenCalled();
|
|
112
|
+
expect(vaultApiFetch).toHaveBeenCalledWith({
|
|
113
|
+
token: "test-token",
|
|
114
|
+
method: "POST",
|
|
115
|
+
path: "/v1/meetings/meeting-123456/company",
|
|
116
|
+
body: { companyId: "unknown", applyToSeries: true },
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("sends applyToSeries false when --no-series is passed", async () => {
|
|
121
|
+
vi.mocked(vaultApiFetch).mockResolvedValueOnce(
|
|
122
|
+
jsonRes({ appliedToSeries: false }),
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const program = buildProgram();
|
|
126
|
+
await program.parseAsync([
|
|
127
|
+
"node",
|
|
128
|
+
"hq",
|
|
129
|
+
"meetings",
|
|
130
|
+
"set-company",
|
|
131
|
+
"meeting-123456",
|
|
132
|
+
"--company",
|
|
133
|
+
"acme",
|
|
134
|
+
"--no-series",
|
|
135
|
+
]);
|
|
136
|
+
|
|
137
|
+
expect(vaultApiFetch).toHaveBeenCalledWith({
|
|
138
|
+
token: "test-token",
|
|
139
|
+
method: "POST",
|
|
140
|
+
path: "/v1/meetings/meeting-123456/company",
|
|
141
|
+
body: { companyId: "cmp_acme", applyToSeries: false },
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("prints the raw response when --json is passed", async () => {
|
|
146
|
+
const body = { meetingId: "meeting-123456", companyId: "cmp_acme" };
|
|
147
|
+
vi.mocked(vaultApiFetch).mockResolvedValueOnce(jsonRes(body));
|
|
148
|
+
|
|
149
|
+
const program = buildProgram();
|
|
150
|
+
await program.parseAsync([
|
|
151
|
+
"node",
|
|
152
|
+
"hq",
|
|
153
|
+
"meetings",
|
|
154
|
+
"--json",
|
|
155
|
+
"set-company",
|
|
156
|
+
"meeting-123456",
|
|
157
|
+
"--company",
|
|
158
|
+
"acme",
|
|
159
|
+
]);
|
|
160
|
+
|
|
161
|
+
expect(logSpy).toHaveBeenCalledWith(JSON.stringify(body, null, 2));
|
|
162
|
+
});
|
|
163
|
+
});
|
package/src/commands/meetings.ts
CHANGED
|
@@ -299,6 +299,66 @@ export function registerMeetingsCommand(program: Command): void {
|
|
|
299
299
|
}
|
|
300
300
|
});
|
|
301
301
|
|
|
302
|
+
// ── hq meetings set-company <id> ──────────────────────────────────
|
|
303
|
+
|
|
304
|
+
meetings
|
|
305
|
+
.command("set-company <meetingId>")
|
|
306
|
+
.description("Set or change the company a meeting is attributed to (use 'unknown' to clear)")
|
|
307
|
+
.option("--company <slug>", "Target company slug, or 'unknown'/'personal' to clear attribution")
|
|
308
|
+
.option("--no-series", "Apply only to this occurrence, not the whole recurring series")
|
|
309
|
+
.action(async (rawId: string, cmdOpts: { company?: string; series?: boolean }) => {
|
|
310
|
+
try {
|
|
311
|
+
const token = await ensureCognitoToken();
|
|
312
|
+
// `--company` is shared with the parent `meetings` command, so commander
|
|
313
|
+
// may bind it to either level depending on position — accept both.
|
|
314
|
+
const target = cmdOpts.company ?? (meetings.opts().company as string | undefined);
|
|
315
|
+
if (!target) {
|
|
316
|
+
console.error(
|
|
317
|
+
chalk.red("Error:"),
|
|
318
|
+
"a target company is required: --company <slug> (or 'unknown'/'personal' to clear)",
|
|
319
|
+
);
|
|
320
|
+
process.exit(1);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// The bot record is person-keyed, so no company scope is needed to
|
|
324
|
+
// resolve the meeting id; pass an empty scope.
|
|
325
|
+
const meetingId = await resolveShortId(token, rawId, {});
|
|
326
|
+
|
|
327
|
+
const lc = target.toLowerCase();
|
|
328
|
+
const companyId =
|
|
329
|
+
lc === "unknown" || lc === "personal"
|
|
330
|
+
? "unknown"
|
|
331
|
+
: await getCompanyUid(token, target);
|
|
332
|
+
|
|
333
|
+
const applyToSeries = cmdOpts.series !== false;
|
|
334
|
+
|
|
335
|
+
const res = await vaultApiFetch({
|
|
336
|
+
token,
|
|
337
|
+
method: "POST",
|
|
338
|
+
path: `/v1/meetings/${encodeURIComponent(meetingId)}/company`,
|
|
339
|
+
body: { companyId, applyToSeries },
|
|
340
|
+
});
|
|
341
|
+
if (!res.ok) await handleApiError(res);
|
|
342
|
+
const data = (await res.json()) as {
|
|
343
|
+
appliedToSeries?: boolean;
|
|
344
|
+
[key: string]: unknown;
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
if (meetings.opts().json) {
|
|
348
|
+
console.log(JSON.stringify(data, null, 2));
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const where = companyId === "unknown" ? "unattributed (personal)" : companyId;
|
|
353
|
+
console.log(chalk.green(`\n✓ Meeting ${chalk.cyan(meetingId)} attributed to ${chalk.bold(where)}.`));
|
|
354
|
+
if (data && data.appliedToSeries) console.log(chalk.dim(" Future occurrences of this recurring series will inherit this attribution."));
|
|
355
|
+
console.log();
|
|
356
|
+
} catch (err) {
|
|
357
|
+
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
358
|
+
process.exit(1);
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
|
|
302
362
|
// ── hq meetings search <query> ─────────────────────────────────────
|
|
303
363
|
|
|
304
364
|
meetings
|
package/src/sentry.ts
CHANGED
|
@@ -2,13 +2,17 @@ import * as Sentry from "@sentry/node";
|
|
|
2
2
|
import { BUNDLED_DSN } from "./sentry-dsn.generated.js";
|
|
3
3
|
import { beforeSend } from "./sentry-before-send.js";
|
|
4
4
|
import { beforeBreadcrumb } from "./utils/breadcrumb-buffer.js";
|
|
5
|
+
import { CLI_VERSION } from "./cli-version.js";
|
|
5
6
|
|
|
6
7
|
export function initSentry(): void {
|
|
7
8
|
const dsn = BUNDLED_DSN || process.env.SENTRY_DSN;
|
|
8
9
|
if (!dsn) return;
|
|
9
10
|
Sentry.init({
|
|
10
11
|
dsn,
|
|
11
|
-
|
|
12
|
+
// CLI_VERSION reads package.json at runtime; npm_package_version is only
|
|
13
|
+
// set under `npm run`, so an installed `hq` binary would always report
|
|
14
|
+
// 0.0.0 and never match the uploaded source maps.
|
|
15
|
+
release: `hq-cli@${CLI_VERSION}`,
|
|
12
16
|
environment: process.env.HQ_CLI_ENV ?? "production",
|
|
13
17
|
initialScope: {
|
|
14
18
|
tags: { repo: "hq-cli" },
|
|
@@ -126,11 +126,15 @@ export function mockVaultService(opts: MockVaultOptions): () => void {
|
|
|
126
126
|
// 404s, which the reader normalizes to NoSuchKey (the not-found path).
|
|
127
127
|
if (method === "POST" && /\/v1\/files\/presign$/.test(url)) {
|
|
128
128
|
const body = init?.body ? JSON.parse(init.body.toString()) : {};
|
|
129
|
-
const keys = (body.keys ?? []) as Array<{ key: string }>;
|
|
129
|
+
const keys = (body.keys ?? []) as Array<{ key: string; op?: string }>;
|
|
130
|
+
// Echo the requested op and omit `error` on success. hq-cloud >=6.11.x
|
|
131
|
+
// validates each presign result with a strict schema (op ∈ get|put|delete;
|
|
132
|
+
// error must be a string when present), so the older `error: null` +
|
|
133
|
+
// missing-op shape no longer passes.
|
|
130
134
|
const results = keys.map((k) => ({
|
|
131
135
|
key: k.key,
|
|
136
|
+
op: k.op ?? "get",
|
|
132
137
|
url: `${PRESIGN_URL_HOST}/obj?key=${encodeURIComponent(k.key)}`,
|
|
133
|
-
error: null,
|
|
134
138
|
}));
|
|
135
139
|
return json({
|
|
136
140
|
results,
|