@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.js CHANGED
@@ -5,10 +5,9 @@ import * as path from 'path';
5
5
  import { join, dirname } from 'path';
6
6
  import { detectTheme } from 'chromaterm';
7
7
  import { input, select, confirm, checkbox } from '@inquirer/prompts';
8
- import { loadConfig, toAttestItConfig, readSealsSync, computeFingerprintSync, verifyGateSeal, verifyAllSeals, computeFingerprint, createAttestation, readAttestations, upsertAttestation, KeyProviderRegistry, getDefaultPrivateKeyPath, FilesystemKeyProvider, writeSignedAttestations, loadLocalConfigSync, getActiveIdentity, isAuthorizedSigner, createSeal, writeSealsSync, checkOpenSSL, getDefaultPublicKeyPath, OnePasswordKeyProvider, MacOSKeychainKeyProvider, generateKeyPair, setKeyPermissions, getGate, loadLocalConfig, generateEd25519KeyPair, saveLocalConfig, findConfigPath, findAttestation } from '@attest-it/core';
8
+ import { loadConfig, toAttestItConfig, readSealsSync, computeFingerprintSync, verifyGateSeal, verifyAllSeals, computeFingerprint, createAttestation, readAttestations, upsertAttestation, KeyProviderRegistry, getDefaultPrivateKeyPath, FilesystemKeyProvider, writeSignedAttestations, loadLocalConfigSync, getActiveIdentity, isAuthorizedSigner, createSeal, writeSealsSync, checkOpenSSL, getDefaultPublicKeyPath, OnePasswordKeyProvider, MacOSKeychainKeyProvider, generateKeyPair, setKeyPermissions, getGate, loadLocalConfig, getAttestItConfigDir, generateEd25519KeyPair, saveLocalConfig, findConfigPath, findAttestation, setAttestItHomeDir } from '@attest-it/core';
9
9
  import { spawn } from 'child_process';
10
10
  import * as os from 'os';
11
- import { homedir } from 'os';
12
11
  import { parse } from 'shell-quote';
13
12
  import * as React7 from 'react';
14
13
  import { useState, useEffect } from 'react';
@@ -17,6 +16,7 @@ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
17
16
  import { mkdir, writeFile, unlink, readFile } from 'fs/promises';
18
17
  import { Spinner, Select, TextInput } from '@inkjs/ui';
19
18
  import { stringify } from 'yaml';
19
+ import tabtab from '@pnpm/tabtab';
20
20
  import { fileURLToPath } from 'url';
21
21
 
22
22
  // src/index.ts
@@ -2464,31 +2464,46 @@ async function runList() {
2464
2464
  process.exit(ExitCode.CONFIG_ERROR);
2465
2465
  }
2466
2466
  }
2467
+
2468
+ // src/commands/identity/validation.ts
2469
+ function validateSlug(value, existingIdentities) {
2470
+ const trimmed = value.trim();
2471
+ if (!trimmed) {
2472
+ return "Slug cannot be empty";
2473
+ }
2474
+ if (!/^[a-z0-9-]+$/.test(trimmed)) {
2475
+ return "Slug must contain only lowercase letters, numbers, and hyphens";
2476
+ }
2477
+ if (existingIdentities?.[trimmed]) {
2478
+ return `Identity "${trimmed}" already exists`;
2479
+ }
2480
+ return true;
2481
+ }
2482
+ var EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
2483
+ function validateEmail(value, required = false) {
2484
+ const trimmed = value.trim();
2485
+ if (!trimmed) {
2486
+ return required ? "Email cannot be empty" : true;
2487
+ }
2488
+ if (!EMAIL_REGEX.test(trimmed)) {
2489
+ return "Please enter a valid email address";
2490
+ }
2491
+ return true;
2492
+ }
2467
2493
  var createCommand = new Command("create").description("Create a new identity with Ed25519 keypair").action(async () => {
2468
2494
  await runCreate();
2469
2495
  });
