@attest-it/cli 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,7 +6,8 @@ import * as path from 'path';
6
6
  import { join, dirname } from 'path';
7
7
  import { detectTheme } from 'chromaterm';
8
8
  import { input, select, confirm, checkbox } from '@inquirer/prompts';
9
- 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
+ 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, YubiKeyProvider, getAttestItConfigDir, generateEd25519KeyPair, saveLocalConfig, findConfigPath, loadPreferences, savePreferences, findAttestation, setAttestItHomeDir } from '@attest-it/core';
10
+ import tabtab2 from '@pnpm/tabtab';
10
11
  import { spawn } from 'child_process';
11
12
  import * as os from 'os';
12
13
  import { parse } from 'shell-quote';
@@ -17,7 +18,6 @@ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
17
18
  import { mkdir, writeFile, unlink, readFile } from 'fs/promises';
18
19
  import { Spinner, Select, TextInput } from '@inkjs/ui';
19
20
  import { stringify } from 'yaml';
20
- import tabtab from '@pnpm/tabtab';
21
21
  import { fileURLToPath } from 'url';
22
22
 
23
23
  var globalOptions = {};
@@ -171,6 +171,87 @@ var ExitCode = {
171
171
  /** Missing required key file */
172
172
  MISSING_KEY: 5
173
173
  };
174
+ var PROGRAM_NAME = "attest-it";
175
+ var PROGRAM_ALIAS = "attest";
176
+ function detectCurrentShell() {
177
+ const shellPath = process.env.SHELL ?? "";
178
+ if (shellPath.endsWith("/bash") || shellPath.endsWith("/bash.exe")) {
179
+ return "bash";
180
+ }
181
+ if (shellPath.endsWith("/zsh") || shellPath.endsWith("/zsh.exe")) {
182
+ return "zsh";
183
+ }
184
+ if (shellPath.endsWith("/fish") || shellPath.endsWith("/fish.exe")) {
185
+ return "fish";
186
+ }
187
+ return null;
188
+ }
189
+ function getSourceCommand(shell) {
190
+ switch (shell) {
191
+ case "bash":
192
+ return "source ~/.bashrc";
193
+ case "zsh":
194
+ return "source ~/.zshrc";
195
+ case "fish":
196
+ return "source ~/.config/fish/config.fish";
197
+ }
198
+ }
199
+ async function offerCompletionInstall() {
200
+ try {
201
+ const prefs = await loadPreferences();
202
+ if (prefs.cliExperience?.declinedCompletionInstall) {
203
+ return false;
204
+ }
205
+ const shell = detectCurrentShell();
206
+ if (!shell) {
207
+ return false;
208
+ }
209
+ log("");
210
+ const shouldInstall = await confirm({
211
+ message: `Would you like to enable shell completions for ${shell}?`,
212
+ default: true
213
+ });
214
+ if (!shouldInstall) {
215
+ await savePreferences({
216
+ ...prefs,
217
+ cliExperience: {
218
+ ...prefs.cliExperience,
219
+ declinedCompletionInstall: true
220
+ }
221
+ });
222
+ log("");
223
+ info("No problem! If you change your mind, you can run:");
224
+ log(" attest-it completion install");
225
+ log("");
226
+ return false;
227
+ }
228
+ await tabtab2.install({
229
+ name: PROGRAM_NAME,
230
+ completer: PROGRAM_NAME,
231
+ shell
232
+ });
233
+ await tabtab2.install({
234
+ name: PROGRAM_ALIAS,
235
+ completer: PROGRAM_ALIAS,
236
+ shell
237
+ });
238
+ log("");
239
+ success(`Shell completions installed for ${shell}!`);
240
+ info(`Completions enabled for both "${PROGRAM_NAME}" and "${PROGRAM_ALIAS}" commands.`);
241
+ log("");
242
+ info("Restart your shell or run:");
243
+ log(` ${getSourceCommand(shell)}`);
244
+ log("");
245
+ return true;
246
+ } catch (err) {
247
+ error(`Failed to install completions: ${err instanceof Error ? err.message : String(err)}`);
248
+ log("");
249
+ info("You can try again later with:");
250
+ log(" attest-it completion install");
251
+ log("");
252
+ return false;
253
+ }
254
+ }
174
255
 
