@attest-it/cli 0.4.0 → 0.5.0

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.cjs CHANGED
@@ -15,9 +15,12 @@ var jsxRuntime = require('react/jsx-runtime');
15
15
  var promises = require('fs/promises');
16
16
  var ui = require('@inkjs/ui');
17
17
  var yaml = require('yaml');
18
+ var tabtab = require('@pnpm/tabtab');
18
19
  var url = require('url');
19
20
 
20
21
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
22
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
23
+
21
24
  function _interopNamespace(e) {
22
25
  if (e && e.__esModule) return e;
23
26
  var n = Object.create(null);
@@ -40,6 +43,7 @@ var fs__namespace = /*#__PURE__*/_interopNamespace(fs);
40
43
  var path__namespace = /*#__PURE__*/_interopNamespace(path);
41
44
  var os__namespace = /*#__PURE__*/_interopNamespace(os);
42
45
  var React7__namespace = /*#__PURE__*/_interopNamespace(React7);
46
+ var tabtab__default = /*#__PURE__*/_interopDefault(tabtab);
43
47
 
44
48
  // src/index.ts
45
49
  var globalOptions = {};
@@ -2486,31 +2490,46 @@ async function runList() {
2486
2490
  process.exit(ExitCode.CONFIG_ERROR);
2487
2491
  }
2488
2492
  }
2493
+
2494
+ // src/commands/identity/validation.ts
2495
+ function validateSlug(value, existingIdentities) {
2496
+ const trimmed = value.trim();
2497
+ if (!trimmed) {
2498
+ return "Slug cannot be empty";
2499
+ }
2500
+ if (!/^[a-z0-9-]+$/.test(trimmed)) {
2501
+ return "Slug must contain only lowercase letters, numbers, and hyphens";
2502
+ }
2503
+ if (existingIdentities?.[trimmed]) {
2504
+ return `Identity "${trimmed}" already exists`;
2505
+ }
2506
+ return true;
2507
+ }
2508
+ var EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
2509
+ function validateEmail(value, required = false) {
2510
+ const trimmed = value.trim();
2511
+ if (!trimmed) {
2512
+ return required ? "Email cannot be empty" : true;
2513
+ }
2514
+ if (!EMAIL_REGEX.test(trimmed)) {
2515
+ return "Please enter a valid email address";
2516
+ }
2517
+ return true;
2518
+ }
2489
2519
  var createCommand = new commander.Command("create").description("Create a new identity with Ed25519 keypair").action(async () => {
2490
2520
  await runCreate();
2491
2521
  });
