@hexclave/cli 1.0.67 → 1.0.68
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/dist/index.js +245 -24
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/commands/init.ts +0 -4
- package/src/commands/project.ts +4 -5
- package/src/commands/team.command.test.ts +124 -0
- package/src/commands/team.test.ts +25 -0
- package/src/commands/team.ts +301 -0
- package/src/index.ts +2 -0
- package/src/lib/create-project.ts +11 -13
- package/src/lib/resolve-team.test.ts +58 -0
- package/src/lib/resolve-team.ts +62 -0
- package/dist/errors-CyPbl0LV.js +0 -21
- package/dist/errors-CyPbl0LV.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { n as CliError, r as errorMessage$1, t as AuthError } from "./errors-CyPbl0LV.js";
|
|
3
2
|
import * as Sentry from "@sentry/node";
|
|
4
3
|
import "@hexclave/shared/dist/utils/env";
|
|
5
4
|
import { StatusError, captureError, registerErrorSink, throwErr } from "@hexclave/shared/dist/utils/errors";
|
|
@@ -35,6 +34,7 @@ import { Readable } from "stream";
|
|
|
35
34
|
import { pipeline } from "stream/promises";
|
|
36
35
|
import extractZip from "extract-zip";
|
|
37
36
|
import { randomBytes as randomBytes$1 } from "node:crypto";
|
|
37
|
+
import { KnownErrors } from "@hexclave/shared";
|
|
38
38
|
//#region \0rolldown/runtime.js
|
|
39
39
|
var __defProp = Object.defineProperty;
|
|
40
40
|
var __exportAll = (all, no_symbols) => {
|
|
@@ -129,6 +129,23 @@ function initSentry() {
|
|
|
129
129
|
});
|
|
130
130
|
}
|
|
131
131
|
//#endregion
|
|
132
|
+
//#region src/lib/errors.ts
|
|
133
|
+
var CliError = class extends Error {
|
|
134
|
+
constructor(message) {
|
|
135
|
+
super(message);
|
|
136
|
+
this.name = "CliError";
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
var AuthError = class extends CliError {
|
|
140
|
+
constructor(message) {
|
|
141
|
+
super(message);
|
|
142
|
+
this.name = "AuthError";
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
function errorMessage$1(error) {
|
|
146
|
+
return error instanceof Error ? error.message : String(error);
|
|
147
|
+
}
|
|
148
|
+
//#endregion
|
|
132
149
|
//#region src/lib/config.ts
|
|
133
150
|
const ENV_CONFIG_PATH = process.env.STACK_CLI_CONFIG_PATH;
|
|
134
151
|
const HEXCLAVE_CONFIG_PATH = path$1.join(os.homedir(), ".config", "hexclave", "credentials.json");
|
|
@@ -1388,6 +1405,39 @@ function isNonInteractiveEnv() {
|
|
|
1388
1405
|
return !!(process.env.CI || process.env.GITHUB_ACTIONS || process.env.NONINTERACTIVE || !process.stdin.isTTY);
|
|
1389
1406
|
}
|
|
1390
1407
|
//#endregion
|
|
1408
|
+
//#region src/lib/resolve-team.ts
|
|
1409
|
+
async function resolveTeam(teams, options = {}) {
|
|
1410
|
+
const teamId = options.teamId?.trim();
|
|
1411
|
+
if (teamId) {
|
|
1412
|
+
const team = teams.find((candidate) => candidate.id === teamId);
|
|
1413
|
+
if (team == null) throw new CliError(`Team '${teamId}' was not found on your account.`);
|
|
1414
|
+
return team;
|
|
1415
|
+
}
|
|
1416
|
+
if (teams.length === 0) {
|
|
1417
|
+
if (options.createTeam != null) {
|
|
1418
|
+
if (isNonInteractiveEnv()) throw new CliError("No teams found. Run `hexclave team create` first, or pass --team-id.");
|
|
1419
|
+
const displayName = (await input({
|
|
1420
|
+
message: "Team display name:",
|
|
1421
|
+
validate: (value) => value.trim().length > 0 || "Display name cannot be empty."
|
|
1422
|
+
})).trim();
|
|
1423
|
+
return await options.createTeam(displayName);
|
|
1424
|
+
}
|
|
1425
|
+
throw new CliError("No teams found. Run `hexclave team create` first.");
|
|
1426
|
+
}
|
|
1427
|
+
if (teams.length === 1) return teams[0];
|
|
1428
|
+
if (isNonInteractiveEnv()) throw new CliError("--team-id is required in non-interactive environments (CI).");
|
|
1429
|
+
const selectedTeamId = await select({
|
|
1430
|
+
message: "Choose a team:",
|
|
1431
|
+
choices: teams.map((team) => ({
|
|
1432
|
+
name: `${team.displayName || "(unnamed)"} (${team.id})`,
|
|
1433
|
+
value: team.id
|
|
1434
|
+
}))
|
|
1435
|
+
});
|
|
1436
|
+
const selectedTeam = teams.find((team) => team.id === selectedTeamId);
|
|
1437
|
+
if (selectedTeam == null) throw new CliError("The selected team was not found.");
|
|
1438
|
+
return selectedTeam;
|
|
1439
|
+
}
|
|
1440
|
+
//#endregion
|
|
1391
1441
|
//#region src/lib/create-project.ts
|
|
1392
1442
|
var create_project_exports = /* @__PURE__ */ __exportAll({ createProjectInteractively: () => createProjectInteractively });
|
|
1393
1443
|
async function createProjectInteractively(user, opts = {}) {
|
|
@@ -1400,14 +1450,14 @@ async function createProjectInteractively(user, opts = {}) {
|
|
|
1400
1450
|
validate: (v) => v.trim().length > 0 || "Display name cannot be empty."
|
|
1401
1451
|
})).trim();
|
|
1402
1452
|
}
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
return await user.createProject({
|
|
1407
|
-
displayName,
|
|
1408
|
-
teamId: teams[0].id
|
|
1409
|
-
});
|
|
1453
|
+
const team = await resolveTeam(await user.listTeams(), {
|
|
1454
|
+
teamId: opts.teamId,
|
|
1455
|
+
createTeam: async (teamDisplayName) => await user.createTeam({ displayName: teamDisplayName })
|
|
1410
1456
|
});
|
|
1457
|
+
return await withProgress("Creating project", async () => await user.createProject({
|
|
1458
|
+
displayName,
|
|
1459
|
+
teamId: team.id
|
|
1460
|
+
}));
|
|
1411
1461
|
}
|
|
1412
1462
|
//#endregion
|
|
1413
1463
|
//#region src/commands/init.ts
|
|
@@ -1590,12 +1640,9 @@ async function writeProjectKeysToEnv(project, outputDir) {
|
|
|
1590
1640
|
}
|
|
1591
1641
|
async function handleCreateCloud(_flags, opts, outputDir) {
|
|
1592
1642
|
const sessionAuth = await ensureLoggedInSession();
|
|
1593
|
-
const
|
|
1594
|
-
const { dashboardUrl } = resolveLoginConfig();
|
|
1595
|
-
const newProject = await createProjectInteractively(user, {
|
|
1643
|
+
const newProject = await createProjectInteractively(await withProgress("Loading account", async () => await getInternalUser(sessionAuth)), {
|
|
1596
1644
|
displayName: opts.displayName,
|
|
1597
|
-
defaultDisplayName: path$1.basename(outputDir)
|
|
1598
|
-
dashboardUrl
|
|
1645
|
+
defaultDisplayName: path$1.basename(outputDir)
|
|
1599
1646
|
});
|
|
1600
1647
|
console.log(`\nCreated project: ${newProject.displayName} (${newProject.id})\n`);
|
|
1601
1648
|
await writeProjectKeysToEnv(newProject, outputDir);
|
|
@@ -1622,11 +1669,7 @@ async function handleLinkFromCloud(_flags, opts, outputDir) {
|
|
|
1622
1669
|
const { dashboardUrl } = resolveLoginConfig();
|
|
1623
1670
|
throw new CliError(`You don't own any projects. Create one at ${dashboardUrl} or re-run and choose to create one.`);
|
|
1624
1671
|
}
|
|
1625
|
-
const
|
|
1626
|
-
const newProject = await createProjectInteractively(user, {
|
|
1627
|
-
defaultDisplayName: path$1.basename(outputDir),
|
|
1628
|
-
dashboardUrl
|
|
1629
|
-
});
|
|
1672
|
+
const newProject = await createProjectInteractively(user, { defaultDisplayName: path$1.basename(outputDir) });
|
|
1630
1673
|
console.log(`\nCreated project: ${newProject.displayName} (${newProject.id})\n`);
|
|
1631
1674
|
projects = [newProject];
|
|
1632
1675
|
autoCreatedProjectId = newProject.id;
|
|
@@ -1741,19 +1784,17 @@ function registerProjectCommand(program) {
|
|
|
1741
1784
|
if (program.opts().json) console.log(JSON.stringify(results, null, 2));
|
|
1742
1785
|
else console.log(formatProjectList(results));
|
|
1743
1786
|
});
|
|
1744
|
-
project.command("create").description("Create a new cloud project").option("--cloud", "Confirm that this creates a cloud project").option("--display-name <name>", "Project display name").action(async (opts) => {
|
|
1787
|
+
project.command("create").description("Create a new cloud project").option("--cloud", "Confirm that this creates a cloud project").option("--display-name <name>", "Project display name").option("--team-id <id>", "Team that owns the project").action(async (opts) => {
|
|
1745
1788
|
if (!opts.cloud) throw new CliError("hexclave project create currently only creates cloud projects. Pass --cloud to confirm.");
|
|
1746
|
-
const [{ getInternalUser }, {
|
|
1789
|
+
const [{ getInternalUser }, { resolveSessionAuth }, { createProjectInteractively }] = await Promise.all([
|
|
1747
1790
|
Promise.resolve().then(() => app_exports),
|
|
1748
1791
|
Promise.resolve().then(() => auth_exports),
|
|
1749
1792
|
Promise.resolve().then(() => create_project_exports)
|
|
1750
1793
|
]);
|
|
1751
1794
|
const auth = resolveSessionAuth();
|
|
1752
|
-
const
|
|
1753
|
-
const { dashboardUrl } = resolveLoginConfig();
|
|
1754
|
-
const newProject = await createProjectInteractively(user, {
|
|
1795
|
+
const newProject = await createProjectInteractively(await withProgress("Loading account", async () => await getInternalUser(auth)), {
|
|
1755
1796
|
displayName: opts.displayName,
|
|
1756
|
-
|
|
1797
|
+
teamId: opts.teamId
|
|
1757
1798
|
});
|
|
1758
1799
|
if (program.opts().json) console.log(JSON.stringify({
|
|
1759
1800
|
id: newProject.id,
|
|
@@ -3323,6 +3364,185 @@ function registerWhoamiCommand(program) {
|
|
|
3323
3364
|
});
|
|
3324
3365
|
}
|
|
3325
3366
|
//#endregion
|
|
3367
|
+
//#region src/commands/team.ts
|
|
3368
|
+
function isJson(program) {
|
|
3369
|
+
return Boolean(program.opts().json);
|
|
3370
|
+
}
|
|
3371
|
+
function printResult(program, result, humanOutput) {
|
|
3372
|
+
console.log(isJson(program) ? JSON.stringify(result, null, 2) : humanOutput);
|
|
3373
|
+
}
|
|
3374
|
+
async function withTeamPermissionError(operation, callback) {
|
|
3375
|
+
try {
|
|
3376
|
+
return await callback();
|
|
3377
|
+
} catch (error) {
|
|
3378
|
+
if (KnownErrors.TeamPermissionRequired.isInstance(error)) throw new CliError(`Cannot ${operation}. You do not have the required permission for this team.`);
|
|
3379
|
+
throw error;
|
|
3380
|
+
}
|
|
3381
|
+
}
|
|
3382
|
+
async function getTeam(user, options) {
|
|
3383
|
+
return await resolveTeam(await user.listTeams(), options);
|
|
3384
|
+
}
|
|
3385
|
+
async function getUser() {
|
|
3386
|
+
return await getInternalUser(resolveSessionAuth());
|
|
3387
|
+
}
|
|
3388
|
+
async function confirmDestructive(action, options) {
|
|
3389
|
+
if (options.yes) return;
|
|
3390
|
+
if (isNonInteractiveEnv()) throw new CliError(`--yes is required to ${action} in non-interactive environments (CI).`);
|
|
3391
|
+
if (!await confirm({
|
|
3392
|
+
message: `Are you sure you want to ${action}?`,
|
|
3393
|
+
default: false
|
|
3394
|
+
})) throw new CliError("Aborted.");
|
|
3395
|
+
}
|
|
3396
|
+
function printTeams(program, teams) {
|
|
3397
|
+
const result = teams.map((team) => ({
|
|
3398
|
+
id: team.id,
|
|
3399
|
+
displayName: team.displayName
|
|
3400
|
+
}));
|
|
3401
|
+
printResult(program, result, formatTeamList(result));
|
|
3402
|
+
}
|
|
3403
|
+
function formatTeamList(teams) {
|
|
3404
|
+
if (teams.length === 0) return "No teams found.";
|
|
3405
|
+
return teams.map((team) => `${team.id}\t${team.displayName}`).join("\n");
|
|
3406
|
+
}
|
|
3407
|
+
function formatTeamMembers(members) {
|
|
3408
|
+
if (members.length === 0) return "No team members found.";
|
|
3409
|
+
return members.map((member) => `${member.id}\t${member.displayName ?? "(none)"}`).join("\n");
|
|
3410
|
+
}
|
|
3411
|
+
function printTeam(program, team, prefix) {
|
|
3412
|
+
const result = {
|
|
3413
|
+
id: team.id,
|
|
3414
|
+
displayName: team.displayName
|
|
3415
|
+
};
|
|
3416
|
+
printResult(program, result, `${prefix}: ${result.id} (${result.displayName})`);
|
|
3417
|
+
}
|
|
3418
|
+
function registerTeamMembersCommand(team, program) {
|
|
3419
|
+
const members = team.command("members").description("Manage team members");
|
|
3420
|
+
members.command("list").description("List members of a team").option("--team-id <id>", "Team ID").action(async (options) => {
|
|
3421
|
+
const selectedTeam = await getTeam(await getUser(), options);
|
|
3422
|
+
const result = (await withTeamPermissionError("list team members", () => selectedTeam.listUsers())).map((member) => ({
|
|
3423
|
+
id: member.id,
|
|
3424
|
+
displayName: member.teamProfile.displayName
|
|
3425
|
+
}));
|
|
3426
|
+
printResult(program, result, formatTeamMembers(result));
|
|
3427
|
+
});
|
|
3428
|
+
members.command("remove").description("Remove a member from a team").option("--team-id <id>", "Team ID").requiredOption("--user-id <id>", "User ID to remove").option("-y, --yes", "Skip the confirmation prompt").action(async (options) => {
|
|
3429
|
+
const selectedTeam = await getTeam(await getUser(), options);
|
|
3430
|
+
await confirmDestructive(`remove user ${options.userId} from team ${selectedTeam.id}`, options);
|
|
3431
|
+
await withTeamPermissionError("remove this team member", () => selectedTeam.removeUser(options.userId));
|
|
3432
|
+
printResult(program, {
|
|
3433
|
+
teamId: selectedTeam.id,
|
|
3434
|
+
userId: options.userId
|
|
3435
|
+
}, `Removed team member: ${options.userId}`);
|
|
3436
|
+
});
|
|
3437
|
+
}
|
|
3438
|
+
function registerTeamInvitationsCommand(team, program) {
|
|
3439
|
+
const invitations = team.command("invitations").description("Manage team invitations");
|
|
3440
|
+
invitations.command("list").description("List invitations sent by a team").option("--team-id <id>", "Team ID").action(async (options) => {
|
|
3441
|
+
const selectedTeam = await getTeam(await getUser(), options);
|
|
3442
|
+
const result = (await withTeamPermissionError("list team invitations", () => selectedTeam.listInvitations())).map((invitation) => ({
|
|
3443
|
+
id: invitation.id,
|
|
3444
|
+
email: invitation.recipientEmail,
|
|
3445
|
+
expiresAt: invitation.expiresAt.toISOString()
|
|
3446
|
+
}));
|
|
3447
|
+
printResult(program, result, result.length === 0 ? "No invitations found." : result.map((invitation) => `${invitation.id}\t${invitation.email ?? "(unknown)"}\t${invitation.expiresAt}`).join("\n"));
|
|
3448
|
+
});
|
|
3449
|
+
invitations.command("revoke").description("Revoke a team invitation").option("--team-id <id>", "Team ID").requiredOption("--invitation-id <id>", "Invitation ID").option("-y, --yes", "Skip the confirmation prompt").action(async (options) => {
|
|
3450
|
+
const selectedTeam = await getTeam(await getUser(), options);
|
|
3451
|
+
await confirmDestructive(`revoke invitation ${options.invitationId}`, options);
|
|
3452
|
+
const invitation = (await withTeamPermissionError("list team invitations", () => selectedTeam.listInvitations())).find((candidate) => candidate.id === options.invitationId);
|
|
3453
|
+
if (invitation == null) throw new CliError(`Invitation '${options.invitationId}' was not found for team '${selectedTeam.id}'.`);
|
|
3454
|
+
await withTeamPermissionError("revoke this invitation", () => invitation.revoke());
|
|
3455
|
+
printResult(program, {
|
|
3456
|
+
teamId: selectedTeam.id,
|
|
3457
|
+
invitationId: options.invitationId
|
|
3458
|
+
}, `Invitation revoked: ${options.invitationId}`);
|
|
3459
|
+
});
|
|
3460
|
+
invitations.command("received").description("List invitations received by the current user").action(async () => {
|
|
3461
|
+
const result = (await (await getUser()).listTeamInvitations()).map((invitation) => ({
|
|
3462
|
+
id: invitation.id,
|
|
3463
|
+
teamId: invitation.teamId,
|
|
3464
|
+
teamDisplayName: invitation.teamDisplayName,
|
|
3465
|
+
email: invitation.recipientEmail,
|
|
3466
|
+
expiresAt: invitation.expiresAt.toISOString()
|
|
3467
|
+
}));
|
|
3468
|
+
printResult(program, result, result.length === 0 ? "No received invitations found." : result.map((invitation) => `${invitation.id}\t${invitation.teamId}\t${invitation.teamDisplayName}\t${invitation.expiresAt}`).join("\n"));
|
|
3469
|
+
});
|
|
3470
|
+
invitations.command("accept").description("Accept an invitation received by the current user").requiredOption("--invitation-id <id>", "Invitation ID").action(async (options) => {
|
|
3471
|
+
const invitation = (await (await getUser()).listTeamInvitations()).find((candidate) => candidate.id === options.invitationId);
|
|
3472
|
+
if (invitation == null) throw new CliError(`Received invitation '${options.invitationId}' was not found.`);
|
|
3473
|
+
await invitation.accept();
|
|
3474
|
+
printResult(program, {
|
|
3475
|
+
invitationId: options.invitationId,
|
|
3476
|
+
teamId: invitation.teamId
|
|
3477
|
+
}, `Invitation accepted for team: ${invitation.teamId}`);
|
|
3478
|
+
});
|
|
3479
|
+
}
|
|
3480
|
+
function registerTeamCommand(program) {
|
|
3481
|
+
const team = program.command("team").description("Manage teams");
|
|
3482
|
+
team.command("list").description("List your teams").action(async () => {
|
|
3483
|
+
printTeams(program, await (await getUser()).listTeams());
|
|
3484
|
+
});
|
|
3485
|
+
team.command("create").description("Create a team").option("--display-name <name>", "Team display name").action(async (options) => {
|
|
3486
|
+
let displayName = options.displayName?.trim();
|
|
3487
|
+
if (!displayName) {
|
|
3488
|
+
if (isNonInteractiveEnv()) throw new CliError("--display-name is required in non-interactive environments (CI).");
|
|
3489
|
+
displayName = (await input({
|
|
3490
|
+
message: "Team display name:",
|
|
3491
|
+
validate: (value) => value.trim().length > 0 || "Display name cannot be empty."
|
|
3492
|
+
})).trim();
|
|
3493
|
+
}
|
|
3494
|
+
printTeam(program, await (await getUser()).createTeam({ displayName }), "Team created");
|
|
3495
|
+
});
|
|
3496
|
+
registerTeamMembersCommand(team, program);
|
|
3497
|
+
team.command("invite").description("Invite a user to a team").option("--team-id <id>", "Team ID").option("--email <email>", "Email address to invite").action(async (options) => {
|
|
3498
|
+
const selectedTeam = await getTeam(await getUser(), options);
|
|
3499
|
+
let email = options.email?.trim();
|
|
3500
|
+
if (!email) {
|
|
3501
|
+
if (isNonInteractiveEnv()) throw new CliError("--email is required in non-interactive environments (CI).");
|
|
3502
|
+
email = (await input({
|
|
3503
|
+
message: "Email address:",
|
|
3504
|
+
validate: (value) => value.trim().length > 0 || "Email address cannot be empty."
|
|
3505
|
+
})).trim();
|
|
3506
|
+
}
|
|
3507
|
+
await withTeamPermissionError("invite this user", () => selectedTeam.inviteUser({ email }));
|
|
3508
|
+
printResult(program, {
|
|
3509
|
+
teamId: selectedTeam.id,
|
|
3510
|
+
email
|
|
3511
|
+
}, `Invitation sent to ${email}.`);
|
|
3512
|
+
});
|
|
3513
|
+
registerTeamInvitationsCommand(team, program);
|
|
3514
|
+
team.command("leave").description("Leave a team").option("--team-id <id>", "Team ID").option("-y, --yes", "Skip the confirmation prompt").action(async (options) => {
|
|
3515
|
+
const user = await getUser();
|
|
3516
|
+
const selectedTeam = await getTeam(user, options);
|
|
3517
|
+
await confirmDestructive(`leave team ${selectedTeam.id}`, options);
|
|
3518
|
+
await user.leaveTeam(selectedTeam);
|
|
3519
|
+
printResult(program, { teamId: selectedTeam.id }, `Left team: ${selectedTeam.id}`);
|
|
3520
|
+
});
|
|
3521
|
+
team.command("update").description("Update a team").option("--team-id <id>", "Team ID").option("--display-name <name>", "New team display name").action(async (options) => {
|
|
3522
|
+
const selectedTeam = await getTeam(await getUser(), options);
|
|
3523
|
+
let displayName = options.displayName?.trim();
|
|
3524
|
+
if (!displayName) {
|
|
3525
|
+
if (isNonInteractiveEnv()) throw new CliError("--display-name is required in non-interactive environments (CI).");
|
|
3526
|
+
displayName = (await input({
|
|
3527
|
+
message: "New team display name:",
|
|
3528
|
+
default: selectedTeam.displayName,
|
|
3529
|
+
validate: (value) => value.trim().length > 0 || "Display name cannot be empty."
|
|
3530
|
+
})).trim();
|
|
3531
|
+
}
|
|
3532
|
+
await withTeamPermissionError("update this team", () => selectedTeam.update({ displayName }));
|
|
3533
|
+
printResult(program, {
|
|
3534
|
+
id: selectedTeam.id,
|
|
3535
|
+
displayName
|
|
3536
|
+
}, `Team updated: ${selectedTeam.id} (${displayName})`);
|
|
3537
|
+
});
|
|
3538
|
+
team.command("delete").description("Delete a team").option("--team-id <id>", "Team ID").option("-y, --yes", "Skip the confirmation prompt").action(async (options) => {
|
|
3539
|
+
const selectedTeam = await getTeam(await getUser(), options);
|
|
3540
|
+
await confirmDestructive(`delete team ${selectedTeam.id}`, options);
|
|
3541
|
+
await withTeamPermissionError("delete this team", () => selectedTeam.delete());
|
|
3542
|
+
printResult(program, { teamId: selectedTeam.id }, `Team deleted: ${selectedTeam.id}`);
|
|
3543
|
+
});
|
|
3544
|
+
}
|
|
3545
|
+
//#endregion
|
|
3326
3546
|
//#region src/index.ts
|
|
3327
3547
|
initSentry();
|
|
3328
3548
|
const program = new Command();
|
|
@@ -3336,6 +3556,7 @@ registerInitCommand(program);
|
|
|
3336
3556
|
registerProjectCommand(program);
|
|
3337
3557
|
registerDevCommand(program);
|
|
3338
3558
|
registerWhoamiCommand(program);
|
|
3559
|
+
registerTeamCommand(program);
|
|
3339
3560
|
registerFixCommand(program);
|
|
3340
3561
|
registerDoctorCommand(program);
|
|
3341
3562
|
async function main() {
|