175
256
  // src/commands/init.ts
176
257
  var initCommand = new Command("init").description("Initialize attest-it configuration").option("-p, --path <path>", "Config file path", ".attest-it/config.yaml").option("-f, --force", "Overwrite existing config").action(async (options) => {
@@ -234,6 +315,7 @@ async function runInit(options) {
234
315
  log(` 1. Edit ${options.path} to define your test suites`);
235
316
  log(" 2. Run: attest-it keygen");
236
317
  log(" 3. Run: attest-it status");
318
+ await offerCompletionInstall();
237
319
  } catch (err) {
238
320
  if (err instanceof Error) {
239
321
  error(err.message);
@@ -1524,6 +1606,15 @@ function createKeyProviderFromIdentity(identity) {
1524
1606
  field: privateKey.field
1525
1607
  }
1526
1608
  });
1609
+ case "yubikey":
1610
+ return KeyProviderRegistry.create({
1611
+ type: "yubikey",
1612
+ options: {
1613
+ encryptedKeyPath: privateKey.encryptedKeyPath,
1614
+ slot: privateKey.slot,
1615
+ serial: privateKey.serial
1616
+ }
1617
+ });
1527
1618
  default: {
1528
1619
  const _exhaustiveCheck = privateKey;
1529
1620
  throw new Error(`Unsupported private key type: ${String(_exhaustiveCheck)}`);
@@ -1539,6 +1630,8 @@ function getKeyRefFromIdentity(identity) {
1539
1630
  return `${privateKey.service}:${privateKey.account}`;
1540
1631
  case "1password":
1541
1632
  return privateKey.item;
1633
+ case "yubikey":
1634
+ return privateKey.encryptedKeyPath;
1542
1635
  default: {
1543
1636
  const _exhaustiveCheck = privateKey;
1544
1637
  throw new Error(`Unsupported private key type: ${String(_exhaustiveCheck)}`);
@@ -1595,7 +1688,7 @@ function KeygenInteractive(props) {
1595
1688
  } else if (value === "1password") {
1596
1689
  setSelectedProvider("1password");
1597
1690
  if (accounts.length === 1 && accounts[0]) {
1598
- setSelectedAccount(accounts[0].email);
1691
+ setSelectedAccount(accounts[0].user_uuid);
1599
1692
  setStep("select-vault");
1600
1693
  } else {
1601
1694
  setStep("select-account");
@@ -1642,8 +1735,12 @@ function KeygenInteractive(props) {
1642
1735
  if (!selectedVault || !itemName) {
1643
1736
  throw new Error("Vault and item name are required for 1Password");
1644
1737
  }
1738
+ const vault = vaults.find((v) => v.id === selectedVault);
1739
+ if (!vault) {
1740
+ throw new Error("Selected vault not found");
1741
+ }
1645
1742
  const providerOptions = {
1646
- vault: selectedVault,
1743
+ vault: vault.name,
1647
1744
  itemName
1648
1745
  };
1649
1746
  if (selectedAccount !== void 0) {
@@ -1660,7 +1757,7 @@ function KeygenInteractive(props) {
1660
1757
  publicKeyPath: result.publicKeyPath,
1661
1758
  privateKeyRef: result.privateKeyRef,
1662
1759
  storageDescription: result.storageDescription,
1663
- vault: selectedVault,
1760
+ vault: vault.name,
1664
1761
  itemName
1665
1762
  };
1666
1763
  if (selectedAccount !== void 0) {
@@ -1726,7 +1823,7 @@ function KeygenInteractive(props) {
1726
1823
  if (step === "select-account") {
1727
1824
  const options = accounts.map((account) => ({
1728
1825
  label: account.email,
1729
- value: account.email
1826
+ value: account.user_uuid
1730
1827
  }));
1731
1828
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
1732
1829
  /* @__PURE__ */ jsx(Text, { bold: true, children: "Select 1Password account:" }),
@@ -1743,7 +1840,7 @@ function KeygenInteractive(props) {
1743
1840
  }
1744
1841
  const options = vaults.map((vault) => ({
1745
1842
  label: vault.name,
1746
- value: vault.name
1843
+ value: vault.id
1747
1844
  }));
1748
1845
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
1749
1846
  /* @__PURE__ */ jsx(Text, { bold: true, children: "Select vault for private key storage:" }),
@@ -2382,6 +2479,15 @@ function createKeyProviderFromIdentity2(identity) {
2382
2479
  field: privateKey.field
2383
2480
  }
2384
2481
  });
2482
+ case "yubikey":
2483
+ return KeyProviderRegistry.create({
2484
+ type: "yubikey",
2485
+ options: {
2486
+ encryptedKeyPath: privateKey.encryptedKeyPath,
2487
+ slot: privateKey.slot,
2488
+ serial: privateKey.serial
2489
+ }
2490
+ });
2385
2491
  default: {
2386
2492
  const _exhaustiveCheck = privateKey;
2387
2493
  throw new Error(`Unsupported private key type: ${String(_exhaustiveCheck)}`);
@@ -2397,6 +2503,8 @@ function getKeyRefFromIdentity2(identity) {
2397
2503
  return privateKey.service;
2398
2504
  case "1password":
2399
2505
  return privateKey.item;
2506
+ case "yubikey":
2507
+ return privateKey.encryptedKeyPath;
2400
2508
  default: {
2401
2509
  const _exhaustiveCheck = privateKey;
2402
2510
  throw new Error(`Unsupported private key type: ${String(_exhaustiveCheck)}`);
@@ -2436,6 +2544,9 @@ async function runList() {
2436
2544
  case "1password":
2437
2545
  keyType = "1password";
2438
2546
  break;
2547
+ case "yubikey":
2548
+ keyType = "yubikey";
2549
+ break;
2439
2550
  }
2440
2551
  log(`${marker} ${theme3.blue(slug)}`);
2441
2552
  log(` Name: ${nameDisplay}`);
@@ -2525,6 +2636,8 @@ async function runCreate() {
2525
2636
  info("Checking available key storage providers...");
2526
2637
  const opAvailable = await OnePasswordKeyProvider.isInstalled();
2527
2638
  const keychainAvailable = MacOSKeychainKeyProvider.isAvailable();
2639
+ const yubikeyInstalled = await YubiKeyProvider.isInstalled();
2640
+ const yubikeyConnected = yubikeyInstalled ? await YubiKeyProvider.isConnected() : false;
2528
2641
  const configDir = getAttestItConfigDir();
2529
2642
  const storageChoices = [
2530
2643
  { name: `File system (${join(configDir, "keys")})`, value: "file" }
@@ -2535,6 +2648,10 @@ async function runCreate() {
2535
2648
  if (opAvailable) {
2536
2649
  storageChoices.push({ name: "1Password", value: "1password" });
2537
2650
  }
2651
+ if (yubikeyInstalled) {
2652
+ const yubikeyLabel = yubikeyConnected ? "YubiKey (encrypted with challenge-response)" : "YubiKey (not connected - insert YubiKey first)";
2653
+ storageChoices.push({ name: yubikeyLabel, value: "yubikey" });
2654
+ }
2538
2655
  const keyStorageType = await select({
2539
2656
  message: "Where should the private key be stored?",
2540
2657
  choices: storageChoices
@@ -2734,6 +2851,75 @@ async function runCreate() {
2734
2851
  keyStorageDescription = `1Password (${selectedVault}/${item})`;
2735
2852
  break;
2736
2853
  }
2854
+ case "yubikey": {
2855
+ if (!await YubiKeyProvider.isConnected()) {
2856
+ error("No YubiKey detected. Please insert your YubiKey and try again.");
2857
+ process.exit(ExitCode.CONFIG_ERROR);
2858
+ }
2859
+ const yubikeys = await YubiKeyProvider.listDevices();
2860
+ if (yubikeys.length === 0) {
2861
+ throw new Error("No YubiKeys detected. Please insert a YubiKey and try again.");
2862
+ }
2863
+ const formatYubiKeyChoice = (yk) => {
2864
+ return `${theme3.blue.bold()(yk.type)} ${theme3.muted(`(Serial: ${yk.serial}, FW: ${yk.firmware})`)}`;
2865
+ };
2866
+ let selectedSerial;
2867
+ if (yubikeys.length === 1 && yubikeys[0]) {
2868
+ selectedSerial = yubikeys[0].serial;
2869
+ info(`Using YubiKey: ${formatYubiKeyChoice(yubikeys[0])}`);
2870
+ } else {
2871
+ selectedSerial = await select({
2872
+ message: "Select YubiKey:",
2873
+ choices: yubikeys.map((yk) => ({
2874
+ name: formatYubiKeyChoice(yk),
2875
+ value: yk.serial
2876
+ }))
2877
+ });
2878
+ }
2879
+ const slot = 2;
2880
+ const isChallengeResponseConfigured = await YubiKeyProvider.isChallengeResponseConfigured(
2881
+ slot,
2882
+ selectedSerial
2883
+ );
2884
+ if (!isChallengeResponseConfigured) {
2885
+ log("");
2886
+ error(`YubiKey slot ${String(slot)} is not configured for HMAC challenge-response.`);
2887
+ log("");
2888
+ log("To configure it, run:");
2889
+ log(theme3.blue(` ykman otp chalresp --generate ${String(slot)}`));
2890
+ log("");
2891
+ log("This will configure slot 2 with a randomly generated secret.");
2892
+ log(theme3.muted("Note: Make sure to back up the secret if needed for recovery."));
2893
+ process.exit(ExitCode.CONFIG_ERROR);
2894
+ }
2895
+ const encryptedKeyName = await input({
2896
+ message: "Encrypted key file name:",
2897
+ default: `${slug}.enc`,
2898
+ validate: (value) => {
2899
+ if (!value || value.trim().length === 0) {
2900
+ return "File name cannot be empty";
2901
+ }
2902
+ return true;
2903
+ }
2904
+ });
2905
+ const keysDir = join(getAttestItConfigDir(), "keys");
2906
+ await mkdir(keysDir, { recursive: true });
2907
+ const encryptedKeyPath = join(keysDir, encryptedKeyName);
2908
+ const result = await YubiKeyProvider.encryptPrivateKey({
2909
+ privateKey: keyPair.privateKey,
2910
+ encryptedKeyPath,
2911
+ slot,
2912
+ serial: selectedSerial
2913
+ });
2914
+ privateKeyRef = {
2915
+ type: "yubikey",
2916
+ encryptedKeyPath: result.encryptedKeyPath,
2917
+ slot,
2918
+ serial: selectedSerial
2919
+ };
2920
+ keyStorageDescription = result.storageDescription;
2921
+ break;
2922
+ }
2737
2923
  default:
2738
2924
  throw new Error(`Unknown key storage type: ${keyStorageType}`);
2739
2925
  }
@@ -2783,6 +2969,7 @@ async function runCreate() {
2783
2969
  log(`To use this identity, run: attest-it identity use ${slug}`);
2784
2970
  log("");
2785
2971
  }
2972
+ await offerCompletionInstall();
2786
2973
  } catch (err) {
2787
2974
  if (err instanceof Error) {
2788
2975
  error(err.message);
@@ -3065,6 +3252,11 @@ function formatKeyLocation(privateKey) {
3065
3252
  }
3066
3253
  return `${theme3.blue.bold()("1Password")}: ${theme3.muted(parts.join("/"))}`;
3067
3254
  }
3255
+ case "yubikey": {
3256
+ const slotInfo = privateKey.slot ? ` (slot ${String(privateKey.slot)})` : "";
3257
+ const serialInfo = privateKey.serial ? ` [${privateKey.serial}]` : "";
3258
+ return `${theme3.blue.bold()("YubiKey")}${serialInfo}${slotInfo}: ${theme3.muted(privateKey.encryptedKeyPath)}`;
3259
+ }
3068
3260
  default:
3069
3261
  return "Unknown storage";
3070
3262
  }
@@ -3738,11 +3930,16 @@ async function runRemove2(slug, options) {
3738
3930
 
3739
3931
  // src/commands/team/index.ts
3740
3932
  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";
3933
+ var PROGRAM_NAME2 = "attest-it";
3934
+ var PROGRAM_ALIAS2 = "attest";
3935
+ var PROGRAM_NAMES = [PROGRAM_NAME2, PROGRAM_ALIAS2];
3936
+ function isSupportedShell(value) {
3937
+ return value === "bash" || value === "zsh" || value === "fish";
3938
+ }
3742
3939
  async function getCompletions(env) {
3743
3940
  let shell;
3744
3941
  try {
3745
- const detectedShell = tabtab.getShellFromEnv(process.env);
3942
+ const detectedShell = tabtab2.getShellFromEnv(process.env);
3746
3943
  shell = detectedShell === "pwsh" ? "bash" : detectedShell;
3747
3944
  } catch {
3748
3945
  shell = "bash";
@@ -3786,15 +3983,15 @@ async function getCompletions(env) {
3786
3983
  const lastWord = env.last;
3787
3984
  const prevWord = env.prev;
3788
3985
  if (prevWord === "--config" || prevWord === "-c") {
3789
- tabtab.logFiles();
3986
+ tabtab2.logFiles();
3790
3987
  return;
3791
3988
  }
3792
3989
  if (lastWord.startsWith("-")) {
3793
- tabtab.log(globalOptions2, shell, console.log);
3990
+ tabtab2.log(globalOptions2, shell, console.log);
3794
3991
  return;
3795
3992
  }
3796
3993
  const commandIndex = words.findIndex(
3797
- (w) => !w.startsWith("-") && w !== PROGRAM_NAME && w !== "npx"
3994
+ (w) => !w.startsWith("-") && !PROGRAM_NAMES.includes(w) && w !== "npx"
3798
3995
  );
3799
3996
  const currentCommand = commandIndex >= 0 ? words[commandIndex] ?? null : null;
3800
3997
  if (currentCommand === "identity") {
@@ -3805,12 +4002,12 @@ async function getCompletions(env) {
3805
4002
  if (subcommand === "use" || subcommand === "remove") {
3806
4003
  const identities = await getIdentitySlugs();
3807
4004
  if (identities.length > 0) {
3808
- tabtab.log(identities, shell, console.log);
4005
+ tabtab2.log(identities, shell, console.log);
3809
4006
  return;
3810
4007
  }
3811
4008
  }
3812
4009
  if (!subcommand || subcommandIndex < 0) {
3813
- tabtab.log(identitySubcommands, shell, console.log);
4010
+ tabtab2.log(identitySubcommands, shell, console.log);
3814
4011
  return;
3815
4012
  }
3816
4013
  }
@@ -3820,7 +4017,7 @@ async function getCompletions(env) {
3820
4017
  );
3821
4018
  const subcommand = subcommandIndex >= 0 ? words[subcommandIndex] ?? null : null;
3822
4019
  if (!subcommand || subcommandIndex < 0) {
3823
- tabtab.log(teamSubcommands, shell, console.log);
4020
+ tabtab2.log(teamSubcommands, shell, console.log);
3824
4021
  return;
3825
4022
  }
3826
4023
  }
@@ -3830,30 +4027,43 @@ async function getCompletions(env) {
3830
4027
  );
3831
4028
  const subcommand = subcommandIndex >= 0 ? words[subcommandIndex] ?? null : null;
3832
4029
  if (subcommand === "install") {
3833
- tabtab.log(["bash", "zsh", "fish"], shell, console.log);
4030
+ tabtab2.log(["bash", "zsh", "fish"], shell, console.log);
3834
4031
  return;
3835
4032
  }
3836
4033
  if (!subcommand || subcommandIndex < 0) {
3837
- tabtab.log(completionSubcommands, shell, console.log);
4034
+ tabtab2.log(completionSubcommands, shell, console.log);
3838
4035
  return;
3839
4036
  }
3840
4037
  }
3841
4038
  if (currentCommand === "status" || currentCommand === "verify" || currentCommand === "seal") {
3842
4039
  const gates = await getGateNames();
3843
4040
  if (gates.length > 0) {
3844
- tabtab.log(gates, shell, console.log);
4041
+ tabtab2.log(gates, shell, console.log);
3845
4042
  return;
3846
4043
  }
3847
4044
  }
3848
4045
  if (currentCommand === "run") {
3849
4046
  const suites = await getSuiteNames();
3850
4047
  if (suites.length > 0) {
3851
- tabtab.log(suites, shell, console.log);
4048
+ tabtab2.log(suites, shell, console.log);
3852
4049
  return;
3853
4050
  }
3854
4051
  }
3855
- if (!currentCommand) {
3856
- tabtab.log([...commands, ...globalOptions2], shell, console.log);
4052
+ const knownCommands = [
4053
+ "init",
4054
+ "status",
4055
+ "run",
4056
+ "verify",
4057
+ "seal",
4058
+ "keygen",
4059
+ "prune",
4060
+ "identity",
4061
+ "team",
4062
+ "whoami",
4063
+ "completion"
4064
+ ];
4065
+ if (!currentCommand || !knownCommands.includes(currentCommand)) {
4066
+ tabtab2.log([...commands, ...globalOptions2], shell, console.log);
3857
4067
  }
3858
4068
  }
3859
4069
  async function getIdentitySlugs() {
@@ -3885,35 +4095,65 @@ async function getSuiteNames() {
3885
4095
  return [];
3886
4096
  }
3887
4097
  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) => {
4098
+ function detectCurrentShell2() {
4099
+ const shellPath = process.env.SHELL ?? "";
4100
+ if (shellPath.endsWith("/bash") || shellPath.endsWith("/bash.exe")) {
4101
+ return "bash";
4102
+ }
4103
+ if (shellPath.endsWith("/zsh") || shellPath.endsWith("/zsh.exe")) {
4104
+ return "zsh";
4105
+ }
4106
+ if (shellPath.endsWith("/fish") || shellPath.endsWith("/fish.exe")) {
4107
+ return "fish";
4108
+ }
4109
+ return null;
4110
+ }
4111
+ function getSourceCommand2(shell) {
4112
+ switch (shell) {
4113
+ case "bash":
4114
+ return "source ~/.bashrc";
4115
+ case "zsh":
4116
+ return "source ~/.zshrc";
4117
+ case "fish":
4118
+ return "source ~/.config/fish/config.fish";
4119
+ }
4120
+ }
4121
+ completionCommand.command("install [shell]").description("Install shell completion (auto-detects shell, or specify bash/zsh/fish)").action(async (shellArg) => {
3889
4122
  try {
3890
4123
  let shell;
3891
4124
  if (shellArg !== void 0) {
3892
- if (tabtab.isShellSupported(shellArg)) {
3893
- shell = shellArg;
3894
- } else {
4125
+ if (!isSupportedShell(shellArg)) {
3895
4126
  error(`Shell "${shellArg}" is not supported. Use bash, zsh, or fish.`);
3896
4127
  process.exit(ExitCode.CONFIG_ERROR);
3897
4128
  }
4129
+ shell = shellArg;
4130
+ } else {
4131
+ const detected = detectCurrentShell2();
4132
+ if (!detected) {
4133
+ error(
4134
+ "Could not detect your shell. Please specify: attest-it completion install <bash|zsh|fish>"
4135
+ );
4136
+ process.exit(ExitCode.CONFIG_ERROR);
4137
+ }
4138
+ shell = detected;
4139
+ info(`Detected shell: ${shell}`);
3898
4140
  }
3899
- await tabtab.install({
3900
- name: PROGRAM_NAME,
3901
- completer: PROGRAM_NAME,
4141
+ await tabtab2.install({
4142
+ name: PROGRAM_NAME2,
4143
+ completer: PROGRAM_NAME2,
4144
+ shell
4145
+ });
4146
+ await tabtab2.install({
4147
+ name: PROGRAM_ALIAS2,
4148
+ completer: PROGRAM_ALIAS2,
3902
4149
  shell
3903
4150
  });
3904
4151
  log("");
3905
- success("Shell completion installed!");
4152
+ success(`Shell completion installed for ${shell}!`);
4153
+ info(`Completions enabled for both "${PROGRAM_NAME2}" and "${PROGRAM_ALIAS2}" commands.`);
3906
4154
  log("");
3907
4155
  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
- }
4156
+ log(` ${getSourceCommand2(shell)}`);
3917
4157
  log("");
3918
4158
  } catch (err) {
3919
4159
  error(`Failed to install completion: ${err instanceof Error ? err.message : String(err)}`);
@@ -3922,8 +4162,11 @@ completionCommand.command("install [shell]").description("Install shell completi
3922
4162
  });
3923
4163
  completionCommand.command("uninstall").description("Uninstall shell completion").action(async () => {
3924
4164
  try {
3925
- await tabtab.uninstall({
3926
- name: PROGRAM_NAME
4165
+ await tabtab2.uninstall({
4166
+ name: PROGRAM_NAME2
4167
+ });
4168
+ await tabtab2.uninstall({
4169
+ name: PROGRAM_ALIAS2
3927
4170
  });
3928
4171
  log("");
3929
4172
  success("Shell completion uninstalled!");
@@ -3934,11 +4177,19 @@ completionCommand.command("uninstall").description("Uninstall shell completion")
3934
4177
  }
3935
4178
  });
3936
4179
  completionCommand.command("server", { hidden: true }).description("Completion server (internal)").action(async () => {
3937
- const env = tabtab.parseEnv(process.env);
4180
+ const env = tabtab2.parseEnv(process.env);
3938
4181
  if (env.complete) {
3939
4182
  await getCompletions(env);
3940
4183
  }
3941
4184
  });
4185
+ function createCompletionServerCommand() {
4186
+ return new Command("completion-server").allowUnknownOption().allowExcessArguments(true).action(async () => {
4187
+ const env = tabtab2.parseEnv(process.env);
4188
+ if (env.complete) {
4189
+ await getCompletions(env);
4190
+ }
4191
+ });
4192
+ }
3942
4193
  function hasVersion(data) {
3943
4194
  return typeof data === "object" && data !== null && "version" in data && // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
3944
4195
  typeof data.version === "string";
@@ -3983,6 +4234,7 @@ program.addCommand(identityCommand);
3983
4234
  program.addCommand(teamCommand);
3984
4235
  program.addCommand(whoamiCommand);
3985
4236
  program.addCommand(completionCommand);
4237
+ program.addCommand(createCompletionServerCommand(), { hidden: true });
3986
4238
  function processHomeDirOption() {
3987
4239
  const homeDirIndex = process.argv.indexOf("--home-dir");
3988
4240
  if (homeDirIndex !== -1 && homeDirIndex + 1 < process.argv.length) {
@@ -3999,7 +4251,10 @@ async function run() {
3999
4251
  console.log(getPackageVersion());
4000
4252
  process.exit(0);
4001
4253
  }
4002
- await initTheme();
4254
+ const isCompletionServer = process.argv.includes("completion-server");
4255
+ if (!isCompletionServer) {
4256
+ await initTheme();
4257
+ }
4003
4258
  program.parse();
4004
4259
  const options = program.opts();
4005
4260
  const outputOptions = {};