2492
2522
  async function runCreate() {
2493
2523
  try {
2494
- const theme3 = getTheme2();
2524
+ const theme3 = getTheme();
2495
2525
  log("");
2496
2526
  log(theme3.blue.bold()("Create New Identity"));
2497
2527
  log("");
2498
2528
  const existingConfig = await core.loadLocalConfig();
2499
- const slug = await prompts.input({
2529
+ const slug = (await prompts.input({
2500
2530
  message: "Identity slug (unique identifier):",
2501
- validate: (value) => {
2502
- if (!value || value.trim().length === 0) {
2503
- return "Slug cannot be empty";
2504
- }
2505
- if (!/^[a-z0-9-]+$/.test(value)) {
2506
- return "Slug must contain only lowercase letters, numbers, and hyphens";
2507
- }
2508
- if (existingConfig?.identities[value]) {
2509
- return `Identity "${value}" already exists`;
2510
- }
2511
- return true;
2512
- }
2513
- });
2531
+ validate: (value) => validateSlug(value, existingConfig?.identities)
2532
+ })).trim();
2514
2533
  const name = await prompts.input({
2515
2534
  message: "Display name:",
2516
2535
  validate: (value) => {
@@ -2520,21 +2539,31 @@ async function runCreate() {
2520
2539
  return true;
2521
2540
  }
2522
2541
  });
2523
- const email = await prompts.input({
2542
+ const email = (await prompts.input({
2524
2543
  message: "Email (optional):",
2525
- default: ""
2526
- });
2544
+ default: "",
2545
+ validate: validateEmail
2546
+ })).trim();
2527
2547
  const github = await prompts.input({
2528
2548
  message: "GitHub username (optional):",
2529
2549
  default: ""
2530
2550
  });
2551
+ info("Checking available key storage providers...");
2552
+ const opAvailable = await core.OnePasswordKeyProvider.isInstalled();
2553
+ const keychainAvailable = core.MacOSKeychainKeyProvider.isAvailable();
2554
+ const configDir = core.getAttestItConfigDir();
2555
+ const storageChoices = [
2556
+ { name: `File system (${path.join(configDir, "keys")})`, value: "file" }
2557
+ ];
2558
+ if (keychainAvailable) {
2559
+ storageChoices.push({ name: "macOS Keychain", value: "keychain" });
2560
+ }
2561
+ if (opAvailable) {
2562
+ storageChoices.push({ name: "1Password", value: "1password" });
2563
+ }
2531
2564
  const keyStorageType = await prompts.select({
2532
2565
  message: "Where should the private key be stored?",
2533
- choices: [
2534
- { name: "File system (~/.config/attest-it/keys/)", value: "file" },
2535
- { name: "macOS Keychain", value: "keychain" },
2536
- { name: "1Password", value: "1password" }
2537
- ]
2566
+ choices: storageChoices
2538
2567
  });
2539
2568
  log("");
2540
2569
  log("Generating Ed25519 keypair...");
@@ -2543,7 +2572,7 @@ async function runCreate() {
2543
2572
  let keyStorageDescription;
2544
2573
  switch (keyStorageType) {
2545
2574
  case "file": {
2546
- const keysDir = path.join(os.homedir(), ".config", "attest-it", "keys");
2575
+ const keysDir = path.join(core.getAttestItConfigDir(), "keys");
2547
2576
  await promises.mkdir(keysDir, { recursive: true });
2548
2577
  const keyPath = path.join(keysDir, `${slug}.pem`);
2549
2578
  await promises.writeFile(keyPath, keyPair.privateKey, { mode: 384 });
@@ -2552,44 +2581,138 @@ async function runCreate() {
2552
2581
  break;
2553
2582
  }
2554
2583
  case "keychain": {
2555
- const { MacOSKeychainKeyProvider: MacOSKeychainKeyProvider3 } = await import('@attest-it/core');
2556
- if (!MacOSKeychainKeyProvider3.isAvailable()) {
2584
+ if (!core.MacOSKeychainKeyProvider.isAvailable()) {
2557
2585
  error("macOS Keychain is not available on this system");
2558
2586
  process.exit(ExitCode.CONFIG_ERROR);
2559
2587
  }
2588
+ const keychains = await core.MacOSKeychainKeyProvider.listKeychains();
2589
+ if (keychains.length === 0) {
2590
+ throw new Error("No keychains found on this system");
2591
+ }
2592
+ const formatKeychainChoice = (kc) => {
2593
+ return `${theme3.blue.bold()(kc.name)} ${theme3.muted(`(${kc.path})`)}`;
2594
+ };
2595
+ let selectedKeychain;
2596
+ if (keychains.length === 1 && keychains[0]) {
2597
+ selectedKeychain = keychains[0];
2598
+ info(`Using keychain: ${formatKeychainChoice(selectedKeychain)}`);
2599
+ } else {
2600
+ const selectedPath = await prompts.select({
2601
+ message: "Select keychain:",
2602
+ choices: keychains.map((kc) => ({
2603
+ name: formatKeychainChoice(kc),
2604
+ value: kc.path
2605
+ }))
2606
+ });
2607
+ const foundKeychain = keychains.find((kc) => kc.path === selectedPath);
2608
+ if (!foundKeychain) {
2609
+ throw new Error("Selected keychain not found");
2610
+ }
2611
+ selectedKeychain = foundKeychain;
2612
+ }
2613
+ const keychainItemName = await prompts.input({
2614
+ message: "Keychain item name:",
2615
+ default: `attest-it-${slug}`,
2616
+ validate: (value) => {
2617
+ if (!value || value.trim().length === 0) {
2618
+ return "Item name cannot be empty";
2619
+ }
2620
+ return true;
2621
+ }
2622
+ });
2560
2623
  const { execFile } = await import('child_process');
2561
2624
  const { promisify } = await import('util');
2562
2625
  const execFileAsync = promisify(execFile);
2563
2626
  const encodedKey = Buffer.from(keyPair.privateKey).toString("base64");
2564
2627
  try {
2565
- await execFileAsync("security", [
2628
+ const addArgs = [
2566
2629
  "add-generic-password",
2567
2630
  "-a",
2568
2631
  "attest-it",
2569
2632
  "-s",
2570
- slug,
2633
+ keychainItemName,
2571
2634
  "-w",
2572
2635
  encodedKey,
2573
- "-U"
2574
- ]);
2636
+ "-U",
2637
+ selectedKeychain.path
2638
+ ];
2639
+ await execFileAsync("security", addArgs);
2575
2640
  } catch (err) {
2576
2641
  throw new Error(
2577
2642
  `Failed to store key in macOS Keychain: ${err instanceof Error ? err.message : String(err)}`
2578
2643
  );
2579
2644
  }
2580
- privateKeyRef = { type: "keychain", service: slug, account: "attest-it" };
2581
- keyStorageDescription = "macOS Keychain (" + slug + "/attest-it)";
2645
+ privateKeyRef = {
2646
+ type: "keychain",
2647
+ service: keychainItemName,
2648
+ account: "attest-it",
2649
+ keychain: selectedKeychain.path
2650
+ };
2651
+ keyStorageDescription = `macOS Keychain: ${selectedKeychain.name}/${keychainItemName}`;
2582
2652
  break;
2583
2653
  }
2584
2654
  case "1password": {
2585
- const vault = await prompts.input({
2586
- message: "1Password vault name:",
2587
- validate: (value) => {
2588
- if (!value || value.trim().length === 0) {
2589
- return "Vault name cannot be empty";
2655
+ const accounts = await core.OnePasswordKeyProvider.listAccounts();
2656
+ if (accounts.length === 0) {
2657
+ throw new Error(
2658
+ '1Password CLI is installed but no accounts are signed in. Run "op signin" first.'
2659
+ );
2660
+ }
2661
+ const { execFile } = await import('child_process');
2662
+ const { promisify } = await import('util');
2663
+ const execFileAsync = promisify(execFile);
2664
+ const accountDetails = await Promise.all(
2665
+ accounts.map(async (acc) => {
2666
+ try {
2667
+ const { stdout } = await execFileAsync("op", [
2668
+ "account",
2669
+ "get",
2670
+ "--account",
2671
+ acc.user_uuid,
2672
+ "--format=json"
2673
+ ]);
2674
+ const details = JSON.parse(stdout);
2675
+ const name2 = details !== null && typeof details === "object" && "name" in details && typeof details.name === "string" ? details.name : acc.url;
2676
+ return {
2677
+ url: acc.url,
2678
+ email: acc.email,
2679
+ name: name2
2680
+ };
2681
+ } catch {
2682
+ return {
2683
+ url: acc.url,
2684
+ email: acc.email,
2685
+ name: acc.url
2686
+ };
2590
2687
  }
2591
- return true;
2592
- }
2688
+ })
2689
+ );
2690
+ const formatAccountChoice = (acc) => {
2691
+ return `${theme3.blue.bold()(acc.name)} ${theme3.muted(`(${acc.url})`)}`;
2692
+ };
2693
+ let selectedAccount;
2694
+ if (accountDetails.length === 1 && accountDetails[0]) {
2695
+ selectedAccount = accountDetails[0].url;
2696
+ info(`Using 1Password account: ${formatAccountChoice(accountDetails[0])}`);
2697
+ } else {
2698
+ selectedAccount = await prompts.select({
2699
+ message: "Select 1Password account:",
2700
+ choices: accountDetails.map((acc) => ({
2701
+ name: formatAccountChoice(acc),
2702
+ value: acc.url
2703
+ }))
2704
+ });
2705
+ }
2706
+ const vaults = await core.OnePasswordKeyProvider.listVaults(selectedAccount);
2707
+ if (vaults.length === 0) {
2708
+ throw new Error(`No vaults found in 1Password account: ${selectedAccount}`);
2709
+ }
2710
+ const selectedVault = await prompts.select({
2711
+ message: "Select vault for private key storage:",
2712
+ choices: vaults.map((v) => ({
2713
+ name: v.name,
2714
+ value: v.name
2715
+ }))
2593
2716
  });
2594
2717
  const item = await prompts.input({
2595
2718
  message: "1Password item name:",
@@ -2601,26 +2724,40 @@ async function runCreate() {
2601
2724
  return true;
2602
2725
  }
2603
2726
  });
2604
- const { execFile } = await import('child_process');
2605
- const { promisify } = await import('util');
2606
- const execFileAsync = promisify(execFile);
2727
+ const { tmpdir } = await import('os');
2728
+ const tempDir = path.join(tmpdir(), `attest-it-${String(Date.now())}`);
2729
+ await promises.mkdir(tempDir, { recursive: true });
2730
+ const tempPrivatePath = path.join(tempDir, "private.pem");
2607
2731
  try {
2608
- await execFileAsync("op", [
2609
- "item",
2732
+ await promises.writeFile(tempPrivatePath, keyPair.privateKey, { mode: 384 });
2733
+ const { execFile: execFile2 } = await import('child_process');
2734
+ const { promisify: promisify2 } = await import('util');
2735
+ const execFileAsync2 = promisify2(execFile2);
2736
+ const opArgs = [
2737
+ "document",
2610
2738
  "create",
2611
- "--category=SecureNote",
2739
+ tempPrivatePath,
2740
+ "--title",
2741
+ item,
2612
2742
  "--vault",
2613
- vault,
2614
- `--title=${item}`,
2615
- `privateKey[password]=${keyPair.privateKey}`
2616
- ]);
2617
- } catch (err) {
2618
- throw new Error(
2619
- `Failed to store key in 1Password: ${err instanceof Error ? err.message : String(err)}`
2620
- );
2743
+ selectedVault
2744
+ ];
2745
+ if (selectedAccount) {
2746
+ opArgs.push("--account", selectedAccount);
2747
+ }
2748
+ await execFileAsync2("op", opArgs);
2749
+ } finally {
2750
+ const { rm } = await import('fs/promises');
2751
+ await rm(tempDir, { recursive: true, force: true }).catch(() => {
2752
+ });
2621
2753
  }
2622
- privateKeyRef = { type: "1password", vault, item, field: "privateKey" };
2623
- keyStorageDescription = `1Password (${vault}/${item})`;
2754
+ privateKeyRef = {
2755
+ type: "1password",
2756
+ vault: selectedVault,
2757
+ item,
2758
+ ...selectedAccount && { account: selectedAccount }
2759
+ };
2760
+ keyStorageDescription = `1Password (${selectedVault}/${item})`;
2624
2761
  break;
2625
2762
  }
2626
2763
  default:
@@ -2932,6 +3069,32 @@ async function runEdit(slug) {
2932
3069
  process.exit(ExitCode.CONFIG_ERROR);
2933
3070
  }
2934
3071
  }
3072
+
3073
+ // src/utils/format-key-location.ts
3074
+ function formatKeyLocation(privateKey) {
3075
+ const theme3 = getTheme();
3076
+ switch (privateKey.type) {
3077
+ case "file":
3078
+ return `${theme3.blue.bold()("File")}: ${theme3.muted(privateKey.path)}`;
3079
+ case "keychain": {
3080
+ let keychainName = "default";
3081
+ if (privateKey.keychain) {
3082
+ const filename = privateKey.keychain.split("/").pop() ?? privateKey.keychain;
3083
+ keychainName = filename.replace(/\.keychain(-db)?$/, "");
3084
+ }
3085
+ return `${theme3.blue.bold()("macOS Keychain")}: ${theme3.muted(`${keychainName}/${privateKey.service}`)}`;
3086
+ }
3087
+ case "1password": {
3088
+ const parts = [privateKey.vault, privateKey.item];
3089
+ if (privateKey.account) {
3090
+ parts.unshift(privateKey.account);
3091
+ }
3092
+ return `${theme3.blue.bold()("1Password")}: ${theme3.muted(parts.join("/"))}`;
3093
+ }
3094
+ default:
3095
+ return "Unknown storage";
3096
+ }
3097
+ }
2935
3098
  var removeCommand = new commander.Command("remove").description("Delete identity and optionally delete private key").argument("<slug>", "Identity slug to remove").action(async (slug) => {
2936
3099
  await runRemove(slug);
2937
3100
  });
@@ -2947,7 +3110,7 @@ async function runRemove(slug) {
2947
3110
  error(`Identity "${slug}" not found`);
2948
3111
  process.exit(ExitCode.CONFIG_ERROR);
2949
3112
  }
2950
- const theme3 = getTheme2();
3113
+ const theme3 = getTheme();
2951
3114
  log("");
2952
3115
  log(theme3.blue.bold()(`Remove Identity: ${slug}`));
2953
3116
  log("");
@@ -2964,6 +3127,9 @@ async function runRemove(slug) {
2964
3127
  log("Cancelled");
2965
3128
  process.exit(ExitCode.CANCELLED);
2966
3129
  }
3130
+ const keyLocation = formatKeyLocation(identity.privateKey);
3131
+ log(` Private key: ${keyLocation}`);
3132
+ log("");
2967
3133
  const deletePrivateKey = await prompts.confirm({
2968
3134
  message: "Also delete the private key from storage?",
2969
3135
  default: false
@@ -2986,13 +3152,17 @@ async function runRemove(slug) {
2986
3152
  const { promisify } = await import('util');
2987
3153
  const execFileAsync = promisify(execFile);
2988
3154
  try {
2989
- await execFileAsync("security", [
3155
+ const deleteArgs = [
2990
3156
  "delete-generic-password",
2991
3157
  "-s",
2992
3158
  identity.privateKey.service,
2993
3159
  "-a",
2994
3160
  identity.privateKey.account
2995
- ]);
3161
+ ];
3162
+ if (identity.privateKey.keychain) {
3163
+ deleteArgs.push(identity.privateKey.keychain);
3164
+ }
3165
+ await execFileAsync("security", deleteArgs);
2996
3166
  log(` Deleted private key from macOS Keychain`);
2997
3167
  } catch (err) {
2998
3168
  if (err instanceof Error && !err.message.includes("could not be found") && !err.message.includes("does not exist")) {
@@ -3130,17 +3300,20 @@ async function runWhoami() {
3130
3300
  error("Active identity not found");
3131
3301
  process.exit(ExitCode.CONFIG_ERROR);
3132
3302
  }
3133
- const theme3 = getTheme2();
3303
+ const theme3 = getTheme();
3134
3304
  log("");
3135
- log(theme3.green.bold()(identity.name));
3305
+ log(theme3.blue.bold()("Active Identity"));
3306
+ log("");
3307
+ log(` Slug: ${theme3.green.bold()(config.activeIdentity)}`);
3308
+ log(` Name: ${identity.name}`);
3136
3309
  if (identity.email) {
3137
- log(theme3.muted(identity.email));
3310
+ log(` Email: ${theme3.muted(identity.email)}`);
3138
3311
  }
3139
3312
  if (identity.github) {
3140
- log(theme3.muted("@" + identity.github));
3313
+ log(` GitHub: ${theme3.muted("@" + identity.github)}`);
3141
3314
  }
3142
- log("");
3143
- log(`Identity: ${theme3.blue(config.activeIdentity)}`);
3315
+ log(` Public Key: ${theme3.muted(identity.publicKey.slice(0, 24) + "...")}`);
3316
+ log(` Key Store: ${formatKeyLocation(identity.privateKey)}`);
3144
3317
  log("");
3145
3318
  } catch (err) {
3146
3319
  if (err instanceof Error) {
@@ -3591,6 +3764,207 @@ async function runRemove2(slug, options) {
3591
3764
 
3592
3765
  // src/commands/team/index.ts
3593
3766
  var teamCommand = new commander.Command("team").description("Manage team members and authorizations").addCommand(listCommand2).addCommand(addCommand).addCommand(editCommand2).addCommand(removeCommand2);
3767
+ var PROGRAM_NAME = "attest-it";
3768
+ async function getCompletions(env) {
3769
+ let shell;
3770
+ try {
3771
+ const detectedShell = tabtab__default.default.getShellFromEnv(process.env);
3772
+ shell = detectedShell === "pwsh" ? "bash" : detectedShell;
3773
+ } catch {
3774
+ shell = "bash";
3775
+ }
3776
+ const commands = [
3777
+ { name: "init", description: "Initialize a new config file" },
3778
+ { name: "status", description: "Show status of all gates" },
3779
+ { name: "run", description: "Run test suites interactively" },
3780
+ { name: "verify", description: "Verify all seals are valid" },
3781
+ { name: "seal", description: "Create a seal for a gate" },
3782
+ { name: "keygen", description: "Generate a new keypair" },
3783
+ { name: "prune", description: "Remove stale attestations" },
3784
+ { name: "identity", description: "Manage identities" },
3785
+ { name: "team", description: "Manage team members" },
3786
+ { name: "whoami", description: "Show active identity" },
3787
+ { name: "completion", description: "Shell completion commands" }
3788
+ ];
3789
+ const globalOptions2 = [
3790
+ { name: "--help", description: "Show help" },
3791
+ { name: "--version", description: "Show version" },
3792
+ { name: "--verbose", description: "Verbose output" },
3793
+ { name: "--quiet", description: "Minimal output" },
3794
+ { name: "--config", description: "Path to config file" }
3795
+ ];
3796
+ const identitySubcommands = [
3797
+ { name: "create", description: "Create a new identity" },
3798
+ { name: "list", description: "List all identities" },
3799
+ { name: "use", description: "Switch active identity" },
3800
+ { name: "remove", description: "Remove an identity" }
3801
+ ];
3802
+ const teamSubcommands = [
3803
+ { name: "add", description: "Add yourself to the team" },
3804
+ { name: "list", description: "List team members" },
3805
+ { name: "remove", description: "Remove a team member" }
3806
+ ];
3807
+ const completionSubcommands = [
3808
+ { name: "install", description: "Install shell completion" },
3809
+ { name: "uninstall", description: "Uninstall shell completion" }
3810
+ ];
3811
+ const words = env.line.split(/\s+/).filter(Boolean);
3812
+ const lastWord = env.last;
3813
+ const prevWord = env.prev;
3814
+ if (prevWord === "--config" || prevWord === "-c") {
3815
+ tabtab__default.default.logFiles();
3816
+ return;
3817
+ }
3818
+ if (lastWord.startsWith("-")) {
3819
+ tabtab__default.default.log(globalOptions2, shell, console.log);
3820
+ return;
3821
+ }
3822
+ const commandIndex = words.findIndex(
3823
+ (w) => !w.startsWith("-") && w !== PROGRAM_NAME && w !== "npx"
3824
+ );
3825
+ const currentCommand = commandIndex >= 0 ? words[commandIndex] ?? null : null;
3826
+ if (currentCommand === "identity") {
3827
+ const subcommandIndex = words.findIndex(
3828
+ (w, i) => i > commandIndex && !w.startsWith("-")
3829
+ );
3830
+ const subcommand = subcommandIndex >= 0 ? words[subcommandIndex] ?? null : null;
3831
+ if (subcommand === "use" || subcommand === "remove") {
3832
+ const identities = await getIdentitySlugs();
3833
+ if (identities.length > 0) {
3834
+ tabtab__default.default.log(identities, shell, console.log);
3835
+ return;
3836
+ }
3837
+ }
3838
+ if (!subcommand || subcommandIndex < 0) {
3839
+ tabtab__default.default.log(identitySubcommands, shell, console.log);
3840
+ return;
3841
+ }
3842
+ }
3843
+ if (currentCommand === "team") {
3844
+ const subcommandIndex = words.findIndex(
3845
+ (w, i) => i > commandIndex && !w.startsWith("-")
3846
+ );
3847
+ const subcommand = subcommandIndex >= 0 ? words[subcommandIndex] ?? null : null;
3848
+ if (!subcommand || subcommandIndex < 0) {
3849
+ tabtab__default.default.log(teamSubcommands, shell, console.log);
3850
+ return;
3851
+ }
3852
+ }
3853
+ if (currentCommand === "completion") {
3854
+ const subcommandIndex = words.findIndex(
3855
+ (w, i) => i > commandIndex && !w.startsWith("-")
3856
+ );
3857
+ const subcommand = subcommandIndex >= 0 ? words[subcommandIndex] ?? null : null;
3858
+ if (subcommand === "install") {
3859
+ tabtab__default.default.log(["bash", "zsh", "fish"], shell, console.log);
3860
+ return;
3861
+ }
3862
+ if (!subcommand || subcommandIndex < 0) {
3863
+ tabtab__default.default.log(completionSubcommands, shell, console.log);
3864
+ return;
3865
+ }
3866
+ }
3867
+ if (currentCommand === "status" || currentCommand === "verify" || currentCommand === "seal") {
3868
+ const gates = await getGateNames();
3869
+ if (gates.length > 0) {
3870
+ tabtab__default.default.log(gates, shell, console.log);
3871
+ return;
3872
+ }
3873
+ }
3874
+ if (currentCommand === "run") {
3875
+ const suites = await getSuiteNames();
3876
+ if (suites.length > 0) {
3877
+ tabtab__default.default.log(suites, shell, console.log);
3878
+ return;
3879
+ }
3880
+ }
3881
+ if (!currentCommand) {
3882
+ tabtab__default.default.log([...commands, ...globalOptions2], shell, console.log);
3883
+ }
3884
+ }
3885
+ async function getIdentitySlugs() {
3886
+ try {
3887
+ const config = await core.loadLocalConfig();
3888
+ if (config?.identities) {
3889
+ return Object.keys(config.identities);
3890
+ }
3891
+ } catch {
3892
+ }
3893
+ return [];
3894
+ }
3895
+ async function getGateNames() {
3896
+ try {
3897
+ const config = await core.loadConfig();
3898
+ if (config.gates) {
3899
+ return Object.keys(config.gates);
3900
+ }
3901
+ } catch {
3902
+ }
3903
+ return [];
3904
+ }
3905
+ async function getSuiteNames() {
3906
+ try {
3907
+ const config = await core.loadConfig();
3908
+ return Object.keys(config.suites);
3909
+ } catch {
3910
+ }
3911
+ return [];
3912
+ }
3913
+ var completionCommand = new commander.Command("completion").description("Shell completion commands");
3914
+ completionCommand.command("install [shell]").description("Install shell completion (bash, zsh, or fish)").action(async (shellArg) => {
3915
+ try {
3916
+ let shell;
3917
+ if (shellArg !== void 0) {
3918
+ if (tabtab__default.default.isShellSupported(shellArg)) {
3919
+ shell = shellArg;
3920
+ } else {
3921
+ error(`Shell "${shellArg}" is not supported. Use bash, zsh, or fish.`);
3922
+ process.exit(ExitCode.CONFIG_ERROR);
3923
+ }
3924
+ }
3925
+ await tabtab__default.default.install({
3926
+ name: PROGRAM_NAME,
3927
+ completer: PROGRAM_NAME,
3928
+ shell
3929
+ });
3930
+ log("");
3931
+ success("Shell completion installed!");
3932
+ log("");
3933
+ info("Restart your shell or run:");
3934
+ if (shell === "bash" || !shell) {
3935
+ log(" source ~/.bashrc");
3936
+ }
3937
+ if (shell === "zsh" || !shell) {
3938
+ log(" source ~/.zshrc");
3939
+ }
3940
+ if (shell === "fish" || !shell) {
3941
+ log(" source ~/.config/fish/config.fish");
3942
+ }
3943
+ log("");
3944
+ } catch (err) {
3945
+ error(`Failed to install completion: ${err instanceof Error ? err.message : String(err)}`);
3946
+ process.exit(ExitCode.CONFIG_ERROR);
3947
+ }
3948
+ });
3949
+ completionCommand.command("uninstall").description("Uninstall shell completion").action(async () => {
3950
+ try {
3951
+ await tabtab__default.default.uninstall({
3952
+ name: PROGRAM_NAME
3953
+ });
3954
+ log("");
3955
+ success("Shell completion uninstalled!");
3956
+ log("");
3957
+ } catch (err) {
3958
+ error(`Failed to uninstall completion: ${err instanceof Error ? err.message : String(err)}`);
3959
+ process.exit(ExitCode.CONFIG_ERROR);
3960
+ }
3961
+ });
3962
+ completionCommand.command("server", { hidden: true }).description("Completion server (internal)").action(async () => {
3963
+ const env = tabtab__default.default.parseEnv(process.env);
3964
+ if (env.complete) {
3965
+ await getCompletions(env);
3966
+ }
3967
+ });
3594
3968
  function hasVersion(data) {
3595
3969
  return typeof data === "object" && data !== null && "version" in data && // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
3596
3970
  typeof data.version === "string";
@@ -3634,7 +4008,19 @@ program.addCommand(sealCommand);
3634
4008
  program.addCommand(identityCommand);
3635
4009
  program.addCommand(teamCommand);
3636
4010
  program.addCommand(whoamiCommand);
4011
+ program.addCommand(completionCommand);
4012
+ function processHomeDirOption() {
4013
+ const homeDirIndex = process.argv.indexOf("--home-dir");
4014
+ if (homeDirIndex !== -1 && homeDirIndex + 1 < process.argv.length) {
4015
+ const homeDir = process.argv[homeDirIndex + 1];
4016
+ if (homeDir && !homeDir.startsWith("-")) {
4017
+ core.setAttestItHomeDir(homeDir);
4018
+ process.argv.splice(homeDirIndex, 2);
4019
+ }
4020
+ }
4021
+ }
3637
4022
  async function run() {
4023
+ processHomeDirOption();
3638
4024
  if (process.argv.includes("--version") || process.argv.includes("-V")) {
3639
4025
  console.log(getPackageVersion());
3640
4026
  process.exit(0);