@attest-it/cli 0.4.0 → 0.6.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/bin/attest-it.js +700 -67
- package/dist/bin/attest-it.js.map +1 -1
- package/dist/index.cjs +702 -65
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +700 -67
- package/dist/index.js.map +1 -1
- package/package.json +8 -3
package/dist/bin/attest-it.js
CHANGED
|
@@ -6,10 +6,10 @@ 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, generateEd25519KeyPair, saveLocalConfig, findConfigPath, findAttestation } 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
|
-
import { homedir } from 'os';
|
|
13
13
|
import { parse } from 'shell-quote';
|
|
14
14
|
import * as React7 from 'react';
|
|
15
15
|
import { useState, useEffect } from 'react';
|
|
@@ -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)}`);
|
|
@@ -2382,6 +2475,15 @@ function createKeyProviderFromIdentity2(identity) {
|
|
|
2382
2475
|
field: privateKey.field
|
|
2383
2476
|
}
|
|
2384
2477
|
});
|
|
2478
|
+
case "yubikey":
|
|
2479
|
+
return KeyProviderRegistry.create({
|
|
2480
|
+
type: "yubikey",
|
|
2481
|
+
options: {
|
|
2482
|
+
encryptedKeyPath: privateKey.encryptedKeyPath,
|
|
2483
|
+
slot: privateKey.slot,
|
|
2484
|
+
serial: privateKey.serial
|
|
2485
|
+
}
|
|
2486
|
+
});
|
|
2385
2487
|
default: {
|
|
2386
2488
|
const _exhaustiveCheck = privateKey;
|
|
2387
2489
|
throw new Error(`Unsupported private key type: ${String(_exhaustiveCheck)}`);
|
|
@@ -2397,6 +2499,8 @@ function getKeyRefFromIdentity2(identity) {
|
|
|
2397
2499
|
return privateKey.service;
|
|
2398
2500
|
case "1password":
|
|
2399
2501
|
return privateKey.item;
|
|
2502
|
+
case "yubikey":
|
|
2503
|
+
return privateKey.encryptedKeyPath;
|
|
2400
2504
|
default: {
|
|
2401
2505
|
const _exhaustiveCheck = privateKey;
|
|
2402
2506
|
throw new Error(`Unsupported private key type: ${String(_exhaustiveCheck)}`);
|
|
@@ -2436,6 +2540,9 @@ async function runList() {
|
|
|
2436
2540
|
case "1password":
|
|
2437
2541
|
keyType = "1password";
|
|
2438
2542
|
break;
|
|
2543
|
+
case "yubikey":
|
|
2544
|
+
keyType = "yubikey";
|
|
2545
|
+
break;
|
|
2439
2546
|
}
|
|
2440
2547
|
log(`${marker} ${theme3.blue(slug)}`);
|
|
2441
2548
|
log(` Name: ${nameDisplay}`);
|
|
@@ -2464,31 +2571,46 @@ async function runList() {
|
|
|
2464
2571
|
process.exit(ExitCode.CONFIG_ERROR);
|
|
2465
2572
|
}
|
|
2466
2573
|
}
|
|
2574
|
+
|
|
2575
|
+
// src/commands/identity/validation.ts
|
|
2576
|
+
function validateSlug(value, existingIdentities) {
|
|
2577
|
+
const trimmed = value.trim();
|
|
2578
|
+
if (!trimmed) {
|
|
2579
|
+
return "Slug cannot be empty";
|
|
2580
|
+
}
|
|
2581
|
+
if (!/^[a-z0-9-]+$/.test(trimmed)) {
|
|
2582
|
+
return "Slug must contain only lowercase letters, numbers, and hyphens";
|
|
2583
|
+
}
|
|
2584
|
+
if (existingIdentities?.[trimmed]) {
|
|
2585
|
+
return `Identity "${trimmed}" already exists`;
|
|
2586
|
+
}
|
|
2587
|
+
return true;
|
|
2588
|
+
}
|
|
2589
|
+
var EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
2590
|
+
function validateEmail(value, required = false) {
|
|
2591
|
+
const trimmed = value.trim();
|
|
2592
|
+
if (!trimmed) {
|
|
2593
|
+
return required ? "Email cannot be empty" : true;
|
|
2594
|
+
}
|
|
2595
|
+
if (!EMAIL_REGEX.test(trimmed)) {
|
|
2596
|
+
return "Please enter a valid email address";
|
|
2597
|
+
}
|
|
2598
|
+
return true;
|
|
2599
|
+
}
|
|
2467
2600
|
var createCommand = new Command("create").description("Create a new identity with Ed25519 keypair").action(async () => {
|
|
2468
2601
|
await runCreate();
|
|
2469
2602
|
});
|
|
2470
2603
|
async function runCreate() {
|
|
2471
2604
|
try {
|
|
2472
|
-
const theme3 =
|
|
2605
|
+
const theme3 = getTheme();
|
|
2473
2606
|
log("");
|
|
2474
2607
|
log(theme3.blue.bold()("Create New Identity"));
|
|
2475
2608
|
log("");
|
|
2476
2609
|
const existingConfig = await loadLocalConfig();
|
|
2477
|
-
const slug = await input({
|
|
2610
|
+
const slug = (await input({
|
|
2478
2611
|
message: "Identity slug (unique identifier):",
|
|
2479
|
-
validate: (value) =>
|
|
2480
|
-
|
|
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
|
-
});
|
|
2612
|
+
validate: (value) => validateSlug(value, existingConfig?.identities)
|
|
2613
|
+
})).trim();
|
|
2492
2614
|
const name = await input({
|
|
2493
2615
|
message: "Display name:",
|
|
2494
2616
|
validate: (value) => {
|
|
@@ -2498,21 +2620,37 @@ async function runCreate() {
|
|
|
2498
2620
|
return true;
|
|
2499
2621
|
}
|
|
2500
2622
|
});
|
|
2501
|
-
const email = await input({
|
|
2623
|
+
const email = (await input({
|
|
2502
2624
|
message: "Email (optional):",
|
|
2503
|
-
default: ""
|
|
2504
|
-
|
|
2625
|
+
default: "",
|
|
2626
|
+
validate: validateEmail
|
|
2627
|
+
})).trim();
|
|
2505
2628
|
const github = await input({
|
|
2506
2629
|
message: "GitHub username (optional):",
|
|
2507
2630
|
default: ""
|
|
2508
2631
|
});
|
|
2632
|
+
info("Checking available key storage providers...");
|
|
2633
|
+
const opAvailable = await OnePasswordKeyProvider.isInstalled();
|
|
2634
|
+
const keychainAvailable = MacOSKeychainKeyProvider.isAvailable();
|
|
2635
|
+
const yubikeyInstalled = await YubiKeyProvider.isInstalled();
|
|
2636
|
+
const yubikeyConnected = yubikeyInstalled ? await YubiKeyProvider.isConnected() : false;
|
|
2637
|
+
const configDir = getAttestItConfigDir();
|
|
2638
|
+
const storageChoices = [
|
|
2639
|
+
{ name: `File system (${join(configDir, "keys")})`, value: "file" }
|
|
2640
|
+
];
|
|
2641
|
+
if (keychainAvailable) {
|
|
2642
|
+
storageChoices.push({ name: "macOS Keychain", value: "keychain" });
|
|
2643
|
+
}
|
|
2644
|
+
if (opAvailable) {
|
|
2645
|
+
storageChoices.push({ name: "1Password", value: "1password" });
|
|
2646
|
+
}
|
|
2647
|
+
if (yubikeyInstalled) {
|
|
2648
|
+
const yubikeyLabel = yubikeyConnected ? "YubiKey (encrypted with challenge-response)" : "YubiKey (not connected - insert YubiKey first)";
|
|
2649
|
+
storageChoices.push({ name: yubikeyLabel, value: "yubikey" });
|
|
2650
|
+
}
|
|
2509
2651
|
const keyStorageType = await select({
|
|
2510
2652
|
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
|
-
]
|
|
2653
|
+
choices: storageChoices
|
|
2516
2654
|
});
|
|
2517
2655
|
log("");
|
|
2518
2656
|
log("Generating Ed25519 keypair...");
|
|
@@ -2521,7 +2659,7 @@ async function runCreate() {
|
|
|
2521
2659
|
let keyStorageDescription;
|
|
2522
2660
|
switch (keyStorageType) {
|
|
2523
2661
|
case "file": {
|
|
2524
|
-
const keysDir = join(
|
|
2662
|
+
const keysDir = join(getAttestItConfigDir(), "keys");
|
|
2525
2663
|
await mkdir(keysDir, { recursive: true });
|
|
2526
2664
|
const keyPath = join(keysDir, `${slug}.pem`);
|
|
2527
2665
|
await writeFile(keyPath, keyPair.privateKey, { mode: 384 });
|
|
@@ -2530,44 +2668,138 @@ async function runCreate() {
|
|
|
2530
2668
|
break;
|
|
2531
2669
|
}
|
|
2532
2670
|
case "keychain": {
|
|
2533
|
-
|
|
2534
|
-
if (!MacOSKeychainKeyProvider3.isAvailable()) {
|
|
2671
|
+
if (!MacOSKeychainKeyProvider.isAvailable()) {
|
|
2535
2672
|
error("macOS Keychain is not available on this system");
|
|
2536
2673
|
process.exit(ExitCode.CONFIG_ERROR);
|
|
2537
2674
|
}
|
|
2675
|
+
const keychains = await MacOSKeychainKeyProvider.listKeychains();
|
|
2676
|
+
if (keychains.length === 0) {
|
|
2677
|
+
throw new Error("No keychains found on this system");
|
|
2678
|
+
}
|
|
2679
|
+
const formatKeychainChoice = (kc) => {
|
|
2680
|
+
return `${theme3.blue.bold()(kc.name)} ${theme3.muted(`(${kc.path})`)}`;
|
|
2681
|
+
};
|
|
2682
|
+
let selectedKeychain;
|
|
2683
|
+
if (keychains.length === 1 && keychains[0]) {
|
|
2684
|
+
selectedKeychain = keychains[0];
|
|
2685
|
+
info(`Using keychain: ${formatKeychainChoice(selectedKeychain)}`);
|
|
2686
|
+
} else {
|
|
2687
|
+
const selectedPath = await select({
|
|
2688
|
+
message: "Select keychain:",
|
|
2689
|
+
choices: keychains.map((kc) => ({
|
|
2690
|
+
name: formatKeychainChoice(kc),
|
|
2691
|
+
value: kc.path
|
|
2692
|
+
}))
|
|
2693
|
+
});
|
|
2694
|
+
const foundKeychain = keychains.find((kc) => kc.path === selectedPath);
|
|
2695
|
+
if (!foundKeychain) {
|
|
2696
|
+
throw new Error("Selected keychain not found");
|
|
2697
|
+
}
|
|
2698
|
+
selectedKeychain = foundKeychain;
|
|
2699
|
+
}
|
|
2700
|
+
const keychainItemName = await input({
|
|
2701
|
+
message: "Keychain item name:",
|
|
2702
|
+
default: `attest-it-${slug}`,
|
|
2703
|
+
validate: (value) => {
|
|
2704
|
+
if (!value || value.trim().length === 0) {
|
|
2705
|
+
return "Item name cannot be empty";
|
|
2706
|
+
}
|
|
2707
|
+
return true;
|
|
2708
|
+
}
|
|
2709
|
+
});
|
|
2538
2710
|
const { execFile } = await import('child_process');
|
|
2539
2711
|
const { promisify } = await import('util');
|
|
2540
2712
|
const execFileAsync = promisify(execFile);
|
|
2541
2713
|
const encodedKey = Buffer.from(keyPair.privateKey).toString("base64");
|
|
2542
2714
|
try {
|
|
2543
|
-
|
|
2715
|
+
const addArgs = [
|
|
2544
2716
|
"add-generic-password",
|
|
2545
2717
|
"-a",
|
|
2546
2718
|
"attest-it",
|
|
2547
2719
|
"-s",
|
|
2548
|
-
|
|
2720
|
+
keychainItemName,
|
|
2549
2721
|
"-w",
|
|
2550
2722
|
encodedKey,
|
|
2551
|
-
"-U"
|
|
2552
|
-
|
|
2723
|
+
"-U",
|
|
2724
|
+
selectedKeychain.path
|
|
2725
|
+
];
|
|
2726
|
+
await execFileAsync("security", addArgs);
|
|
2553
2727
|
} catch (err) {
|
|
2554
2728
|
throw new Error(
|
|
2555
2729
|
`Failed to store key in macOS Keychain: ${err instanceof Error ? err.message : String(err)}`
|
|
2556
2730
|
);
|
|
2557
2731
|
}
|
|
2558
|
-
privateKeyRef = {
|
|
2559
|
-
|
|
2732
|
+
privateKeyRef = {
|
|
2733
|
+
type: "keychain",
|
|
2734
|
+
service: keychainItemName,
|
|
2735
|
+
account: "attest-it",
|
|
2736
|
+
keychain: selectedKeychain.path
|
|
2737
|
+
};
|
|
2738
|
+
keyStorageDescription = `macOS Keychain: ${selectedKeychain.name}/${keychainItemName}`;
|
|
2560
2739
|
break;
|
|
2561
2740
|
}
|
|
2562
2741
|
case "1password": {
|
|
2563
|
-
const
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2742
|
+
const accounts = await OnePasswordKeyProvider.listAccounts();
|
|
2743
|
+
if (accounts.length === 0) {
|
|
2744
|
+
throw new Error(
|
|
2745
|
+
'1Password CLI is installed but no accounts are signed in. Run "op signin" first.'
|
|
2746
|
+
);
|
|
2747
|
+
}
|
|
2748
|
+
const { execFile } = await import('child_process');
|
|
2749
|
+
const { promisify } = await import('util');
|
|
2750
|
+
const execFileAsync = promisify(execFile);
|
|
2751
|
+
const accountDetails = await Promise.all(
|
|
2752
|
+
accounts.map(async (acc) => {
|
|
2753
|
+
try {
|
|
2754
|
+
const { stdout } = await execFileAsync("op", [
|
|
2755
|
+
"account",
|
|
2756
|
+
"get",
|
|
2757
|
+
"--account",
|
|
2758
|
+
acc.user_uuid,
|
|
2759
|
+
"--format=json"
|
|
2760
|
+
]);
|
|
2761
|
+
const details = JSON.parse(stdout);
|
|
2762
|
+
const name2 = details !== null && typeof details === "object" && "name" in details && typeof details.name === "string" ? details.name : acc.url;
|
|
2763
|
+
return {
|
|
2764
|
+
url: acc.url,
|
|
2765
|
+
email: acc.email,
|
|
2766
|
+
name: name2
|
|
2767
|
+
};
|
|
2768
|
+
} catch {
|
|
2769
|
+
return {
|
|
2770
|
+
url: acc.url,
|
|
2771
|
+
email: acc.email,
|
|
2772
|
+
name: acc.url
|
|
2773
|
+
};
|
|
2568
2774
|
}
|
|
2569
|
-
|
|
2570
|
-
|
|
2775
|
+
})
|
|
2776
|
+
);
|
|
2777
|
+
const formatAccountChoice = (acc) => {
|
|
2778
|
+
return `${theme3.blue.bold()(acc.name)} ${theme3.muted(`(${acc.url})`)}`;
|
|
2779
|
+
};
|
|
2780
|
+
let selectedAccount;
|
|
2781
|
+
if (accountDetails.length === 1 && accountDetails[0]) {
|
|
2782
|
+
selectedAccount = accountDetails[0].url;
|
|
2783
|
+
info(`Using 1Password account: ${formatAccountChoice(accountDetails[0])}`);
|
|
2784
|
+
} else {
|
|
2785
|
+
selectedAccount = await select({
|
|
2786
|
+
message: "Select 1Password account:",
|
|
2787
|
+
choices: accountDetails.map((acc) => ({
|
|
2788
|
+
name: formatAccountChoice(acc),
|
|
2789
|
+
value: acc.url
|
|
2790
|
+
}))
|
|
2791
|
+
});
|
|
2792
|
+
}
|
|
2793
|
+
const vaults = await OnePasswordKeyProvider.listVaults(selectedAccount);
|
|
2794
|
+
if (vaults.length === 0) {
|
|
2795
|
+
throw new Error(`No vaults found in 1Password account: ${selectedAccount}`);
|
|
2796
|
+
}
|
|
2797
|
+
const selectedVault = await select({
|
|
2798
|
+
message: "Select vault for private key storage:",
|
|
2799
|
+
choices: vaults.map((v) => ({
|
|
2800
|
+
name: v.name,
|
|
2801
|
+
value: v.name
|
|
2802
|
+
}))
|
|
2571
2803
|
});
|
|
2572
2804
|
const item = await input({
|
|
2573
2805
|
message: "1Password item name:",
|
|
@@ -2579,26 +2811,109 @@ async function runCreate() {
|
|
|
2579
2811
|
return true;
|
|
2580
2812
|
}
|
|
2581
2813
|
});
|
|
2582
|
-
const {
|
|
2583
|
-
const
|
|
2584
|
-
|
|
2814
|
+
const { tmpdir } = await import('os');
|
|
2815
|
+
const tempDir = join(tmpdir(), `attest-it-${String(Date.now())}`);
|
|
2816
|
+
await mkdir(tempDir, { recursive: true });
|
|
2817
|
+
const tempPrivatePath = join(tempDir, "private.pem");
|
|
2585
2818
|
try {
|
|
2586
|
-
await
|
|
2587
|
-
|
|
2819
|
+
await writeFile(tempPrivatePath, keyPair.privateKey, { mode: 384 });
|
|
2820
|
+
const { execFile: execFile2 } = await import('child_process');
|
|
2821
|
+
const { promisify: promisify2 } = await import('util');
|
|
2822
|
+
const execFileAsync2 = promisify2(execFile2);
|
|
2823
|
+
const opArgs = [
|
|
2824
|
+
"document",
|
|
2588
2825
|
"create",
|
|
2589
|
-
|
|
2826
|
+
tempPrivatePath,
|
|
2827
|
+
"--title",
|
|
2828
|
+
item,
|
|
2590
2829
|
"--vault",
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
);
|
|
2830
|
+
selectedVault
|
|
2831
|
+
];
|
|
2832
|
+
if (selectedAccount) {
|
|
2833
|
+
opArgs.push("--account", selectedAccount);
|
|
2834
|
+
}
|
|
2835
|
+
await execFileAsync2("op", opArgs);
|
|
2836
|
+
} finally {
|
|
2837
|
+
const { rm } = await import('fs/promises');
|
|
2838
|
+
await rm(tempDir, { recursive: true, force: true }).catch(() => {
|
|
2839
|
+
});
|
|
2840
|
+
}
|
|
2841
|
+
privateKeyRef = {
|
|
2842
|
+
type: "1password",
|
|
2843
|
+
vault: selectedVault,
|
|
2844
|
+
item,
|
|
2845
|
+
...selectedAccount && { account: selectedAccount }
|
|
2846
|
+
};
|
|
2847
|
+
keyStorageDescription = `1Password (${selectedVault}/${item})`;
|
|
2848
|
+
break;
|
|
2849
|
+
}
|
|
2850
|
+
case "yubikey": {
|
|
2851
|
+
if (!await YubiKeyProvider.isConnected()) {
|
|
2852
|
+
error("No YubiKey detected. Please insert your YubiKey and try again.");
|
|
2853
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
2854
|
+
}
|
|
2855
|
+
const yubikeys = await YubiKeyProvider.listDevices();
|
|
2856
|
+
if (yubikeys.length === 0) {
|
|
2857
|
+
throw new Error("No YubiKeys detected. Please insert a YubiKey and try again.");
|
|
2858
|
+
}
|
|
2859
|
+
const formatYubiKeyChoice = (yk) => {
|
|
2860
|
+
return `${theme3.blue.bold()(yk.type)} ${theme3.muted(`(Serial: ${yk.serial}, FW: ${yk.firmware})`)}`;
|
|
2861
|
+
};
|
|
2862
|
+
let selectedSerial;
|
|
2863
|
+
if (yubikeys.length === 1 && yubikeys[0]) {
|
|
2864
|
+
selectedSerial = yubikeys[0].serial;
|
|
2865
|
+
info(`Using YubiKey: ${formatYubiKeyChoice(yubikeys[0])}`);
|
|
2866
|
+
} else {
|
|
2867
|
+
selectedSerial = await select({
|
|
2868
|
+
message: "Select YubiKey:",
|
|
2869
|
+
choices: yubikeys.map((yk) => ({
|
|
2870
|
+
name: formatYubiKeyChoice(yk),
|
|
2871
|
+
value: yk.serial
|
|
2872
|
+
}))
|
|
2873
|
+
});
|
|
2599
2874
|
}
|
|
2600
|
-
|
|
2601
|
-
|
|
2875
|
+
const slot = 2;
|
|
2876
|
+
const isChallengeResponseConfigured = await YubiKeyProvider.isChallengeResponseConfigured(
|
|
2877
|
+
slot,
|
|
2878
|
+
selectedSerial
|
|
2879
|
+
);
|
|
2880
|
+
if (!isChallengeResponseConfigured) {
|
|
2881
|
+
log("");
|
|
2882
|
+
error(`YubiKey slot ${String(slot)} is not configured for HMAC challenge-response.`);
|
|
2883
|
+
log("");
|
|
2884
|
+
log("To configure it, run:");
|
|
2885
|
+
log(theme3.blue(` ykman otp chalresp --generate ${String(slot)}`));
|
|
2886
|
+
log("");
|
|
2887
|
+
log("This will configure slot 2 with a randomly generated secret.");
|
|
2888
|
+
log(theme3.muted("Note: Make sure to back up the secret if needed for recovery."));
|
|
2889
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
2890
|
+
}
|
|
2891
|
+
const encryptedKeyName = await input({
|
|
2892
|
+
message: "Encrypted key file name:",
|
|
2893
|
+
default: `${slug}.enc`,
|
|
2894
|
+
validate: (value) => {
|
|
2895
|
+
if (!value || value.trim().length === 0) {
|
|
2896
|
+
return "File name cannot be empty";
|
|
2897
|
+
}
|
|
2898
|
+
return true;
|
|
2899
|
+
}
|
|
2900
|
+
});
|
|
2901
|
+
const keysDir = join(getAttestItConfigDir(), "keys");
|
|
2902
|
+
await mkdir(keysDir, { recursive: true });
|
|
2903
|
+
const encryptedKeyPath = join(keysDir, encryptedKeyName);
|
|
2904
|
+
const result = await YubiKeyProvider.encryptPrivateKey({
|
|
2905
|
+
privateKey: keyPair.privateKey,
|
|
2906
|
+
encryptedKeyPath,
|
|
2907
|
+
slot,
|
|
2908
|
+
serial: selectedSerial
|
|
2909
|
+
});
|
|
2910
|
+
privateKeyRef = {
|
|
2911
|
+
type: "yubikey",
|
|
2912
|
+
encryptedKeyPath: result.encryptedKeyPath,
|
|
2913
|
+
slot,
|
|
2914
|
+
serial: selectedSerial
|
|
2915
|
+
};
|
|
2916
|
+
keyStorageDescription = result.storageDescription;
|
|
2602
2917
|
break;
|
|
2603
2918
|
}
|
|
2604
2919
|
default:
|
|
@@ -2650,6 +2965,7 @@ async function runCreate() {
|
|
|
2650
2965
|
log(`To use this identity, run: attest-it identity use ${slug}`);
|
|
2651
2966
|
log("");
|
|
2652
2967
|
}
|
|
2968
|
+
await offerCompletionInstall();
|
|
2653
2969
|
} catch (err) {
|
|
2654
2970
|
if (err instanceof Error) {
|
|
2655
2971
|
error(err.message);
|
|
@@ -2910,6 +3226,37 @@ async function runEdit(slug) {
|
|
|
2910
3226
|
process.exit(ExitCode.CONFIG_ERROR);
|
|
2911
3227
|
}
|
|
2912
3228
|
}
|
|
3229
|
+
|
|
3230
|
+
// src/utils/format-key-location.ts
|
|
3231
|
+
function formatKeyLocation(privateKey) {
|
|
3232
|
+
const theme3 = getTheme();
|
|
3233
|
+
switch (privateKey.type) {
|
|
3234
|
+
case "file":
|
|
3235
|
+
return `${theme3.blue.bold()("File")}: ${theme3.muted(privateKey.path)}`;
|
|
3236
|
+
case "keychain": {
|
|
3237
|
+
let keychainName = "default";
|
|
3238
|
+
if (privateKey.keychain) {
|
|
3239
|
+
const filename = privateKey.keychain.split("/").pop() ?? privateKey.keychain;
|
|
3240
|
+
keychainName = filename.replace(/\.keychain(-db)?$/, "");
|
|
3241
|
+
}
|
|
3242
|
+
return `${theme3.blue.bold()("macOS Keychain")}: ${theme3.muted(`${keychainName}/${privateKey.service}`)}`;
|
|
3243
|
+
}
|
|
3244
|
+
case "1password": {
|
|
3245
|
+
const parts = [privateKey.vault, privateKey.item];
|
|
3246
|
+
if (privateKey.account) {
|
|
3247
|
+
parts.unshift(privateKey.account);
|
|
3248
|
+
}
|
|
3249
|
+
return `${theme3.blue.bold()("1Password")}: ${theme3.muted(parts.join("/"))}`;
|
|
3250
|
+
}
|
|
3251
|
+
case "yubikey": {
|
|
3252
|
+
const slotInfo = privateKey.slot ? ` (slot ${String(privateKey.slot)})` : "";
|
|
3253
|
+
const serialInfo = privateKey.serial ? ` [${privateKey.serial}]` : "";
|
|
3254
|
+
return `${theme3.blue.bold()("YubiKey")}${serialInfo}${slotInfo}: ${theme3.muted(privateKey.encryptedKeyPath)}`;
|
|
3255
|
+
}
|
|
3256
|
+
default:
|
|
3257
|
+
return "Unknown storage";
|
|
3258
|
+
}
|
|
3259
|
+
}
|
|
2913
3260
|
var removeCommand = new Command("remove").description("Delete identity and optionally delete private key").argument("<slug>", "Identity slug to remove").action(async (slug) => {
|
|
2914
3261
|
await runRemove(slug);
|
|
2915
3262
|
});
|
|
@@ -2925,7 +3272,7 @@ async function runRemove(slug) {
|
|
|
2925
3272
|
error(`Identity "${slug}" not found`);
|
|
2926
3273
|
process.exit(ExitCode.CONFIG_ERROR);
|
|
2927
3274
|
}
|
|
2928
|
-
const theme3 =
|
|
3275
|
+
const theme3 = getTheme();
|
|
2929
3276
|
log("");
|
|
2930
3277
|
log(theme3.blue.bold()(`Remove Identity: ${slug}`));
|
|
2931
3278
|
log("");
|
|
@@ -2942,6 +3289,9 @@ async function runRemove(slug) {
|
|
|
2942
3289
|
log("Cancelled");
|
|
2943
3290
|
process.exit(ExitCode.CANCELLED);
|
|
2944
3291
|
}
|
|
3292
|
+
const keyLocation = formatKeyLocation(identity.privateKey);
|
|
3293
|
+
log(` Private key: ${keyLocation}`);
|
|
3294
|
+
log("");
|
|
2945
3295
|
const deletePrivateKey = await confirm({
|
|
2946
3296
|
message: "Also delete the private key from storage?",
|
|
2947
3297
|
default: false
|
|
@@ -2964,13 +3314,17 @@ async function runRemove(slug) {
|
|
|
2964
3314
|
const { promisify } = await import('util');
|
|
2965
3315
|
const execFileAsync = promisify(execFile);
|
|
2966
3316
|
try {
|
|
2967
|
-
|
|
3317
|
+
const deleteArgs = [
|
|
2968
3318
|
"delete-generic-password",
|
|
2969
3319
|
"-s",
|
|
2970
3320
|
identity.privateKey.service,
|
|
2971
3321
|
"-a",
|
|
2972
3322
|
identity.privateKey.account
|
|
2973
|
-
]
|
|
3323
|
+
];
|
|
3324
|
+
if (identity.privateKey.keychain) {
|
|
3325
|
+
deleteArgs.push(identity.privateKey.keychain);
|
|
3326
|
+
}
|
|
3327
|
+
await execFileAsync("security", deleteArgs);
|
|
2974
3328
|
log(` Deleted private key from macOS Keychain`);
|
|
2975
3329
|
} catch (err) {
|
|
2976
3330
|
if (err instanceof Error && !err.message.includes("could not be found") && !err.message.includes("does not exist")) {
|
|
@@ -3108,17 +3462,20 @@ async function runWhoami() {
|
|
|
3108
3462
|
error("Active identity not found");
|
|
3109
3463
|
process.exit(ExitCode.CONFIG_ERROR);
|
|
3110
3464
|
}
|
|
3111
|
-
const theme3 =
|
|
3465
|
+
const theme3 = getTheme();
|
|
3466
|
+
log("");
|
|
3467
|
+
log(theme3.blue.bold()("Active Identity"));
|
|
3112
3468
|
log("");
|
|
3113
|
-
log(theme3.green.bold()(
|
|
3469
|
+
log(` Slug: ${theme3.green.bold()(config.activeIdentity)}`);
|
|
3470
|
+
log(` Name: ${identity.name}`);
|
|
3114
3471
|
if (identity.email) {
|
|
3115
|
-
log(theme3.muted(identity.email));
|
|
3472
|
+
log(` Email: ${theme3.muted(identity.email)}`);
|
|
3116
3473
|
}
|
|
3117
3474
|
if (identity.github) {
|
|
3118
|
-
log(theme3.muted("@" + identity.github));
|
|
3475
|
+
log(` GitHub: ${theme3.muted("@" + identity.github)}`);
|
|
3119
3476
|
}
|
|
3120
|
-
log("");
|
|
3121
|
-
log(`
|
|
3477
|
+
log(` Public Key: ${theme3.muted(identity.publicKey.slice(0, 24) + "...")}`);
|
|
3478
|
+
log(` Key Store: ${formatKeyLocation(identity.privateKey)}`);
|
|
3122
3479
|
log("");
|
|
3123
3480
|
} catch (err) {
|
|
3124
3481
|
if (err instanceof Error) {
|
|
@@ -3569,6 +3926,266 @@ async function runRemove2(slug, options) {
|
|
|
3569
3926
|
|
|
3570
3927
|
// src/commands/team/index.ts
|
|
3571
3928
|
var teamCommand = new Command("team").description("Manage team members and authorizations").addCommand(listCommand2).addCommand(addCommand).addCommand(editCommand2).addCommand(removeCommand2);
|
|
3929
|
+
var PROGRAM_NAME2 = "attest-it";
|
|
3930
|
+
var PROGRAM_ALIAS2 = "attest";
|
|
3931
|
+
var PROGRAM_NAMES = [PROGRAM_NAME2, PROGRAM_ALIAS2];
|
|
3932
|
+
function isSupportedShell(value) {
|
|
3933
|
+
return value === "bash" || value === "zsh" || value === "fish";
|
|
3934
|
+
}
|
|
3935
|
+
async function getCompletions(env) {
|
|
3936
|
+
let shell;
|
|
3937
|
+
try {
|
|
3938
|
+
const detectedShell = tabtab2.getShellFromEnv(process.env);
|
|
3939
|
+
shell = detectedShell === "pwsh" ? "bash" : detectedShell;
|
|
3940
|
+
} catch {
|
|
3941
|
+
shell = "bash";
|
|
3942
|
+
}
|
|
3943
|
+
const commands = [
|
|
3944
|
+
{ name: "init", description: "Initialize a new config file" },
|
|
3945
|
+
{ name: "status", description: "Show status of all gates" },
|
|
3946
|
+
{ name: "run", description: "Run test suites interactively" },
|
|
3947
|
+
{ name: "verify", description: "Verify all seals are valid" },
|
|
3948
|
+
{ name: "seal", description: "Create a seal for a gate" },
|
|
3949
|
+
{ name: "keygen", description: "Generate a new keypair" },
|
|
3950
|
+
{ name: "prune", description: "Remove stale attestations" },
|
|
3951
|
+
{ name: "identity", description: "Manage identities" },
|
|
3952
|
+
{ name: "team", description: "Manage team members" },
|
|
3953
|
+
{ name: "whoami", description: "Show active identity" },
|
|
3954
|
+
{ name: "completion", description: "Shell completion commands" }
|
|
3955
|
+
];
|
|
3956
|
+
const globalOptions2 = [
|
|
3957
|
+
{ name: "--help", description: "Show help" },
|
|
3958
|
+
{ name: "--version", description: "Show version" },
|
|
3959
|
+
{ name: "--verbose", description: "Verbose output" },
|
|
3960
|
+
{ name: "--quiet", description: "Minimal output" },
|
|
3961
|
+
{ name: "--config", description: "Path to config file" }
|
|
3962
|
+
];
|
|
3963
|
+
const identitySubcommands = [
|
|
3964
|
+
{ name: "create", description: "Create a new identity" },
|
|
3965
|
+
{ name: "list", description: "List all identities" },
|
|
3966
|
+
{ name: "use", description: "Switch active identity" },
|
|
3967
|
+
{ name: "remove", description: "Remove an identity" }
|
|
3968
|
+
];
|
|
3969
|
+
const teamSubcommands = [
|
|
3970
|
+
{ name: "add", description: "Add yourself to the team" },
|
|
3971
|
+
{ name: "list", description: "List team members" },
|
|
3972
|
+
{ name: "remove", description: "Remove a team member" }
|
|
3973
|
+
];
|
|
3974
|
+
const completionSubcommands = [
|
|
3975
|
+
{ name: "install", description: "Install shell completion" },
|
|
3976
|
+
{ name: "uninstall", description: "Uninstall shell completion" }
|
|
3977
|
+
];
|
|
3978
|
+
const words = env.line.split(/\s+/).filter(Boolean);
|
|
3979
|
+
const lastWord = env.last;
|
|
3980
|
+
const prevWord = env.prev;
|
|
3981
|
+
if (prevWord === "--config" || prevWord === "-c") {
|
|
3982
|
+
tabtab2.logFiles();
|
|
3983
|
+
return;
|
|
3984
|
+
}
|
|
3985
|
+
if (lastWord.startsWith("-")) {
|
|
3986
|
+
tabtab2.log(globalOptions2, shell, console.log);
|
|
3987
|
+
return;
|
|
3988
|
+
}
|
|
3989
|
+
const commandIndex = words.findIndex(
|
|
3990
|
+
(w) => !w.startsWith("-") && !PROGRAM_NAMES.includes(w) && w !== "npx"
|
|
3991
|
+
);
|
|
3992
|
+
const currentCommand = commandIndex >= 0 ? words[commandIndex] ?? null : null;
|
|
3993
|
+
if (currentCommand === "identity") {
|
|
3994
|
+
const subcommandIndex = words.findIndex(
|
|
3995
|
+
(w, i) => i > commandIndex && !w.startsWith("-")
|
|
3996
|
+
);
|
|
3997
|
+
const subcommand = subcommandIndex >= 0 ? words[subcommandIndex] ?? null : null;
|
|
3998
|
+
if (subcommand === "use" || subcommand === "remove") {
|
|
3999
|
+
const identities = await getIdentitySlugs();
|
|
4000
|
+
if (identities.length > 0) {
|
|
4001
|
+
tabtab2.log(identities, shell, console.log);
|
|
4002
|
+
return;
|
|
4003
|
+
}
|
|
4004
|
+
}
|
|
4005
|
+
if (!subcommand || subcommandIndex < 0) {
|
|
4006
|
+
tabtab2.log(identitySubcommands, shell, console.log);
|
|
4007
|
+
return;
|
|
4008
|
+
}
|
|
4009
|
+
}
|
|
4010
|
+
if (currentCommand === "team") {
|
|
4011
|
+
const subcommandIndex = words.findIndex(
|
|
4012
|
+
(w, i) => i > commandIndex && !w.startsWith("-")
|
|
4013
|
+
);
|
|
4014
|
+
const subcommand = subcommandIndex >= 0 ? words[subcommandIndex] ?? null : null;
|
|
4015
|
+
if (!subcommand || subcommandIndex < 0) {
|
|
4016
|
+
tabtab2.log(teamSubcommands, shell, console.log);
|
|
4017
|
+
return;
|
|
4018
|
+
}
|
|
4019
|
+
}
|
|
4020
|
+
if (currentCommand === "completion") {
|
|
4021
|
+
const subcommandIndex = words.findIndex(
|
|
4022
|
+
(w, i) => i > commandIndex && !w.startsWith("-")
|
|
4023
|
+
);
|
|
4024
|
+
const subcommand = subcommandIndex >= 0 ? words[subcommandIndex] ?? null : null;
|
|
4025
|
+
if (subcommand === "install") {
|
|
4026
|
+
tabtab2.log(["bash", "zsh", "fish"], shell, console.log);
|
|
4027
|
+
return;
|
|
4028
|
+
}
|
|
4029
|
+
if (!subcommand || subcommandIndex < 0) {
|
|
4030
|
+
tabtab2.log(completionSubcommands, shell, console.log);
|
|
4031
|
+
return;
|
|
4032
|
+
}
|
|
4033
|
+
}
|
|
4034
|
+
if (currentCommand === "status" || currentCommand === "verify" || currentCommand === "seal") {
|
|
4035
|
+
const gates = await getGateNames();
|
|
4036
|
+
if (gates.length > 0) {
|
|
4037
|
+
tabtab2.log(gates, shell, console.log);
|
|
4038
|
+
return;
|
|
4039
|
+
}
|
|
4040
|
+
}
|
|
4041
|
+
if (currentCommand === "run") {
|
|
4042
|
+
const suites = await getSuiteNames();
|
|
4043
|
+
if (suites.length > 0) {
|
|
4044
|
+
tabtab2.log(suites, shell, console.log);
|
|
4045
|
+
return;
|
|
4046
|
+
}
|
|
4047
|
+
}
|
|
4048
|
+
const knownCommands = [
|
|
4049
|
+
"init",
|
|
4050
|
+
"status",
|
|
4051
|
+
"run",
|
|
4052
|
+
"verify",
|
|
4053
|
+
"seal",
|
|
4054
|
+
"keygen",
|
|
4055
|
+
"prune",
|
|
4056
|
+
"identity",
|
|
4057
|
+
"team",
|
|
4058
|
+
"whoami",
|
|
4059
|
+
"completion"
|
|
4060
|
+
];
|
|
4061
|
+
if (!currentCommand || !knownCommands.includes(currentCommand)) {
|
|
4062
|
+
tabtab2.log([...commands, ...globalOptions2], shell, console.log);
|
|
4063
|
+
}
|
|
4064
|
+
}
|
|
4065
|
+
async function getIdentitySlugs() {
|
|
4066
|
+
try {
|
|
4067
|
+
const config = await loadLocalConfig();
|
|
4068
|
+
if (config?.identities) {
|
|
4069
|
+
return Object.keys(config.identities);
|
|
4070
|
+
}
|
|
4071
|
+
} catch {
|
|
4072
|
+
}
|
|
4073
|
+
return [];
|
|
4074
|
+
}
|
|
4075
|
+
async function getGateNames() {
|
|
4076
|
+
try {
|
|
4077
|
+
const config = await loadConfig();
|
|
4078
|
+
if (config.gates) {
|
|
4079
|
+
return Object.keys(config.gates);
|
|
4080
|
+
}
|
|
4081
|
+
} catch {
|
|
4082
|
+
}
|
|
4083
|
+
return [];
|
|
4084
|
+
}
|
|
4085
|
+
async function getSuiteNames() {
|
|
4086
|
+
try {
|
|
4087
|
+
const config = await loadConfig();
|
|
4088
|
+
return Object.keys(config.suites);
|
|
4089
|
+
} catch {
|
|
4090
|
+
}
|
|
4091
|
+
return [];
|
|
4092
|
+
}
|
|
4093
|
+
var completionCommand = new Command("completion").description("Shell completion commands");
|
|
4094
|
+
function detectCurrentShell2() {
|
|
4095
|
+
const shellPath = process.env.SHELL ?? "";
|
|
4096
|
+
if (shellPath.endsWith("/bash") || shellPath.endsWith("/bash.exe")) {
|
|
4097
|
+
return "bash";
|
|
4098
|
+
}
|
|
4099
|
+
if (shellPath.endsWith("/zsh") || shellPath.endsWith("/zsh.exe")) {
|
|
4100
|
+
return "zsh";
|
|
4101
|
+
}
|
|
4102
|
+
if (shellPath.endsWith("/fish") || shellPath.endsWith("/fish.exe")) {
|
|
4103
|
+
return "fish";
|
|
4104
|
+
}
|
|
4105
|
+
return null;
|
|
4106
|
+
}
|
|
4107
|
+
function getSourceCommand2(shell) {
|
|
4108
|
+
switch (shell) {
|
|
4109
|
+
case "bash":
|
|
4110
|
+
return "source ~/.bashrc";
|
|
4111
|
+
case "zsh":
|
|
4112
|
+
return "source ~/.zshrc";
|
|
4113
|
+
case "fish":
|
|
4114
|
+
return "source ~/.config/fish/config.fish";
|
|
4115
|
+
}
|
|
4116
|
+
}
|
|
4117
|
+
completionCommand.command("install [shell]").description("Install shell completion (auto-detects shell, or specify bash/zsh/fish)").action(async (shellArg) => {
|
|
4118
|
+
try {
|
|
4119
|
+
let shell;
|
|
4120
|
+
if (shellArg !== void 0) {
|
|
4121
|
+
if (!isSupportedShell(shellArg)) {
|
|
4122
|
+
error(`Shell "${shellArg}" is not supported. Use bash, zsh, or fish.`);
|
|
4123
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
4124
|
+
}
|
|
4125
|
+
shell = shellArg;
|
|
4126
|
+
} else {
|
|
4127
|
+
const detected = detectCurrentShell2();
|
|
4128
|
+
if (!detected) {
|
|
4129
|
+
error(
|
|
4130
|
+
"Could not detect your shell. Please specify: attest-it completion install <bash|zsh|fish>"
|
|
4131
|
+
);
|
|
4132
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
4133
|
+
}
|
|
4134
|
+
shell = detected;
|
|
4135
|
+
info(`Detected shell: ${shell}`);
|
|
4136
|
+
}
|
|
4137
|
+
await tabtab2.install({
|
|
4138
|
+
name: PROGRAM_NAME2,
|
|
4139
|
+
completer: PROGRAM_NAME2,
|
|
4140
|
+
shell
|
|
4141
|
+
});
|
|
4142
|
+
await tabtab2.install({
|
|
4143
|
+
name: PROGRAM_ALIAS2,
|
|
4144
|
+
completer: PROGRAM_ALIAS2,
|
|
4145
|
+
shell
|
|
4146
|
+
});
|
|
4147
|
+
log("");
|
|
4148
|
+
success(`Shell completion installed for ${shell}!`);
|
|
4149
|
+
info(`Completions enabled for both "${PROGRAM_NAME2}" and "${PROGRAM_ALIAS2}" commands.`);
|
|
4150
|
+
log("");
|
|
4151
|
+
info("Restart your shell or run:");
|
|
4152
|
+
log(` ${getSourceCommand2(shell)}`);
|
|
4153
|
+
log("");
|
|
4154
|
+
} catch (err) {
|
|
4155
|
+
error(`Failed to install completion: ${err instanceof Error ? err.message : String(err)}`);
|
|
4156
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
4157
|
+
}
|
|
4158
|
+
});
|
|
4159
|
+
completionCommand.command("uninstall").description("Uninstall shell completion").action(async () => {
|
|
4160
|
+
try {
|
|
4161
|
+
await tabtab2.uninstall({
|
|
4162
|
+
name: PROGRAM_NAME2
|
|
4163
|
+
});
|
|
4164
|
+
await tabtab2.uninstall({
|
|
4165
|
+
name: PROGRAM_ALIAS2
|
|
4166
|
+
});
|
|
4167
|
+
log("");
|
|
4168
|
+
success("Shell completion uninstalled!");
|
|
4169
|
+
log("");
|
|
4170
|
+
} catch (err) {
|
|
4171
|
+
error(`Failed to uninstall completion: ${err instanceof Error ? err.message : String(err)}`);
|
|
4172
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
4173
|
+
}
|
|
4174
|
+
});
|
|
4175
|
+
completionCommand.command("server", { hidden: true }).description("Completion server (internal)").action(async () => {
|
|
4176
|
+
const env = tabtab2.parseEnv(process.env);
|
|
4177
|
+
if (env.complete) {
|
|
4178
|
+
await getCompletions(env);
|
|
4179
|
+
}
|
|
4180
|
+
});
|
|
4181
|
+
function createCompletionServerCommand() {
|
|
4182
|
+
return new Command("completion-server").allowUnknownOption().allowExcessArguments(true).action(async () => {
|
|
4183
|
+
const env = tabtab2.parseEnv(process.env);
|
|
4184
|
+
if (env.complete) {
|
|
4185
|
+
await getCompletions(env);
|
|
4186
|
+
}
|
|
4187
|
+
});
|
|
4188
|
+
}
|
|
3572
4189
|
function hasVersion(data) {
|
|
3573
4190
|
return typeof data === "object" && data !== null && "version" in data && // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
3574
4191
|
typeof data.version === "string";
|
|
@@ -3612,12 +4229,28 @@ program.addCommand(sealCommand);
|
|
|
3612
4229
|
program.addCommand(identityCommand);
|
|
3613
4230
|
program.addCommand(teamCommand);
|
|
3614
4231
|
program.addCommand(whoamiCommand);
|
|
4232
|
+
program.addCommand(completionCommand);
|
|
4233
|
+
program.addCommand(createCompletionServerCommand(), { hidden: true });
|
|
4234
|
+
function processHomeDirOption() {
|
|
4235
|
+
const homeDirIndex = process.argv.indexOf("--home-dir");
|
|
4236
|
+
if (homeDirIndex !== -1 && homeDirIndex + 1 < process.argv.length) {
|
|
4237
|
+
const homeDir = process.argv[homeDirIndex + 1];
|
|
4238
|
+
if (homeDir && !homeDir.startsWith("-")) {
|
|
4239
|
+
setAttestItHomeDir(homeDir);
|
|
4240
|
+
process.argv.splice(homeDirIndex, 2);
|
|
4241
|
+
}
|
|
4242
|
+
}
|
|
4243
|
+
}
|
|
3615
4244
|
async function run() {
|
|
4245
|
+
processHomeDirOption();
|
|
3616
4246
|
if (process.argv.includes("--version") || process.argv.includes("-V")) {
|
|
3617
4247
|
console.log(getPackageVersion());
|
|
3618
4248
|
process.exit(0);
|
|
3619
4249
|
}
|
|
3620
|
-
|
|
4250
|
+
const isCompletionServer = process.argv.includes("completion-server");
|
|
4251
|
+
if (!isCompletionServer) {
|
|
4252
|
+
await initTheme();
|
|
4253
|
+
}
|
|
3621
4254
|
program.parse();
|
|
3622
4255
|
const options = program.opts();
|
|
3623
4256
|
const outputOptions = {};
|