@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 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
- return await withProgress("Creating project", async () => {
1404
- const teams = await user.listTeams();
1405
- if (teams.length === 0) throw new CliError(`No teams found on your account. Create a team at ${opts.dashboardUrl ?? "https://app.hexclave.com"} first.`);
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 user = await withProgress("Loading account", async () => await getInternalUser(sessionAuth));
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 { dashboardUrl } = resolveLoginConfig();
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 }, { resolveLoginConfig, resolveSessionAuth }, { createProjectInteractively }] = await Promise.all([
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 user = await withProgress("Loading account", async () => await getInternalUser(auth));
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
- dashboardUrl
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() {