@indigoai-us/hq-cli 5.47.0 → 5.47.1
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.
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `hq onboard join` planning helper.
|
|
3
|
+
*
|
|
4
|
+
* Modern HQ invites are email-keyed and TOKENLESS (hq-pro schemaVersion 2+):
|
|
5
|
+
* the membership row is claimed when the invitee signs into HQ with the
|
|
6
|
+
* invited email, and `hq sync` then discovers every company they belong to
|
|
7
|
+
* and pulls it locally. There is no token to redeem.
|
|
8
|
+
*
|
|
9
|
+
* The legacy path (schemaVersion ≤ 1) handed out a random `inviteToken` the
|
|
10
|
+
* invitee redeemed via `hq://accept/{token}`. `hq onboard join` historically
|
|
11
|
+
* declared `--invite-token` as a commander `requiredOption`, so a modern
|
|
12
|
+
* invitee — who has no token — could not even reach the action handler. They
|
|
13
|
+
* got `error: required option '--invite-token <token>' not specified`, which
|
|
14
|
+
* reads as "my HQ doesn't know I'm a member / I'm missing a token" when in
|
|
15
|
+
* fact the invite is fine and they simply need to sync.
|
|
16
|
+
*
|
|
17
|
+
* This module decides which path a given invocation is on and produces the
|
|
18
|
+
* tokenless guidance, as a pure function so it can be unit-tested without
|
|
19
|
+
* commander, the network, or a VaultClient (same shape as
|
|
20
|
+
* `onboard-identity-guard.ts`).
|
|
21
|
+
*/
|
|
22
|
+
export interface JoinTokenless {
|
|
23
|
+
kind: "tokenless";
|
|
24
|
+
/** Human-facing guidance pointing at the real (sync) path. */
|
|
25
|
+
guidance: string;
|
|
26
|
+
}
|
|
27
|
+
export interface JoinLegacy {
|
|
28
|
+
kind: "legacy";
|
|
29
|
+
inviteToken: string;
|
|
30
|
+
email: string;
|
|
31
|
+
personName: string;
|
|
32
|
+
}
|
|
33
|
+
export interface JoinError {
|
|
34
|
+
kind: "error";
|
|
35
|
+
message: string;
|
|
36
|
+
}
|
|
37
|
+
export type JoinPlan = JoinTokenless | JoinLegacy | JoinError;
|
|
38
|
+
export interface JoinOptionsInput {
|
|
39
|
+
inviteToken?: string;
|
|
40
|
+
email?: string;
|
|
41
|
+
personName?: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Guidance shown when `hq onboard join` is run without a token. Covers both
|
|
45
|
+
* the CLI (`hq sync`) and the HQ Sync menubar app (the Sync button), since
|
|
46
|
+
* most invitees are on the app rather than the CLI.
|
|
47
|
+
*/
|
|
48
|
+
export declare function tokenlessJoinGuidance(): string;
|
|
49
|
+
/**
|
|
50
|
+
* Decide how an `hq onboard join` invocation should proceed.
|
|
51
|
+
*
|
|
52
|
+
* - No token → tokenless: the invitee belongs on the `hq sync` path. Email
|
|
53
|
+
* and person-name are irrelevant here (sync uses the cached Cognito
|
|
54
|
+
* identity), so they are ignored rather than required.
|
|
55
|
+
* - Token → legacy: the historical redeem path. `--email` and
|
|
56
|
+
* `--person-name` are still required to bootstrap the person entity; a
|
|
57
|
+
* missing one yields an actionable error instead of a commander stack trace.
|
|
58
|
+
*/
|
|
59
|
+
export declare function planOnboardJoin(opts: JoinOptionsInput): JoinPlan;
|
|
60
|
+
//# sourceMappingURL=onboard-join.d.ts.map
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `hq onboard join` planning helper.
|
|
3
|
+
*
|
|
4
|
+
* Modern HQ invites are email-keyed and TOKENLESS (hq-pro schemaVersion 2+):
|
|
5
|
+
* the membership row is claimed when the invitee signs into HQ with the
|
|
6
|
+
* invited email, and `hq sync` then discovers every company they belong to
|
|
7
|
+
* and pulls it locally. There is no token to redeem.
|
|
8
|
+
*
|
|
9
|
+
* The legacy path (schemaVersion ≤ 1) handed out a random `inviteToken` the
|
|
10
|
+
* invitee redeemed via `hq://accept/{token}`. `hq onboard join` historically
|
|
11
|
+
* declared `--invite-token` as a commander `requiredOption`, so a modern
|
|
12
|
+
* invitee — who has no token — could not even reach the action handler. They
|
|
13
|
+
* got `error: required option '--invite-token <token>' not specified`, which
|
|
14
|
+
* reads as "my HQ doesn't know I'm a member / I'm missing a token" when in
|
|
15
|
+
* fact the invite is fine and they simply need to sync.
|
|
16
|
+
*
|
|
17
|
+
* This module decides which path a given invocation is on and produces the
|
|
18
|
+
* tokenless guidance, as a pure function so it can be unit-tested without
|
|
19
|
+
* commander, the network, or a VaultClient (same shape as
|
|
20
|
+
* `onboard-identity-guard.ts`).
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Guidance shown when `hq onboard join` is run without a token. Covers both
|
|
24
|
+
* the CLI (`hq sync`) and the HQ Sync menubar app (the Sync button), since
|
|
25
|
+
* most invitees are on the app rather than the CLI.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
!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]="38623485-912f-52db-bf3d-f1c911246026")}catch(e){}}();
|
|
29
|
+
export function tokenlessJoinGuidance() {
|
|
30
|
+
return [
|
|
31
|
+
"Modern HQ invites don't use a token — there's nothing to redeem.",
|
|
32
|
+
"",
|
|
33
|
+
"If you've accepted your invite (the email link or HQ Console), your",
|
|
34
|
+
"membership already exists. To pull it onto this machine, just sync:",
|
|
35
|
+
"",
|
|
36
|
+
" hq sync",
|
|
37
|
+
"",
|
|
38
|
+
"That signs you in, finds every company you've been added to, and pulls",
|
|
39
|
+
"it locally. In the HQ Sync menubar app, click Sync — same result.",
|
|
40
|
+
"",
|
|
41
|
+
"(--invite-token is only for legacy token invites and is no longer issued.)",
|
|
42
|
+
].join("\n");
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Decide how an `hq onboard join` invocation should proceed.
|
|
46
|
+
*
|
|
47
|
+
* - No token → tokenless: the invitee belongs on the `hq sync` path. Email
|
|
48
|
+
* and person-name are irrelevant here (sync uses the cached Cognito
|
|
49
|
+
* identity), so they are ignored rather than required.
|
|
50
|
+
* - Token → legacy: the historical redeem path. `--email` and
|
|
51
|
+
* `--person-name` are still required to bootstrap the person entity; a
|
|
52
|
+
* missing one yields an actionable error instead of a commander stack trace.
|
|
53
|
+
*/
|
|
54
|
+
export function planOnboardJoin(opts) {
|
|
55
|
+
const inviteToken = (opts.inviteToken ?? "").trim();
|
|
56
|
+
if (!inviteToken) {
|
|
57
|
+
return { kind: "tokenless", guidance: tokenlessJoinGuidance() };
|
|
58
|
+
}
|
|
59
|
+
const email = (opts.email ?? "").trim();
|
|
60
|
+
const personName = (opts.personName ?? "").trim();
|
|
61
|
+
const missing = [];
|
|
62
|
+
if (!email)
|
|
63
|
+
missing.push("--email");
|
|
64
|
+
if (!personName)
|
|
65
|
+
missing.push("--person-name");
|
|
66
|
+
if (missing.length > 0) {
|
|
67
|
+
return {
|
|
68
|
+
kind: "error",
|
|
69
|
+
message: `Legacy token join (--invite-token) also requires ${missing.join(" and ")}. ` +
|
|
70
|
+
"Most invites are tokenless — omit --invite-token and run `hq sync` instead.",
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return { kind: "legacy", inviteToken, email, personName };
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=onboard-join.js.map
|
|
76
|
+
//# debugId=38623485-912f-52db-bf3d-f1c911246026
|
package/dist/commands/onboard.js
CHANGED
|
@@ -19,12 +19,13 @@
|
|
|
19
19
|
* browser-OAuth flow opens automatically.
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
!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]="
|
|
22
|
+
!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]="05a49c9b-23bc-5b96-b5e1-cc2e573b11c3")}catch(e){}}();
|
|
23
23
|
import chalk from "chalk";
|
|
24
24
|
import { runOnboardCli, readCheckpoint } from "@indigoai-us/hq-onboarding";
|
|
25
25
|
import { DEFAULT_HQ_ROOT, DEFAULT_VAULT_API_URL, ensureCognitoToken, buildVaultConfig, } from "../utils/cognito-session.js";
|
|
26
26
|
import { createDefaultVaultClient } from "./cloud-provision.js";
|
|
27
27
|
import { detectOnboardingIdentityMismatch } from "./onboard-identity-guard.js";
|
|
28
|
+
import { planOnboardJoin } from "./onboard-join.js";
|
|
28
29
|
// ---------------------------------------------------------------------------
|
|
29
30
|
// Command registration
|
|
30
31
|
// ---------------------------------------------------------------------------
|
|
@@ -49,8 +50,8 @@ export function registerOnboardCommand(program) {
|
|
|
49
50
|
console.log(` Person: ${options.personName} <${options.email}>`);
|
|
50
51
|
console.log(` HQ root: ${options.hqRoot}\n`);
|
|
51
52
|
console.log(chalk.gray(" This creates a NEW company you own. Joining a teammate's existing\n" +
|
|
52
|
-
" company? Stop
|
|
53
|
-
"
|
|
53
|
+
" company? Stop — accept your invite, then run `hq sync` to pull it.\n" +
|
|
54
|
+
" Creating a same-named company leaves you alone in a separate one.\n"));
|
|
54
55
|
const accessToken = await ensureCognitoToken();
|
|
55
56
|
const result = await runOnboardCli({
|
|
56
57
|
mode: "create-company",
|
|
@@ -74,21 +75,39 @@ export function registerOnboardCommand(program) {
|
|
|
74
75
|
onboard
|
|
75
76
|
.command("join")
|
|
76
77
|
.description("Accept an invite and join an existing company")
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
// Modern invites are tokenless (email-keyed); the token, email, and
|
|
79
|
+
// person-name are required ONLY on the legacy redeem path. Declaring them
|
|
80
|
+
// as plain options (not requiredOption) lets a tokenless invitee reach the
|
|
81
|
+
// action and be routed to `hq sync` instead of hitting a commander
|
|
82
|
+
// "required option not specified" wall that reads as "I have no token".
|
|
83
|
+
.option("--invite-token <token>", "Legacy magic-link token (modern invites don't need one)")
|
|
84
|
+
.option("--email <email>", "Your email (legacy token join only; must match Cognito sign-in)")
|
|
85
|
+
.option("--person-name <name>", "Your display name (legacy token join only)")
|
|
80
86
|
.option("--hq-root <path>", `Local HQ tree root (default: ${DEFAULT_HQ_ROOT})`, DEFAULT_HQ_ROOT)
|
|
81
87
|
.action(async (options) => {
|
|
82
88
|
try {
|
|
83
89
|
console.log(chalk.bold(`\nHQ Onboard — Join Company`));
|
|
84
|
-
|
|
90
|
+
const plan = planOnboardJoin({
|
|
91
|
+
inviteToken: options.inviteToken,
|
|
92
|
+
email: options.email,
|
|
93
|
+
personName: options.personName,
|
|
94
|
+
});
|
|
95
|
+
if (plan.kind === "tokenless") {
|
|
96
|
+
console.log(`\n${plan.guidance}\n`);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (plan.kind === "error") {
|
|
100
|
+
console.error(chalk.red(`\n✗ ${plan.message}`));
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
console.log(` Person: ${plan.personName} <${plan.email}>`);
|
|
85
104
|
console.log(` HQ root: ${options.hqRoot}\n`);
|
|
86
105
|
const accessToken = await ensureCognitoToken();
|
|
87
106
|
const result = await runOnboardCli({
|
|
88
107
|
mode: "join-company",
|
|
89
|
-
personName:
|
|
90
|
-
personEmail:
|
|
91
|
-
inviteToken:
|
|
108
|
+
personName: plan.personName,
|
|
109
|
+
personEmail: plan.email,
|
|
110
|
+
inviteToken: plan.inviteToken,
|
|
92
111
|
vaultConfig: buildVaultConfig(accessToken),
|
|
93
112
|
hqRoot: options.hqRoot,
|
|
94
113
|
});
|
|
@@ -179,4 +198,4 @@ export function registerOnboardCommand(program) {
|
|
|
179
198
|
});
|
|
180
199
|
}
|
|
181
200
|
//# sourceMappingURL=onboard.js.map
|
|
182
|
-
//# debugId=
|
|
201
|
+
//# debugId=05a49c9b-23bc-5b96-b5e1-cc2e573b11c3
|
package/package.json
CHANGED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression tests for the `hq onboard join` tokenless routing
|
|
3
|
+
* (invite-accept-local-attach).
|
|
4
|
+
*
|
|
5
|
+
* The bug: `hq onboard join` declared `--invite-token` as a commander
|
|
6
|
+
* requiredOption, so a modern (tokenless, email-keyed) invitee could not reach
|
|
7
|
+
* the action — they got "required option '--invite-token' not specified",
|
|
8
|
+
* which reads as "my HQ doesn't know I'm a member". The fix routes the
|
|
9
|
+
* no-token case to the real path (`hq sync`) and keeps the legacy token path
|
|
10
|
+
* intact. `planOnboardJoin` is a pure function — no network, no VaultClient.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, expect, it } from "vitest";
|
|
14
|
+
|
|
15
|
+
import { planOnboardJoin, tokenlessJoinGuidance } from "./onboard-join.js";
|
|
16
|
+
|
|
17
|
+
describe("planOnboardJoin", () => {
|
|
18
|
+
it("routes a no-token invocation to the tokenless (sync) path", () => {
|
|
19
|
+
const plan = planOnboardJoin({});
|
|
20
|
+
expect(plan.kind).toBe("tokenless");
|
|
21
|
+
if (plan.kind === "tokenless") {
|
|
22
|
+
expect(plan.guidance).toBe(tokenlessJoinGuidance());
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("treats an empty / whitespace-only token as tokenless", () => {
|
|
27
|
+
expect(planOnboardJoin({ inviteToken: "" }).kind).toBe("tokenless");
|
|
28
|
+
expect(planOnboardJoin({ inviteToken: " " }).kind).toBe("tokenless");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("ignores email / person-name on the tokenless path (sync uses cached identity)", () => {
|
|
32
|
+
// A user who copy-pastes old instructions may still pass these — they must
|
|
33
|
+
// not flip the decision or be required.
|
|
34
|
+
const plan = planOnboardJoin({
|
|
35
|
+
email: "richard@sender.agency",
|
|
36
|
+
personName: "Richard",
|
|
37
|
+
});
|
|
38
|
+
expect(plan.kind).toBe("tokenless");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("keeps the legacy path when a token + email + person-name are all present", () => {
|
|
42
|
+
const plan = planOnboardJoin({
|
|
43
|
+
inviteToken: "tok_abc123",
|
|
44
|
+
email: "richard@sender.agency",
|
|
45
|
+
personName: "Richard",
|
|
46
|
+
});
|
|
47
|
+
expect(plan.kind).toBe("legacy");
|
|
48
|
+
if (plan.kind === "legacy") {
|
|
49
|
+
expect(plan.inviteToken).toBe("tok_abc123");
|
|
50
|
+
expect(plan.email).toBe("richard@sender.agency");
|
|
51
|
+
expect(plan.personName).toBe("Richard");
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("trims the token before deciding", () => {
|
|
56
|
+
const plan = planOnboardJoin({
|
|
57
|
+
inviteToken: " tok_abc123 ",
|
|
58
|
+
email: "r@x.co",
|
|
59
|
+
personName: "R",
|
|
60
|
+
});
|
|
61
|
+
expect(plan.kind).toBe("legacy");
|
|
62
|
+
if (plan.kind === "legacy") {
|
|
63
|
+
expect(plan.inviteToken).toBe("tok_abc123");
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("errors actionably when a token is given but email/person-name are missing", () => {
|
|
68
|
+
const plan = planOnboardJoin({ inviteToken: "tok_abc123" });
|
|
69
|
+
expect(plan.kind).toBe("error");
|
|
70
|
+
if (plan.kind === "error") {
|
|
71
|
+
expect(plan.message).toContain("--email");
|
|
72
|
+
expect(plan.message).toContain("--person-name");
|
|
73
|
+
// Steers the likely-confused user back to the tokenless path.
|
|
74
|
+
expect(plan.message).toContain("hq sync");
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("names only the missing legacy field", () => {
|
|
79
|
+
const plan = planOnboardJoin({ inviteToken: "tok_abc123", email: "r@x.co" });
|
|
80
|
+
expect(plan.kind).toBe("error");
|
|
81
|
+
if (plan.kind === "error") {
|
|
82
|
+
expect(plan.message).toContain("--person-name");
|
|
83
|
+
expect(plan.message).not.toContain("--email and");
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("tokenlessJoinGuidance", () => {
|
|
89
|
+
it("points at both the CLI sync and the menubar Sync button, and disclaims the token", () => {
|
|
90
|
+
const g = tokenlessJoinGuidance();
|
|
91
|
+
expect(g).toContain("hq sync");
|
|
92
|
+
expect(g).toMatch(/menubar|Sync/);
|
|
93
|
+
expect(g).toContain("--invite-token");
|
|
94
|
+
expect(g.toLowerCase()).toContain("don't use a token");
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `hq onboard join` planning helper.
|
|
3
|
+
*
|
|
4
|
+
* Modern HQ invites are email-keyed and TOKENLESS (hq-pro schemaVersion 2+):
|
|
5
|
+
* the membership row is claimed when the invitee signs into HQ with the
|
|
6
|
+
* invited email, and `hq sync` then discovers every company they belong to
|
|
7
|
+
* and pulls it locally. There is no token to redeem.
|
|
8
|
+
*
|
|
9
|
+
* The legacy path (schemaVersion ≤ 1) handed out a random `inviteToken` the
|
|
10
|
+
* invitee redeemed via `hq://accept/{token}`. `hq onboard join` historically
|
|
11
|
+
* declared `--invite-token` as a commander `requiredOption`, so a modern
|
|
12
|
+
* invitee — who has no token — could not even reach the action handler. They
|
|
13
|
+
* got `error: required option '--invite-token <token>' not specified`, which
|
|
14
|
+
* reads as "my HQ doesn't know I'm a member / I'm missing a token" when in
|
|
15
|
+
* fact the invite is fine and they simply need to sync.
|
|
16
|
+
*
|
|
17
|
+
* This module decides which path a given invocation is on and produces the
|
|
18
|
+
* tokenless guidance, as a pure function so it can be unit-tested without
|
|
19
|
+
* commander, the network, or a VaultClient (same shape as
|
|
20
|
+
* `onboard-identity-guard.ts`).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export interface JoinTokenless {
|
|
24
|
+
kind: "tokenless";
|
|
25
|
+
/** Human-facing guidance pointing at the real (sync) path. */
|
|
26
|
+
guidance: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface JoinLegacy {
|
|
30
|
+
kind: "legacy";
|
|
31
|
+
inviteToken: string;
|
|
32
|
+
email: string;
|
|
33
|
+
personName: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface JoinError {
|
|
37
|
+
kind: "error";
|
|
38
|
+
message: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type JoinPlan = JoinTokenless | JoinLegacy | JoinError;
|
|
42
|
+
|
|
43
|
+
export interface JoinOptionsInput {
|
|
44
|
+
inviteToken?: string;
|
|
45
|
+
email?: string;
|
|
46
|
+
personName?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Guidance shown when `hq onboard join` is run without a token. Covers both
|
|
51
|
+
* the CLI (`hq sync`) and the HQ Sync menubar app (the Sync button), since
|
|
52
|
+
* most invitees are on the app rather than the CLI.
|
|
53
|
+
*/
|
|
54
|
+
export function tokenlessJoinGuidance(): string {
|
|
55
|
+
return [
|
|
56
|
+
"Modern HQ invites don't use a token — there's nothing to redeem.",
|
|
57
|
+
"",
|
|
58
|
+
"If you've accepted your invite (the email link or HQ Console), your",
|
|
59
|
+
"membership already exists. To pull it onto this machine, just sync:",
|
|
60
|
+
"",
|
|
61
|
+
" hq sync",
|
|
62
|
+
"",
|
|
63
|
+
"That signs you in, finds every company you've been added to, and pulls",
|
|
64
|
+
"it locally. In the HQ Sync menubar app, click Sync — same result.",
|
|
65
|
+
"",
|
|
66
|
+
"(--invite-token is only for legacy token invites and is no longer issued.)",
|
|
67
|
+
].join("\n");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Decide how an `hq onboard join` invocation should proceed.
|
|
72
|
+
*
|
|
73
|
+
* - No token → tokenless: the invitee belongs on the `hq sync` path. Email
|
|
74
|
+
* and person-name are irrelevant here (sync uses the cached Cognito
|
|
75
|
+
* identity), so they are ignored rather than required.
|
|
76
|
+
* - Token → legacy: the historical redeem path. `--email` and
|
|
77
|
+
* `--person-name` are still required to bootstrap the person entity; a
|
|
78
|
+
* missing one yields an actionable error instead of a commander stack trace.
|
|
79
|
+
*/
|
|
80
|
+
export function planOnboardJoin(opts: JoinOptionsInput): JoinPlan {
|
|
81
|
+
const inviteToken = (opts.inviteToken ?? "").trim();
|
|
82
|
+
if (!inviteToken) {
|
|
83
|
+
return { kind: "tokenless", guidance: tokenlessJoinGuidance() };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const email = (opts.email ?? "").trim();
|
|
87
|
+
const personName = (opts.personName ?? "").trim();
|
|
88
|
+
const missing: string[] = [];
|
|
89
|
+
if (!email) missing.push("--email");
|
|
90
|
+
if (!personName) missing.push("--person-name");
|
|
91
|
+
if (missing.length > 0) {
|
|
92
|
+
return {
|
|
93
|
+
kind: "error",
|
|
94
|
+
message:
|
|
95
|
+
`Legacy token join (--invite-token) also requires ${missing.join(" and ")}. ` +
|
|
96
|
+
"Most invites are tokenless — omit --invite-token and run `hq sync` instead.",
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return { kind: "legacy", inviteToken, email, personName };
|
|
101
|
+
}
|
package/src/commands/onboard.ts
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
} from "../utils/cognito-session.js";
|
|
32
32
|
import { createDefaultVaultClient } from "./cloud-provision.js";
|
|
33
33
|
import { detectOnboardingIdentityMismatch } from "./onboard-identity-guard.js";
|
|
34
|
+
import { planOnboardJoin } from "./onboard-join.js";
|
|
34
35
|
|
|
35
36
|
// ---------------------------------------------------------------------------
|
|
36
37
|
// Command registration
|
|
@@ -72,8 +73,8 @@ export function registerOnboardCommand(program: Command): void {
|
|
|
72
73
|
console.log(
|
|
73
74
|
chalk.gray(
|
|
74
75
|
" This creates a NEW company you own. Joining a teammate's existing\n" +
|
|
75
|
-
" company? Stop
|
|
76
|
-
"
|
|
76
|
+
" company? Stop — accept your invite, then run `hq sync` to pull it.\n" +
|
|
77
|
+
" Creating a same-named company leaves you alone in a separate one.\n",
|
|
77
78
|
),
|
|
78
79
|
);
|
|
79
80
|
|
|
@@ -104,31 +105,53 @@ export function registerOnboardCommand(program: Command): void {
|
|
|
104
105
|
onboard
|
|
105
106
|
.command("join")
|
|
106
107
|
.description("Accept an invite and join an existing company")
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
108
|
+
// Modern invites are tokenless (email-keyed); the token, email, and
|
|
109
|
+
// person-name are required ONLY on the legacy redeem path. Declaring them
|
|
110
|
+
// as plain options (not requiredOption) lets a tokenless invitee reach the
|
|
111
|
+
// action and be routed to `hq sync` instead of hitting a commander
|
|
112
|
+
// "required option not specified" wall that reads as "I have no token".
|
|
113
|
+
.option("--invite-token <token>", "Legacy magic-link token (modern invites don't need one)")
|
|
114
|
+
.option("--email <email>", "Your email (legacy token join only; must match Cognito sign-in)")
|
|
115
|
+
.option("--person-name <name>", "Your display name (legacy token join only)")
|
|
110
116
|
.option(
|
|
111
117
|
"--hq-root <path>",
|
|
112
118
|
`Local HQ tree root (default: ${DEFAULT_HQ_ROOT})`,
|
|
113
119
|
DEFAULT_HQ_ROOT,
|
|
114
120
|
)
|
|
115
121
|
.action(async (options: {
|
|
116
|
-
inviteToken
|
|
117
|
-
email
|
|
118
|
-
personName
|
|
122
|
+
inviteToken?: string;
|
|
123
|
+
email?: string;
|
|
124
|
+
personName?: string;
|
|
119
125
|
hqRoot: string;
|
|
120
126
|
}) => {
|
|
121
127
|
try {
|
|
122
128
|
console.log(chalk.bold(`\nHQ Onboard — Join Company`));
|
|
123
|
-
|
|
129
|
+
|
|
130
|
+
const plan = planOnboardJoin({
|
|
131
|
+
inviteToken: options.inviteToken,
|
|
132
|
+
email: options.email,
|
|
133
|
+
personName: options.personName,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
if (plan.kind === "tokenless") {
|
|
137
|
+
console.log(`\n${plan.guidance}\n`);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (plan.kind === "error") {
|
|
142
|
+
console.error(chalk.red(`\n✗ ${plan.message}`));
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
console.log(` Person: ${plan.personName} <${plan.email}>`);
|
|
124
147
|
console.log(` HQ root: ${options.hqRoot}\n`);
|
|
125
148
|
|
|
126
149
|
const accessToken = await ensureCognitoToken();
|
|
127
150
|
const result = await runOnboardCli({
|
|
128
151
|
mode: "join-company",
|
|
129
|
-
personName:
|
|
130
|
-
personEmail:
|
|
131
|
-
inviteToken:
|
|
152
|
+
personName: plan.personName,
|
|
153
|
+
personEmail: plan.email,
|
|
154
|
+
inviteToken: plan.inviteToken,
|
|
132
155
|
vaultConfig: buildVaultConfig(accessToken),
|
|
133
156
|
hqRoot: options.hqRoot,
|
|
134
157
|
});
|