@indigoai-us/hq-cli 5.48.0 → 5.49.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 +18 -1
- package/dist/commands/meetings.js +10 -3
- package/dist/index.js +4 -2
- package/dist/sentry.js +12 -2
- package/dist/utils/sentry-identity.d.ts +11 -0
- package/dist/utils/sentry-identity.js +43 -0
- package/e2e/cli.test.ts +93 -0
- package/eslint.config.js +37 -0
- package/package.json +6 -1
- package/src/commands/meetings.test.ts +64 -0
- package/src/commands/meetings.ts +11 -1
- package/src/index.ts +2 -0
- package/src/sentry.ts +9 -0
- package/src/utils/sentry-identity.ts +45 -0
- package/vitest.e2e.config.ts +7 -0
package/.github/workflows/ci.yml
CHANGED
|
@@ -24,6 +24,23 @@ jobs:
|
|
|
24
24
|
pnpm-version: "10"
|
|
25
25
|
node-version: ${{ matrix.node-version }}
|
|
26
26
|
run-build: true
|
|
27
|
-
run-lint:
|
|
27
|
+
run-lint: true
|
|
28
28
|
run-coverage: true
|
|
29
29
|
secrets: inherit
|
|
30
|
+
|
|
31
|
+
e2e:
|
|
32
|
+
name: e2e (node 22)
|
|
33
|
+
runs-on: ubuntu-latest
|
|
34
|
+
timeout-minutes: 10
|
|
35
|
+
steps:
|
|
36
|
+
- uses: actions/checkout@v4
|
|
37
|
+
- uses: pnpm/action-setup@v4
|
|
38
|
+
with:
|
|
39
|
+
version: "10"
|
|
40
|
+
- uses: actions/setup-node@v4
|
|
41
|
+
with:
|
|
42
|
+
node-version: "22"
|
|
43
|
+
cache: pnpm
|
|
44
|
+
- run: pnpm install --frozen-lockfile
|
|
45
|
+
- run: pnpm build
|
|
46
|
+
- run: pnpm test:e2e
|
|
@@ -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]="ae9ed929-9821-5f8b-bbbe-8f7b6105f5ad")}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";
|
|
@@ -246,8 +246,15 @@ export function registerMeetingsCommand(program) {
|
|
|
246
246
|
}
|
|
247
247
|
const where = companyId === "unknown" ? "unattributed (personal)" : companyId;
|
|
248
248
|
console.log(chalk.green(`\n✓ Meeting ${chalk.cyan(meetingId)} attributed to ${chalk.bold(where)}.`));
|
|
249
|
-
if (data && data.
|
|
249
|
+
if (typeof data.occurrencesUpdated === "number" && data.occurrencesUpdated > 1) {
|
|
250
|
+
console.log(chalk.dim(` Applied to ${data.occurrencesUpdated} meetings in this recurring series (including past occurrences).`));
|
|
251
|
+
}
|
|
252
|
+
else if (data.appliedToSeries) {
|
|
250
253
|
console.log(chalk.dim(" Future occurrences of this recurring series will inherit this attribution."));
|
|
254
|
+
}
|
|
255
|
+
if (typeof data.refiledCount === "number" && data.refiledCount > 0) {
|
|
256
|
+
console.log(chalk.dim(` Refiled ${data.refiledCount} transcript${data.refiledCount === 1 ? "" : "s"} into the new company vault.`));
|
|
257
|
+
}
|
|
251
258
|
console.log();
|
|
252
259
|
}
|
|
253
260
|
catch (err) {
|
|
@@ -419,4 +426,4 @@ export function registerMeetingsCommand(program) {
|
|
|
419
426
|
});
|
|
420
427
|
}
|
|
421
428
|
//# sourceMappingURL=meetings.js.map
|
|
422
|
-
//# debugId=
|
|
429
|
+
//# debugId=ae9ed929-9821-5f8b-bbbe-8f7b6105f5ad
|
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]="60f911e0-ddd5-5a2d-91c7-18ba47457b57")}catch(e){}}();
|
|
7
7
|
import { Command } from "commander";
|
|
8
8
|
import { initSentry, Sentry } from "./sentry.js";
|
|
9
9
|
import { registerAddCommand } from "./commands/add.js";
|
|
@@ -180,8 +180,10 @@ registerRescueCommand(program);
|
|
|
180
180
|
process.exitCode = 1;
|
|
181
181
|
}
|
|
182
182
|
finally {
|
|
183
|
+
// Release health: finalize the per-run session before the flush.
|
|
184
|
+
Sentry.endSession();
|
|
183
185
|
await Promise.allSettled([refreshVersionCache(), Sentry.flush(2000)]);
|
|
184
186
|
}
|
|
185
187
|
})();
|
|
186
188
|
//# sourceMappingURL=index.js.map
|
|
187
|
-
//# debugId=
|
|
189
|
+
//# debugId=60f911e0-ddd5-5a2d-91c7-18ba47457b57
|
package/dist/sentry.js
CHANGED
|
@@ -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]="a260ea42-f6d8-52d0-90c6-6e586de5efd6")}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
7
|
import { CLI_VERSION } from "./cli-version.js";
|
|
8
|
+
import { getCachedSentryUser } from "./utils/sentry-identity.js";
|
|
8
9
|
export function initSentry() {
|
|
9
10
|
const dsn = BUNDLED_DSN || process.env.SENTRY_DSN;
|
|
10
11
|
if (!dsn)
|
|
@@ -22,7 +23,16 @@ export function initSentry() {
|
|
|
22
23
|
beforeSend,
|
|
23
24
|
beforeBreadcrumb,
|
|
24
25
|
});
|
|
26
|
+
// Attribute events to the logged-in HQ identity (best-effort; null when not
|
|
27
|
+
// logged in). A CLI process is one user, so global setUser is correct here.
|
|
28
|
+
const user = getCachedSentryUser();
|
|
29
|
+
if (user)
|
|
30
|
+
Sentry.setUser(user);
|
|
31
|
+
// Release health: one session per CLI run. Ended + flushed in index.ts's
|
|
32
|
+
// finally (covers every exit path). A captured error auto-marks the session
|
|
33
|
+
// "errored", so crash-free numbers per release stay accurate.
|
|
34
|
+
Sentry.startSession();
|
|
25
35
|
}
|
|
26
36
|
export { Sentry };
|
|
27
37
|
//# sourceMappingURL=sentry.js.map
|
|
28
|
-
//# debugId=
|
|
38
|
+
//# debugId=a260ea42-f6d8-52d0-90c6-6e586de5efd6
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Best-effort Sentry user from the cached HQ session, so CLI events are
|
|
3
|
+
* attributed to a user ("users affected" + per-user triage). Returns null when
|
|
4
|
+
* not logged in. A CLI process is single-user, so this is set once at init.
|
|
5
|
+
*/
|
|
6
|
+
export declare function getCachedSentryUser(): {
|
|
7
|
+
id?: string;
|
|
8
|
+
email?: string;
|
|
9
|
+
username?: string;
|
|
10
|
+
} | null;
|
|
11
|
+
//# sourceMappingURL=sentry-identity.d.ts.map
|
|
@@ -0,0 +1,43 @@
|
|
|
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]="1ca7cb4e-47e2-5cd0-a400-bbdaafa2b0d5")}catch(e){}}();
|
|
3
|
+
import { loadCachedTokens, isMachineIdentity, loadMachineCreds, } from "@indigoai-us/hq-cloud";
|
|
4
|
+
/** Decode the (untrusted) id-token payload — for attribution/display only. */
|
|
5
|
+
function peekIdToken(idToken) {
|
|
6
|
+
try {
|
|
7
|
+
const payload = idToken.split(".")[1];
|
|
8
|
+
if (!payload)
|
|
9
|
+
return {};
|
|
10
|
+
const pad = payload.length % 4 === 0 ? "" : "=".repeat(4 - (payload.length % 4));
|
|
11
|
+
const normalized = payload.replace(/-/g, "+").replace(/_/g, "/") + pad;
|
|
12
|
+
const decoded = JSON.parse(Buffer.from(normalized, "base64").toString("utf-8"));
|
|
13
|
+
return { email: decoded.email, sub: decoded.sub };
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Best-effort Sentry user from the cached HQ session, so CLI events are
|
|
21
|
+
* attributed to a user ("users affected" + per-user triage). Returns null when
|
|
22
|
+
* not logged in. A CLI process is single-user, so this is set once at init.
|
|
23
|
+
*/
|
|
24
|
+
export function getCachedSentryUser() {
|
|
25
|
+
try {
|
|
26
|
+
if (isMachineIdentity()) {
|
|
27
|
+
const username = loadMachineCreds()?.username;
|
|
28
|
+
return username ? { id: username, username } : null;
|
|
29
|
+
}
|
|
30
|
+
const cached = loadCachedTokens();
|
|
31
|
+
if (!cached)
|
|
32
|
+
return null;
|
|
33
|
+
const { email, sub } = peekIdToken(cached.idToken);
|
|
34
|
+
if (!sub && !email)
|
|
35
|
+
return null;
|
|
36
|
+
return { id: sub, email };
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=sentry-identity.js.map
|
|
43
|
+
//# debugId=1ca7cb4e-47e2-5cd0-a400-bbdaafa2b0d5
|
package/e2e/cli.test.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
6
|
+
import { version } from "../package.json";
|
|
7
|
+
|
|
8
|
+
const cliEntry = path.resolve("dist/index.js");
|
|
9
|
+
const tempDirs: string[] = [];
|
|
10
|
+
|
|
11
|
+
interface CliResult {
|
|
12
|
+
code: number | null;
|
|
13
|
+
stdout: string;
|
|
14
|
+
stderr: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function runHq(args: string[], options: { cwd?: string; env?: NodeJS.ProcessEnv } = {}) {
|
|
18
|
+
return new Promise<CliResult>((resolve, reject) => {
|
|
19
|
+
const child = spawn(process.execPath, [cliEntry, ...args], {
|
|
20
|
+
cwd: options.cwd,
|
|
21
|
+
env: {
|
|
22
|
+
...process.env,
|
|
23
|
+
HQ_NO_UPDATE_CHECK: "1",
|
|
24
|
+
...options.env,
|
|
25
|
+
},
|
|
26
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
let stdout = "";
|
|
30
|
+
let stderr = "";
|
|
31
|
+
child.stdout.setEncoding("utf8");
|
|
32
|
+
child.stderr.setEncoding("utf8");
|
|
33
|
+
child.stdout.on("data", (chunk) => {
|
|
34
|
+
stdout += chunk;
|
|
35
|
+
});
|
|
36
|
+
child.stderr.on("data", (chunk) => {
|
|
37
|
+
stderr += chunk;
|
|
38
|
+
});
|
|
39
|
+
child.on("error", reject);
|
|
40
|
+
child.on("close", (code) => resolve({ code, stdout, stderr }));
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function makeTempDir(prefix: string) {
|
|
45
|
+
const dir = await mkdtemp(path.join(tmpdir(), prefix));
|
|
46
|
+
tempDirs.push(dir);
|
|
47
|
+
return dir;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
afterEach(async () => {
|
|
51
|
+
await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("built hq CLI", () => {
|
|
55
|
+
it("prints help from the built entrypoint", async () => {
|
|
56
|
+
const result = await runHq(["--help"]);
|
|
57
|
+
|
|
58
|
+
expect(result.code).toBe(0);
|
|
59
|
+
expect(result.stderr).toBe("");
|
|
60
|
+
expect(result.stdout).toContain("Usage: hq [options] [command]");
|
|
61
|
+
expect(result.stdout).toContain("HQ management CLI");
|
|
62
|
+
expect(result.stdout).toContain("whoami");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("prints the package version from the built entrypoint", async () => {
|
|
66
|
+
const result = await runHq(["--version"]);
|
|
67
|
+
|
|
68
|
+
expect(result.code).toBe(0);
|
|
69
|
+
expect(result.stderr).toBe("");
|
|
70
|
+
expect(result.stdout.trim()).toBe(version);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("reports logged-out state without network or credentials", async () => {
|
|
74
|
+
const home = await makeTempDir("hq-cli-e2e-home-");
|
|
75
|
+
const cwd = await makeTempDir("hq-cli-e2e-cwd-");
|
|
76
|
+
|
|
77
|
+
const result = await runHq(["whoami"], {
|
|
78
|
+
cwd,
|
|
79
|
+
env: {
|
|
80
|
+
HOME: home,
|
|
81
|
+
USERPROFILE: home,
|
|
82
|
+
XDG_CONFIG_HOME: path.join(home, ".config"),
|
|
83
|
+
XDG_CACHE_HOME: path.join(home, ".cache"),
|
|
84
|
+
HQ_MACHINE_IDENTITY: undefined,
|
|
85
|
+
HQ_MACHINE_CREDS: undefined,
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
expect(result.code).toBe(0);
|
|
90
|
+
expect(result.stderr).toBe("");
|
|
91
|
+
expect(result.stdout.trim()).toBe("Not logged in. Run 'hq login' to authenticate.");
|
|
92
|
+
});
|
|
93
|
+
});
|
package/eslint.config.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import js from "@eslint/js";
|
|
2
|
+
import tseslint from "typescript-eslint";
|
|
3
|
+
|
|
4
|
+
const sourceFiles = ["src/**/*.ts"];
|
|
5
|
+
const recommended = tseslint
|
|
6
|
+
.config(js.configs.recommended, ...tseslint.configs.recommended)
|
|
7
|
+
.map((config) => ({
|
|
8
|
+
...config,
|
|
9
|
+
files: sourceFiles,
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
export default [
|
|
13
|
+
{
|
|
14
|
+
ignores: ["dist/**", "generated/**", "node_modules/**"],
|
|
15
|
+
},
|
|
16
|
+
...recommended,
|
|
17
|
+
{
|
|
18
|
+
files: sourceFiles,
|
|
19
|
+
rules: {
|
|
20
|
+
"no-undef": "off",
|
|
21
|
+
"no-useless-assignment": "warn",
|
|
22
|
+
"no-useless-escape": "warn",
|
|
23
|
+
"prefer-const": "warn",
|
|
24
|
+
"preserve-caught-error": "off",
|
|
25
|
+
"@typescript-eslint/no-explicit-any": "warn",
|
|
26
|
+
"@typescript-eslint/no-require-imports": "warn",
|
|
27
|
+
"@typescript-eslint/no-unused-vars": [
|
|
28
|
+
"warn",
|
|
29
|
+
{
|
|
30
|
+
argsIgnorePattern: "^_",
|
|
31
|
+
caughtErrorsIgnorePattern: "^_",
|
|
32
|
+
varsIgnorePattern: "^_",
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@indigoai-us/hq-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.49.0",
|
|
4
4
|
"description": "HQ by Indigo management CLI — modules and cloud sync",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
"scripts": {
|
|
11
11
|
"build": "node scripts/generate-dsn.mjs && tsc",
|
|
12
12
|
"typecheck": "tsc --noEmit",
|
|
13
|
+
"lint": "eslint .",
|
|
13
14
|
"test": "vitest run",
|
|
15
|
+
"test:e2e": "vitest run --config vitest.e2e.config.ts",
|
|
14
16
|
"coverage": "vitest run --coverage --coverage.reporter=text-summary --coverage.reporter=json-summary",
|
|
15
17
|
"vitest": "vitest",
|
|
16
18
|
"clean": "rm -rf dist"
|
|
@@ -29,11 +31,14 @@
|
|
|
29
31
|
},
|
|
30
32
|
"devDependencies": {
|
|
31
33
|
"@aws-sdk/client-s3": "^3.1049.0",
|
|
34
|
+
"@eslint/js": "^10.0.1",
|
|
32
35
|
"@types/js-yaml": "^4.0.9",
|
|
33
36
|
"@types/node": "^22.0.0",
|
|
34
37
|
"@types/semver": "^7.5.8",
|
|
35
38
|
"@vitest/coverage-v8": "4.1.6",
|
|
39
|
+
"eslint": "^10.5.0",
|
|
36
40
|
"typescript": "^5.7.0",
|
|
41
|
+
"typescript-eslint": "^8.61.1",
|
|
37
42
|
"vitest": "^4.1.2"
|
|
38
43
|
},
|
|
39
44
|
"repository": {
|
|
@@ -160,4 +160,68 @@ describe("meetings set-company", () => {
|
|
|
160
160
|
|
|
161
161
|
expect(logSpy).toHaveBeenCalledWith(JSON.stringify(body, null, 2));
|
|
162
162
|
});
|
|
163
|
+
|
|
164
|
+
it("prints recurring series and refiled transcript counts from the response", async () => {
|
|
165
|
+
vi.mocked(vaultApiFetch).mockResolvedValueOnce(
|
|
166
|
+
jsonRes({
|
|
167
|
+
ok: true,
|
|
168
|
+
meetingId: "meeting-123456",
|
|
169
|
+
companyId: "cmp_acme",
|
|
170
|
+
seriesKey: "series-123",
|
|
171
|
+
appliedToSeries: true,
|
|
172
|
+
occurrencesUpdated: 3,
|
|
173
|
+
refiled: true,
|
|
174
|
+
refiledCount: 2,
|
|
175
|
+
}),
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
const program = buildProgram();
|
|
179
|
+
await program.parseAsync([
|
|
180
|
+
"node",
|
|
181
|
+
"hq",
|
|
182
|
+
"meetings",
|
|
183
|
+
"set-company",
|
|
184
|
+
"meeting-123456",
|
|
185
|
+
"--company",
|
|
186
|
+
"acme",
|
|
187
|
+
]);
|
|
188
|
+
|
|
189
|
+
expect(logSpy).toHaveBeenCalledWith(
|
|
190
|
+
expect.stringContaining("Applied to 3 meetings in this recurring series"),
|
|
191
|
+
);
|
|
192
|
+
expect(logSpy).toHaveBeenCalledWith(
|
|
193
|
+
expect.stringContaining("Refiled 2 transcripts into the new company vault."),
|
|
194
|
+
);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("does not print multi-meeting or refile copy for a single occurrence without refiles", async () => {
|
|
198
|
+
vi.mocked(vaultApiFetch).mockResolvedValueOnce(
|
|
199
|
+
jsonRes({
|
|
200
|
+
ok: true,
|
|
201
|
+
meetingId: "meeting-123456",
|
|
202
|
+
companyId: "cmp_acme",
|
|
203
|
+
seriesKey: null,
|
|
204
|
+
appliedToSeries: false,
|
|
205
|
+
occurrencesUpdated: 1,
|
|
206
|
+
refiled: false,
|
|
207
|
+
refiledCount: 0,
|
|
208
|
+
}),
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
const program = buildProgram();
|
|
212
|
+
await program.parseAsync([
|
|
213
|
+
"node",
|
|
214
|
+
"hq",
|
|
215
|
+
"meetings",
|
|
216
|
+
"set-company",
|
|
217
|
+
"meeting-123456",
|
|
218
|
+
"--company",
|
|
219
|
+
"acme",
|
|
220
|
+
]);
|
|
221
|
+
|
|
222
|
+
const output = logSpy.mock.calls.map(([line]) => String(line)).join("\n");
|
|
223
|
+
expect(output).not.toContain("Applied to 1 meetings");
|
|
224
|
+
expect(output).not.toContain("Refiled 0 transcripts");
|
|
225
|
+
expect(output).not.toContain("Future occurrences of this recurring series will inherit this attribution.");
|
|
226
|
+
});
|
|
163
227
|
});
|
package/src/commands/meetings.ts
CHANGED
|
@@ -341,6 +341,9 @@ export function registerMeetingsCommand(program: Command): void {
|
|
|
341
341
|
if (!res.ok) await handleApiError(res);
|
|
342
342
|
const data = (await res.json()) as {
|
|
343
343
|
appliedToSeries?: boolean;
|
|
344
|
+
occurrencesUpdated?: number;
|
|
345
|
+
refiled?: boolean;
|
|
346
|
+
refiledCount?: number;
|
|
344
347
|
[key: string]: unknown;
|
|
345
348
|
};
|
|
346
349
|
|
|
@@ -351,7 +354,14 @@ export function registerMeetingsCommand(program: Command): void {
|
|
|
351
354
|
|
|
352
355
|
const where = companyId === "unknown" ? "unattributed (personal)" : companyId;
|
|
353
356
|
console.log(chalk.green(`\n✓ Meeting ${chalk.cyan(meetingId)} attributed to ${chalk.bold(where)}.`));
|
|
354
|
-
if (
|
|
357
|
+
if (typeof data.occurrencesUpdated === "number" && data.occurrencesUpdated > 1) {
|
|
358
|
+
console.log(chalk.dim(` Applied to ${data.occurrencesUpdated} meetings in this recurring series (including past occurrences).`));
|
|
359
|
+
} else if (data.appliedToSeries) {
|
|
360
|
+
console.log(chalk.dim(" Future occurrences of this recurring series will inherit this attribution."));
|
|
361
|
+
}
|
|
362
|
+
if (typeof data.refiledCount === "number" && data.refiledCount > 0) {
|
|
363
|
+
console.log(chalk.dim(` Refiled ${data.refiledCount} transcript${data.refiledCount === 1 ? "" : "s"} into the new company vault.`));
|
|
364
|
+
}
|
|
355
365
|
console.log();
|
|
356
366
|
} catch (err) {
|
|
357
367
|
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
package/src/index.ts
CHANGED
|
@@ -218,6 +218,8 @@ registerRescueCommand(program);
|
|
|
218
218
|
Sentry.captureException(err);
|
|
219
219
|
process.exitCode = 1;
|
|
220
220
|
} finally {
|
|
221
|
+
// Release health: finalize the per-run session before the flush.
|
|
222
|
+
Sentry.endSession();
|
|
221
223
|
await Promise.allSettled([refreshVersionCache(), Sentry.flush(2000)]);
|
|
222
224
|
}
|
|
223
225
|
})();
|
package/src/sentry.ts
CHANGED
|
@@ -3,6 +3,7 @@ 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
5
|
import { CLI_VERSION } from "./cli-version.js";
|
|
6
|
+
import { getCachedSentryUser } from "./utils/sentry-identity.js";
|
|
6
7
|
|
|
7
8
|
export function initSentry(): void {
|
|
8
9
|
const dsn = BUNDLED_DSN || process.env.SENTRY_DSN;
|
|
@@ -20,6 +21,14 @@ export function initSentry(): void {
|
|
|
20
21
|
beforeSend,
|
|
21
22
|
beforeBreadcrumb,
|
|
22
23
|
});
|
|
24
|
+
// Attribute events to the logged-in HQ identity (best-effort; null when not
|
|
25
|
+
// logged in). A CLI process is one user, so global setUser is correct here.
|
|
26
|
+
const user = getCachedSentryUser();
|
|
27
|
+
if (user) Sentry.setUser(user);
|
|
28
|
+
// Release health: one session per CLI run. Ended + flushed in index.ts's
|
|
29
|
+
// finally (covers every exit path). A captured error auto-marks the session
|
|
30
|
+
// "errored", so crash-free numbers per release stay accurate.
|
|
31
|
+
Sentry.startSession();
|
|
23
32
|
}
|
|
24
33
|
|
|
25
34
|
export { Sentry };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import {
|
|
2
|
+
loadCachedTokens,
|
|
3
|
+
isMachineIdentity,
|
|
4
|
+
loadMachineCreds,
|
|
5
|
+
} from "@indigoai-us/hq-cloud";
|
|
6
|
+
|
|
7
|
+
/** Decode the (untrusted) id-token payload — for attribution/display only. */
|
|
8
|
+
function peekIdToken(idToken: string): { email?: string; sub?: string } {
|
|
9
|
+
try {
|
|
10
|
+
const payload = idToken.split(".")[1];
|
|
11
|
+
if (!payload) return {};
|
|
12
|
+
const pad =
|
|
13
|
+
payload.length % 4 === 0 ? "" : "=".repeat(4 - (payload.length % 4));
|
|
14
|
+
const normalized = payload.replace(/-/g, "+").replace(/_/g, "/") + pad;
|
|
15
|
+
const decoded = JSON.parse(
|
|
16
|
+
Buffer.from(normalized, "base64").toString("utf-8"),
|
|
17
|
+
) as { email?: string; sub?: string };
|
|
18
|
+
return { email: decoded.email, sub: decoded.sub };
|
|
19
|
+
} catch {
|
|
20
|
+
return {};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Best-effort Sentry user from the cached HQ session, so CLI events are
|
|
26
|
+
* attributed to a user ("users affected" + per-user triage). Returns null when
|
|
27
|
+
* not logged in. A CLI process is single-user, so this is set once at init.
|
|
28
|
+
*/
|
|
29
|
+
export function getCachedSentryUser():
|
|
30
|
+
| { id?: string; email?: string; username?: string }
|
|
31
|
+
| null {
|
|
32
|
+
try {
|
|
33
|
+
if (isMachineIdentity()) {
|
|
34
|
+
const username = loadMachineCreds()?.username;
|
|
35
|
+
return username ? { id: username, username } : null;
|
|
36
|
+
}
|
|
37
|
+
const cached = loadCachedTokens();
|
|
38
|
+
if (!cached) return null;
|
|
39
|
+
const { email, sub } = peekIdToken(cached.idToken);
|
|
40
|
+
if (!sub && !email) return null;
|
|
41
|
+
return { id: sub, email };
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|