@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/index.cjs
CHANGED
|
@@ -6,6 +6,7 @@ var path = require('path');
|
|
|
6
6
|
var chromaterm = require('chromaterm');
|
|
7
7
|
var prompts = require('@inquirer/prompts');
|
|
8
8
|
var core = require('@attest-it/core');
|
|
9
|
+
var tabtab2 = require('@pnpm/tabtab');
|
|
9
10
|
var child_process = require('child_process');
|
|
10
11
|
var os = require('os');
|
|
11
12
|
var shellQuote = require('shell-quote');
|
|
@@ -18,6 +19,8 @@ var yaml = require('yaml');
|
|
|
18
19
|
var url = require('url');
|
|
19
20
|
|
|
20
21
|
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
22
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
23
|
+
|
|
21
24
|
function _interopNamespace(e) {
|
|
22
25
|
if (e && e.__esModule) return e;
|
|
23
26
|
var n = Object.create(null);
|
|
@@ -38,6 +41,7 @@ function _interopNamespace(e) {
|
|
|
38
41
|
|
|
39
42
|
var fs__namespace = /*#__PURE__*/_interopNamespace(fs);
|
|
40
43
|
var path__namespace = /*#__PURE__*/_interopNamespace(path);
|
|
44
|
+
var tabtab2__default = /*#__PURE__*/_interopDefault(tabtab2);
|
|
41
45
|
var os__namespace = /*#__PURE__*/_interopNamespace(os);
|
|
42
46
|
var React7__namespace = /*#__PURE__*/_interopNamespace(React7);
|
|
43
47
|
|
|
@@ -193,6 +197,87 @@ var ExitCode = {
|
|
|
193
197
|
/** Missing required key file */
|
|
194
198
|
MISSING_KEY: 5
|
|
195
199
|
};
|
|
200
|
+
var PROGRAM_NAME = "attest-it";
|
|
201
|
+
var PROGRAM_ALIAS = "attest";
|
|
202
|
+
function detectCurrentShell() {
|
|
203
|
+
const shellPath = process.env.SHELL ?? "";
|
|
204
|
+
if (shellPath.endsWith("/bash") || shellPath.endsWith("/bash.exe")) {
|
|
205
|
+
return "bash";
|
|
206
|
+
}
|
|
207
|
+
if (shellPath.endsWith("/zsh") || shellPath.endsWith("/zsh.exe")) {
|
|
208
|
+
return "zsh";
|
|
209
|
+
}
|
|
210
|
+
if (shellPath.endsWith("/fish") || shellPath.endsWith("/fish.exe")) {
|
|
211
|
+
return "fish";
|
|
212
|
+
}
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
function getSourceCommand(shell) {
|
|
216
|
+
switch (shell) {
|
|
217
|
+
case "bash":
|
|
218
|
+
return "source ~/.bashrc";
|
|
219
|
+
case "zsh":
|
|
220
|
+
return "source ~/.zshrc";
|
|
221
|
+
case "fish":
|
|
222
|
+
return "source ~/.config/fish/config.fish";
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
async function offerCompletionInstall() {
|
|
226
|
+
try {
|
|
227
|
+
const prefs = await core.loadPreferences();
|
|
228
|
+
if (prefs.cliExperience?.declinedCompletionInstall) {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
const shell = detectCurrentShell();
|
|
232
|
+
if (!shell) {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
log("");
|
|
236
|
+
const shouldInstall = await prompts.confirm({
|
|
237
|
+
message: `Would you like to enable shell completions for ${shell}?`,
|
|
238
|
+
default: true
|
|
239
|
+
});
|
|
240
|
+
if (!shouldInstall) {
|
|
241
|
+
await core.savePreferences({
|
|
242
|
+
...prefs,
|
|
243
|
+
cliExperience: {
|
|
244
|
+
...prefs.cliExperience,
|
|
245
|
+
declinedCompletionInstall: true
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
log("");
|
|
249
|
+
info("No problem! If you change your mind, you can run:");
|
|
250
|
+
log(" attest-it completion install");
|
|
251
|
+
log("");
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
await tabtab2__default.default.install({
|
|
255
|
+
name: PROGRAM_NAME,
|
|
256
|
+
completer: PROGRAM_NAME,
|
|
257
|
+
shell
|
|
258
|
+
});
|
|
259
|
+
await tabtab2__default.default.install({
|
|
260
|
+
name: PROGRAM_ALIAS,
|
|
261
|
+
completer: PROGRAM_ALIAS,
|
|
262
|
+
shell
|
|
263
|
+
});
|
|
264
|
+
log("");
|
|
265
|
+
success(`Shell completions installed for ${shell}!`);
|
|
266
|
+
info(`Completions enabled for both "${PROGRAM_NAME}" and "${PROGRAM_ALIAS}" commands.`);
|
|
267
|
+
log("");
|
|
268
|
+
info("Restart your shell or run:");
|
|
269
|
+
log(` ${getSourceCommand(shell)}`);
|
|
270
|
+
log("");
|
|
271
|
+
return true;
|
|
272
|
+
} catch (err) {
|
|
273
|
+
error(`Failed to install completions: ${err instanceof Error ? err.message : String(err)}`);
|
|
274
|
+
log("");
|
|
275
|
+
info("You can try again later with:");
|
|
276
|
+
log(" attest-it completion install");
|
|
277
|
+
log("");
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
196
281
|
|
|
197
282
|
// src/commands/init.ts
|
|
198
283
|
var initCommand = new commander.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) => {
|
|
@@ -256,6 +341,7 @@ async function runInit(options) {
|
|
|
256
341
|
log(` 1. Edit ${options.path} to define your test suites`);
|
|
257
342
|
log(" 2. Run: attest-it keygen");
|
|
258
343
|
log(" 3. Run: attest-it status");
|
|
344
|
+
await offerCompletionInstall();
|
|
259
345
|
} catch (err) {
|
|
260
346
|
if (err instanceof Error) {
|
|
261
347
|
error(err.message);
|
|
@@ -1546,6 +1632,15 @@ function createKeyProviderFromIdentity(identity) {
|
|
|
1546
1632
|
field: privateKey.field
|
|
1547
1633
|
}
|
|
1548
1634
|
});
|
|
1635
|
+
case "yubikey":
|
|
1636
|
+
return core.KeyProviderRegistry.create({
|
|
1637
|
+
type: "yubikey",
|
|
1638
|
+
options: {
|
|
1639
|
+
encryptedKeyPath: privateKey.encryptedKeyPath,
|
|
1640
|
+
slot: privateKey.slot,
|
|
1641
|
+
serial: privateKey.serial
|
|
1642
|
+
}
|
|
1643
|
+
});
|
|
1549
1644
|
default: {
|
|
1550
1645
|
const _exhaustiveCheck = privateKey;
|
|
1551
1646
|
throw new Error(`Unsupported private key type: ${String(_exhaustiveCheck)}`);
|
|
@@ -1561,6 +1656,8 @@ function getKeyRefFromIdentity(identity) {
|
|
|
1561
1656
|
return `${privateKey.service}:${privateKey.account}`;
|
|
1562
1657
|
case "1password":
|
|
1563
1658
|
return privateKey.item;
|
|
1659
|
+
case "yubikey":
|
|
1660
|
+
return privateKey.encryptedKeyPath;
|
|
1564
1661
|
default: {
|
|
1565
1662
|
const _exhaustiveCheck = privateKey;
|
|
1566
1663
|
throw new Error(`Unsupported private key type: ${String(_exhaustiveCheck)}`);
|
|
@@ -2404,6 +2501,15 @@ function createKeyProviderFromIdentity2(identity) {
|
|
|
2404
2501
|
field: privateKey.field
|
|
2405
2502
|
}
|
|
2406
2503
|
});
|
|
2504
|
+
case "yubikey":
|
|
2505
|
+
return core.KeyProviderRegistry.create({
|
|
2506
|
+
type: "yubikey",
|
|
2507
|
+
options: {
|
|
2508
|
+
encryptedKeyPath: privateKey.encryptedKeyPath,
|
|
2509
|
+
slot: privateKey.slot,
|
|
2510
|
+
serial: privateKey.serial
|
|
2511
|
+
}
|
|
2512
|
+
});
|
|
2407
2513
|
default: {
|
|
2408
2514
|
const _exhaustiveCheck = privateKey;
|
|
2409
2515
|
throw new Error(`Unsupported private key type: ${String(_exhaustiveCheck)}`);
|
|
@@ -2419,6 +2525,8 @@ function getKeyRefFromIdentity2(identity) {
|
|
|
2419
2525
|
return privateKey.service;
|
|
2420
2526
|
case "1password":
|
|
2421
2527
|
return privateKey.item;
|
|
2528
|
+
case "yubikey":
|
|
2529
|
+
return privateKey.encryptedKeyPath;
|
|
2422
2530
|
default: {
|
|
2423
2531
|
const _exhaustiveCheck = privateKey;
|
|
2424
2532
|
throw new Error(`Unsupported private key type: ${String(_exhaustiveCheck)}`);
|
|
@@ -2458,6 +2566,9 @@ async function runList() {
|
|
|
2458
2566
|
case "1password":
|
|
2459
2567
|
keyType = "1password";
|
|
2460
2568
|
break;
|
|
2569
|
+
case "yubikey":
|
|
2570
|
+
keyType = "yubikey";
|
|
2571
|
+
break;
|
|
2461
2572
|
}
|
|
2462
2573
|
log(`${marker} ${theme3.blue(slug)}`);
|
|
2463
2574
|
log(` Name: ${nameDisplay}`);
|
|
@@ -2486,31 +2597,46 @@ async function runList() {
|
|
|
2486
2597
|
process.exit(ExitCode.CONFIG_ERROR);
|
|
2487
2598
|
}
|
|
2488
2599
|
}
|
|
2600
|
+
|
|
2601
|
+
// src/commands/identity/validation.ts
|
|
2602
|
+
function validateSlug(value, existingIdentities) {
|
|
2603
|
+
const trimmed = value.trim();
|
|
2604
|
+
if (!trimmed) {
|
|
2605
|
+
return "Slug cannot be empty";
|
|
2606
|
+
}
|
|
2607
|
+
if (!/^[a-z0-9-]+$/.test(trimmed)) {
|
|
2608
|
+
return "Slug must contain only lowercase letters, numbers, and hyphens";
|
|
2609
|
+
}
|
|
2610
|
+
if (existingIdentities?.[trimmed]) {
|
|
2611
|
+
return `Identity "${trimmed}" already exists`;
|
|
2612
|
+
}
|
|
2613
|
+
return true;
|
|
2614
|
+
}
|
|
2615
|
+
var EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
2616
|
+
function validateEmail(value, required = false) {
|
|
2617
|
+
const trimmed = value.trim();
|
|
2618
|
+
if (!trimmed) {
|
|
2619
|
+
return required ? "Email cannot be empty" : true;
|
|
2620
|
+
}
|
|
2621
|
+
if (!EMAIL_REGEX.test(trimmed)) {
|
|
2622
|
+
return "Please enter a valid email address";
|
|
2623
|
+
}
|
|
2624
|
+
return true;
|
|
2625
|
+
}
|
|
2489
2626
|
var createCommand = new commander.Command("create").description("Create a new identity with Ed25519 keypair").action(async () => {
|
|
2490
2627
|
await runCreate();
|
|
2491
2628
|
});
|
|
2492
2629
|
async function runCreate() {
|
|
2493
2630
|
try {
|
|
2494
|
-
const theme3 =
|
|
2631
|
+
const theme3 = getTheme();
|
|
2495
2632
|
log("");
|
|
2496
2633
|
log(theme3.blue.bold()("Create New Identity"));
|
|
2497
2634
|
log("");
|
|
2498
2635
|
const existingConfig = await core.loadLocalConfig();
|
|
2499
|
-
const slug = await prompts.input({
|
|
2636
|
+
const slug = (await prompts.input({
|
|
2500
2637
|
message: "Identity slug (unique identifier):",
|
|
2501
|
-
validate: (value) =>
|
|
2502
|
-
|
|
2503
|
-
return "Slug cannot be empty";
|
|
2504
|
-
}
|
|
2505
|
-
if (!/^[a-z0-9-]+$/.test(value)) {
|
|
2506
|
-
return "Slug must contain only lowercase letters, numbers, and hyphens";
|
|
2507
|
-
}
|
|
2508
|
-
if (existingConfig?.identities[value]) {
|
|
2509
|
-
return `Identity "${value}" already exists`;
|
|
2510
|
-
}
|
|
2511
|
-
return true;
|
|
2512
|
-
}
|
|
2513
|
-
});
|
|
2638
|
+
validate: (value) => validateSlug(value, existingConfig?.identities)
|
|
2639
|
+
})).trim();
|
|
2514
2640
|
const name = await prompts.input({
|
|
2515
2641
|
message: "Display name:",
|
|
2516
2642
|
validate: (value) => {
|
|
@@ -2520,21 +2646,37 @@ async function runCreate() {
|
|
|
2520
2646
|
return true;
|
|
2521
2647
|
}
|
|
2522
2648
|
});
|
|
2523
|
-
const email = await prompts.input({
|
|
2649
|
+
const email = (await prompts.input({
|
|
2524
2650
|
message: "Email (optional):",
|
|
2525
|
-
default: ""
|
|
2526
|
-
|
|
2651
|
+
default: "",
|
|
2652
|
+
validate: validateEmail
|
|
2653
|
+
})).trim();
|
|
2527
2654
|
const github = await prompts.input({
|
|
2528
2655
|
message: "GitHub username (optional):",
|
|
2529
2656
|
default: ""
|
|
2530
2657
|
});
|
|
2658
|
+
info("Checking available key storage providers...");
|
|
2659
|
+
const opAvailable = await core.OnePasswordKeyProvider.isInstalled();
|
|
2660
|
+
const keychainAvailable = core.MacOSKeychainKeyProvider.isAvailable();
|
|
2661
|
+
const yubikeyInstalled = await core.YubiKeyProvider.isInstalled();
|
|
2662
|
+
const yubikeyConnected = yubikeyInstalled ? await core.YubiKeyProvider.isConnected() : false;
|
|
2663
|
+
const configDir = core.getAttestItConfigDir();
|
|
2664
|
+
const storageChoices = [
|
|
2665
|
+
{ name: `File system (${path.join(configDir, "keys")})`, value: "file" }
|
|
2666
|
+
];
|
|
2667
|
+
if (keychainAvailable) {
|
|
2668
|
+
storageChoices.push({ name: "macOS Keychain", value: "keychain" });
|
|
2669
|
+
}
|
|
2670
|
+
if (opAvailable) {
|
|
2671
|
+
storageChoices.push({ name: "1Password", value: "1password" });
|
|
2672
|
+
}
|
|
2673
|
+
if (yubikeyInstalled) {
|
|
2674
|
+
const yubikeyLabel = yubikeyConnected ? "YubiKey (encrypted with challenge-response)" : "YubiKey (not connected - insert YubiKey first)";
|
|
2675
|
+
storageChoices.push({ name: yubikeyLabel, value: "yubikey" });
|
|
2676
|
+
}
|
|
2531
2677
|
const keyStorageType = await prompts.select({
|
|
2532
2678
|
message: "Where should the private key be stored?",
|
|
2533
|
-
choices:
|
|
2534
|
-
{ name: "File system (~/.config/attest-it/keys/)", value: "file" },
|
|
2535
|
-
{ name: "macOS Keychain", value: "keychain" },
|
|
2536
|
-
{ name: "1Password", value: "1password" }
|
|
2537
|
-
]
|
|
2679
|
+
choices: storageChoices
|
|
2538
2680
|
});
|
|
2539
2681
|
log("");
|
|
2540
2682
|
log("Generating Ed25519 keypair...");
|
|
@@ -2543,7 +2685,7 @@ async function runCreate() {
|
|
|
2543
2685
|
let keyStorageDescription;
|
|
2544
2686
|
switch (keyStorageType) {
|
|
2545
2687
|
case "file": {
|
|
2546
|
-
const keysDir = path.join(
|
|
2688
|
+
const keysDir = path.join(core.getAttestItConfigDir(), "keys");
|
|
2547
2689
|
await promises.mkdir(keysDir, { recursive: true });
|
|
2548
2690
|
const keyPath = path.join(keysDir, `${slug}.pem`);
|
|
2549
2691
|
await promises.writeFile(keyPath, keyPair.privateKey, { mode: 384 });
|
|
@@ -2552,44 +2694,138 @@ async function runCreate() {
|
|
|
2552
2694
|
break;
|
|
2553
2695
|
}
|
|
2554
2696
|
case "keychain": {
|
|
2555
|
-
|
|
2556
|
-
if (!MacOSKeychainKeyProvider3.isAvailable()) {
|
|
2697
|
+
if (!core.MacOSKeychainKeyProvider.isAvailable()) {
|
|
2557
2698
|
error("macOS Keychain is not available on this system");
|
|
2558
2699
|
process.exit(ExitCode.CONFIG_ERROR);
|
|
2559
2700
|
}
|
|
2701
|
+
const keychains = await core.MacOSKeychainKeyProvider.listKeychains();
|
|
2702
|
+
if (keychains.length === 0) {
|
|
2703
|
+
throw new Error("No keychains found on this system");
|
|
2704
|
+
}
|
|
2705
|
+
const formatKeychainChoice = (kc) => {
|
|
2706
|
+
return `${theme3.blue.bold()(kc.name)} ${theme3.muted(`(${kc.path})`)}`;
|
|
2707
|
+
};
|
|
2708
|
+
let selectedKeychain;
|
|
2709
|
+
if (keychains.length === 1 && keychains[0]) {
|
|
2710
|
+
selectedKeychain = keychains[0];
|
|
2711
|
+
info(`Using keychain: ${formatKeychainChoice(selectedKeychain)}`);
|
|
2712
|
+
} else {
|
|
2713
|
+
const selectedPath = await prompts.select({
|
|
2714
|
+
message: "Select keychain:",
|
|
2715
|
+
choices: keychains.map((kc) => ({
|
|
2716
|
+
name: formatKeychainChoice(kc),
|
|
2717
|
+
value: kc.path
|
|
2718
|
+
}))
|
|
2719
|
+
});
|
|
2720
|
+
const foundKeychain = keychains.find((kc) => kc.path === selectedPath);
|
|
2721
|
+
if (!foundKeychain) {
|
|
2722
|
+
throw new Error("Selected keychain not found");
|
|
2723
|
+
}
|
|
2724
|
+
selectedKeychain = foundKeychain;
|
|
2725
|
+
}
|
|
2726
|
+
const keychainItemName = await prompts.input({
|
|
2727
|
+
message: "Keychain item name:",
|
|
2728
|
+
default: `attest-it-${slug}`,
|
|
2729
|
+
validate: (value) => {
|
|
2730
|
+
if (!value || value.trim().length === 0) {
|
|
2731
|
+
return "Item name cannot be empty";
|
|
2732
|
+
}
|
|
2733
|
+
return true;
|
|
2734
|
+
}
|
|
2735
|
+
});
|
|
2560
2736
|
const { execFile } = await import('child_process');
|
|
2561
2737
|
const { promisify } = await import('util');
|
|
2562
2738
|
const execFileAsync = promisify(execFile);
|
|
2563
2739
|
const encodedKey = Buffer.from(keyPair.privateKey).toString("base64");
|
|
2564
2740
|
try {
|
|
2565
|
-
|
|
2741
|
+
const addArgs = [
|
|
2566
2742
|
"add-generic-password",
|
|
2567
2743
|
"-a",
|
|
2568
2744
|
"attest-it",
|
|
2569
2745
|
"-s",
|
|
2570
|
-
|
|
2746
|
+
keychainItemName,
|
|
2571
2747
|
"-w",
|
|
2572
2748
|
encodedKey,
|
|
2573
|
-
"-U"
|
|
2574
|
-
|
|
2749
|
+
"-U",
|
|
2750
|
+
selectedKeychain.path
|
|
2751
|
+
];
|
|
2752
|
+
await execFileAsync("security", addArgs);
|
|
2575
2753
|
} catch (err) {
|
|
2576
2754
|
throw new Error(
|
|
2577
2755
|
`Failed to store key in macOS Keychain: ${err instanceof Error ? err.message : String(err)}`
|
|
2578
2756
|
);
|
|
2579
2757
|
}
|
|
2580
|
-
privateKeyRef = {
|
|
2581
|
-
|
|
2758
|
+
privateKeyRef = {
|
|
2759
|
+
type: "keychain",
|
|
2760
|
+
service: keychainItemName,
|
|
2761
|
+
account: "attest-it",
|
|
2762
|
+
keychain: selectedKeychain.path
|
|
2763
|
+
};
|
|
2764
|
+
keyStorageDescription = `macOS Keychain: ${selectedKeychain.name}/${keychainItemName}`;
|
|
2582
2765
|
break;
|
|
2583
2766
|
}
|
|
2584
2767
|
case "1password": {
|
|
2585
|
-
const
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2768
|
+
const accounts = await core.OnePasswordKeyProvider.listAccounts();
|
|
2769
|
+
if (accounts.length === 0) {
|
|
2770
|
+
throw new Error(
|
|
2771
|
+
'1Password CLI is installed but no accounts are signed in. Run "op signin" first.'
|
|
2772
|
+
);
|
|
2773
|
+
}
|
|
2774
|
+
const { execFile } = await import('child_process');
|
|
2775
|
+
const { promisify } = await import('util');
|
|
2776
|
+
const execFileAsync = promisify(execFile);
|
|
2777
|
+
const accountDetails = await Promise.all(
|
|
2778
|
+
accounts.map(async (acc) => {
|
|
2779
|
+
try {
|
|
2780
|
+
const { stdout } = await execFileAsync("op", [
|
|
2781
|
+
"account",
|
|
2782
|
+
"get",
|
|
2783
|
+
"--account",
|
|
2784
|
+
acc.user_uuid,
|
|
2785
|
+
"--format=json"
|
|
2786
|
+
]);
|
|
2787
|
+
const details = JSON.parse(stdout);
|
|
2788
|
+
const name2 = details !== null && typeof details === "object" && "name" in details && typeof details.name === "string" ? details.name : acc.url;
|
|
2789
|
+
return {
|
|
2790
|
+
url: acc.url,
|
|
2791
|
+
email: acc.email,
|
|
2792
|
+
name: name2
|
|
2793
|
+
};
|
|
2794
|
+
} catch {
|
|
2795
|
+
return {
|
|
2796
|
+
url: acc.url,
|
|
2797
|
+
email: acc.email,
|
|
2798
|
+
name: acc.url
|
|
2799
|
+
};
|
|
2590
2800
|
}
|
|
2591
|
-
|
|
2592
|
-
|
|
2801
|
+
})
|
|
2802
|
+
);
|
|
2803
|
+
const formatAccountChoice = (acc) => {
|
|
2804
|
+
return `${theme3.blue.bold()(acc.name)} ${theme3.muted(`(${acc.url})`)}`;
|
|
2805
|
+
};
|
|
2806
|
+
let selectedAccount;
|
|
2807
|
+
if (accountDetails.length === 1 && accountDetails[0]) {
|
|
2808
|
+
selectedAccount = accountDetails[0].url;
|
|
2809
|
+
info(`Using 1Password account: ${formatAccountChoice(accountDetails[0])}`);
|
|
2810
|
+
} else {
|
|
2811
|
+
selectedAccount = await prompts.select({
|
|
2812
|
+
message: "Select 1Password account:",
|
|
2813
|
+
choices: accountDetails.map((acc) => ({
|
|
2814
|
+
name: formatAccountChoice(acc),
|
|
2815
|
+
value: acc.url
|
|
2816
|
+
}))
|
|
2817
|
+
});
|
|
2818
|
+
}
|
|
2819
|
+
const vaults = await core.OnePasswordKeyProvider.listVaults(selectedAccount);
|
|
2820
|
+
if (vaults.length === 0) {
|
|
2821
|
+
throw new Error(`No vaults found in 1Password account: ${selectedAccount}`);
|
|
2822
|
+
}
|
|
2823
|
+
const selectedVault = await prompts.select({
|
|
2824
|
+
message: "Select vault for private key storage:",
|
|
2825
|
+
choices: vaults.map((v) => ({
|
|
2826
|
+
name: v.name,
|
|
2827
|
+
value: v.name
|
|
2828
|
+
}))
|
|
2593
2829
|
});
|
|
2594
2830
|
const item = await prompts.input({
|
|
2595
2831
|
message: "1Password item name:",
|
|
@@ -2601,26 +2837,109 @@ async function runCreate() {
|
|
|
2601
2837
|
return true;
|
|
2602
2838
|
}
|
|
2603
2839
|
});
|
|
2604
|
-
const {
|
|
2605
|
-
const
|
|
2606
|
-
|
|
2840
|
+
const { tmpdir } = await import('os');
|
|
2841
|
+
const tempDir = path.join(tmpdir(), `attest-it-${String(Date.now())}`);
|
|
2842
|
+
await promises.mkdir(tempDir, { recursive: true });
|
|
2843
|
+
const tempPrivatePath = path.join(tempDir, "private.pem");
|
|
2607
2844
|
try {
|
|
2608
|
-
await
|
|
2609
|
-
|
|
2845
|
+
await promises.writeFile(tempPrivatePath, keyPair.privateKey, { mode: 384 });
|
|
2846
|
+
const { execFile: execFile2 } = await import('child_process');
|
|
2847
|
+
const { promisify: promisify2 } = await import('util');
|
|
2848
|
+
const execFileAsync2 = promisify2(execFile2);
|
|
2849
|
+
const opArgs = [
|
|
2850
|
+
"document",
|
|
2610
2851
|
"create",
|
|
2611
|
-
|
|
2852
|
+
tempPrivatePath,
|
|
2853
|
+
"--title",
|
|
2854
|
+
item,
|
|
2612
2855
|
"--vault",
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
);
|
|
2856
|
+
selectedVault
|
|
2857
|
+
];
|
|
2858
|
+
if (selectedAccount) {
|
|
2859
|
+
opArgs.push("--account", selectedAccount);
|
|
2860
|
+
}
|
|
2861
|
+
await execFileAsync2("op", opArgs);
|
|
2862
|
+
} finally {
|
|
2863
|
+
const { rm } = await import('fs/promises');
|
|
2864
|
+
await rm(tempDir, { recursive: true, force: true }).catch(() => {
|
|
2865
|
+
});
|
|
2621
2866
|
}
|
|
2622
|
-
privateKeyRef = {
|
|
2623
|
-
|
|
2867
|
+
privateKeyRef = {
|
|
2868
|
+
type: "1password",
|
|
2869
|
+
vault: selectedVault,
|
|
2870
|
+
item,
|
|
2871
|
+
...selectedAccount && { account: selectedAccount }
|
|
2872
|
+
};
|
|
2873
|
+
keyStorageDescription = `1Password (${selectedVault}/${item})`;
|
|
2874
|
+
break;
|
|
2875
|
+
}
|
|
2876
|
+
case "yubikey": {
|
|
2877
|
+
if (!await core.YubiKeyProvider.isConnected()) {
|
|
2878
|
+
error("No YubiKey detected. Please insert your YubiKey and try again.");
|
|
2879
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
2880
|
+
}
|
|
2881
|
+
const yubikeys = await core.YubiKeyProvider.listDevices();
|
|
2882
|
+
if (yubikeys.length === 0) {
|
|
2883
|
+
throw new Error("No YubiKeys detected. Please insert a YubiKey and try again.");
|
|
2884
|
+
}
|
|
2885
|
+
const formatYubiKeyChoice = (yk) => {
|
|
2886
|
+
return `${theme3.blue.bold()(yk.type)} ${theme3.muted(`(Serial: ${yk.serial}, FW: ${yk.firmware})`)}`;
|
|
2887
|
+
};
|
|
2888
|
+
let selectedSerial;
|
|
2889
|
+
if (yubikeys.length === 1 && yubikeys[0]) {
|
|
2890
|
+
selectedSerial = yubikeys[0].serial;
|
|
2891
|
+
info(`Using YubiKey: ${formatYubiKeyChoice(yubikeys[0])}`);
|
|
2892
|
+
} else {
|
|
2893
|
+
selectedSerial = await prompts.select({
|
|
2894
|
+
message: "Select YubiKey:",
|
|
2895
|
+
choices: yubikeys.map((yk) => ({
|
|
2896
|
+
name: formatYubiKeyChoice(yk),
|
|
2897
|
+
value: yk.serial
|
|
2898
|
+
}))
|
|
2899
|
+
});
|
|
2900
|
+
}
|
|
2901
|
+
const slot = 2;
|
|
2902
|
+
const isChallengeResponseConfigured = await core.YubiKeyProvider.isChallengeResponseConfigured(
|
|
2903
|
+
slot,
|
|
2904
|
+
selectedSerial
|
|
2905
|
+
);
|
|
2906
|
+
if (!isChallengeResponseConfigured) {
|
|
2907
|
+
log("");
|
|
2908
|
+
error(`YubiKey slot ${String(slot)} is not configured for HMAC challenge-response.`);
|
|
2909
|
+
log("");
|
|
2910
|
+
log("To configure it, run:");
|
|
2911
|
+
log(theme3.blue(` ykman otp chalresp --generate ${String(slot)}`));
|
|
2912
|
+
log("");
|
|
2913
|
+
log("This will configure slot 2 with a randomly generated secret.");
|
|
2914
|
+
log(theme3.muted("Note: Make sure to back up the secret if needed for recovery."));
|
|
2915
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
2916
|
+
}
|
|
2917
|
+
const encryptedKeyName = await prompts.input({
|
|
2918
|
+
message: "Encrypted key file name:",
|
|
2919
|
+
default: `${slug}.enc`,
|
|
2920
|
+
validate: (value) => {
|
|
2921
|
+
if (!value || value.trim().length === 0) {
|
|
2922
|
+
return "File name cannot be empty";
|
|
2923
|
+
}
|
|
2924
|
+
return true;
|
|
2925
|
+
}
|
|
2926
|
+
});
|
|
2927
|
+
const keysDir = path.join(core.getAttestItConfigDir(), "keys");
|
|
2928
|
+
await promises.mkdir(keysDir, { recursive: true });
|
|
2929
|
+
const encryptedKeyPath = path.join(keysDir, encryptedKeyName);
|
|
2930
|
+
const result = await core.YubiKeyProvider.encryptPrivateKey({
|
|
2931
|
+
privateKey: keyPair.privateKey,
|
|
2932
|
+
encryptedKeyPath,
|
|
2933
|
+
slot,
|
|
2934
|
+
serial: selectedSerial
|
|
2935
|
+
});
|
|
2936
|
+
privateKeyRef = {
|
|
2937
|
+
type: "yubikey",
|
|
2938
|
+
encryptedKeyPath: result.encryptedKeyPath,
|
|
2939
|
+
slot,
|
|
2940
|
+
serial: selectedSerial
|
|
2941
|
+
};
|
|
2942
|
+
keyStorageDescription = result.storageDescription;
|
|
2624
2943
|
break;
|
|
2625
2944
|
}
|
|
2626
2945
|
default:
|
|
@@ -2672,6 +2991,7 @@ async function runCreate() {
|
|
|
2672
2991
|
log(`To use this identity, run: attest-it identity use ${slug}`);
|
|
2673
2992
|
log("");
|
|
2674
2993
|
}
|
|
2994
|
+
await offerCompletionInstall();
|
|
2675
2995
|
} catch (err) {
|
|
2676
2996
|
if (err instanceof Error) {
|
|
2677
2997
|
error(err.message);
|
|
@@ -2932,6 +3252,37 @@ async function runEdit(slug) {
|
|
|
2932
3252
|
process.exit(ExitCode.CONFIG_ERROR);
|
|
2933
3253
|
}
|
|
2934
3254
|
}
|
|
3255
|
+
|
|
3256
|
+
// src/utils/format-key-location.ts
|
|
3257
|
+
function formatKeyLocation(privateKey) {
|
|
3258
|
+
const theme3 = getTheme();
|
|
3259
|
+
switch (privateKey.type) {
|
|
3260
|
+
case "file":
|
|
3261
|
+
return `${theme3.blue.bold()("File")}: ${theme3.muted(privateKey.path)}`;
|
|
3262
|
+
case "keychain": {
|
|
3263
|
+
let keychainName = "default";
|
|
3264
|
+
if (privateKey.keychain) {
|
|
3265
|
+
const filename = privateKey.keychain.split("/").pop() ?? privateKey.keychain;
|
|
3266
|
+
keychainName = filename.replace(/\.keychain(-db)?$/, "");
|
|
3267
|
+
}
|
|
3268
|
+
return `${theme3.blue.bold()("macOS Keychain")}: ${theme3.muted(`${keychainName}/${privateKey.service}`)}`;
|
|
3269
|
+
}
|
|
3270
|
+
case "1password": {
|
|
3271
|
+
const parts = [privateKey.vault, privateKey.item];
|
|
3272
|
+
if (privateKey.account) {
|
|
3273
|
+
parts.unshift(privateKey.account);
|
|
3274
|
+
}
|
|
3275
|
+
return `${theme3.blue.bold()("1Password")}: ${theme3.muted(parts.join("/"))}`;
|
|
3276
|
+
}
|
|
3277
|
+
case "yubikey": {
|
|
3278
|
+
const slotInfo = privateKey.slot ? ` (slot ${String(privateKey.slot)})` : "";
|
|
3279
|
+
const serialInfo = privateKey.serial ? ` [${privateKey.serial}]` : "";
|
|
3280
|
+
return `${theme3.blue.bold()("YubiKey")}${serialInfo}${slotInfo}: ${theme3.muted(privateKey.encryptedKeyPath)}`;
|
|
3281
|
+
}
|
|
3282
|
+
default:
|
|
3283
|
+
return "Unknown storage";
|
|
3284
|
+
}
|
|
3285
|
+
}
|
|
2935
3286
|
var removeCommand = new commander.Command("remove").description("Delete identity and optionally delete private key").argument("<slug>", "Identity slug to remove").action(async (slug) => {
|
|
2936
3287
|
await runRemove(slug);
|
|
2937
3288
|
});
|
|
@@ -2947,7 +3298,7 @@ async function runRemove(slug) {
|
|
|
2947
3298
|
error(`Identity "${slug}" not found`);
|
|
2948
3299
|
process.exit(ExitCode.CONFIG_ERROR);
|
|
2949
3300
|
}
|
|
2950
|
-
const theme3 =
|
|
3301
|
+
const theme3 = getTheme();
|
|
2951
3302
|
log("");
|
|
2952
3303
|
log(theme3.blue.bold()(`Remove Identity: ${slug}`));
|
|
2953
3304
|
log("");
|
|
@@ -2964,6 +3315,9 @@ async function runRemove(slug) {
|
|
|
2964
3315
|
log("Cancelled");
|
|
2965
3316
|
process.exit(ExitCode.CANCELLED);
|
|
2966
3317
|
}
|
|
3318
|
+
const keyLocation = formatKeyLocation(identity.privateKey);
|
|
3319
|
+
log(` Private key: ${keyLocation}`);
|
|
3320
|
+
log("");
|
|
2967
3321
|
const deletePrivateKey = await prompts.confirm({
|
|
2968
3322
|
message: "Also delete the private key from storage?",
|
|
2969
3323
|
default: false
|
|
@@ -2986,13 +3340,17 @@ async function runRemove(slug) {
|
|
|
2986
3340
|
const { promisify } = await import('util');
|
|
2987
3341
|
const execFileAsync = promisify(execFile);
|
|
2988
3342
|
try {
|
|
2989
|
-
|
|
3343
|
+
const deleteArgs = [
|
|
2990
3344
|
"delete-generic-password",
|
|
2991
3345
|
"-s",
|
|
2992
3346
|
identity.privateKey.service,
|
|
2993
3347
|
"-a",
|
|
2994
3348
|
identity.privateKey.account
|
|
2995
|
-
]
|
|
3349
|
+
];
|
|
3350
|
+
if (identity.privateKey.keychain) {
|
|
3351
|
+
deleteArgs.push(identity.privateKey.keychain);
|
|
3352
|
+
}
|
|
3353
|
+
await execFileAsync("security", deleteArgs);
|
|
2996
3354
|
log(` Deleted private key from macOS Keychain`);
|
|
2997
3355
|
} catch (err) {
|
|
2998
3356
|
if (err instanceof Error && !err.message.includes("could not be found") && !err.message.includes("does not exist")) {
|
|
@@ -3130,17 +3488,20 @@ async function runWhoami() {
|
|
|
3130
3488
|
error("Active identity not found");
|
|
3131
3489
|
process.exit(ExitCode.CONFIG_ERROR);
|
|
3132
3490
|
}
|
|
3133
|
-
const theme3 =
|
|
3491
|
+
const theme3 = getTheme();
|
|
3492
|
+
log("");
|
|
3493
|
+
log(theme3.blue.bold()("Active Identity"));
|
|
3134
3494
|
log("");
|
|
3135
|
-
log(theme3.green.bold()(
|
|
3495
|
+
log(` Slug: ${theme3.green.bold()(config.activeIdentity)}`);
|
|
3496
|
+
log(` Name: ${identity.name}`);
|
|
3136
3497
|
if (identity.email) {
|
|
3137
|
-
log(theme3.muted(identity.email));
|
|
3498
|
+
log(` Email: ${theme3.muted(identity.email)}`);
|
|
3138
3499
|
}
|
|
3139
3500
|
if (identity.github) {
|
|
3140
|
-
log(theme3.muted("@" + identity.github));
|
|
3501
|
+
log(` GitHub: ${theme3.muted("@" + identity.github)}`);
|
|
3141
3502
|
}
|
|
3142
|
-
log("");
|
|
3143
|
-
log(`
|
|
3503
|
+
log(` Public Key: ${theme3.muted(identity.publicKey.slice(0, 24) + "...")}`);
|
|
3504
|
+
log(` Key Store: ${formatKeyLocation(identity.privateKey)}`);
|
|
3144
3505
|
log("");
|
|
3145
3506
|
} catch (err) {
|
|
3146
3507
|
if (err instanceof Error) {
|
|
@@ -3591,6 +3952,266 @@ async function runRemove2(slug, options) {
|
|
|
3591
3952
|
|
|
3592
3953
|
// src/commands/team/index.ts
|
|
3593
3954
|
var teamCommand = new commander.Command("team").description("Manage team members and authorizations").addCommand(listCommand2).addCommand(addCommand).addCommand(editCommand2).addCommand(removeCommand2);
|
|
3955
|
+
var PROGRAM_NAME2 = "attest-it";
|
|
3956
|
+
var PROGRAM_ALIAS2 = "attest";
|
|
3957
|
+
var PROGRAM_NAMES = [PROGRAM_NAME2, PROGRAM_ALIAS2];
|
|
3958
|
+
function isSupportedShell(value) {
|
|
3959
|
+
return value === "bash" || value === "zsh" || value === "fish";
|
|
3960
|
+
}
|
|
3961
|
+
async function getCompletions(env) {
|
|
3962
|
+
let shell;
|
|
3963
|
+
try {
|
|
3964
|
+
const detectedShell = tabtab2__default.default.getShellFromEnv(process.env);
|
|
3965
|
+
shell = detectedShell === "pwsh" ? "bash" : detectedShell;
|
|
3966
|
+
} catch {
|
|
3967
|
+
shell = "bash";
|
|
3968
|
+
}
|
|
3969
|
+
const commands = [
|
|
3970
|
+
{ name: "init", description: "Initialize a new config file" },
|
|
3971
|
+
{ name: "status", description: "Show status of all gates" },
|
|
3972
|
+
{ name: "run", description: "Run test suites interactively" },
|
|
3973
|
+
{ name: "verify", description: "Verify all seals are valid" },
|
|
3974
|
+
{ name: "seal", description: "Create a seal for a gate" },
|
|
3975
|
+
{ name: "keygen", description: "Generate a new keypair" },
|
|
3976
|
+
{ name: "prune", description: "Remove stale attestations" },
|
|
3977
|
+
{ name: "identity", description: "Manage identities" },
|
|
3978
|
+
{ name: "team", description: "Manage team members" },
|
|
3979
|
+
{ name: "whoami", description: "Show active identity" },
|
|
3980
|
+
{ name: "completion", description: "Shell completion commands" }
|
|
3981
|
+
];
|
|
3982
|
+
const globalOptions2 = [
|
|
3983
|
+
{ name: "--help", description: "Show help" },
|
|
3984
|
+
{ name: "--version", description: "Show version" },
|
|
3985
|
+
{ name: "--verbose", description: "Verbose output" },
|
|
3986
|
+
{ name: "--quiet", description: "Minimal output" },
|
|
3987
|
+
{ name: "--config", description: "Path to config file" }
|
|
3988
|
+
];
|
|
3989
|
+
const identitySubcommands = [
|
|
3990
|
+
{ name: "create", description: "Create a new identity" },
|
|
3991
|
+
{ name: "list", description: "List all identities" },
|
|
3992
|
+
{ name: "use", description: "Switch active identity" },
|
|
3993
|
+
{ name: "remove", description: "Remove an identity" }
|
|
3994
|
+
];
|
|
3995
|
+
const teamSubcommands = [
|
|
3996
|
+
{ name: "add", description: "Add yourself to the team" },
|
|
3997
|
+
{ name: "list", description: "List team members" },
|
|
3998
|
+
{ name: "remove", description: "Remove a team member" }
|
|
3999
|
+
];
|
|
4000
|
+
const completionSubcommands = [
|
|
4001
|
+
{ name: "install", description: "Install shell completion" },
|
|
4002
|
+
{ name: "uninstall", description: "Uninstall shell completion" }
|
|
4003
|
+
];
|
|
4004
|
+
const words = env.line.split(/\s+/).filter(Boolean);
|
|
4005
|
+
const lastWord = env.last;
|
|
4006
|
+
const prevWord = env.prev;
|
|
4007
|
+
if (prevWord === "--config" || prevWord === "-c") {
|
|
4008
|
+
tabtab2__default.default.logFiles();
|
|
4009
|
+
return;
|
|
4010
|
+
}
|
|
4011
|
+
if (lastWord.startsWith("-")) {
|
|
4012
|
+
tabtab2__default.default.log(globalOptions2, shell, console.log);
|
|
4013
|
+
return;
|
|
4014
|
+
}
|
|
4015
|
+
const commandIndex = words.findIndex(
|
|
4016
|
+
(w) => !w.startsWith("-") && !PROGRAM_NAMES.includes(w) && w !== "npx"
|
|
4017
|
+
);
|
|
4018
|
+
const currentCommand = commandIndex >= 0 ? words[commandIndex] ?? null : null;
|
|
4019
|
+
if (currentCommand === "identity") {
|
|
4020
|
+
const subcommandIndex = words.findIndex(
|
|
4021
|
+
(w, i) => i > commandIndex && !w.startsWith("-")
|
|
4022
|
+
);
|
|
4023
|
+
const subcommand = subcommandIndex >= 0 ? words[subcommandIndex] ?? null : null;
|
|
4024
|
+
if (subcommand === "use" || subcommand === "remove") {
|
|
4025
|
+
const identities = await getIdentitySlugs();
|
|
4026
|
+
if (identities.length > 0) {
|
|
4027
|
+
tabtab2__default.default.log(identities, shell, console.log);
|
|
4028
|
+
return;
|
|
4029
|
+
}
|
|
4030
|
+
}
|
|
4031
|
+
if (!subcommand || subcommandIndex < 0) {
|
|
4032
|
+
tabtab2__default.default.log(identitySubcommands, shell, console.log);
|
|
4033
|
+
return;
|
|
4034
|
+
}
|
|
4035
|
+
}
|
|
4036
|
+
if (currentCommand === "team") {
|
|
4037
|
+
const subcommandIndex = words.findIndex(
|
|
4038
|
+
(w, i) => i > commandIndex && !w.startsWith("-")
|
|
4039
|
+
);
|
|
4040
|
+
const subcommand = subcommandIndex >= 0 ? words[subcommandIndex] ?? null : null;
|
|
4041
|
+
if (!subcommand || subcommandIndex < 0) {
|
|
4042
|
+
tabtab2__default.default.log(teamSubcommands, shell, console.log);
|
|
4043
|
+
return;
|
|
4044
|
+
}
|
|
4045
|
+
}
|
|
4046
|
+
if (currentCommand === "completion") {
|
|
4047
|
+
const subcommandIndex = words.findIndex(
|
|
4048
|
+
(w, i) => i > commandIndex && !w.startsWith("-")
|
|
4049
|
+
);
|
|
4050
|
+
const subcommand = subcommandIndex >= 0 ? words[subcommandIndex] ?? null : null;
|
|
4051
|
+
if (subcommand === "install") {
|
|
4052
|
+
tabtab2__default.default.log(["bash", "zsh", "fish"], shell, console.log);
|
|
4053
|
+
return;
|
|
4054
|
+
}
|
|
4055
|
+
if (!subcommand || subcommandIndex < 0) {
|
|
4056
|
+
tabtab2__default.default.log(completionSubcommands, shell, console.log);
|
|
4057
|
+
return;
|
|
4058
|
+
}
|
|
4059
|
+
}
|
|
4060
|
+
if (currentCommand === "status" || currentCommand === "verify" || currentCommand === "seal") {
|
|
4061
|
+
const gates = await getGateNames();
|
|
4062
|
+
if (gates.length > 0) {
|
|
4063
|
+
tabtab2__default.default.log(gates, shell, console.log);
|
|
4064
|
+
return;
|
|
4065
|
+
}
|
|
4066
|
+
}
|
|
4067
|
+
if (currentCommand === "run") {
|
|
4068
|
+
const suites = await getSuiteNames();
|
|
4069
|
+
if (suites.length > 0) {
|
|
4070
|
+
tabtab2__default.default.log(suites, shell, console.log);
|
|
4071
|
+
return;
|
|
4072
|
+
}
|
|
4073
|
+
}
|
|
4074
|
+
const knownCommands = [
|
|
4075
|
+
"init",
|
|
4076
|
+
"status",
|
|
4077
|
+
"run",
|
|
4078
|
+
"verify",
|
|
4079
|
+
"seal",
|
|
4080
|
+
"keygen",
|
|
4081
|
+
"prune",
|
|
4082
|
+
"identity",
|
|
4083
|
+
"team",
|
|
4084
|
+
"whoami",
|
|
4085
|
+
"completion"
|
|
4086
|
+
];
|
|
4087
|
+
if (!currentCommand || !knownCommands.includes(currentCommand)) {
|
|
4088
|
+
tabtab2__default.default.log([...commands, ...globalOptions2], shell, console.log);
|
|
4089
|
+
}
|
|
4090
|
+
}
|
|
4091
|
+
async function getIdentitySlugs() {
|
|
4092
|
+
try {
|
|
4093
|
+
const config = await core.loadLocalConfig();
|
|
4094
|
+
if (config?.identities) {
|
|
4095
|
+
return Object.keys(config.identities);
|
|
4096
|
+
}
|
|
4097
|
+
} catch {
|
|
4098
|
+
}
|
|
4099
|
+
return [];
|
|
4100
|
+
}
|
|
4101
|
+
async function getGateNames() {
|
|
4102
|
+
try {
|
|
4103
|
+
const config = await core.loadConfig();
|
|
4104
|
+
if (config.gates) {
|
|
4105
|
+
return Object.keys(config.gates);
|
|
4106
|
+
}
|
|
4107
|
+
} catch {
|
|
4108
|
+
}
|
|
4109
|
+
return [];
|
|
4110
|
+
}
|
|
4111
|
+
async function getSuiteNames() {
|
|
4112
|
+
try {
|
|
4113
|
+
const config = await core.loadConfig();
|
|
4114
|
+
return Object.keys(config.suites);
|
|
4115
|
+
} catch {
|
|
4116
|
+
}
|
|
4117
|
+
return [];
|
|
4118
|
+
}
|
|
4119
|
+
var completionCommand = new commander.Command("completion").description("Shell completion commands");
|
|
4120
|
+
function detectCurrentShell2() {
|
|
4121
|
+
const shellPath = process.env.SHELL ?? "";
|
|
4122
|
+
if (shellPath.endsWith("/bash") || shellPath.endsWith("/bash.exe")) {
|
|
4123
|
+
return "bash";
|
|
4124
|
+
}
|
|
4125
|
+
if (shellPath.endsWith("/zsh") || shellPath.endsWith("/zsh.exe")) {
|
|
4126
|
+
return "zsh";
|
|
4127
|
+
}
|
|
4128
|
+
if (shellPath.endsWith("/fish") || shellPath.endsWith("/fish.exe")) {
|
|
4129
|
+
return "fish";
|
|
4130
|
+
}
|
|
4131
|
+
return null;
|
|
4132
|
+
}
|
|
4133
|
+
function getSourceCommand2(shell) {
|
|
4134
|
+
switch (shell) {
|
|
4135
|
+
case "bash":
|
|
4136
|
+
return "source ~/.bashrc";
|
|
4137
|
+
case "zsh":
|
|
4138
|
+
return "source ~/.zshrc";
|
|
4139
|
+
case "fish":
|
|
4140
|
+
return "source ~/.config/fish/config.fish";
|
|
4141
|
+
}
|
|
4142
|
+
}
|
|
4143
|
+
completionCommand.command("install [shell]").description("Install shell completion (auto-detects shell, or specify bash/zsh/fish)").action(async (shellArg) => {
|
|
4144
|
+
try {
|
|
4145
|
+
let shell;
|
|
4146
|
+
if (shellArg !== void 0) {
|
|
4147
|
+
if (!isSupportedShell(shellArg)) {
|
|
4148
|
+
error(`Shell "${shellArg}" is not supported. Use bash, zsh, or fish.`);
|
|
4149
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
4150
|
+
}
|
|
4151
|
+
shell = shellArg;
|
|
4152
|
+
} else {
|
|
4153
|
+
const detected = detectCurrentShell2();
|
|
4154
|
+
if (!detected) {
|
|
4155
|
+
error(
|
|
4156
|
+
"Could not detect your shell. Please specify: attest-it completion install <bash|zsh|fish>"
|
|
4157
|
+
);
|
|
4158
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
4159
|
+
}
|
|
4160
|
+
shell = detected;
|
|
4161
|
+
info(`Detected shell: ${shell}`);
|
|
4162
|
+
}
|
|
4163
|
+
await tabtab2__default.default.install({
|
|
4164
|
+
name: PROGRAM_NAME2,
|
|
4165
|
+
completer: PROGRAM_NAME2,
|
|
4166
|
+
shell
|
|
4167
|
+
});
|
|
4168
|
+
await tabtab2__default.default.install({
|
|
4169
|
+
name: PROGRAM_ALIAS2,
|
|
4170
|
+
completer: PROGRAM_ALIAS2,
|
|
4171
|
+
shell
|
|
4172
|
+
});
|
|
4173
|
+
log("");
|
|
4174
|
+
success(`Shell completion installed for ${shell}!`);
|
|
4175
|
+
info(`Completions enabled for both "${PROGRAM_NAME2}" and "${PROGRAM_ALIAS2}" commands.`);
|
|
4176
|
+
log("");
|
|
4177
|
+
info("Restart your shell or run:");
|
|
4178
|
+
log(` ${getSourceCommand2(shell)}`);
|
|
4179
|
+
log("");
|
|
4180
|
+
} catch (err) {
|
|
4181
|
+
error(`Failed to install completion: ${err instanceof Error ? err.message : String(err)}`);
|
|
4182
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
4183
|
+
}
|
|
4184
|
+
});
|
|
4185
|
+
completionCommand.command("uninstall").description("Uninstall shell completion").action(async () => {
|
|
4186
|
+
try {
|
|
4187
|
+
await tabtab2__default.default.uninstall({
|
|
4188
|
+
name: PROGRAM_NAME2
|
|
4189
|
+
});
|
|
4190
|
+
await tabtab2__default.default.uninstall({
|
|
4191
|
+
name: PROGRAM_ALIAS2
|
|
4192
|
+
});
|
|
4193
|
+
log("");
|
|
4194
|
+
success("Shell completion uninstalled!");
|
|
4195
|
+
log("");
|
|
4196
|
+
} catch (err) {
|
|
4197
|
+
error(`Failed to uninstall completion: ${err instanceof Error ? err.message : String(err)}`);
|
|
4198
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
4199
|
+
}
|
|
4200
|
+
});
|
|
4201
|
+
completionCommand.command("server", { hidden: true }).description("Completion server (internal)").action(async () => {
|
|
4202
|
+
const env = tabtab2__default.default.parseEnv(process.env);
|
|
4203
|
+
if (env.complete) {
|
|
4204
|
+
await getCompletions(env);
|
|
4205
|
+
}
|
|
4206
|
+
});
|
|
4207
|
+
function createCompletionServerCommand() {
|
|
4208
|
+
return new commander.Command("completion-server").allowUnknownOption().allowExcessArguments(true).action(async () => {
|
|
4209
|
+
const env = tabtab2__default.default.parseEnv(process.env);
|
|
4210
|
+
if (env.complete) {
|
|
4211
|
+
await getCompletions(env);
|
|
4212
|
+
}
|
|
4213
|
+
});
|
|
4214
|
+
}
|
|
3594
4215
|
function hasVersion(data) {
|
|
3595
4216
|
return typeof data === "object" && data !== null && "version" in data && // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
3596
4217
|
typeof data.version === "string";
|
|
@@ -3634,12 +4255,28 @@ program.addCommand(sealCommand);
|
|
|
3634
4255
|
program.addCommand(identityCommand);
|
|
3635
4256
|
program.addCommand(teamCommand);
|
|
3636
4257
|
program.addCommand(whoamiCommand);
|
|
4258
|
+
program.addCommand(completionCommand);
|
|
4259
|
+
program.addCommand(createCompletionServerCommand(), { hidden: true });
|
|
4260
|
+
function processHomeDirOption() {
|
|
4261
|
+
const homeDirIndex = process.argv.indexOf("--home-dir");
|
|
4262
|
+
if (homeDirIndex !== -1 && homeDirIndex + 1 < process.argv.length) {
|
|
4263
|
+
const homeDir = process.argv[homeDirIndex + 1];
|
|
4264
|
+
if (homeDir && !homeDir.startsWith("-")) {
|
|
4265
|
+
core.setAttestItHomeDir(homeDir);
|
|
4266
|
+
process.argv.splice(homeDirIndex, 2);
|
|
4267
|
+
}
|
|
4268
|
+
}
|
|
4269
|
+
}
|
|
3637
4270
|
async function run() {
|
|
4271
|
+
processHomeDirOption();
|
|
3638
4272
|
if (process.argv.includes("--version") || process.argv.includes("-V")) {
|
|
3639
4273
|
console.log(getPackageVersion());
|
|
3640
4274
|
process.exit(0);
|
|
3641
4275
|
}
|
|
3642
|
-
|
|
4276
|
+
const isCompletionServer = process.argv.includes("completion-server");
|
|
4277
|
+
if (!isCompletionServer) {
|
|
4278
|
+
await initTheme();
|
|
4279
|
+
}
|
|
3643
4280
|
program.parse();
|
|
3644
4281
|
const options = program.opts();
|
|
3645
4282
|
const outputOptions = {};
|