@indigoai-us/hq-cli 5.77.1 → 5.77.3

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 CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.77.2]
6
+
7
+ ### Fixed
8
+
9
+ - `hq auth login` now recognizes callback-port collisions and provides recovery
10
+ guidance instead of surfacing a raw bind exception.
11
+ - `hq company settings set --accounting-enabled <true|false>` now forwards the
12
+ accounting setting to the server, restoring the documented accounting
13
+ activation path.
14
+
5
15
  ## [5.77.1]
6
16
 
7
17
  ### Fixed
@@ -15,7 +15,7 @@
15
15
  */
16
16
  import chalk from "chalk";
17
17
  import { browserLogin, clearCachedTokens, loadCachedTokens, isExpiring, isMachineIdentity, loadMachineCreds, CognitoAuthError, } from "@indigoai-us/hq-cloud";
18
- import { refreshCachedSession, } from "../utils/cognito-session.js";
18
+ import { DEFAULT_COGNITO, refreshCachedSession, } from "../utils/cognito-session.js";
19
19
  import { cognitoConfigForLoginProvider } from "../utils/login-provider.js";
20
20
  /**
21
21
  * Decode the (unverified) ID token payload for display purposes only.
@@ -46,6 +46,17 @@ function machineIdentityLabel() {
46
46
  const creds = loadMachineCreds();
47
47
  return creds ? `machine identity ${creds.username}` : "machine identity";
48
48
  }
49
+ function isCallbackPortCollision(error) {
50
+ if (!error || typeof error !== "object")
51
+ return false;
52
+ const { code, message } = error;
53
+ return code === "EADDRINUSE" || message?.includes("EADDRINUSE") === true;
54
+ }
55
+ function callbackPortCollisionGuidance(port) {
56
+ return (` The browser-login callback port (127.0.0.1:${port}) is already in use. ` +
57
+ "Another `hq auth login` may still be waiting for browser sign-in. Finish that login, " +
58
+ "or stop its terminal/process, then retry.");
59
+ }
49
60
  export function registerAuthCommands(program) {
50
61
  const authCmd = program
51
62
  .command("auth")
@@ -72,13 +83,21 @@ export function registerAuthCommands(program) {
72
83
  console.log(chalk.dim(` Token cached at ~/.hq/cognito-tokens.json (expires ${tokens.expiresAt})`));
73
84
  }
74
85
  catch (err) {
75
- const msg = err instanceof CognitoAuthError
76
- ? err.message
77
- : err instanceof Error
86
+ const callbackPortCollision = isCallbackPortCollision(err);
87
+ const msg = callbackPortCollision
88
+ ? "Browser-login callback port is already in use."
89
+ : err instanceof CognitoAuthError
78
90
  ? err.message
79
- : String(err);
91
+ : err instanceof Error
92
+ ? err.message
93
+ : String(err);
80
94
  console.error(chalk.red(`Login failed: ${msg}`));
81
- console.error(chalk.dim(" If you do not have an account, sign up at https://onboarding.indigo-hq.com"));
95
+ if (callbackPortCollision) {
96
+ console.error(chalk.dim(callbackPortCollisionGuidance(DEFAULT_COGNITO.port ?? 8765)));
97
+ }
98
+ else {
99
+ console.error(chalk.dim(" If you do not have an account, sign up at https://onboarding.indigo-hq.com"));
100
+ }
82
101
  process.exit(1);
83
102
  }
84
103
  });
@@ -25,9 +25,10 @@ export function registerCompanyCommand(program) {
25
25
  settings
26
26
  .command("set")
27
27
  .description("Set per-company settings (PUT /company-settings). Require at least one " +
28
- "of --crm-enabled / --ontology-enabled.")
28
+ "of --crm-enabled / --ontology-enabled / --accounting-enabled.")
29
29
  .option("--crm-enabled <true|false>", "Enable/disable the native CRM for this company")
30
30
  .option("--ontology-enabled <true|false>", "Enable/disable the ontology gardener for this company")
31
+ .option("--accounting-enabled <true|false>", "Enable/disable the accounting pack for this company")
31
32
  .action(async (opts) => {
32
33
  try {
33
34
  const companySlug = company.opts().company;
@@ -35,8 +36,10 @@ export function registerCompanyCommand(program) {
35
36
  console.error(chalk.red("Error: --company <slug> is required."));
36
37
  process.exit(1);
37
38
  }
38
- if (opts.crmEnabled === undefined && opts.ontologyEnabled === undefined) {
39
- console.error(chalk.red("Error: provide at least one of --crm-enabled / --ontology-enabled."));
39
+ if (opts.crmEnabled === undefined &&
40
+ opts.ontologyEnabled === undefined &&
41
+ opts.accountingEnabled === undefined) {
42
+ console.error(chalk.red("Error: provide at least one of --crm-enabled / --ontology-enabled / --accounting-enabled."));
40
43
  process.exit(1);
41
44
  }
42
45
  const body = {};
@@ -46,6 +49,9 @@ export function registerCompanyCommand(program) {
46
49
  if (opts.ontologyEnabled !== undefined) {
47
50
  body.ontologyEnabled = parseBoolFlag(opts.ontologyEnabled, "--ontology-enabled");
48
51
  }
52
+ if (opts.accountingEnabled !== undefined) {
53
+ body.accountingEnabled = parseBoolFlag(opts.accountingEnabled, "--accounting-enabled");
54
+ }
49
55
  const token = await ensureCognitoToken();
50
56
  const companyUid = await getEntityUid(token, { companySlug });
51
57
  body.companyUid = companyUid;
@@ -381,7 +381,7 @@ export function registerFilesCommand(program) {
381
381
  });
382
382
  files
383
383
  .command("versions <path>")
384
- .description("List prior content versions and delete markers for one exact vault key. Use --personal for your personal vault.")
384
+ .description("List prior content versions and delete markers for one exact vault key. Restore a listed version with restore --version-id <id>. Use --personal for your personal vault.")
385
385
  .option("--personal", "Target your own personal vault instead of a company vault (mutually exclusive with --company)")
386
386
  .action(async (path, opts) => {
387
387
  try {
@@ -397,9 +397,9 @@ export function registerFilesCommand(program) {
397
397
  });
398
398
  files
399
399
  .command("restore <path>")
400
- .description("Restore a prior version or undelete an exact vault key. Prompts before overwriting unless --yes. Use --personal for your personal vault.")
400
+ .description("Restore a prior version with --version-id or undelete an exact vault key. Prompts before overwriting unless --yes. Use --personal for your personal vault.")
401
401
  .option("--personal", "Target your own personal vault instead of a company vault (mutually exclusive with --company)")
402
- .option("--version <id>", "Content version ID to restore")
402
+ .option("--version-id <id>", "Content version ID to restore")
403
403
  .option("-y, --yes", "Skip the overwrite confirmation prompt (for scripts)")
404
404
  .action(async (path, opts) => {
405
405
  try {
@@ -408,7 +408,7 @@ export function registerFilesCommand(program) {
408
408
  assertRecoveryScope(personal, companySlug);
409
409
  await runFilesRestore({
410
410
  key: path,
411
- versionId: opts.version,
411
+ versionId: opts.versionId,
412
412
  yes: opts.yes === true,
413
413
  personal,
414
414
  companySlug,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.77.1",
3
+ "version": "5.77.3",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -0,0 +1,82 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { Command } from "commander";
3
+
4
+ const mocks = vi.hoisted(() => ({
5
+ browserLogin: vi.fn(),
6
+ isExpiring: vi.fn(),
7
+ isMachineIdentity: vi.fn(),
8
+ loadCachedTokens: vi.fn(),
9
+ loadMachineCreds: vi.fn(),
10
+ CognitoAuthError: class TestCognitoAuthError extends Error {},
11
+ }));
12
+
13
+ vi.mock("@indigoai-us/hq-cloud", () => ({
14
+ browserLogin: mocks.browserLogin,
15
+ clearCachedTokens: vi.fn(),
16
+ isExpiring: mocks.isExpiring,
17
+ isMachineIdentity: mocks.isMachineIdentity,
18
+ loadCachedTokens: mocks.loadCachedTokens,
19
+ loadMachineCreds: mocks.loadMachineCreds,
20
+ CognitoAuthError: mocks.CognitoAuthError,
21
+ }));
22
+
23
+ vi.mock("../utils/cognito-session.js", () => ({
24
+ DEFAULT_COGNITO: {
25
+ region: "us-east-1",
26
+ userPoolDomain: "vault-indigo-hq-prod",
27
+ clientId: "client-123",
28
+ port: 8765,
29
+ identityProvider: "Google",
30
+ },
31
+ refreshCachedSession: vi.fn(),
32
+ }));
33
+
34
+ import { registerAuthCommands } from "./auth.js";
35
+
36
+ class ProcessExitError extends Error {
37
+ constructor(readonly code: number) {
38
+ super(`process.exit(${code})`);
39
+ }
40
+ }
41
+
42
+ describe("hq auth login", () => {
43
+ it("REGRESSION: explains how to recover when the callback port is held by another login", async () => {
44
+ mocks.isMachineIdentity.mockReturnValue(false);
45
+ mocks.loadCachedTokens.mockReturnValue(undefined);
46
+ mocks.browserLogin.mockRejectedValue(
47
+ Object.assign(new Error("listen EADDRINUSE: address already in use 127.0.0.1:8765"), {
48
+ code: "EADDRINUSE",
49
+ }),
50
+ );
51
+
52
+ const errors: string[] = [];
53
+ const errorSpy = vi
54
+ .spyOn(console, "error")
55
+ .mockImplementation((...args: unknown[]) => errors.push(args.map(String).join(" ")));
56
+ const exitSpy = vi
57
+ .spyOn(process, "exit")
58
+ .mockImplementation(((code?: number) => {
59
+ throw new ProcessExitError(code ?? 0);
60
+ }) as never);
61
+ const program = new Command();
62
+ program.exitOverride();
63
+ registerAuthCommands(program);
64
+
65
+ try {
66
+ await expect(program.parseAsync(["auth", "login"], { from: "user" })).rejects.toMatchObject({
67
+ code: 1,
68
+ });
69
+ } finally {
70
+ errorSpy.mockRestore();
71
+ exitSpy.mockRestore();
72
+ }
73
+
74
+ const output = errors.join("\n");
75
+ expect(output).toContain("Login failed: Browser-login callback port is already in use.");
76
+ expect(output).not.toContain("EADDRINUSE");
77
+ expect(output).toContain("callback port (127.0.0.1:8765) is already in use");
78
+ expect(output).toContain("Another `hq auth login` may still be waiting");
79
+ expect(output).toContain("stop its terminal/process, then retry");
80
+ expect(output).not.toContain("sign up at");
81
+ });
82
+ });
@@ -66,6 +66,20 @@ function machineIdentityLabel(): string {
66
66
  return creds ? `machine identity ${creds.username}` : "machine identity";
67
67
  }
68
68
 
69
+ function isCallbackPortCollision(error: unknown): boolean {
70
+ if (!error || typeof error !== "object") return false;
71
+ const { code, message } = error as NodeJS.ErrnoException;
72
+ return code === "EADDRINUSE" || message?.includes("EADDRINUSE") === true;
73
+ }
74
+
75
+ function callbackPortCollisionGuidance(port: number): string {
76
+ return (
77
+ ` The browser-login callback port (127.0.0.1:${port}) is already in use. ` +
78
+ "Another `hq auth login` may still be waiting for browser sign-in. Finish that login, " +
79
+ "or stop its terminal/process, then retry."
80
+ );
81
+ }
82
+
69
83
  export function registerAuthCommands(program: Command): void {
70
84
  const authCmd = program
71
85
  .command("auth")
@@ -105,18 +119,27 @@ export function registerAuthCommands(program: Command): void {
105
119
  chalk.dim(` Token cached at ~/.hq/cognito-tokens.json (expires ${tokens.expiresAt})`),
106
120
  );
107
121
  } catch (err) {
122
+ const callbackPortCollision = isCallbackPortCollision(err);
108
123
  const msg =
109
- err instanceof CognitoAuthError
110
- ? err.message
111
- : err instanceof Error
124
+ callbackPortCollision
125
+ ? "Browser-login callback port is already in use."
126
+ : err instanceof CognitoAuthError
112
127
  ? err.message
113
- : String(err);
128
+ : err instanceof Error
129
+ ? err.message
130
+ : String(err);
114
131
  console.error(chalk.red(`Login failed: ${msg}`));
115
- console.error(
116
- chalk.dim(
117
- " If you do not have an account, sign up at https://onboarding.indigo-hq.com",
118
- ),
119
- );
132
+ if (callbackPortCollision) {
133
+ console.error(
134
+ chalk.dim(callbackPortCollisionGuidance(DEFAULT_COGNITO.port ?? 8765)),
135
+ );
136
+ } else {
137
+ console.error(
138
+ chalk.dim(
139
+ " If you do not have an account, sign up at https://onboarding.indigo-hq.com",
140
+ ),
141
+ );
142
+ }
120
143
  process.exit(1);
121
144
  }
122
145
  });
@@ -104,6 +104,31 @@ describe("hq company settings set", () => {
104
104
  expect(sent.ontologyEnabled).toBeUndefined();
105
105
  });
106
106
 
107
+ it("PUTs accountingEnabled=true with the resolved companyUid", async () => {
108
+ fetchSpy.mockResolvedValueOnce(
109
+ jsonResponse(200, { companySettings: { accountingEnabled: true } }),
110
+ );
111
+
112
+ await run([
113
+ "company",
114
+ "settings",
115
+ "set",
116
+ "--company",
117
+ "acme",
118
+ "--accounting-enabled",
119
+ "true",
120
+ ]);
121
+
122
+ expect(fetchSpy).toHaveBeenCalledTimes(1);
123
+ const sent = JSON.parse(fetchSpy.mock.calls[0][1]?.body as string);
124
+ expect(sent).toMatchObject({
125
+ companyUid: "cmp_acme",
126
+ accountingEnabled: true,
127
+ });
128
+ expect(sent.crmEnabled).toBeUndefined();
129
+ expect(sent.ontologyEnabled).toBeUndefined();
130
+ });
131
+
107
132
  it("PUTs both flags when both are supplied", async () => {
108
133
  fetchSpy.mockResolvedValueOnce(jsonResponse(200, {}));
109
134
 
@@ -7,8 +7,9 @@ import { vaultApiFetch, getEntityUid } from "../utils/vault-api.js";
7
7
  * `hq company settings set` — owner-only per-company settings toggles
8
8
  * (agency group-grants US-007 / hq-native-crm US-003). Wraps
9
9
  * `PUT /company-settings`, setting the runtime-flippable `crmEnabled` /
10
- * `ontologyEnabled` flags so an owner can enable the native CRM (and the
11
- * ontology gardener) for a company without a redeploy.
10
+ * `ontologyEnabled` / `accountingEnabled` flags so an owner can enable the
11
+ * native CRM, ontology gardener, or accounting pack for a company without a
12
+ * redeploy.
12
13
  *
13
14
  * Conventions mirror `secrets.ts`: ensureCognitoToken() → getEntityUid() →
14
15
  * vaultApiFetch() → error-check → chalk output.
@@ -17,6 +18,7 @@ import { vaultApiFetch, getEntityUid } from "../utils/vault-api.js";
17
18
  interface SettingsSetFlags {
18
19
  crmEnabled?: string;
19
20
  ontologyEnabled?: string;
21
+ accountingEnabled?: string;
20
22
  }
21
23
 
22
24
  /**
@@ -45,7 +47,7 @@ export function registerCompanyCommand(program: Command): void {
45
47
  .command("set")
46
48
  .description(
47
49
  "Set per-company settings (PUT /company-settings). Require at least one " +
48
- "of --crm-enabled / --ontology-enabled.",
50
+ "of --crm-enabled / --ontology-enabled / --accounting-enabled.",
49
51
  )
50
52
  .option(
51
53
  "--crm-enabled <true|false>",
@@ -55,6 +57,10 @@ export function registerCompanyCommand(program: Command): void {
55
57
  "--ontology-enabled <true|false>",
56
58
  "Enable/disable the ontology gardener for this company",
57
59
  )
60
+ .option(
61
+ "--accounting-enabled <true|false>",
62
+ "Enable/disable the accounting pack for this company",
63
+ )
58
64
  .action(async (opts: SettingsSetFlags) => {
59
65
  try {
60
66
  const companySlug = company.opts().company as string | undefined;
@@ -62,10 +68,14 @@ export function registerCompanyCommand(program: Command): void {
62
68
  console.error(chalk.red("Error: --company <slug> is required."));
63
69
  process.exit(1);
64
70
  }
65
- if (opts.crmEnabled === undefined && opts.ontologyEnabled === undefined) {
71
+ if (
72
+ opts.crmEnabled === undefined &&
73
+ opts.ontologyEnabled === undefined &&
74
+ opts.accountingEnabled === undefined
75
+ ) {
66
76
  console.error(
67
77
  chalk.red(
68
- "Error: provide at least one of --crm-enabled / --ontology-enabled.",
78
+ "Error: provide at least one of --crm-enabled / --ontology-enabled / --accounting-enabled.",
69
79
  ),
70
80
  );
71
81
  process.exit(1);
@@ -81,6 +91,12 @@ export function registerCompanyCommand(program: Command): void {
81
91
  "--ontology-enabled",
82
92
  );
83
93
  }
94
+ if (opts.accountingEnabled !== undefined) {
95
+ body.accountingEnabled = parseBoolFlag(
96
+ opts.accountingEnabled,
97
+ "--accounting-enabled",
98
+ );
99
+ }
84
100
 
85
101
  const token = await ensureCognitoToken();
86
102
  const companyUid = await getEntityUid(token, { companySlug });
@@ -308,18 +308,20 @@ describe("hq files trash", () => {
308
308
  });
309
309
 
310
310
  describe("hq files recovery command help", () => {
311
- it("documents the finalized --version and --prefix option forms", () => {
311
+ it("documents the finalized --version-id and --prefix option forms", () => {
312
312
  const program = buildProgram();
313
313
  const files = program.commands.find((command) => command.name() === "files");
314
314
  const restore = files?.commands.find((command) => command.name() === "restore");
315
315
  const trash = files?.commands.find((command) => command.name() === "trash");
316
316
 
317
317
  expect(files?.helpInformation()).toContain("versions [options] <path>");
318
- expect(restore?.helpInformation()).toContain("--version <id>");
318
+ expect(files?.helpInformation()).toContain("restore --version-id <id>");
319
+ expect(restore?.helpInformation()).toContain("--version-id <id>");
320
+ expect(restore?.helpInformation()).not.toContain("--version <id>");
319
321
  expect(trash?.helpInformation()).toContain("--prefix <prefix>");
320
322
  });
321
323
 
322
- it("passes --version to the restore request", async () => {
324
+ it("passes --version-id to the restore request", async () => {
323
325
  fetchSpy.mockResolvedValueOnce(membershipResponse());
324
326
  fetchSpy.mockResolvedValueOnce(
325
327
  jsonResponse(200, {
@@ -331,7 +333,14 @@ describe("hq files recovery command help", () => {
331
333
  );
332
334
 
333
335
  await buildProgram().parseAsync(
334
- ["files", "restore", "notes/a.md", "--version", "old-version", "--yes"],
336
+ [
337
+ "files",
338
+ "restore",
339
+ "notes/a.md",
340
+ "--version-id",
341
+ "old-version",
342
+ "--yes",
343
+ ],
335
344
  { from: "user" },
336
345
  );
337
346
 
@@ -515,7 +515,7 @@ export function registerFilesCommand(program: Command): Command {
515
515
  files
516
516
  .command("versions <path>")
517
517
  .description(
518
- "List prior content versions and delete markers for one exact vault key. Use --personal for your personal vault.",
518
+ "List prior content versions and delete markers for one exact vault key. Restore a listed version with restore --version-id <id>. Use --personal for your personal vault.",
519
519
  )
520
520
  .option(
521
521
  "--personal",
@@ -539,18 +539,18 @@ export function registerFilesCommand(program: Command): Command {
539
539
  files
540
540
  .command("restore <path>")
541
541
  .description(
542
- "Restore a prior version or undelete an exact vault key. Prompts before overwriting unless --yes. Use --personal for your personal vault.",
542
+ "Restore a prior version with --version-id or undelete an exact vault key. Prompts before overwriting unless --yes. Use --personal for your personal vault.",
543
543
  )
544
544
  .option(
545
545
  "--personal",
546
546
  "Target your own personal vault instead of a company vault (mutually exclusive with --company)",
547
547
  )
548
- .option("--version <id>", "Content version ID to restore")
548
+ .option("--version-id <id>", "Content version ID to restore")
549
549
  .option("-y, --yes", "Skip the overwrite confirmation prompt (for scripts)")
550
550
  .action(
551
551
  async (
552
552
  path: string,
553
- opts: { personal?: boolean; yes?: boolean; version?: string },
553
+ opts: { personal?: boolean; yes?: boolean; versionId?: string },
554
554
  ) => {
555
555
  try {
556
556
  const companySlug = files.opts().company as string | undefined;
@@ -558,7 +558,7 @@ export function registerFilesCommand(program: Command): Command {
558
558
  assertRecoveryScope(personal, companySlug);
559
559
  await runFilesRestore({
560
560
  key: path,
561
- versionId: opts.version,
561
+ versionId: opts.versionId,
562
562
  yes: opts.yes === true,
563
563
  personal,
564
564
  companySlug,