@attest-it/cli 0.7.0 → 0.9.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.
@@ -4,21 +4,19 @@ import * as fs from 'fs';
4
4
  import { readFileSync } from 'fs';
5
5
  import * as path from 'path';
6
6
  import { join, dirname } from 'path';
7
+ import { fileURLToPath } from 'url';
7
8
  import { detectTheme } from 'chromaterm';
8
9
  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, YubiKeyProvider, getAttestItConfigDir, generateEd25519KeyPair, saveLocalConfig, savePublicKey, findConfigPath, loadPreferences, savePreferences, findAttestation, setAttestItHomeDir, getDefaultYubiKeyEncryptedKeyPath } from '@attest-it/core';
10
+ import { loadConfig, toAttestItConfig, readSealsSync, computeFingerprintSync, verifyGateSeal, verifyAllSeals, computeFingerprint, createAttestation, readAttestations, upsertAttestation, KeyProviderRegistry, getDefaultPrivateKeyPath, FilesystemKeyProvider, writeSignedAttestations, loadLocalConfigSync, getActiveIdentity, isAuthorizedSigner, createSeal, writeSealsSync, getGate, loadLocalConfig, OnePasswordKeyProvider, MacOSKeychainKeyProvider, YubiKeyProvider, getAttestItConfigDir, generateEd25519KeyPair, saveLocalConfig, savePublicKey, findConfigPath, loadPreferences, savePreferences, findAttestation, setAttestItHomeDir } from '@attest-it/core';
10
11
  import tabtab2 from '@pnpm/tabtab';
11
12
  import { spawn } from 'child_process';
12
13
  import * as os from 'os';
13
14
  import { parse } from 'shell-quote';
14
15
  import * as React7 from 'react';
15
- import { useState, useEffect } from 'react';
16
16
  import { render, useApp, Box, Text, useInput } from 'ink';
17
17
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
18
18
  import { mkdir, writeFile, unlink, readFile } from 'fs/promises';
19
- import { Spinner, Select, TextInput } from '@inkjs/ui';
20
19
  import { stringify } from 'yaml';
21
- import { fileURLToPath } from 'url';
22
20
 
23
21
  var globalOptions = {};
24
22
  var theme;
@@ -252,47 +250,93 @@ async function offerCompletionInstall() {
252
250
  return false;
253
251
  }
254
252
  }
253
+ function hasVersion(data) {
254
+ return typeof data === "object" && data !== null && "version" in data && // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
255
+ typeof data.version === "string";
256
+ }
257
+ var cachedVersion;
258
+ function getPackageVersion() {
259
+ if (cachedVersion !== void 0) {
260
+ return cachedVersion;
261
+ }
262
+ const __filename = fileURLToPath(import.meta.url);
263
+ const __dirname = dirname(__filename);
264
+ const possiblePaths = [join(__dirname, "../package.json"), join(__dirname, "../../package.json")];
265
+ for (const packageJsonPath of possiblePaths) {
266
+ try {
267
+ const content = readFileSync(packageJsonPath, "utf-8");
268
+ const packageJsonData = JSON.parse(content);
269
+ if (!hasVersion(packageJsonData)) {
270
+ throw new Error(`Invalid package.json at ${packageJsonPath}: missing version field`);
271
+ }
272
+ cachedVersion = packageJsonData.version;
273
+ return cachedVersion;
274
+ } catch (error2) {
275
+ if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") {
276
+ continue;
277
+ }
278
+ throw error2;
279
+ }
280
+ }
281
+ throw new Error("Could not find package.json");
282
+ }
255
283
 
256
284
  // src/commands/init.ts
257
285
  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) => {
258
286
  await runInit(options);
259
287
  });
