@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/bin/attest-it.js +448 -66
- package/dist/bin/attest-it.js.map +1 -1
- package/dist/index.cjs +450 -64
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +448 -66
- package/dist/index.js.map +1 -1
- package/package.json +7 -3
package/dist/bin/attest-it.js
CHANGED
|
@@ -6,10 +6,9 @@ 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, getAttestItConfigDir, generateEd25519KeyPair, saveLocalConfig, findConfigPath, findAttestation, setAttestItHomeDir } from '@attest-it/core';
|
|
10
10
|
import { spawn } from 'child_process';
|
|
11
11
|
import * as os from 'os';
|
|
12
|
-
import { homedir } from 'os';
|
|
13
12
|
import { parse } from 'shell-quote';
|
|
14
13
|
import * as React7 from 'react';
|
|
15
14
|
import { useState, useEffect } from 'react';
|
|
@@ -18,6 +17,7 @@ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
|
18
17
|
import { mkdir, writeFile, unlink, readFile } from 'fs/promises';
|
|
19
18
|
import { Spinner, Select, TextInput } from '@inkjs/ui';
|
|
20
19
|
import { stringify } from 'yaml';
|
|
20
|
+
import tabtab from '@pnpm/tabtab';
|
|
21
21
|
import { fileURLToPath } from 'url';
|
|
22
22
|
|
|
23
23
|
var globalOptions = {};
|
|
@@ -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 =
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
2602
|
+
const addArgs = [
|
|
2544
2603
|
"add-generic-password",
|
|
2545
2604
|
"-a",
|
|
2546
2605
|
"attest-it",
|
|
2547
2606
|
"-s",
|
|
2548
|
-
|
|
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 = {
|
|
2559
|
-
|
|
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
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
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
|
-
|
|
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 {
|
|
2583
|
-
const
|
|
2584
|
-
|
|
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
|
|
2587
|
-
|
|
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
|
-
|
|
2713
|
+
tempPrivatePath,
|
|
2714
|
+
"--title",
|
|
2715
|
+
item,
|
|
2590
2716
|
"--vault",
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
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 = {
|
|
2601
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
3277
|
+
const theme3 = getTheme();
|
|
3112
3278
|
log("");
|
|
3113
|
-
log(theme3.
|
|
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(`
|
|
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);
|