@indigoai-us/hq-cli 5.77.1 → 5.77.2

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;
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.2",
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 });