2470
2496
  async function runCreate() {
2471
2497
  try {
2472
- const theme3 = getTheme2();
2498
+ const theme3 = getTheme();
2473
2499
  log("");
2474
2500
  log(theme3.blue.bold()("Create New Identity"));
2475
2501
  log("");
2476
2502
  const existingConfig = await loadLocalConfig();
2477
- const slug = await input({
2503
+ const slug = (await input({
2478
2504
  message: "Identity slug (unique identifier):",
2479
- validate: (value) => {
2480
- if (!value || value.trim().length === 0) {
2481
- return "Slug cannot be empty";
2482
- }
2483
- if (!/^[a-z0-9-]+$/.test(value)) {
2484
- return "Slug must contain only lowercase letters, numbers, and hyphens";
2485
- }
2486
- if (existingConfig?.identities[value]) {
2487
- return `Identity "${value}" already exists`;
2488
- }
2489
- return true;
2490
- }
2491
- });
2505
+ validate: (value) => validateSlug(value, existingConfig?.identities)
2506
+ })).trim();
2492
2507
  const name = await input({
2493
2508
  message: "Display name:",
2494
2509
  validate: (value) => {
@@ -2498,21 +2513,31 @@ async function runCreate() {
2498
2513
  return true;
2499
2514
  }
2500
2515
  });
2501
- const email = await input({
2516
+ const email = (await input({
2502
2517
  message: "Email (optional):",
2503
- default: ""
2504
- });
2518
+ default: "",
2519
+ validate: validateEmail
2520
+ })).trim();
2505
2521
  const github = await input({
2506
2522
  message: "GitHub username (optional):",
2507
2523
  default: ""
2508
2524
  });
2525
+ info("Checking available key storage providers...");
2526
+ const opAvailable = await OnePasswordKeyProvider.isInstalled();
2527
+ const keychainAvailable = MacOSKeychainKeyProvider.isAvailable();
2528
+ const configDir = getAttestItConfigDir();
2529
+ const storageChoices = [
2530
+ { name: `File system (${join(configDir, "keys")})`, value: "file" }
2531
+ ];
2532
+ if (keychainAvailable) {
2533
+ storageChoices.push({ name: "macOS Keychain", value: "keychain" });
2534
+ }
2535
+ if (opAvailable) {
2536
+ storageChoices.push({ name: "1Password", value: "1password" });
2537
+ }
2509
2538
  const keyStorageType = await select({
2510
2539
  message: "Where should the private key be stored?",
2511
- choices: [
2512
- { name: "File system (~/.config/attest-it/keys/)", value: "file" },
2513
- { name: "macOS Keychain", value: "keychain" },
2514
- { name: "1Password", value: "1password" }
2515
- ]
2540
+ choices: storageChoices
2516
2541
  });
2517
2542
  log("");
2518
2543
  log("Generating Ed25519 keypair...");
@@ -2521,7 +2546,7 @@ async function runCreate() {
2521
2546
  let keyStorageDescription;
2522
2547
  switch (keyStorageType) {
2523
2548
  case "file": {
2524
- const keysDir = join(homedir(), ".config", "attest-it", "keys");
2549
+ const keysDir = join(getAttestItConfigDir(), "keys");
2525
2550
  await mkdir(keysDir, { recursive: true });
2526
2551
  const keyPath = join(keysDir, `${slug}.pem`);
2527
2552
  await writeFile(keyPath, keyPair.privateKey, { mode: 384 });
@@ -2530,44 +2555,138 @@ async function runCreate() {
2530
2555
  break;
2531
2556
  }
2532
2557
  case "keychain": {
2533
- const { MacOSKeychainKeyProvider: MacOSKeychainKeyProvider3 } = await import('@attest-it/core');
2534
- if (!MacOSKeychainKeyProvider3.isAvailable()) {
2558
+ if (!MacOSKeychainKeyProvider.isAvailable()) {
2535
2559
  error("macOS Keychain is not available on this system");
2536
2560
  process.exit(ExitCode.CONFIG_ERROR);
2537
2561
  }
2562
+ const keychains = await MacOSKeychainKeyProvider.listKeychains();
2563
+ if (keychains.length === 0) {
2564
+ throw new Error("No keychains found on this system");
2565
+ }
2566
+ const formatKeychainChoice = (kc) => {
2567
+ return `${theme3.blue.bold()(kc.name)} ${theme3.muted(`(${kc.path})`)}`;
2568
+ };
2569
+ let selectedKeychain;
2570
+ if (keychains.length === 1 && keychains[0]) {
2571
+ selectedKeychain = keychains[0];
2572
+ info(`Using keychain: ${formatKeychainChoice(selectedKeychain)}`);
2573
+ } else {
2574
+ const selectedPath = await select({
2575
+ message: "Select keychain:",
2576
+ choices: keychains.map((kc) => ({
2577
+ name: formatKeychainChoice(kc),
2578
+ value: kc.path
2579
+ }))
2580
+ });
2581
+ const foundKeychain = keychains.find((kc) => kc.path === selectedPath);
2582
+ if (!foundKeychain) {
2583
+ throw new Error("Selected keychain not found");
2584
+ }
2585
+ selectedKeychain = foundKeychain;
2586
+ }
2587
+ const keychainItemName = await input({
2588
+ message: "Keychain item name:",
2589
+ default: `attest-it-${slug}`,
2590
+ validate: (value) => {
2591
+ if (!value || value.trim().length === 0) {
2592
+ return "Item name cannot be empty";
2593
+ }
2594
+ return true;
2595
+ }
2596
+ });
2538
2597
  const { execFile } = await import('child_process');
2539
2598
  const { promisify } = await import('util');
2540
2599
  const execFileAsync = promisify(execFile);
2541
2600
  const encodedKey = Buffer.from(keyPair.privateKey).toString("base64");
2542
2601
  try {
2543
- await execFileAsync("security", [
2602
+ const addArgs = [
2544
2603
  "add-generic-password",
2545
2604
  "-a",
2546
2605
  "attest-it",
2547
2606
  "-s",
2548
- slug,
2607
+ keychainItemName,
2549
2608
  "-w",
2550
2609
  encodedKey,
2551
- "-U"
2552
- ]);
2610
+ "-U",
2611
+ selectedKeychain.path
2612
+ ];
2613
+ await execFileAsync("security", addArgs);
2553
2614
  } catch (err) {
2554
2615
  throw new Error(
2555
2616
  `Failed to store key in macOS Keychain: ${err instanceof Error ? err.message : String(err)}`
2556
2617
  );
2557
2618
  }
2558
- privateKeyRef = { type: "keychain", service: slug, account: "attest-it" };
2559
- keyStorageDescription = "macOS Keychain (" + slug + "/attest-it)";
2619
+ privateKeyRef = {
2620
+ type: "keychain",
2621
+ service: keychainItemName,
2622
+ account: "attest-it",
2623
+ keychain: selectedKeychain.path
2624
+ };
2625
+ keyStorageDescription = `macOS Keychain: ${selectedKeychain.name}/${keychainItemName}`;
2560
2626
  break;
2561
2627
  }
2562
2628
  case "1password": {
2563
- const vault = await input({
2564
- message: "1Password vault name:",
2565
- validate: (value) => {
2566
- if (!value || value.trim().length === 0) {
2567
- return "Vault name cannot be empty";
2629
+ const accounts = await OnePasswordKeyProvider.listAccounts();
2630
+ if (accounts.length === 0) {
2631
+ throw new Error(
2632
+ '1Password CLI is installed but no accounts are signed in. Run "op signin" first.'
2633
+ );
2634
+ }
2635
+ const { execFile } = await import('child_process');
2636
+ const { promisify } = await import('util');
2637
+ const execFileAsync = promisify(execFile);
2638
+ const accountDetails = await Promise.all(
2639
+ accounts.map(async (acc) => {
2640
+ try {
2641
+ const { stdout } = await execFileAsync("op", [
2642
+ "account",
2643
+ "get",
2644
+ "--account",
2645
+ acc.user_uuid,
2646
+ "--format=json"
2647
+ ]);
2648
+ const details = JSON.parse(stdout);
2649
+ const name2 = details !== null && typeof details === "object" && "name" in details && typeof details.name === "string" ? details.name : acc.url;
2650
+ return {
2651
+ url: acc.url,
2652
+ email: acc.email,
2653
+ name: name2
2654
+ };
2655
+ } catch {
2656
+ return {
2657
+ url: acc.url,
2658
+ email: acc.email,
2659
+ name: acc.url
2660
+ };
2568
2661
  }
2569
- return true;
2570
- }
2662
+ })
2663
+ );
2664
+ const formatAccountChoice = (acc) => {
2665
+ return `${theme3.blue.bold()(acc.name)} ${theme3.muted(`(${acc.url})`)}`;
2666
+ };
2667
+ let selectedAccount;
2668
+ if (accountDetails.length === 1 && accountDetails[0]) {
2669
+ selectedAccount = accountDetails[0].url;
2670
+ info(`Using 1Password account: ${formatAccountChoice(accountDetails[0])}`);
2671
+ } else {
2672
+ selectedAccount = await select({
2673
+ message: "Select 1Password account:",
2674
+ choices: accountDetails.map((acc) => ({
2675
+ name: formatAccountChoice(acc),
2676
+ value: acc.url
2677
+ }))
2678
+ });
2679
+ }
2680
+ const vaults = await OnePasswordKeyProvider.listVaults(selectedAccount);
2681
+ if (vaults.length === 0) {
2682
+ throw new Error(`No vaults found in 1Password account: ${selectedAccount}`);
2683
+ }
2684
+ const selectedVault = await select({
2685
+ message: "Select vault for private key storage:",
2686
+ choices: vaults.map((v) => ({
2687
+ name: v.name,
2688
+ value: v.name
2689
+ }))
2571
2690
  });
2572
2691
  const item = await input({
2573
2692
  message: "1Password item name:",
@@ -2579,26 +2698,40 @@ async function runCreate() {
2579
2698
  return true;
2580
2699
  }
2581
2700
  });
2582
- const { execFile } = await import('child_process');
2583
- const { promisify } = await import('util');
2584
- const execFileAsync = promisify(execFile);
2701
+ const { tmpdir } = await import('os');
2702
+ const tempDir = join(tmpdir(), `attest-it-${String(Date.now())}`);
2703
+ await mkdir(tempDir, { recursive: true });
2704
+ const tempPrivatePath = join(tempDir, "private.pem");
2585
2705
  try {
2586
- await execFileAsync("op", [
2587
- "item",
2706
+ await writeFile(tempPrivatePath, keyPair.privateKey, { mode: 384 });
2707
+ const { execFile: execFile2 } = await import('child_process');
2708
+ const { promisify: promisify2 } = await import('util');
2709
+ const execFileAsync2 = promisify2(execFile2);
2710
+ const opArgs = [
2711
+ "document",
2588
2712
  "create",
2589
- "--category=SecureNote",
2713
+ tempPrivatePath,
2714
+ "--title",
2715
+ item,
2590
2716
  "--vault",
2591
- vault,
2592
- `--title=${item}`,
2593
- `privateKey[password]=${keyPair.privateKey}`
2594
- ]);
2595
- } catch (err) {
2596
- throw new Error(
2597
- `Failed to store key in 1Password: ${err instanceof Error ? err.message : String(err)}`
2598
- );
2717
+ selectedVault
2718
+ ];
2719
+ if (selectedAccount) {
2720
+ opArgs.push("--account", selectedAccount);
2721
+ }
2722
+ await execFileAsync2("op", opArgs);
2723
+ } finally {
2724
+ const { rm } = await import('fs/promises');
2725
+ await rm(tempDir, { recursive: true, force: true }).catch(() => {
2726
+ });
2599
2727
  }
2600
- privateKeyRef = { type: "1password", vault, item, field: "privateKey" };
2601
- keyStorageDescription = `1Password (${vault}/${item})`;
2728
+ privateKeyRef = {
2729
+ type: "1password",
2730
+ vault: selectedVault,
2731
+ item,
2732
+ ...selectedAccount && { account: selectedAccount }
2733
+ };
2734
+ keyStorageDescription = `1Password (${selectedVault}/${item})`;
2602
2735
  break;
2603
2736
  }
2604
2737
  default:
@@ -2910,6 +3043,32 @@ async function runEdit(slug) {
2910
3043
  process.exit(ExitCode.CONFIG_ERROR);
2911
3044
  }
2912
3045
  }
3046
+
3047
+ // src/utils/format-key-location.ts
3048
+ function formatKeyLocation(privateKey) {
3049
+ const theme3 = getTheme();
3050
+ switch (privateKey.type) {
3051
+ case "file":
3052
+ return `${theme3.blue.bold()("File")}: ${theme3.muted(privateKey.path)}`;
3053
+ case "keychain": {
3054
+ let keychainName = "default";
3055
+ if (privateKey.keychain) {
3056
+ const filename = privateKey.keychain.split("/").pop() ?? privateKey.keychain;
3057
+ keychainName = filename.replace(/\.keychain(-db)?$/, "");
3058
+ }
3059
+ return `${theme3.blue.bold()("macOS Keychain")}: ${theme3.muted(`${keychainName}/${privateKey.service}`)}`;
3060
+ }
3061
+ case "1password": {
3062
+ const parts = [privateKey.vault, privateKey.item];
3063
+ if (privateKey.account) {
3064
+ parts.unshift(privateKey.account);
3065
+ }
3066
+ return `${theme3.blue.bold()("1Password")}: ${theme3.muted(parts.join("/"))}`;
3067
+ }
3068
+ default:
3069
+ return "Unknown storage";
3070
+ }
3071
+ }
2913
3072
  var removeCommand = new Command("remove").description("Delete identity and optionally delete private key").argument("<slug>", "Identity slug to remove").action(async (slug) => {
2914
3073
  await runRemove(slug);
2915
3074
  });
@@ -2925,7 +3084,7 @@ async function runRemove(slug) {
2925
3084
  error(`Identity "${slug}" not found`);
2926
3085
  process.exit(ExitCode.CONFIG_ERROR);
2927
3086
  }
2928
- const theme3 = getTheme2();
3087
+ const theme3 = getTheme();
2929
3088
  log("");
2930
3089
  log(theme3.blue.bold()(`Remove Identity: ${slug}`));
2931
3090
  log("");
@@ -2942,6 +3101,9 @@ async function runRemove(slug) {
2942
3101
  log("Cancelled");
2943
3102
  process.exit(ExitCode.CANCELLED);
2944
3103
  }
3104
+ const keyLocation = formatKeyLocation(identity.privateKey);
3105
+ log(` Private key: ${keyLocation}`);
3106
+ log("");
2945
3107
  const deletePrivateKey = await confirm({
2946
3108
  message: "Also delete the private key from storage?",
2947
3109
  default: false
@@ -2964,13 +3126,17 @@ async function runRemove(slug) {
2964
3126
  const { promisify } = await import('util');
2965
3127
  const execFileAsync = promisify(execFile);
2966
3128
  try {
2967
- await execFileAsync("security", [
3129
+ const deleteArgs = [
2968
3130
  "delete-generic-password",
2969
3131
  "-s",
2970
3132
  identity.privateKey.service,
2971
3133
  "-a",
2972
3134
  identity.privateKey.account
2973
- ]);
3135
+ ];
3136
+ if (identity.privateKey.keychain) {
3137
+ deleteArgs.push(identity.privateKey.keychain);
3138
+ }
3139
+ await execFileAsync("security", deleteArgs);
2974
3140
  log(` Deleted private key from macOS Keychain`);
2975
3141
  } catch (err) {
2976
3142
  if (err instanceof Error && !err.message.includes("could not be found") && !err.message.includes("does not exist")) {
@@ -3108,17 +3274,20 @@ async function runWhoami() {
3108
3274
  error("Active identity not found");
3109
3275
  process.exit(ExitCode.CONFIG_ERROR);
3110
3276
  }
3111
- const theme3 = getTheme2();
3277
+ const theme3 = getTheme();
3112
3278
  log("");
3113
- log(theme3.green.bold()(identity.name));
3279
+ log(theme3.blue.bold()("Active Identity"));
3280
+ log("");
3281
+ log(` Slug: ${theme3.green.bold()(config.activeIdentity)}`);
3282
+ log(` Name: ${identity.name}`);
3114
3283
  if (identity.email) {
3115
- log(theme3.muted(identity.email));
3284
+ log(` Email: ${theme3.muted(identity.email)}`);
3116
3285
  }
3117
3286
  if (identity.github) {
3118
- log(theme3.muted("@" + identity.github));
3287
+ log(` GitHub: ${theme3.muted("@" + identity.github)}`);
3119
3288
  }
3120
- log("");
3121
- log(`Identity: ${theme3.blue(config.activeIdentity)}`);
3289
+ log(` Public Key: ${theme3.muted(identity.publicKey.slice(0, 24) + "...")}`);
3290
+ log(` Key Store: ${formatKeyLocation(identity.privateKey)}`);
3122
3291
  log("");
3123
3292
  } catch (err) {
3124
3293
  if (err instanceof Error) {
@@ -3569,6 +3738,207 @@ async function runRemove2(slug, options) {
3569
3738
 
3570
3739
  // src/commands/team/index.ts
3571
3740
  var teamCommand = new Command("team").description("Manage team members and authorizations").addCommand(listCommand2).addCommand(addCommand).addCommand(editCommand2).addCommand(removeCommand2);
3741
+ var PROGRAM_NAME = "attest-it";
3742
+ async function getCompletions(env) {
3743
+ let shell;
3744
+ try {
3745
+ const detectedShell = tabtab.getShellFromEnv(process.env);
3746
+ shell = detectedShell === "pwsh" ? "bash" : detectedShell;
3747
+ } catch {
3748
+ shell = "bash";
3749
+ }
3750
+ const commands = [
3751
+ { name: "init", description: "Initialize a new config file" },
3752
+ { name: "status", description: "Show status of all gates" },
3753
+ { name: "run", description: "Run test suites interactively" },
3754
+ { name: "verify", description: "Verify all seals are valid" },
3755
+ { name: "seal", description: "Create a seal for a gate" },
3756
+ { name: "keygen", description: "Generate a new keypair" },
3757
+ { name: "prune", description: "Remove stale attestations" },
3758
+ { name: "identity", description: "Manage identities" },
3759
+ { name: "team", description: "Manage team members" },
3760
+ { name: "whoami", description: "Show active identity" },
3761
+ { name: "completion", description: "Shell completion commands" }
3762
+ ];
3763
+ const globalOptions2 = [
3764
+ { name: "--help", description: "Show help" },
3765
+ { name: "--version", description: "Show version" },
3766
+ { name: "--verbose", description: "Verbose output" },
3767
+ { name: "--quiet", description: "Minimal output" },
3768
+ { name: "--config", description: "Path to config file" }
3769
+ ];
3770
+ const identitySubcommands = [
3771
+ { name: "create", description: "Create a new identity" },
3772
+ { name: "list", description: "List all identities" },
3773
+ { name: "use", description: "Switch active identity" },
3774
+ { name: "remove", description: "Remove an identity" }
3775
+ ];
3776
+ const teamSubcommands = [
3777
+ { name: "add", description: "Add yourself to the team" },
3778
+ { name: "list", description: "List team members" },
3779
+ { name: "remove", description: "Remove a team member" }
3780
+ ];
3781
+ const completionSubcommands = [
3782
+ { name: "install", description: "Install shell completion" },
3783
+ { name: "uninstall", description: "Uninstall shell completion" }
3784
+ ];
3785
+ const words = env.line.split(/\s+/).filter(Boolean);
3786
+ const lastWord = env.last;
3787
+ const prevWord = env.prev;
3788
+ if (prevWord === "--config" || prevWord === "-c") {
3789
+ tabtab.logFiles();
3790
+ return;
3791
+ }
3792
+ if (lastWord.startsWith("-")) {
3793
+ tabtab.log(globalOptions2, shell, console.log);
3794
+ return;
3795
+ }
3796
+ const commandIndex = words.findIndex(
3797
+ (w) => !w.startsWith("-") && w !== PROGRAM_NAME && w !== "npx"
3798
+ );
3799
+ const currentCommand = commandIndex >= 0 ? words[commandIndex] ?? null : null;
3800
+ if (currentCommand === "identity") {
3801
+ const subcommandIndex = words.findIndex(
3802
+ (w, i) => i > commandIndex && !w.startsWith("-")
3803
+ );
3804
+ const subcommand = subcommandIndex >= 0 ? words[subcommandIndex] ?? null : null;
3805
+ if (subcommand === "use" || subcommand === "remove") {
3806
+ const identities = await getIdentitySlugs();
3807
+ if (identities.length > 0) {
3808
+ tabtab.log(identities, shell, console.log);
3809
+ return;
3810
+ }
3811
+ }
3812
+ if (!subcommand || subcommandIndex < 0) {
3813
+ tabtab.log(identitySubcommands, shell, console.log);
3814
+ return;
3815
+ }
3816
+ }
3817
+ if (currentCommand === "team") {
3818
+ const subcommandIndex = words.findIndex(
3819
+ (w, i) => i > commandIndex && !w.startsWith("-")
3820
+ );
3821
+ const subcommand = subcommandIndex >= 0 ? words[subcommandIndex] ?? null : null;
3822
+ if (!subcommand || subcommandIndex < 0) {
3823
+ tabtab.log(teamSubcommands, shell, console.log);
3824
+ return;
3825
+ }
3826
+ }
3827
+ if (currentCommand === "completion") {
3828
+ const subcommandIndex = words.findIndex(
3829
+ (w, i) => i > commandIndex && !w.startsWith("-")
3830
+ );
3831
+ const subcommand = subcommandIndex >= 0 ? words[subcommandIndex] ?? null : null;
3832
+ if (subcommand === "install") {
3833
+ tabtab.log(["bash", "zsh", "fish"], shell, console.log);
3834
+ return;
3835
+ }
3836
+ if (!subcommand || subcommandIndex < 0) {
3837
+ tabtab.log(completionSubcommands, shell, console.log);
3838
+ return;
3839
+ }
3840
+ }
3841
+ if (currentCommand === "status" || currentCommand === "verify" || currentCommand === "seal") {
3842
+ const gates = await getGateNames();
3843
+ if (gates.length > 0) {
3844
+ tabtab.log(gates, shell, console.log);
3845
+ return;
3846
+ }
3847
+ }
3848
+ if (currentCommand === "run") {
3849
+ const suites = await getSuiteNames();
3850
+ if (suites.length > 0) {
3851
+ tabtab.log(suites, shell, console.log);
3852
+ return;
3853
+ }
3854
+ }
3855
+ if (!currentCommand) {
3856
+ tabtab.log([...commands, ...globalOptions2], shell, console.log);
3857
+ }
3858
+ }
3859
+ async function getIdentitySlugs() {
3860
+ try {
3861
+ const config = await loadLocalConfig();
3862
+ if (config?.identities) {
3863
+ return Object.keys(config.identities);
3864
+ }
3865
+ } catch {
3866
+ }
3867
+ return [];
3868
+ }
3869
+ async function getGateNames() {
3870
+ try {
3871
+ const config = await loadConfig();
3872
+ if (config.gates) {
3873
+ return Object.keys(config.gates);
3874
+ }
3875
+ } catch {
3876
+ }
3877
+ return [];
3878
+ }
3879
+ async function getSuiteNames() {
3880
+ try {
3881
+ const config = await loadConfig();
3882
+ return Object.keys(config.suites);
3883
+ } catch {
3884
+ }
3885
+ return [];
3886
+ }
3887
+ var completionCommand = new Command("completion").description("Shell completion commands");
3888
+ completionCommand.command("install [shell]").description("Install shell completion (bash, zsh, or fish)").action(async (shellArg) => {
3889
+ try {
3890
+ let shell;
3891
+ if (shellArg !== void 0) {
3892
+ if (tabtab.isShellSupported(shellArg)) {
3893
+ shell = shellArg;
3894
+ } else {
3895
+ error(`Shell "${shellArg}" is not supported. Use bash, zsh, or fish.`);
3896
+ process.exit(ExitCode.CONFIG_ERROR);
3897
+ }
3898
+ }
3899
+ await tabtab.install({
3900
+ name: PROGRAM_NAME,
3901
+ completer: PROGRAM_NAME,
3902
+ shell
3903
+ });
3904
+ log("");
3905
+ success("Shell completion installed!");
3906
+ log("");
3907
+ info("Restart your shell or run:");
3908
+ if (shell === "bash" || !shell) {
3909
+ log(" source ~/.bashrc");
3910
+ }
3911
+ if (shell === "zsh" || !shell) {
3912
+ log(" source ~/.zshrc");
3913
+ }
3914
+ if (shell === "fish" || !shell) {
3915
+ log(" source ~/.config/fish/config.fish");
3916
+ }
3917
+ log("");
3918
+ } catch (err) {
3919
+ error(`Failed to install completion: ${err instanceof Error ? err.message : String(err)}`);
3920
+ process.exit(ExitCode.CONFIG_ERROR);
3921
+ }
3922
+ });
3923
+ completionCommand.command("uninstall").description("Uninstall shell completion").action(async () => {
3924
+ try {
3925
+ await tabtab.uninstall({
3926
+ name: PROGRAM_NAME
3927
+ });
3928
+ log("");
3929
+ success("Shell completion uninstalled!");
3930
+ log("");
3931
+ } catch (err) {
3932
+ error(`Failed to uninstall completion: ${err instanceof Error ? err.message : String(err)}`);
3933
+ process.exit(ExitCode.CONFIG_ERROR);
3934
+ }
3935
+ });
3936
+ completionCommand.command("server", { hidden: true }).description("Completion server (internal)").action(async () => {
3937
+ const env = tabtab.parseEnv(process.env);
3938
+ if (env.complete) {
3939
+ await getCompletions(env);
3940
+ }
3941
+ });
3572
3942
  function hasVersion(data) {
3573
3943
  return typeof data === "object" && data !== null && "version" in data && // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
3574
3944
  typeof data.version === "string";
@@ -3612,7 +3982,19 @@ program.addCommand(sealCommand);
3612
3982
  program.addCommand(identityCommand);
3613
3983
  program.addCommand(teamCommand);
3614
3984
  program.addCommand(whoamiCommand);
3985
+ program.addCommand(completionCommand);
3986
+ function processHomeDirOption() {
3987
+ const homeDirIndex = process.argv.indexOf("--home-dir");
3988
+ if (homeDirIndex !== -1 && homeDirIndex + 1 < process.argv.length) {
3989
+ const homeDir = process.argv[homeDirIndex + 1];
3990
+ if (homeDir && !homeDir.startsWith("-")) {
3991
+ setAttestItHomeDir(homeDir);
3992
+ process.argv.splice(homeDirIndex, 2);
3993
+ }
3994
+ }
3995
+ }
3615
3996
  async function run() {
3997
+ processHomeDirOption();
3616
3998
  if (process.argv.includes("--version") || process.argv.includes("-V")) {
3617
3999
  console.log(getPackageVersion());
3618
4000
  process.exit(0);