260
- var CONFIG_TEMPLATE = `# attest-it configuration
261
- # See https://github.com/attest-it/attest-it for documentation
262
-
263
- version: 1
264
-
265
- settings:
266
- # How long attestations remain valid (in days)
267
- maxAgeDays: 30
268
- # Path to the public key used for signature verification
269
- publicKeyPath: .attest-it/pubkey.pem
270
- # Path to the attestations file
271
- attestationsPath: .attest-it/attestations.json
272
- # Signing algorithm
273
- algorithm: rsa
274
-
275
- # Define your test suites below. Each suite groups tests that require
276
- # human verification before their attestations are accepted.
277
- #
278
- # Example:
279
- #
280
- # suites:
281
- # visual-tests:
282
- # description: Visual regression tests requiring human review
283
- # packages:
284
- # - packages/ui
285
- # - packages/components
286
- # command: pnpm vitest packages/ui packages/components
287
- #
288
- # integration:
289
- # description: Integration tests with external services
290
- # packages:
291
- # - packages/api
292
- # command: pnpm vitest packages/api --project=integration
293
-
294
- suites: {}
295
- `;
288
+ function loadConfigTemplate() {
289
+ const __filename = fileURLToPath(import.meta.url);
290
+ const __dirname = dirname(__filename);
291
+ const possiblePaths = [
292
+ join(__dirname, "../../templates/config.yaml"),
293
+ join(__dirname, "../templates/config.yaml")
294
+ ];
295
+ for (const templatePath of possiblePaths) {
296
+ try {
297
+ return fs.readFileSync(templatePath, "utf-8");
298
+ } catch (error2) {
299
+ if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") {
300
+ continue;
301
+ }
302
+ throw error2;
303
+ }
304
+ }
305
+ throw new Error("Could not find config.yaml template");
306
+ }
307
+ function isPackageJson(data) {
308
+ return typeof data === "object" && data !== null && "name" in data && "version" in data && // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
309
+ typeof data.name === "string" && // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
310
+ typeof data.version === "string";
311
+ }
312
+ function detectPackageManager() {
313
+ if (fs.existsSync("pnpm-lock.yaml")) return "pnpm";
314
+ if (fs.existsSync("yarn.lock")) return "yarn";
315
+ if (fs.existsSync("bun.lockb")) return "bun";
316
+ return "npm";
317
+ }
318
+ async function ensureDevDependency() {
319
+ const packageJsonPath = "package.json";
320
+ const packageManager = detectPackageManager();
321
+ let created = false;
322
+ let packageJson;
323
+ if (fs.existsSync(packageJsonPath)) {
324
+ const content = await fs.promises.readFile(packageJsonPath, "utf8");
325
+ const parsed = JSON.parse(content);
326
+ if (!isPackageJson(parsed)) {
327
+ throw new Error("Invalid package.json: missing required name or version field");
328
+ }
329
+ packageJson = parsed;
330
+ } else {
331
+ packageJson = { name: path.basename(process.cwd()), version: "1.0.0" };
332
+ created = true;
333
+ }
334
+ const devDeps = packageJson.devDependencies ?? {};
335
+ devDeps["attest-it"] = "^" + getPackageVersion();
336
+ packageJson.devDependencies = devDeps;
337
+ await fs.promises.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2) + "\n");
338
+ return { packageManager, created };
339
+ }
296
340
  async function runInit(options) {
297
341
  try {
298
342
  const configPath = path.resolve(options.path);
@@ -307,14 +351,22 @@ async function runInit(options) {
307
351
  process.exit(ExitCode.CANCELLED);
308
352
  }
309
353
  }
354
+ const { packageManager, created } = await ensureDevDependency();
355
+ if (created) {
356
+ success("Created package.json");
357
+ } else {
358
+ success("Updated package.json with attest-it devDependency");
359
+ }
310
360
  await fs.promises.mkdir(configDir, { recursive: true });
311
- await fs.promises.writeFile(configPath, CONFIG_TEMPLATE, "utf-8");
361
+ const configTemplate = loadConfigTemplate();
362
+ await fs.promises.writeFile(configPath, configTemplate, "utf-8");
312
363
  success(`Configuration created at ${configPath}`);
313
364
  log("");
314
365
  log("Next steps:");
315
- log(` 1. Edit ${options.path} to define your test suites`);
316
- log(" 2. Run: attest-it keygen");
317
- log(" 3. Run: attest-it status");
366
+ log(` 1. Run: ${packageManager} install`);
367
+ log(" 2. Run: attest-it identity create (if you haven't already)");
368
+ log(" 3. Run: attest-it team join");
369
+ log(" 4. Edit .attest-it/config.yaml to define your gates and suites");
318
370
  await offerCompletionInstall();
319
371
  } catch (err) {
320
372
  if (err instanceof Error) {
@@ -337,7 +389,7 @@ async function runStatus(gates, options) {
337
389
  process.exit(ExitCode.CONFIG_ERROR);
338
390
  }
339
391
  const projectRoot = process.cwd();
340
- const sealsFile = readSealsSync(projectRoot);
392
+ const sealsFile = readSealsSync(projectRoot, attestItConfig.settings.sealsPath);
341
393
  const gatesToCheck = gates.length > 0 ? gates : Object.keys(attestItConfig.gates);
342
394
  for (const gateId of gatesToCheck) {
343
395
  if (!attestItConfig.gates[gateId]) {
@@ -546,14 +598,14 @@ function SelectionPrompt({
546
598
  onSelect,
547
599
  groups
548
600
  }) {
549
- useInput((input5) => {
550
- const matchedOption = options.find((opt) => opt.hint === input5);
601
+ useInput((input4) => {
602
+ const matchedOption = options.find((opt) => opt.hint === input4);
551
603
  if (matchedOption) {
552
604
  onSelect(matchedOption.value);
553
605
  return;
554
606
  }
555
607
  if (groups) {
556
- const matchedGroup = groups.find((group) => group.name === input5);
608
+ const matchedGroup = groups.find((group) => group.name === input4);
557
609
  if (matchedGroup) {
558
610
  onSelect(matchedGroup.name);
559
611
  }
@@ -603,17 +655,17 @@ function SuiteSelector({
603
655
  return next;
604
656
  });
605
657
  }, []);
606
- useInput((input5, key) => {
607
- if (input5 === "a") {
658
+ useInput((input4, key) => {
659
+ if (input4 === "a") {
608
660
  setSelectedSuites(new Set(pendingSuites.map((s) => s.name)));
609
661
  return;
610
662
  }
611
- if (input5 === "n") {
663
+ if (input4 === "n") {
612
664
  onExit();
613
665
  return;
614
666
  }
615
- if (/^[1-9]$/.test(input5)) {
616
- const idx = parseInt(input5, 10) - 1;
667
+ if (/^[1-9]$/.test(input4)) {
668
+ const idx = parseInt(input4, 10) - 1;
617
669
  if (idx < pendingSuites.length) {
618
670
  const suite = pendingSuites[idx];
619
671
  if (suite) {
@@ -622,8 +674,8 @@ function SuiteSelector({
622
674
  }
623
675
  return;
624
676
  }
625
- if (input5.startsWith("g") && groups) {
626
- const groupIdx = parseInt(input5.slice(1), 10) - 1;
677
+ if (input4.startsWith("g") && groups) {
678
+ const groupIdx = parseInt(input4.slice(1), 10) - 1;
627
679
  const groupNames = Object.keys(groups);
628
680
  if (groupIdx >= 0 && groupIdx < groupNames.length) {
629
681
  const groupName = groupNames[groupIdx];
@@ -640,7 +692,7 @@ function SuiteSelector({
640
692
  onSelect(Array.from(selectedSuites));
641
693
  return;
642
694
  }
643
- if (input5 === " ") {
695
+ if (input4 === " ") {
644
696
  const currentSuite = pendingSuites[cursorIndex];
645
697
  if (currentSuite) {
646
698
  toggleSuite(currentSuite.name);
@@ -773,11 +825,11 @@ function TestRunner({
773
825
  };
774
826
  }, [currentIndex, phase, suites, executeTest, onComplete]);
775
827
  useInput(
776
- (input5, key) => {
828
+ (input4, key) => {
777
829
  if (phase !== "confirming") return;
778
830
  const currentSuite2 = suites[currentIndex];
779
831
  if (!currentSuite2) return;
780
- if (input5.toLowerCase() === "y" || key.return) {
832
+ if (input4.toLowerCase() === "y" || key.return) {
781
833
  createAttestation3(currentSuite2).then(() => {
782
834
  setResults((prev) => ({
783
835
  ...prev,
@@ -794,7 +846,7 @@ function TestRunner({
794
846
  setPhase("running");
795
847
  });
796
848
  }
797
- if (input5.toLowerCase() === "n") {
849
+ if (input4.toLowerCase() === "n") {
798
850
  setResults((prev) => ({
799
851
  ...prev,
800
852
  skipped: [...prev.skipped, currentSuite2]
@@ -989,12 +1041,24 @@ async function getAllSuiteStatuses(config) {
989
1041
  const attestations = attestationsFile?.attestations ?? [];
990
1042
  const results = [];
991
1043
  for (const [suiteName, suiteConfig] of Object.entries(config.suites)) {
992
- if (!suiteConfig.packages) {
1044
+ let packages;
1045
+ let ignore;
1046
+ if (suiteConfig.gate && config.gates) {
1047
+ const gateConfig = config.gates[suiteConfig.gate];
1048
+ if (gateConfig) {
1049
+ packages = gateConfig.fingerprint.paths;
1050
+ ignore = gateConfig.fingerprint.exclude;
1051
+ }
1052
+ } else if (suiteConfig.packages) {
1053
+ packages = suiteConfig.packages;
1054
+ ignore = suiteConfig.ignore;
1055
+ }
1056
+ if (!packages || packages.length === 0) {
993
1057
  continue;
994
1058
  }
995
1059
  const fingerprintResult = await computeFingerprint({
996
- packages: suiteConfig.packages,
997
- ...suiteConfig.ignore && { ignore: suiteConfig.ignore }
1060
+ packages,
1061
+ ...ignore && { ignore }
998
1062
  });
999
1063
  const attestation = findAttestation(
1000
1064
  { schemaVersion: "1", attestations, signature: "" },
@@ -1495,9 +1559,9 @@ async function runSingleSuite(suiteName, config, options) {
1495
1559
  if (!await keyProvider.keyExists(keyRef)) {
1496
1560
  error(`Private key not found in ${keyProvider.displayName}`);
1497
1561
  if (keyProvider.type === "filesystem") {
1498
- error('Run "attest-it keygen" first to generate a keypair.');
1562
+ error('Run "attest-it identity create" first to generate a keypair.');
1499
1563
  } else {
1500
- error('Run "attest-it keygen" to generate and store a key.');
1564
+ error('Run "attest-it identity create" to generate and store a key.');
1501
1565
  }
1502
1566
  process.exit(ExitCode.MISSING_KEY);
1503
1567
  }
@@ -1521,7 +1585,7 @@ async function promptForSeal(suiteName, gateId, config) {
1521
1585
  const localConfig = loadLocalConfigSync();
1522
1586
  if (!localConfig) {
1523
1587
  warn("No local identity configuration found - cannot create seal");
1524
- warn('Run "attest-it keygen" to set up your identity');
1588
+ warn('Run "attest-it identity create" to set up your identity');
1525
1589
  return;
1526
1590
  }
1527
1591
  const identity = getActiveIdentity(localConfig);
@@ -1556,21 +1620,22 @@ async function promptForSeal(suiteName, gateId, config) {
1556
1620
  const keyProvider = createKeyProviderFromIdentity(identity);
1557
1621
  const keyRef = getKeyRefFromIdentity(identity);
1558
1622
  const keyResult = await keyProvider.getPrivateKey(keyRef);
1559
- const fs4 = await import('fs/promises');
1560
- const privateKeyPem = await fs4.readFile(keyResult.keyPath, "utf8");
1623
+ const fs3 = await import('fs/promises');
1624
+ const privateKeyPem = await fs3.readFile(keyResult.keyPath, "utf8");
1561
1625
  await keyResult.cleanup();
1626
+ const identitySlug = localConfig.activeIdentity;
1562
1627
  const seal = createSeal({
1563
1628
  gateId,
1564
1629
  fingerprint: gateFingerprint.fingerprint,
1565
- sealedBy: identity.name,
1630
+ sealedBy: identitySlug,
1566
1631
  privateKey: privateKeyPem
1567
1632
  });
1568
1633
  const projectRoot = process.cwd();
1569
- const sealsFile = readSealsSync(projectRoot);
1634
+ const sealsFile = readSealsSync(projectRoot, attestItConfig.settings.sealsPath);
1570
1635
  sealsFile.seals[gateId] = seal;
1571
- writeSealsSync(projectRoot, sealsFile);
1636
+ writeSealsSync(projectRoot, sealsFile, attestItConfig.settings.sealsPath);
1572
1637
  success(`Seal created for gate '${gateId}'`);
1573
- log(` Sealed by: ${identity.name}`);
1638
+ log(` Sealed by: ${identitySlug} (${identity.name})`);
1574
1639
  log(` Timestamp: ${seal.timestamp}`);
1575
1640
  } catch (err) {
1576
1641
  if (err instanceof Error) {
@@ -1638,626 +1703,6 @@ function getKeyRefFromIdentity(identity) {
1638
1703
  }
1639
1704
  }
1640
1705
  }
1641
- function KeygenInteractive(props) {
1642
- const { onComplete, onCancel, onError } = props;
1643
- const [step, setStep] = useState("checking-providers");
1644
- const [opAvailable, setOpAvailable] = useState(false);
1645
- const [keychainAvailable, setKeychainAvailable] = useState(false);
1646
- const [yubiKeyAvailable, setYubiKeyAvailable] = useState(false);
1647
- const [accounts, setAccounts] = useState([]);
1648
- const [vaults, setVaults] = useState([]);
1649
- const [yubiKeyDevices, setYubiKeyDevices] = useState([]);
1650
- const [_selectedProvider, setSelectedProvider] = useState();
1651
- const [selectedAccount, setSelectedAccount] = useState();
1652
- const [selectedVault, setSelectedVault] = useState();
1653
- const [itemName, setItemName] = useState("attest-it-private-key");
1654
- const [keychainItemName, setKeychainItemName] = useState("attest-it-private-key");
1655
- const [selectedYubiKeySerial, setSelectedYubiKeySerial] = useState();
1656
- const [selectedYubiKeySlot, setSelectedYubiKeySlot] = useState(2);
1657
- const [slot1Configured, setSlot1Configured] = useState(false);
1658
- const [slot2Configured, setSlot2Configured] = useState(false);
1659
- useInput((_input, key) => {
1660
- if (key.escape) {
1661
- onCancel();
1662
- }
1663
- });
1664
- useEffect(() => {
1665
- const checkProviders = async () => {
1666
- try {
1667
- const isInstalled = await OnePasswordKeyProvider.isInstalled();
1668
- setOpAvailable(isInstalled);
1669
- if (isInstalled) {
1670
- const accountList = await OnePasswordKeyProvider.listAccounts();
1671
- setAccounts(accountList);
1672
- }
1673
- } catch {
1674
- setOpAvailable(false);
1675
- }
1676
- const isKeychainAvailable = MacOSKeychainKeyProvider.isAvailable();
1677
- setKeychainAvailable(isKeychainAvailable);
1678
- try {
1679
- const isInstalled = await YubiKeyProvider.isInstalled();
1680
- if (isInstalled) {
1681
- const isConnected = await YubiKeyProvider.isConnected();
1682
- if (isConnected) {
1683
- const devices = await YubiKeyProvider.listDevices();
1684
- setYubiKeyDevices(devices);
1685
- setYubiKeyAvailable(devices.length > 0);
1686
- }
1687
- }
1688
- } catch {
1689
- setYubiKeyAvailable(false);
1690
- }
1691
- setStep("select-provider");
1692
- };
1693
- void checkProviders();
1694
- }, []);
1695
- useEffect(() => {
1696
- if (step === "select-vault" && selectedAccount) {
1697
- const fetchVaults = async () => {
1698
- try {
1699
- const vaultList = await OnePasswordKeyProvider.listVaults(selectedAccount);
1700
- setVaults(vaultList);
1701
- } catch (err) {
1702
- onError(err instanceof Error ? err : new Error("Failed to fetch vaults"));
1703
- }
1704
- };
1705
- void fetchVaults();
1706
- }
1707
- }, [step, selectedAccount, onError]);
1708
- const checkYubiKeySlots = async (serial) => {
1709
- try {
1710
- const slot1 = await YubiKeyProvider.isChallengeResponseConfigured(1, serial);
1711
- const slot2 = await YubiKeyProvider.isChallengeResponseConfigured(2, serial);
1712
- setSlot1Configured(slot1);
1713
- setSlot2Configured(slot2);
1714
- if (slot1 && slot2) {
1715
- setStep("select-yubikey-slot");
1716
- } else if (slot2) {
1717
- setSelectedYubiKeySlot(2);
1718
- void generateKeys("yubikey");
1719
- } else if (slot1) {
1720
- setSelectedYubiKeySlot(1);
1721
- void generateKeys("yubikey");
1722
- } else {
1723
- setStep("yubikey-offer-setup");
1724
- }
1725
- } catch (err) {
1726
- onError(err instanceof Error ? err : new Error("Failed to check YubiKey slots"));
1727
- }
1728
- };
1729
- const handleProviderSelect = (value) => {
1730
- if (value === "filesystem") {
1731
- setSelectedProvider("filesystem");
1732
- void generateKeys("filesystem");
1733
- } else if (value === "1password") {
1734
- setSelectedProvider("1password");
1735
- if (accounts.length === 1 && accounts[0]) {
1736
- setSelectedAccount(accounts[0].user_uuid);
1737
- setStep("select-vault");
1738
- } else {
1739
- setStep("select-account");
1740
- }
1741
- } else if (value === "macos-keychain") {
1742
- setSelectedProvider("macos-keychain");
1743
- setStep("enter-keychain-item-name");
1744
- } else if (value === "yubikey") {
1745
- setSelectedProvider("yubikey");
1746
- if (yubiKeyDevices.length > 1) {
1747
- setStep("select-yubikey-device");
1748
- } else if (yubiKeyDevices.length === 1 && yubiKeyDevices[0]) {
1749
- setSelectedYubiKeySerial(yubiKeyDevices[0].serial);
1750
- void checkYubiKeySlots(yubiKeyDevices[0].serial);
1751
- }
1752
- }
1753
- };
1754
- const handleAccountSelect = (value) => {
1755
- setSelectedAccount(value);
1756
- setStep("select-vault");
1757
- };
1758
- const handleVaultSelect = (value) => {
1759
- setSelectedVault(value);
1760
- setStep("enter-item-name");
1761
- };
1762
- const handleItemNameSubmit = (value) => {
1763
- setItemName(value);
1764
- void generateKeys("1password");
1765
- };
1766
- const handleKeychainItemNameSubmit = (value) => {
1767
- setKeychainItemName(value);
1768
- void generateKeys("macos-keychain");
1769
- };
1770
- const handleYubiKeyDeviceSelect = (value) => {
1771
- setSelectedYubiKeySerial(value);
1772
- void checkYubiKeySlots(value);
1773
- };
1774
- const handleYubiKeySlotSelect = (value) => {
1775
- const slot = value === "1" ? 1 : 2;
1776
- setSelectedYubiKeySlot(slot);
1777
- void generateKeys("yubikey");
1778
- };
1779
- const handleYubiKeySetupConfirm = (value) => {
1780
- if (value === "yes") {
1781
- void setupYubiKeySlot();
1782
- } else {
1783
- onError(new Error("YubiKey setup cancelled"));
1784
- }
1785
- };
1786
- const setupYubiKeySlot = async () => {
1787
- setStep("yubikey-configuring");
1788
- try {
1789
- const { spawn: spawn3 } = await import('child_process');
1790
- const args = ["otp", "chalresp", "--touch", "--generate", "2"];
1791
- if (selectedYubiKeySerial) {
1792
- args.unshift("--device", selectedYubiKeySerial);
1793
- }
1794
- await new Promise((resolve2, reject) => {
1795
- const proc = spawn3("ykman", args, { stdio: "inherit" });
1796
- proc.on("close", (code) => {
1797
- if (code === 0) {
1798
- resolve2();
1799
- } else {
1800
- reject(new Error(`ykman exited with code ${String(code)}`));
1801
- }
1802
- });
1803
- proc.on("error", reject);
1804
- });
1805
- setSelectedYubiKeySlot(2);
1806
- void generateKeys("yubikey");
1807
- } catch (err) {
1808
- onError(err instanceof Error ? err : new Error("Failed to configure YubiKey"));
1809
- }
1810
- };
1811
- const generateKeys = async (provider) => {
1812
- setStep("generating");
1813
- try {
1814
- const publicKeyPath = props.publicKeyPath ?? getDefaultPublicKeyPath();
1815
- if (provider === "filesystem") {
1816
- const fsProvider = new FilesystemKeyProvider();
1817
- const genOptions = { publicKeyPath };
1818
- if (props.force !== void 0) {
1819
- genOptions.force = props.force;
1820
- }
1821
- const result = await fsProvider.generateKeyPair(genOptions);
1822
- onComplete({
1823
- provider: "filesystem",
1824
- publicKeyPath: result.publicKeyPath,
1825
- privateKeyRef: result.privateKeyRef,
1826
- storageDescription: result.storageDescription
1827
- });
1828
- } else if (provider === "1password") {
1829
- if (!selectedVault || !itemName) {
1830
- throw new Error("Vault and item name are required for 1Password");
1831
- }
1832
- const vault = vaults.find((v) => v.id === selectedVault);
1833
- if (!vault) {
1834
- throw new Error("Selected vault not found");
1835
- }
1836
- const providerOptions = {
1837
- vault: vault.name,
1838
- itemName
1839
- };
1840
- if (selectedAccount !== void 0) {
1841
- providerOptions.account = selectedAccount;
1842
- }
1843
- const opProvider = new OnePasswordKeyProvider(providerOptions);
1844
- const genOptions = { publicKeyPath };
1845
- if (props.force !== void 0) {
1846
- genOptions.force = props.force;
1847
- }
1848
- const result = await opProvider.generateKeyPair(genOptions);
1849
- const completionResult = {
1850
- provider: "1password",
1851
- publicKeyPath: result.publicKeyPath,
1852
- privateKeyRef: result.privateKeyRef,
1853
- storageDescription: result.storageDescription,
1854
- vault: vault.name,
1855
- itemName
1856
- };
1857
- if (selectedAccount !== void 0) {
1858
- completionResult.account = selectedAccount;
1859
- }
1860
- onComplete(completionResult);
1861
- } else if (provider === "macos-keychain") {
1862
- if (!keychainItemName) {
1863
- throw new Error("Item name is required for macOS Keychain");
1864
- }
1865
- const keychainProvider = new MacOSKeychainKeyProvider({
1866
- itemName: keychainItemName
1867
- });
1868
- const genOptions = { publicKeyPath };
1869
- if (props.force !== void 0) {
1870
- genOptions.force = props.force;
1871
- }
1872
- const result = await keychainProvider.generateKeyPair(genOptions);
1873
- onComplete({
1874
- provider: "macos-keychain",
1875
- publicKeyPath: result.publicKeyPath,
1876
- privateKeyRef: result.privateKeyRef,
1877
- storageDescription: result.storageDescription,
1878
- itemName: keychainItemName
1879
- });
1880
- } else {
1881
- const encryptedKeyPath = getDefaultYubiKeyEncryptedKeyPath();
1882
- const providerOptions = {
1883
- encryptedKeyPath,
1884
- slot: selectedYubiKeySlot
1885
- };
1886
- if (selectedYubiKeySerial !== void 0) {
1887
- providerOptions.serial = selectedYubiKeySerial;
1888
- }
1889
- const ykProvider = new YubiKeyProvider(providerOptions);
1890
- const genOptions = { publicKeyPath };
1891
- if (props.force !== void 0) {
1892
- genOptions.force = props.force;
1893
- }
1894
- const result = await ykProvider.generateKeyPair(genOptions);
1895
- const completionResult = {
1896
- provider: "yubikey",
1897
- publicKeyPath: result.publicKeyPath,
1898
- privateKeyRef: result.privateKeyRef,
1899
- storageDescription: result.storageDescription,
1900
- slot: selectedYubiKeySlot,
1901
- encryptedKeyPath
1902
- };
1903
- if (selectedYubiKeySerial !== void 0) {
1904
- completionResult.serial = selectedYubiKeySerial;
1905
- }
1906
- onComplete(completionResult);
1907
- }
1908
- setStep("done");
1909
- } catch (err) {
1910
- onError(err instanceof Error ? err : new Error("Key generation failed"));
1911
- }
1912
- };
1913
- if (step === "checking-providers") {
1914
- return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
1915
- /* @__PURE__ */ jsx(Spinner, {}),
1916
- /* @__PURE__ */ jsx(Text, { children: "Checking available key storage providers..." })
1917
- ] }) });
1918
- }
1919
- if (step === "select-provider") {
1920
- const options = [
1921
- {
1922
- label: `Local Filesystem (${getDefaultPrivateKeyPath()})`,
1923
- value: "filesystem"
1924
- }
1925
- ];
1926
- if (keychainAvailable) {
1927
- options.push({
1928
- label: "macOS Keychain",
1929
- value: "macos-keychain"
1930
- });
1931
- }
1932
- if (yubiKeyAvailable) {
1933
- options.push({
1934
- label: "YubiKey (hardware security key)",
1935
- value: "yubikey"
1936
- });
1937
- }
1938
- if (opAvailable) {
1939
- options.push({
1940
- label: "1Password (requires op CLI)",
1941
- value: "1password"
1942
- });
1943
- }
1944
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
1945
- /* @__PURE__ */ jsx(Text, { bold: true, children: "Where would you like to store your private key?" }),
1946
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "" }),
1947
- /* @__PURE__ */ jsx(Select, { options, onChange: handleProviderSelect })
1948
- ] });
1949
- }
1950
- if (step === "select-account") {
1951
- const options = accounts.map((account) => ({
1952
- label: account.name ? `${account.name} (${account.email})` : account.email,
1953
- value: account.user_uuid
1954
- }));
1955
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
1956
- /* @__PURE__ */ jsx(Text, { bold: true, children: "Select 1Password account:" }),
1957
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "" }),
1958
- /* @__PURE__ */ jsx(Select, { options, onChange: handleAccountSelect })
1959
- ] });
1960
- }
1961
- if (step === "select-vault") {
1962
- if (vaults.length === 0) {
1963
- return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
1964
- /* @__PURE__ */ jsx(Spinner, {}),
1965
- /* @__PURE__ */ jsx(Text, { children: "Loading vaults..." })
1966
- ] }) });
1967
- }
1968
- const options = vaults.map((vault) => ({
1969
- label: vault.name,
1970
- value: vault.id
1971
- }));
1972
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
1973
- /* @__PURE__ */ jsx(Text, { bold: true, children: "Select vault for private key storage:" }),
1974
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "" }),
1975
- /* @__PURE__ */ jsx(Select, { options, onChange: handleVaultSelect })
1976
- ] });
1977
- }
1978
- if (step === "enter-item-name") {
1979
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
1980
- /* @__PURE__ */ jsx(Text, { bold: true, children: "Enter name for the key item:" }),
1981
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "(This will be visible in your 1Password vault)" }),
1982
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "" }),
1983
- /* @__PURE__ */ jsx(TextInput, { defaultValue: itemName, onSubmit: handleItemNameSubmit })
1984
- ] });
1985
- }
1986
- if (step === "enter-keychain-item-name") {
1987
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
1988
- /* @__PURE__ */ jsx(Text, { bold: true, children: "Enter name for the keychain item:" }),
1989
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "(This will be the service name in your macOS Keychain)" }),
1990
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "" }),
1991
- /* @__PURE__ */ jsx(TextInput, { defaultValue: keychainItemName, onSubmit: handleKeychainItemNameSubmit })
1992
- ] });
1993
- }
1994
- if (step === "select-yubikey-device") {
1995
- const options = yubiKeyDevices.map((device) => ({
1996
- label: `${device.type} (Serial: ${device.serial})`,
1997
- value: device.serial
1998
- }));
1999
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
2000
- /* @__PURE__ */ jsx(Text, { bold: true, children: "Select YubiKey device:" }),
2001
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "" }),
2002
- /* @__PURE__ */ jsx(Select, { options, onChange: handleYubiKeyDeviceSelect })
2003
- ] });
2004
- }
2005
- if (step === "select-yubikey-slot") {
2006
- const options = [];
2007
- if (slot2Configured) {
2008
- options.push({
2009
- label: "Slot 2 (recommended)",
2010
- value: "2"
2011
- });
2012
- }
2013
- if (slot1Configured) {
2014
- options.push({
2015
- label: "Slot 1",
2016
- value: "1"
2017
- });
2018
- }
2019
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
2020
- /* @__PURE__ */ jsx(Text, { bold: true, children: "Select YubiKey slot for challenge-response:" }),
2021
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "" }),
2022
- /* @__PURE__ */ jsx(Select, { options, onChange: handleYubiKeySlotSelect })
2023
- ] });
2024
- }
2025
- if (step === "yubikey-offer-setup") {
2026
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
2027
- /* @__PURE__ */ jsx(Text, { bold: true, children: "Your YubiKey is not configured for challenge-response." }),
2028
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "" }),
2029
- /* @__PURE__ */ jsx(Text, { children: "Would you like to configure slot 2 for challenge-response now?" }),
2030
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "This will enable touch-to-sign functionality." }),
2031
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "" }),
2032
- /* @__PURE__ */ jsx(
2033
- Select,
2034
- {
2035
- options: [
2036
- { label: "Yes, configure my YubiKey", value: "yes" },
2037
- { label: "No, cancel", value: "no" }
2038
- ],
2039
- onChange: handleYubiKeySetupConfirm
2040
- }
2041
- )
2042
- ] });
2043
- }
2044
- if (step === "yubikey-configuring") {
2045
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
2046
- /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
2047
- /* @__PURE__ */ jsx(Spinner, {}),
2048
- /* @__PURE__ */ jsx(Text, { children: "Configuring YubiKey slot 2 for challenge-response..." })
2049
- ] }),
2050
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Touch your YubiKey when it flashes." })
2051
- ] });
2052
- }
2053
- if (step === "generating") {
2054
- return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
2055
- /* @__PURE__ */ jsx(Spinner, {}),
2056
- /* @__PURE__ */ jsx(Text, { children: "Generating RSA-2048 keypair..." })
2057
- ] }) });
2058
- }
2059
- return /* @__PURE__ */ jsx(Box, {});
2060
- }
2061
- async function runKeygenInteractive(options) {
2062
- return new Promise((resolve2, reject) => {
2063
- const props = {
2064
- onComplete: (result) => {
2065
- unmount();
2066
- resolve2(result);
2067
- },
2068
- onCancel: () => {
2069
- unmount();
2070
- reject(new Error("Keygen cancelled"));
2071
- },
2072
- onError: (error2) => {
2073
- unmount();
2074
- reject(error2);
2075
- }
2076
- };
2077
- if (options.publicKeyPath !== void 0) {
2078
- props.publicKeyPath = options.publicKeyPath;
2079
- }
2080
- if (options.force !== void 0) {
2081
- props.force = options.force;
2082
- }
2083
- const { unmount } = render(/* @__PURE__ */ jsx(KeygenInteractive, { ...props }));
2084
- });
2085
- }
2086
-
2087
- // src/commands/keygen.ts
2088
- var keygenCommand = new Command("keygen").description("Generate a new RSA keypair for signing attestations").option("-o, --output <path>", "Public key output path").option("-p, --private <path>", "Private key output path (filesystem only)").option(
2089
- "--provider <type>",
2090
- "Key provider: filesystem, 1password, or macos-keychain (skips interactive)"
2091
- ).option("--vault <name>", "1Password vault name").option("--item-name <name>", "1Password/macOS Keychain item name").option("--account <email>", "1Password account").option("-f, --force", "Overwrite existing keys").option("--no-interactive", "Disable interactive mode").action(async (options) => {
2092
- await runKeygen(options);
2093
- });
2094
- async function runKeygen(options) {
2095
- try {
2096
- const useInteractive = options.interactive !== false && !options.provider;
2097
- if (useInteractive) {
2098
- const interactiveOptions = {};
2099
- if (options.output !== void 0) {
2100
- interactiveOptions.publicKeyPath = options.output;
2101
- }
2102
- if (options.force !== void 0) {
2103
- interactiveOptions.force = options.force;
2104
- }
2105
- const result = await runKeygenInteractive(interactiveOptions);
2106
- success("Keypair generated successfully!");
2107
- log("");
2108
- log("Private key stored in:");
2109
- log(` ${result.storageDescription}`);
2110
- log("");
2111
- log("Public key (commit to repo):");
2112
- log(` ${result.publicKeyPath}`);
2113
- log("");
2114
- if (result.provider === "1password") {
2115
- log("Add to your .attest-it/config.yaml:");
2116
- log("");
2117
- log("settings:");
2118
- log(` publicKeyPath: ${result.publicKeyPath}`);
2119
- log(" keyProvider:");
2120
- log(" type: 1password");
2121
- log(" options:");
2122
- if (result.account) {
2123
- log(` account: ${result.account}`);
2124
- }
2125
- log(` vault: ${result.vault ?? ""}`);
2126
- log(` itemName: ${result.itemName ?? ""}`);
2127
- log("");
2128
- } else if (result.provider === "macos-keychain") {
2129
- log("Add to your .attest-it/config.yaml:");
2130
- log("");
2131
- log("settings:");
2132
- log(` publicKeyPath: ${result.publicKeyPath}`);
2133
- log(" keyProvider:");
2134
- log(" type: macos-keychain");
2135
- log(" options:");
2136
- log(` itemName: ${result.itemName ?? ""}`);
2137
- log("");
2138
- }
2139
- log("Next steps:");
2140
- log(` 1. git add ${result.publicKeyPath}`);
2141
- if (result.provider === "1password" || result.provider === "macos-keychain") {
2142
- log(" 2. Update .attest-it/config.yaml with keyProvider settings");
2143
- } else {
2144
- log(" 2. Update .attest-it/config.yaml publicKeyPath if needed");
2145
- }
2146
- log(" 3. attest-it run --suite <suite-name>");
2147
- } else {
2148
- await runNonInteractiveKeygen(options);
2149
- }
2150
- } catch (err) {
2151
- if (err instanceof Error) {
2152
- error(err.message);
2153
- } else {
2154
- error("Unknown error occurred");
2155
- }
2156
- process.exit(ExitCode.CONFIG_ERROR);
2157
- }
2158
- }
2159
- async function runNonInteractiveKeygen(options) {
2160
- log("Checking OpenSSL...");
2161
- const version = await checkOpenSSL();
2162
- info(`OpenSSL: ${version}`);
2163
- const publicPath = options.output ?? getDefaultPublicKeyPath();
2164
- if (options.provider === "1password") {
2165
- if (!options.vault || !options.itemName) {
2166
- throw new Error("--vault and --item-name are required for 1password provider");
2167
- }
2168
- const providerOptions = {
2169
- vault: options.vault,
2170
- itemName: options.itemName
2171
- };
2172
- if (options.account !== void 0) {
2173
- providerOptions.account = options.account;
2174
- }
2175
- const provider = new OnePasswordKeyProvider(providerOptions);
2176
- log(`Generating keypair with 1Password storage...`);
2177
- log(`Vault: ${options.vault}`);
2178
- log(`Item: ${options.itemName}`);
2179
- const genOptions = { publicKeyPath: publicPath };
2180
- if (options.force !== void 0) {
2181
- genOptions.force = options.force;
2182
- }
2183
- const result = await provider.generateKeyPair(genOptions);
2184
- success("Keypair generated successfully!");
2185
- log("");
2186
- log("Private key stored in:");
2187
- log(` ${result.storageDescription}`);
2188
- log("");
2189
- log("Public key (commit to repo):");
2190
- log(` ${result.publicKeyPath}`);
2191
- } else if (options.provider === "macos-keychain") {
2192
- if (!options.itemName) {
2193
- throw new Error("--item-name is required for macos-keychain provider");
2194
- }
2195
- const isAvailable = MacOSKeychainKeyProvider.isAvailable();
2196
- if (!isAvailable) {
2197
- throw new Error("macOS Keychain is not available on this platform");
2198
- }
2199
- const provider = new MacOSKeychainKeyProvider({
2200
- itemName: options.itemName
2201
- });
2202
- log(`Generating keypair with macOS Keychain storage...`);
2203
- log(`Item: ${options.itemName}`);
2204
- const genOptions = { publicKeyPath: publicPath };
2205
- if (options.force !== void 0) {
2206
- genOptions.force = options.force;
2207
- }
2208
- const result = await provider.generateKeyPair(genOptions);
2209
- success("Keypair generated successfully!");
2210
- log("");
2211
- log("Private key stored in:");
2212
- log(` ${result.storageDescription}`);
2213
- log("");
2214
- log("Public key (commit to repo):");
2215
- log(` ${result.publicKeyPath}`);
2216
- } else {
2217
- const privatePath = options.private ?? getDefaultPrivateKeyPath();
2218
- log(`Private key: ${privatePath}`);
2219
- log(`Public key: ${publicPath}`);
2220
- const privateExists = fs.existsSync(privatePath);
2221
- const publicExists = fs.existsSync(publicPath);
2222
- if ((privateExists || publicExists) && !options.force) {
2223
- if (privateExists) {
2224
- warn(`Private key already exists: ${privatePath}`);
2225
- }
2226
- if (publicExists) {
2227
- warn(`Public key already exists: ${publicPath}`);
2228
- }
2229
- const shouldOverwrite = await confirmAction({
2230
- message: "Overwrite existing keys?",
2231
- default: false
2232
- });
2233
- if (!shouldOverwrite) {
2234
- error("Keygen cancelled");
2235
- process.exit(ExitCode.CANCELLED);
2236
- }
2237
- }
2238
- log("\nGenerating RSA-2048 keypair...");
2239
- const result = await generateKeyPair({
2240
- privatePath,
2241
- publicPath,
2242
- force: true
2243
- });
2244
- await setKeyPermissions(result.privatePath);
2245
- success("Keypair generated successfully!");
2246
- log("");
2247
- log("Private key (KEEP SECRET):");
2248
- log(` ${result.privatePath}`);
2249
- log("");
2250
- log("Public key (commit to repo):");
2251
- log(` ${result.publicPath}`);
2252
- }
2253
- log("");
2254
- info("Important: Back up your private key securely!");
2255
- log("");
2256
- log("Next steps:");
2257
- log(` 1. git add ${publicPath}`);
2258
- log(" 2. Update .attest-it/config.yaml publicKeyPath if needed");
2259
- log(" 3. attest-it run --suite <suite-name>");
2260
- }
2261
1706
  var pruneCommand = new Command("prune").description("Remove stale attestations").option("-n, --dry-run", "Show what would be removed without removing").option("-k, --keep-days <n>", "Keep attestations newer than n days", "30").action(async (options) => {
2262
1707
  await runPrune(options);
2263
1708
  });
