@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 +10 -0
- package/dist/commands/auth.js +25 -6
- package/dist/commands/company.js +9 -3
- package/package.json +1 -1
- package/src/commands/auth.test.ts +82 -0
- package/src/commands/auth.ts +32 -9
- package/src/commands/company.test.ts +25 -0
- package/src/commands/company.ts +21 -5
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
|
package/dist/commands/auth.js
CHANGED
|
@@ -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
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
:
|
|
91
|
+
: err instanceof Error
|
|
92
|
+
? err.message
|
|
93
|
+
: String(err);
|
|
80
94
|
console.error(chalk.red(`Login failed: ${msg}`));
|
|
81
|
-
|
|
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
|
});
|
package/dist/commands/company.js
CHANGED
|
@@ -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 &&
|
|
39
|
-
|
|
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
|
@@ -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
|
+
});
|
package/src/commands/auth.ts
CHANGED
|
@@ -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
|
-
|
|
110
|
-
?
|
|
111
|
-
: err instanceof
|
|
124
|
+
callbackPortCollision
|
|
125
|
+
? "Browser-login callback port is already in use."
|
|
126
|
+
: err instanceof CognitoAuthError
|
|
112
127
|
? err.message
|
|
113
|
-
:
|
|
128
|
+
: err instanceof Error
|
|
129
|
+
? err.message
|
|
130
|
+
: String(err);
|
|
114
131
|
console.error(chalk.red(`Login failed: ${msg}`));
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
|
package/src/commands/company.ts
CHANGED
|
@@ -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
|
|
11
|
-
* ontology gardener
|
|
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 (
|
|
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 });
|