@@ -2362,7 +1807,7 @@ async function runVerify(gates, options) {
2362
1807
  process.exit(ExitCode.CONFIG_ERROR);
2363
1808
  }
2364
1809
  const projectRoot = process.cwd();
2365
- const sealsFile = readSealsSync(projectRoot);
1810
+ const sealsFile = readSealsSync(projectRoot, attestItConfig.settings.sealsPath);
2366
1811
  const gatesToVerify = gates.length > 0 ? gates : Object.keys(attestItConfig.gates);
2367
1812
  for (const gateId of gatesToVerify) {
2368
1813
  if (!attestItConfig.gates[gateId]) {
@@ -2509,7 +1954,7 @@ async function runSeal(gates, options) {
2509
1954
  const localConfig = loadLocalConfigSync();
2510
1955
  if (!localConfig) {
2511
1956
  error("No local identity configuration found");
2512
- error('Run "attest-it keygen" first to set up your identity');
1957
+ error('Run "attest-it identity create" first to set up your identity');
2513
1958
  process.exit(ExitCode.CONFIG_ERROR);
2514
1959
  }
2515
1960
  const identity = getActiveIdentity(localConfig);
@@ -2518,7 +1963,7 @@ async function runSeal(gates, options) {
2518
1963
  process.exit(ExitCode.CONFIG_ERROR);
2519
1964
  }
2520
1965
  const projectRoot = process.cwd();
2521
- const sealsFile = readSealsSync(projectRoot);
1966
+ const sealsFile = readSealsSync(projectRoot, attestItConfig.settings.sealsPath);
2522
1967
  const gatesToSeal = gates.length > 0 ? gates : getAllGateIds(attestItConfig);
2523
1968
  for (const gateId of gatesToSeal) {
2524
1969
  if (!attestItConfig.gates[gateId]) {
@@ -2531,9 +1976,17 @@ async function runSeal(gates, options) {
2531
1976
  skipped: [],
2532
1977
  failed: []
2533
1978
  };
1979
+ const identitySlug = localConfig.activeIdentity;
2534
1980
  for (const gateId of gatesToSeal) {
2535
1981
  try {
2536
- const result = await processSingleGate(gateId, attestItConfig, identity, sealsFile, options);
1982
+ const result = await processSingleGate(
1983
+ gateId,
1984
+ attestItConfig,
1985
+ identity,
1986
+ identitySlug,
1987
+ sealsFile,
1988
+ options
1989
+ );
2537
1990
  if (result.sealed) {
2538
1991
  summary.sealed.push(gateId);
2539
1992
  } else if (result.skipped) {
@@ -2545,7 +1998,7 @@ async function runSeal(gates, options) {
2545
1998
  }
2546
1999
  }
2547
2000
  if (!options.dryRun && summary.sealed.length > 0) {
2548
- writeSealsSync(projectRoot, sealsFile);
2001
+ writeSealsSync(projectRoot, sealsFile, attestItConfig.settings.sealsPath);
2549
2002
  }
2550
2003
  displaySummary(summary, options.dryRun);
2551
2004
  if (summary.failed.length > 0) {
@@ -2564,7 +2017,7 @@ async function runSeal(gates, options) {
2564
2017
  process.exit(ExitCode.CONFIG_ERROR);
2565
2018
  }
2566
2019
  }
2567
- async function processSingleGate(gateId, config, identity, sealsFile, options) {
2020
+ async function processSingleGate(gateId, config, identity, identitySlug, sealsFile, options) {
2568
2021
  verbose(`Processing gate: ${gateId}`);
2569
2022
  const gate = getGate(config, gateId);
2570
2023
  if (!gate) {
@@ -2598,18 +2051,18 @@ async function processSingleGate(gateId, config, identity, sealsFile, options) {
2598
2051
  const keyProvider = createKeyProviderFromIdentity2(identity);
2599
2052
  const keyRef = getKeyRefFromIdentity2(identity);
2600
2053
  const keyResult = await keyProvider.getPrivateKey(keyRef);
2601
- const fs4 = await import('fs/promises');
2602
- const privateKeyPem = await fs4.readFile(keyResult.keyPath, "utf8");
2054
+ const fs3 = await import('fs/promises');
2055
+ const privateKeyPem = await fs3.readFile(keyResult.keyPath, "utf8");
2603
2056
  await keyResult.cleanup();
2604
2057
  const seal = createSeal({
2605
2058
  gateId,
2606
2059
  fingerprint: fingerprintResult.fingerprint,
2607
- sealedBy: identity.name,
2060
+ sealedBy: identitySlug,
2608
2061
  privateKey: privateKeyPem
2609
2062
  });
2610
2063
  sealsFile.seals[gateId] = seal;
2611
2064
  log(` Sealed gate: ${gateId}`);
2612
- verbose(` Sealed by: ${identity.name}`);
2065
+ verbose(` Sealed by: ${identitySlug} (${identity.name})`);
2613
2066
  verbose(` Timestamp: ${seal.timestamp}`);
2614
2067
  return { sealed: true, skipped: false };
2615
2068
  }
@@ -3151,9 +2604,9 @@ async function runCreate() {
3151
2604
  log("");
3152
2605
  log(theme3.blue.bold()("Public key saved to:"));
3153
2606
  log(` ${publicKeyResult.homePath}`);
3154
- if (publicKeyResult.projectPath) {
3155
- log(` ${publicKeyResult.projectPath}`);
3156
- }
2607
+ log("");
2608
+ log("To add yourself to a project, run:");
2609
+ log(theme3.blue(" attest-it team join"));
3157
2610
  log("");
3158
2611
  if (!existingConfig) {
3159
2612
  success(`Set as active identity`);
@@ -3274,155 +2727,6 @@ async function runShow(slug) {
3274
2727
  process.exit(ExitCode.CONFIG_ERROR);
3275
2728
  }
3276
2729
  }
3277
- var editCommand = new Command("edit").description("Edit identity or rotate keypair").argument("<slug>", "Identity slug to edit").action(async (slug) => {
3278
- await runEdit(slug);
3279
- });
3280
- async function runEdit(slug) {
3281
- try {
3282
- const config = await loadLocalConfig();
3283
- if (!config) {
3284
- error("No identities configured");
3285
- process.exit(ExitCode.CONFIG_ERROR);
3286
- }
3287
- const identity = config.identities[slug];
3288
- if (!identity) {
3289
- error(`Identity "${slug}" not found`);
3290
- process.exit(ExitCode.CONFIG_ERROR);
3291
- }
3292
- const theme3 = getTheme2();
3293
- log("");
3294
- log(theme3.blue.bold()(`Edit Identity: ${slug}`));
3295
- log("");
3296
- const name = await input({
3297
- message: "Display name:",
3298
- default: identity.name,
3299
- validate: (value) => {
3300
- if (!value || value.trim().length === 0) {
3301
- return "Name cannot be empty";
3302
- }
3303
- return true;
3304
- }
3305
- });
3306
- const email = await input({
3307
- message: "Email (optional):",
3308
- default: identity.email ?? ""
3309
- });
3310
- const github = await input({
3311
- message: "GitHub username (optional):",
3312
- default: identity.github ?? ""
3313
- });
3314
- const rotateKey = await confirm({
3315
- message: "Rotate keypair (generate new keys)?",
3316
- default: false
3317
- });
3318
- let publicKey = identity.publicKey;
3319
- const privateKeyRef = identity.privateKey;
3320
- if (rotateKey) {
3321
- log("");
3322
- log("Generating new Ed25519 keypair...");
3323
- const keyPair = generateEd25519KeyPair();
3324
- publicKey = keyPair.publicKey;
3325
- switch (identity.privateKey.type) {
3326
- case "file": {
3327
- await writeFile(identity.privateKey.path, keyPair.privateKey, { mode: 384 });
3328
- log(` Updated private key at: ${identity.privateKey.path}`);
3329
- break;
3330
- }
3331
- case "keychain": {
3332
- const { execFile } = await import('child_process');
3333
- const { promisify } = await import('util');
3334
- const execFileAsync = promisify(execFile);
3335
- const encodedKey = Buffer.from(keyPair.privateKey).toString("base64");
3336
- try {
3337
- await execFileAsync("security", [
3338
- "delete-generic-password",
3339
- "-s",
3340
- identity.privateKey.service,
3341
- "-a",
3342
- identity.privateKey.account
3343
- ]);
3344
- await execFileAsync("security", [
3345
- "add-generic-password",
3346
- "-s",
3347
- identity.privateKey.service,
3348
- "-a",
3349
- identity.privateKey.account,
3350
- "-w",
3351
- encodedKey,
3352
- "-U"
3353
- ]);
3354
- log(` Updated private key in macOS Keychain`);
3355
- } catch (err) {
3356
- throw new Error(
3357
- `Failed to update key in macOS Keychain: ${err instanceof Error ? err.message : String(err)}`
3358
- );
3359
- }
3360
- break;
3361
- }
3362
- case "1password": {
3363
- const { execFile } = await import('child_process');
3364
- const { promisify } = await import('util');
3365
- const execFileAsync = promisify(execFile);
3366
- try {
3367
- const opArgs = [
3368
- "item",
3369
- "edit",
3370
- identity.privateKey.item,
3371
- "--vault",
3372
- identity.privateKey.vault,
3373
- `privateKey[password]=${keyPair.privateKey}`
3374
- ];
3375
- if (identity.privateKey.account) {
3376
- opArgs.push("--account", identity.privateKey.account);
3377
- }
3378
- await execFileAsync("op", opArgs);
3379
- log(` Updated private key in 1Password`);
3380
- } catch (err) {
3381
- throw new Error(
3382
- `Failed to update key in 1Password: ${err instanceof Error ? err.message : String(err)}`
3383
- );
3384
- }
3385
- break;
3386
- }
3387
- }
3388
- }
3389
- const updatedIdentity = {
3390
- name,
3391
- publicKey,
3392
- privateKey: privateKeyRef,
3393
- ...email && { email },
3394
- ...github && { github }
3395
- };
3396
- const newConfig = {
3397
- ...config,
3398
- identities: {
3399
- ...config.identities,
3400
- [slug]: updatedIdentity
3401
- }
3402
- };
3403
- await saveLocalConfig(newConfig);
3404
- log("");
3405
- success("Identity updated successfully");
3406
- log("");
3407
- if (rotateKey) {
3408
- log(" New Public Key: " + publicKey.slice(0, 32) + "...");
3409
- log("");
3410
- log(
3411
- theme3.yellow(
3412
- " Warning: If this identity is used in team configurations,\n you must update those configurations with the new public key."
3413
- )
3414
- );
3415
- log("");
3416
- }
3417
- } catch (err) {
3418
- if (err instanceof Error) {
3419
- error(err.message);
3420
- } else {
3421
- error("Unknown error occurred");
3422
- }
3423
- process.exit(ExitCode.CONFIG_ERROR);
3424
- }
3425
- }
3426
2730
 
3427
2731
  // src/utils/format-key-location.ts
3428
2732
  function formatKeyLocation(privateKey) {
@@ -3641,7 +2945,7 @@ async function runExport(slug) {
3641
2945
  }
3642
2946
 
3643
2947
  // src/commands/identity/index.ts
3644
- var identityCommand = new Command("identity").description("Manage local identities and keypairs").addCommand(listCommand).addCommand(createCommand).addCommand(useCommand).addCommand(showCommand).addCommand(editCommand).addCommand(removeCommand).addCommand(exportCommand);
2948
+ var identityCommand = new Command("identity").description("Manage local identities and keypairs").addCommand(listCommand).addCommand(createCommand).addCommand(useCommand).addCommand(showCommand).addCommand(removeCommand).addCommand(exportCommand);
3645
2949
  var whoamiCommand = new Command("whoami").description("Show the current active identity").action(async () => {
3646
2950
  await runWhoami();
3647
2951
  });
@@ -3742,6 +3046,49 @@ async function runList2() {
3742
3046
  process.exit(ExitCode.CONFIG_ERROR);
3743
3047
  }
3744
3048
  }
3049
+ async function promptForGateAuthorization(gates) {
3050
+ if (!gates || Object.keys(gates).length === 0) {
3051
+ return [];
3052
+ }
3053
+ const gateChoices = Object.entries(gates).map(([gateId, gate]) => ({
3054
+ name: `${gateId} - ${gate.name}`,
3055
+ value: gateId
3056
+ }));
3057
+ const authorizedGates = await checkbox({
3058
+ message: "Select gates to authorize (use space to select):",
3059
+ choices: gateChoices
3060
+ });
3061
+ return authorizedGates;
3062
+ }
3063
+ function addTeamMemberToConfig(config, memberSlug, memberData, authorizedGates) {
3064
+ const existingTeam = config.team ?? {};
3065
+ const updatedConfig = {
3066
+ ...config,
3067
+ team: {
3068
+ ...existingTeam,
3069
+ [memberSlug]: {
3070
+ name: memberData.name,
3071
+ publicKey: memberData.publicKey,
3072
+ publicKeyAlgorithm: memberData.publicKeyAlgorithm ?? "ed25519",
3073
+ ...memberData.email ? { email: memberData.email } : {},
3074
+ ...memberData.github ? { github: memberData.github } : {}
3075
+ }
3076
+ }
3077
+ };
3078
+ if (authorizedGates.length > 0 && updatedConfig.gates) {
3079
+ for (const gateId of authorizedGates) {
3080
+ const gate = updatedConfig.gates[gateId];
3081
+ if (gate) {
3082
+ if (!gate.authorizedSigners.includes(memberSlug)) {
3083
+ gate.authorizedSigners.push(memberSlug);
3084
+ }
3085
+ }
3086
+ }
3087
+ }
3088
+ return updatedConfig;
3089
+ }
3090
+
3091
+ // src/commands/team/add.ts
3745
3092
  var addCommand = new Command("add").description("Add a new team member").action(async () => {
3746
3093
  await runAdd();
3747
3094
  });
@@ -3813,45 +3160,22 @@ async function runAdd() {
3813
3160
  message: "Public key:",
3814
3161
  validate: validatePublicKey
3815
3162
  });
3816
- let authorizedGates = [];
3817
- if (attestItConfig.gates && Object.keys(attestItConfig.gates).length > 0) {
3818
- log("");
3819
- const gateChoices = Object.entries(attestItConfig.gates).map(([gateId, gate]) => ({
3820
- name: `${gateId} - ${gate.name}`,
3821
- value: gateId
3822
- }));
3823
- authorizedGates = await checkbox({
3824
- message: "Select gates to authorize (use space to select):",
3825
- choices: gateChoices
3826
- });
3827
- }
3828
- const teamMember = {
3163
+ log("");
3164
+ const authorizedGates = await promptForGateAuthorization(attestItConfig.gates);
3165
+ const memberData = {
3829
3166
  name,
3830
- publicKey: publicKey.trim()
3167
+ publicKey: publicKey.trim(),
3168
+ publicKeyAlgorithm: "ed25519"
3831
3169
  };
3832
- if (email && email.trim().length > 0) {
3833
- teamMember.email = email.trim();
3834
- }
3835
- if (github && github.trim().length > 0) {
3836
- teamMember.github = github.trim();
3170
+ const trimmedEmail = email.trim();
3171
+ const trimmedGithub = github.trim();
3172
+ if (trimmedEmail && trimmedEmail.length > 0) {
3173
+ memberData.email = trimmedEmail;
3837
3174
  }
3838
- const updatedConfig = {
3839
- ...config,
3840
- team: {
3841
- ...existingTeam,
3842
- [slug]: teamMember
3843
- }
3844
- };
3845
- if (authorizedGates.length > 0 && updatedConfig.gates) {
3846
- for (const gateId of authorizedGates) {
3847
- const gate = updatedConfig.gates[gateId];
3848
- if (gate) {
3849
- if (!gate.authorizedSigners.includes(slug)) {
3850
- gate.authorizedSigners.push(slug);
3851
- }
3852
- }
3853
- }
3175
+ if (trimmedGithub && trimmedGithub.length > 0) {
3176
+ memberData.github = trimmedGithub;
3854
3177
  }
3178
+ const updatedConfig = addTeamMemberToConfig(config, slug, memberData, authorizedGates);
3855
3179
  const configPath = findConfigPath();
3856
3180
  if (!configPath) {
3857
3181
  error("Configuration file not found");
@@ -3874,127 +3198,73 @@ async function runAdd() {
3874
3198
  process.exit(ExitCode.CONFIG_ERROR);
3875
3199
  }
3876
3200
  }
3877
- var editCommand2 = new Command("edit").description("Edit a team member").argument("<slug>", "Team member slug to edit").action(async (slug) => {
3878
- await runEdit2(slug);
3201
+ var joinCommand = new Command("join").description("Add yourself to the project team using your active identity").action(async () => {
3202
+ await runJoin();
3879
3203
  });
3880
- function validatePublicKey2(value) {
3881
- if (!value || value.trim().length === 0) {
3882
- return "Public key cannot be empty";
3883
- }
3884
- const base64Regex = /^[A-Za-z0-9+/]+=*$/;
3885
- if (!base64Regex.test(value)) {
3886
- return "Public key must be valid Base64";
3887
- }
3888
- if (value.length !== 44) {
3889
- return "Public key must be 44 characters (32 bytes in Base64)";
3890
- }
3891
- try {
3892
- const decoded = Buffer.from(value, "base64");
3893
- if (decoded.length !== 32) {
3894
- return "Public key must decode to 32 bytes";
3895
- }
3896
- } catch {
3897
- return "Invalid Base64 encoding";
3898
- }
3899
- return true;
3900
- }
3901
- async function runEdit2(slug) {
3204
+ async function runJoin() {
3902
3205
  try {
3903
3206
  const theme3 = getTheme2();
3904
- const config = await loadConfig();
3905
- const attestItConfig = toAttestItConfig(config);
3906
- const existingMember = attestItConfig.team?.[slug];
3907
- if (!existingMember) {
3908
- error(`Team member "${slug}" not found`);
3909
- process.exit(ExitCode.CONFIG_ERROR);
3910
- }
3911
3207
  log("");
3912
- log(theme3.blue.bold()(`Edit Team Member: ${slug}`));
3208
+ log(theme3.blue.bold()("Join Project Team"));
3913
3209
  log("");
3914
- log(theme3.muted("Leave blank to keep current value"));
3210
+ const localConfig = await loadLocalConfig();
3211
+ if (!localConfig) {
3212
+ error('No identity found. Run "attest-it identity create" first.');
3213
+ process.exit(ExitCode.CONFIG_ERROR);
3214
+ }
3215
+ const activeIdentity = getActiveIdentity(localConfig);
3216
+ if (!activeIdentity) {
3217
+ error('No active identity. Run "attest-it identity use <slug>" to select one.');
3218
+ process.exit(ExitCode.CONFIG_ERROR);
3219
+ }
3220
+ const activeSlug = localConfig.activeIdentity;
3221
+ info(`Using identity: ${activeSlug}`);
3222
+ log(` Name: ${activeIdentity.name}`);
3223
+ log(` Public Key: ${activeIdentity.publicKey.slice(0, 32)}...`);
3915
3224
  log("");
3916
- const name = await input({
3917
- message: "Display name:",
3918
- default: existingMember.name,
3919
- validate: (value) => {
3920
- if (!value || value.trim().length === 0) {
3921
- return "Name cannot be empty";
3922
- }
3923
- return true;
3924
- }
3925
- });
3926
- const email = await input({
3927
- message: "Email (optional):",
3928
- default: existingMember.email ?? ""
3929
- });
3930
- const github = await input({
3931
- message: "GitHub username (optional):",
3932
- default: existingMember.github ?? ""
3933
- });
3934
- const updateKey = await confirm({
3935
- message: "Update public key?",
3936
- default: false
3937
- });
3938
- let publicKey = existingMember.publicKey;
3939
- if (updateKey) {
3940
- log("");
3941
- log("Paste the new public key:");
3942
- publicKey = await input({
3943
- message: "Public key:",
3944
- default: existingMember.publicKey,
3945
- validate: validatePublicKey2
3946
- });
3225
+ const config = await loadConfig();
3226
+ const attestItConfig = toAttestItConfig(config);
3227
+ const existingTeam = attestItConfig.team ?? {};
3228
+ const existingMemberWithKey = Object.entries(existingTeam).find(
3229
+ ([, member]) => member.publicKey === activeIdentity.publicKey
3230
+ );
3231
+ if (existingMemberWithKey) {
3232
+ error(`You're already a team member as "${existingMemberWithKey[0]}"`);
3233
+ process.exit(ExitCode.CONFIG_ERROR);
3947
3234
  }
3948
- const currentGates = [];
3949
- if (attestItConfig.gates) {
3950
- for (const [gateId, gate] of Object.entries(attestItConfig.gates)) {
3951
- if (gate.authorizedSigners.includes(slug)) {
3952
- currentGates.push(gateId);
3235
+ let slug = activeSlug;
3236
+ if (existingTeam[slug]) {
3237
+ log(`Slug "${slug}" is already taken by another team member.`);
3238
+ slug = await input({
3239
+ message: "Choose a different slug:",
3240
+ validate: (value) => {
3241
+ if (!value || value.trim().length === 0) {
3242
+ return "Slug cannot be empty";
3243
+ }
3244
+ if (!/^[a-z0-9-]+$/.test(value)) {
3245
+ return "Slug must contain only lowercase letters, numbers, and hyphens";
3246
+ }
3247
+ if (existingTeam[value]) {
3248
+ return `Slug "${value}" is already taken`;
3249
+ }
3250
+ return true;
3953
3251
  }
3954
- }
3955
- }
3956
- let selectedGates = currentGates;
3957
- if (attestItConfig.gates && Object.keys(attestItConfig.gates).length > 0) {
3958
- log("");
3959
- const gateChoices = Object.entries(attestItConfig.gates).map(([gateId, gate]) => ({
3960
- name: `${gateId} - ${gate.name}`,
3961
- value: gateId,
3962
- checked: currentGates.includes(gateId)
3963
- }));
3964
- selectedGates = await checkbox({
3965
- message: "Select gates to authorize (use space to select):",
3966
- choices: gateChoices
3967
3252
  });
3968
3253
  }
3969
- const updatedMember = {
3970
- name: name.trim(),
3971
- publicKey: publicKey.trim()
3254
+ log("");
3255
+ const authorizedGates = await promptForGateAuthorization(attestItConfig.gates);
3256
+ const memberData = {
3257
+ name: activeIdentity.name,
3258
+ publicKey: activeIdentity.publicKey,
3259
+ publicKeyAlgorithm: "ed25519"
3972
3260
  };
3973
- if (email && email.trim().length > 0) {
3974
- updatedMember.email = email.trim();
3261
+ if (activeIdentity.email) {
3262
+ memberData.email = activeIdentity.email;
3975
3263
  }
3976
- if (github && github.trim().length > 0) {
3977
- updatedMember.github = github.trim();
3978
- }
3979
- const updatedConfig = {
3980
- ...config,
3981
- team: {
3982
- ...attestItConfig.team,
3983
- [slug]: updatedMember
3984
- }
3985
- };
3986
- if (updatedConfig.gates) {
3987
- for (const [gateId, gate] of Object.entries(updatedConfig.gates)) {
3988
- if (currentGates.includes(gateId) && !selectedGates.includes(gateId)) {
3989
- gate.authorizedSigners = gate.authorizedSigners.filter((s) => s !== slug);
3990
- }
3991
- if (!currentGates.includes(gateId) && selectedGates.includes(gateId)) {
3992
- if (!gate.authorizedSigners.includes(slug)) {
3993
- gate.authorizedSigners.push(slug);
3994
- }
3995
- }
3996
- }
3264
+ if (activeIdentity.github) {
3265
+ memberData.github = activeIdentity.github;
3997
3266
  }
3267
+ const updatedConfig = addTeamMemberToConfig(config, slug, memberData, authorizedGates);
3998
3268
  const configPath = findConfigPath();
3999
3269
  if (!configPath) {
4000
3270
  error("Configuration file not found");
@@ -4003,11 +3273,9 @@ async function runEdit2(slug) {
4003
3273
  const yamlContent = stringify(updatedConfig);
4004
3274
  await writeFile(configPath, yamlContent, "utf8");
4005
3275
  log("");
4006
- success(`Team member "${slug}" updated successfully`);
4007
- if (selectedGates.length > 0) {
4008
- log(`Authorized for gates: ${selectedGates.join(", ")}`);
4009
- } else {
4010
- log("Not authorized for any gates");
3276
+ success(`Team member "${slug}" added successfully`);
3277
+ if (authorizedGates.length > 0) {
3278
+ log(`Authorized for gates: ${authorizedGates.join(", ")}`);
4011
3279
  }
4012
3280
  log("");
4013
3281
  } catch (err) {
@@ -4046,7 +3314,7 @@ async function runRemove2(slug, options) {
4046
3314
  const projectRoot = process.cwd();
4047
3315
  let sealsFile;
4048
3316
  try {
4049
- sealsFile = readSealsSync(projectRoot);
3317
+ sealsFile = readSealsSync(projectRoot, attestItConfig.settings.sealsPath);
4050
3318
  } catch {
4051
3319
  sealsFile = { version: 1, seals: {} };
4052
3320
  }
@@ -4122,7 +3390,7 @@ async function runRemove2(slug, options) {
4122
3390
  }
4123
3391
 
4124
3392
  // src/commands/team/index.ts
4125
- var teamCommand = new Command("team").description("Manage team members and authorizations").addCommand(listCommand2).addCommand(addCommand).addCommand(editCommand2).addCommand(removeCommand2);
3393
+ var teamCommand = new Command("team").description("Manage team members and authorizations").addCommand(listCommand2).addCommand(addCommand).addCommand(joinCommand).addCommand(removeCommand2);
4126
3394
  var PROGRAM_NAME2 = "attest-it";
4127
3395
  var PROGRAM_ALIAS2 = "attest";
4128
3396
  var PROGRAM_NAMES = [PROGRAM_NAME2, PROGRAM_ALIAS2];
@@ -4143,7 +3411,6 @@ async function getCompletions(env) {
4143
3411
  { name: "run", description: "Run test suites interactively" },
4144
3412
  { name: "verify", description: "Verify all seals are valid" },
4145
3413
  { name: "seal", description: "Create a seal for a gate" },
4146
- { name: "keygen", description: "Generate a new keypair" },
4147
3414
  { name: "prune", description: "Remove stale attestations" },
4148
3415
  { name: "identity", description: "Manage identities" },
4149
3416
  { name: "team", description: "Manage team members" },
@@ -4248,7 +3515,6 @@ async function getCompletions(env) {
4248
3515
  "run",
4249
3516
  "verify",
4250
3517
  "seal",
4251
- "keygen",
4252
3518
  "prune",
4253
3519
  "identity",
4254
3520
  "team",
@@ -4383,43 +3649,12 @@ function createCompletionServerCommand() {
4383
3649
  }
4384
3650
  });
4385
3651
  }
4386
- function hasVersion(data) {
4387
- return typeof data === "object" && data !== null && "version" in data && // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
4388
- typeof data.version === "string";
4389
- }
4390
- var cachedVersion;
4391
- function getPackageVersion() {
4392
- if (cachedVersion !== void 0) {
4393
- return cachedVersion;
4394
- }
4395
- const __filename = fileURLToPath(import.meta.url);
4396
- const __dirname = dirname(__filename);
4397
- const possiblePaths = [join(__dirname, "../package.json"), join(__dirname, "../../package.json")];
4398
- for (const packageJsonPath of possiblePaths) {
4399
- try {
4400
- const content = readFileSync(packageJsonPath, "utf-8");
4401
- const packageJsonData = JSON.parse(content);
4402
- if (!hasVersion(packageJsonData)) {
4403
- throw new Error(`Invalid package.json at ${packageJsonPath}: missing version field`);
4404
- }
4405
- cachedVersion = packageJsonData.version;
4406
- return cachedVersion;
4407
- } catch (error2) {
4408
- if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") {
4409
- continue;
4410
- }
4411
- throw error2;
4412
- }
4413
- }
4414
- throw new Error("Could not find package.json");
4415
- }
4416
3652
  var program = new Command();
4417
3653
  program.name("attest-it").description("Human-gated test attestation system").option("-c, --config <path>", "Path to config file").option("-v, --verbose", "Verbose output").option("-q, --quiet", "Minimal output");
4418
3654
  program.option("-V, --version", "output the version number");
4419
3655
  program.addCommand(initCommand);
4420
3656
  program.addCommand(statusCommand);
4421
3657
  program.addCommand(runCommand);
4422
- program.addCommand(keygenCommand);
4423
3658
  program.addCommand(pruneCommand);
4424
3659
  program.addCommand(verifyCommand);
4425
3660
  program.addCommand(sealCommand);