@dotenc/cli 0.4.5 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +377 -42
  2. package/package.json +2 -1
package/dist/cli.js CHANGED
@@ -7,7 +7,7 @@ var package_default;
7
7
  var init_package = __esm(() => {
8
8
  package_default = {
9
9
  name: "@dotenc/cli",
10
- version: "0.4.5",
10
+ version: "0.5.0",
11
11
  description: "🔐 Git-native encrypted environments powered by your SSH keys",
12
12
  author: "Ivan Filho <i@ivanfilho.com>",
13
13
  license: "MIT",
@@ -21,6 +21,7 @@ var init_package = __esm(() => {
21
21
  scripts: {
22
22
  dev: "bun src/cli.ts",
23
23
  start: "node dist/cli.js",
24
+ typecheck: "tsc --noEmit -p tsconfig.json",
24
25
  build: "bun build src/cli.ts --outdir dist --target node --packages external && { echo '#!/usr/bin/env node'; cat dist/cli.js; } > dist/cli.tmp && mv dist/cli.tmp dist/cli.js",
25
26
  "build:binary": "bun run build:binary:darwin-arm64 && bun run build:binary:darwin-x64 && bun run build:binary:linux-x64 && bun run build:binary:linux-arm64 && bun run build:binary:windows-x64",
26
27
  "build:binary:darwin-arm64": "bun build src/cli.ts --compile --target=bun-darwin-arm64 --outfile dist/dotenc-darwin-arm64",
@@ -1052,8 +1053,8 @@ var getCurrentKeyName = async (deps = { getPrivateKeys, getPublicKeys }) => {
1052
1053
  const { keys: privateKeys } = await deps.getPrivateKeys();
1053
1054
  const publicKeys = await deps.getPublicKeys();
1054
1055
  const privateFingerprints = new Set(privateKeys.map((k) => k.fingerprint));
1055
- const match = publicKeys.find((pub) => privateFingerprints.has(pub.fingerprint));
1056
- return match?.name;
1056
+ const matches = publicKeys.filter((pub) => privateFingerprints.has(pub.fingerprint));
1057
+ return matches.map((m) => m.name);
1057
1058
  };
1058
1059
  var init_getCurrentKeyName = __esm(() => {
1059
1060
  init_getPrivateKeys();
@@ -1150,12 +1151,29 @@ var init_run = __esm(() => {
1150
1151
 
1151
1152
  // src/commands/dev.ts
1152
1153
  import chalk8 from "chalk";
1153
- var defaultDevCommandDeps, devCommand = async (command, args, deps = defaultDevCommandDeps) => {
1154
- const keyName = await deps.getCurrentKeyName();
1155
- if (!keyName) {
1154
+ import inquirer3 from "inquirer";
1155
+ var defaultSelect = async (message, choices) => {
1156
+ const { selected } = await inquirer3.prompt([
1157
+ {
1158
+ type: "list",
1159
+ name: "selected",
1160
+ message,
1161
+ choices
1162
+ }
1163
+ ]);
1164
+ return selected;
1165
+ }, defaultDevCommandDeps, devCommand = async (command, args, deps = defaultDevCommandDeps) => {
1166
+ const keyNames = await deps.getCurrentKeyName();
1167
+ if (keyNames.length === 0) {
1156
1168
  deps.logError(`${chalk8.red("Error:")} could not resolve your identity. Run ${chalk8.gray("dotenc init")} first.`);
1157
1169
  deps.exit(1);
1158
1170
  }
1171
+ let keyName;
1172
+ if (keyNames.length === 1) {
1173
+ keyName = keyNames[0];
1174
+ } else {
1175
+ keyName = await deps.select("Multiple identities found. Which one do you want to use?", keyNames.map((name) => ({ name, value: name })));
1176
+ }
1159
1177
  await deps.runCommand(command, args, { env: `development,${keyName}` });
1160
1178
  };
1161
1179
  var init_dev = __esm(() => {
@@ -1165,7 +1183,8 @@ var init_dev = __esm(() => {
1165
1183
  getCurrentKeyName,
1166
1184
  runCommand,
1167
1185
  logError: console.error,
1168
- exit: process.exit
1186
+ exit: process.exit,
1187
+ select: defaultSelect
1169
1188
  };
1170
1189
  });
1171
1190
 
@@ -1207,9 +1226,9 @@ var init_projectConfig = __esm(() => {
1207
1226
  });
1208
1227
 
1209
1228
  // src/prompts/createEnvironment.ts
1210
- import inquirer3 from "inquirer";
1229
+ import inquirer4 from "inquirer";
1211
1230
  var createEnvironmentPrompt = async (message, defaultValue) => {
1212
- const result = await inquirer3.prompt([
1231
+ const result = await inquirer4.prompt([
1213
1232
  {
1214
1233
  type: "input",
1215
1234
  name: "environment",
@@ -1704,9 +1723,9 @@ var setupGitDiff = () => {
1704
1723
  var init_setupGitDiff = () => {};
1705
1724
 
1706
1725
  // src/prompts/inputName.ts
1707
- import inquirer4 from "inquirer";
1726
+ import inquirer5 from "inquirer";
1708
1727
  var inputNamePrompt = async (message, defaultValue) => {
1709
- const result = await inquirer4.prompt([
1728
+ const result = await inquirer5.prompt([
1710
1729
  {
1711
1730
  type: "input",
1712
1731
  name: "name",
@@ -1791,9 +1810,9 @@ function validatePublicKey(key) {
1791
1810
  }
1792
1811
 
1793
1812
  // src/prompts/inputKey.ts
1794
- import inquirer5 from "inquirer";
1813
+ import inquirer6 from "inquirer";
1795
1814
  var inputKeyPrompt = async (message, defaultValue) => {
1796
- const result = await inquirer5.prompt([
1815
+ const result = await inquirer6.prompt([
1797
1816
  {
1798
1817
  type: "password",
1799
1818
  name: "key",
@@ -1813,7 +1832,7 @@ import fs12 from "node:fs/promises";
1813
1832
  import os4 from "node:os";
1814
1833
  import path12 from "node:path";
1815
1834
  import chalk12 from "chalk";
1816
- import inquirer6 from "inquirer";
1835
+ import inquirer7 from "inquirer";
1817
1836
  var keyAddCommand = async (nameArg, options) => {
1818
1837
  const { projectId } = await getProjectConfig();
1819
1838
  if (!projectId) {
@@ -1911,7 +1930,7 @@ ${passphraseProtectedKeys.map((k) => ` - ${k}`).join(`
1911
1930
  name: `${key.name} (${key.algorithm})`,
1912
1931
  value: key.name
1913
1932
  }));
1914
- const modePrompt = await inquirer6.prompt({
1933
+ const modePrompt = await inquirer7.prompt({
1915
1934
  type: "list",
1916
1935
  name: "mode",
1917
1936
  message: "Would you like to add one of your SSH keys or paste a public key?",
@@ -1934,7 +1953,7 @@ ${passphraseProtectedKeys.map((k) => ` - ${k}`).join(`
1934
1953
  process.exit(1);
1935
1954
  }
1936
1955
  } else {
1937
- const keyPrompt = await inquirer6.prompt({
1956
+ const keyPrompt = await inquirer7.prompt({
1938
1957
  type: "list",
1939
1958
  name: "key",
1940
1959
  message: "Which SSH key do you want to add?",
@@ -1994,7 +2013,7 @@ import fs13 from "node:fs/promises";
1994
2013
  import os5 from "node:os";
1995
2014
  import path13 from "node:path";
1996
2015
  import chalk13 from "chalk";
1997
- import inquirer7 from "inquirer";
2016
+ import inquirer8 from "inquirer";
1998
2017
  var initCommand = async (options) => {
1999
2018
  const { keys: privateKeys, passphraseProtectedKeys } = await getPrivateKeys();
2000
2019
  if (!privateKeys.length) {
@@ -2025,7 +2044,7 @@ var initCommand = async (options) => {
2025
2044
  if (privateKeys.length === 1) {
2026
2045
  keyToAdd = privateKeys[0].name;
2027
2046
  } else {
2028
- const result = await inquirer7.prompt([
2047
+ const result = await inquirer8.prompt([
2029
2048
  {
2030
2049
  type: "list",
2031
2050
  name: "key",
@@ -2072,6 +2091,12 @@ Some useful tips:`);
2072
2091
  console.log(`- To edit your personal environment: ${editCmd}`);
2073
2092
  const devCmd = chalk13.gray("dotenc dev <command>");
2074
2093
  console.log(`- To run with your encrypted env: ${devCmd}`);
2094
+ if (existsSync8(".claude") || existsSync8("CLAUDE.md")) {
2095
+ console.log(`- Install the Claude Code skill: ${chalk13.gray("dotenc tools install-claude-code-skill")}`);
2096
+ }
2097
+ if (existsSync8(".vscode") || existsSync8(".cursor") || existsSync8(".windsurf")) {
2098
+ console.log(`- Add the editor extension: ${chalk13.gray("dotenc tools install-vscode-extension")}`);
2099
+ }
2075
2100
  };
2076
2101
  var init_init = __esm(() => {
2077
2102
  init_createProject();
@@ -2099,9 +2124,9 @@ var init_list3 = __esm(() => {
2099
2124
  });
2100
2125
 
2101
2126
  // src/prompts/confirm.ts
2102
- import inquirer8 from "inquirer";
2127
+ import inquirer9 from "inquirer";
2103
2128
  var confirmPrompt = async (message) => {
2104
- const result = await inquirer8.prompt([
2129
+ const result = await inquirer9.prompt([
2105
2130
  {
2106
2131
  type: "confirm",
2107
2132
  name: "confirm",
@@ -2193,13 +2218,312 @@ var init_textconv = __esm(() => {
2193
2218
  init_getEnvironmentByPath();
2194
2219
  });
2195
2220
 
2221
+ // src/commands/tools/install-claude-code-skill.ts
2222
+ import { existsSync as existsSync10 } from "node:fs";
2223
+ import fs16 from "node:fs/promises";
2224
+ import os6 from "node:os";
2225
+ import path16 from "node:path";
2226
+ import chalk15 from "chalk";
2227
+ import inquirer10 from "inquirer";
2228
+ var SKILL_MD_CONTENT = `---
2229
+ name: dotenc
2230
+ description: Manage dotenc encrypted environments. Use when the user asks about environment variables, secrets, dotenc setup, encrypted environments, adding teammates, rotating keys, or running commands with secrets injected.
2231
+ allowed-tools: Bash, Read, Glob, Grep
2232
+ argument-hint: [command]
2233
+ ---
2234
+
2235
+ ## Project context
2236
+
2237
+ > If any command below shows an error, dotenc is likely not initialized — suggest running \`dotenc init\` first.
2238
+
2239
+ **Current identity:**
2240
+ !\`dotenc whoami 2>&1 || true\`
2241
+
2242
+ **Available environments:**
2243
+ !\`dotenc env list 2>&1 || true\`
2244
+
2245
+ **Project public keys:**
2246
+ !\`dotenc key list 2>&1 || true\`
2247
+
2248
+ ## What is dotenc?
2249
+
2250
+ dotenc is a Git-native encrypted environment management tool powered by SSH keys. It encrypts environment variables using AES-256-GCM and manages per-user access control using existing SSH key infrastructure. Private keys never leave \`~/.ssh/\`; only public keys are stored in the \`.dotenc/\` project folder. Encrypted files are safe to commit to Git.
2251
+
2252
+ ## Common workflows
2253
+
2254
+ **First-time setup**
2255
+ \`\`\`bash
2256
+ dotenc init --name alice
2257
+ dotenc key add alice --from-ssh ~/.ssh/id_ed25519
2258
+ dotenc env create development alice
2259
+ dotenc env edit development # opens editor to add secrets
2260
+ \`\`\`
2261
+
2262
+ **Onboard a new teammate**
2263
+ \`\`\`bash
2264
+ dotenc key add bob --from-ssh /path/to/bob_key.pub
2265
+ dotenc auth grant development bob
2266
+ dotenc auth grant production bob # only if they need it
2267
+ \`\`\`
2268
+
2269
+ **Run the app locally with secrets**
2270
+ \`\`\`bash
2271
+ dotenc dev npm start # injects development + personal env
2272
+ dotenc run -e production node app.js
2273
+ \`\`\`
2274
+
2275
+ **Rotate keys before someone leaves**
2276
+ \`\`\`bash
2277
+ dotenc auth revoke production alice
2278
+ dotenc auth revoke development alice
2279
+ dotenc key remove alice
2280
+ dotenc env rotate production # re-encrypts with remaining keys
2281
+ \`\`\`
2282
+
2283
+ **Add a CI/CD key**
2284
+ \`\`\`bash
2285
+ dotenc key add ci --from-file ci_key.pub
2286
+ dotenc auth grant production ci
2287
+ \`\`\`
2288
+
2289
+ ## CLI command reference
2290
+
2291
+ ### Initialization & identity
2292
+
2293
+ | Command | Description |
2294
+ |---------|-------------|
2295
+ | \`dotenc init [--name <name>]\` | Initialize a dotenc project in the current directory |
2296
+ | \`dotenc whoami\` | Show your identity, active SSH key, fingerprint, and environment access |
2297
+ | \`dotenc config <key> [value] [--remove]\` | Get, set, or remove a global configuration key |
2298
+
2299
+ ### Environment management
2300
+
2301
+ | Command | Description |
2302
+ |---------|-------------|
2303
+ | \`dotenc env list\` | List all encrypted environments |
2304
+ | \`dotenc env create [environment] [publicKey]\` | Create a new encrypted environment |
2305
+ | \`dotenc env edit [environment]\` | Edit an environment in your configured editor |
2306
+ | \`dotenc env rotate [environment]\` | Rotate the data key for an environment |
2307
+ | \`dotenc env decrypt <environment> [--json]\` | Decrypt an environment to stdout |
2308
+ | \`dotenc env encrypt <environment> [--stdin] [--json]\` | Encrypt plaintext into an environment file |
2309
+
2310
+ ### Access control
2311
+
2312
+ | Command | Description |
2313
+ |---------|-------------|
2314
+ | \`dotenc auth list [environment]\` | List public keys with access to an environment |
2315
+ | \`dotenc auth grant [environment] [publicKey]\` | Grant a key access to an environment |
2316
+ | \`dotenc auth revoke [environment] [publicKey]\` | Revoke a key's access from an environment |
2317
+
2318
+ ### Key management
2319
+
2320
+ | Command | Description |
2321
+ |---------|-------------|
2322
+ | \`dotenc key list\` | List all public keys in the project |
2323
+ | \`dotenc key add [name] [--from-ssh <path>] [-f <file>] [-s <string>]\` | Add a public key to the project |
2324
+ | \`dotenc key remove [name]\` | Remove a public key and revoke from all environments |
2325
+
2326
+ ### Running commands with decrypted variables
2327
+
2328
+ | Command | Description |
2329
+ |---------|-------------|
2330
+ | \`dotenc run -e <env1>[,env2] <command> [args...]\` | Run a command with decrypted environment variables |
2331
+ | \`dotenc dev <command> [args...]\` | Shortcut: run with \`development\` + your personal environment |
2332
+
2333
+ ### Git integration
2334
+
2335
+ | Command | Description |
2336
+ |---------|-------------|
2337
+ | \`dotenc textconv <filepath>\` | Decrypt an environment file for \`git diff\` |
2338
+
2339
+ ## Guidelines
2340
+
2341
+ - **Never expose decrypted secrets in chat output.** Do not run \`dotenc env decrypt\` and display the result. If the user needs to inspect values, suggest \`dotenc env edit\` which opens their editor.
2342
+ - **Prefer \`dotenc dev\` / \`dotenc run\`** over manually decrypting and exporting variables.
2343
+ - **Warn before destructive operations.** Confirm with the user before running \`dotenc key remove\`, \`dotenc auth revoke\`, or \`dotenc env rotate\`, as these can lock users out of environments.
2344
+ - **Always pass arguments explicitly.** All commands support interactive mode when arguments are omitted, but Claude Code is non-interactive — always provide the full command with arguments.
2345
+ - **Encrypted files are Git-safe.** Files like \`.env.production.enc\` are meant to be committed; never instruct the user to add them to \`.gitignore\`.
2346
+ `, installClaudeCodeSkillCommand = async (options) => {
2347
+ const { scope } = await inquirer10.prompt([
2348
+ {
2349
+ type: "list",
2350
+ name: "scope",
2351
+ message: "Install locally or globally?",
2352
+ choices: [
2353
+ { name: "Locally (this project)", value: "local" },
2354
+ { name: "Globally (all projects)", value: "global" }
2355
+ ]
2356
+ }
2357
+ ]);
2358
+ const baseDir = scope === "global" ? path16.join(os6.homedir(), ".claude") : path16.join(process.cwd(), ".claude");
2359
+ const skillDir = path16.join(baseDir, "skills", "dotenc");
2360
+ const skillPath = path16.join(skillDir, "SKILL.md");
2361
+ if (existsSync10(skillPath) && !options.force) {
2362
+ console.error(`${chalk15.red("Error:")} Claude Code skill already exists at ${chalk15.gray(skillPath)}.`);
2363
+ console.error(`Run with ${chalk15.gray("--force")} to overwrite.`);
2364
+ process.exit(1);
2365
+ }
2366
+ await fs16.mkdir(skillDir, { recursive: true });
2367
+ await fs16.writeFile(skillPath, SKILL_MD_CONTENT, "utf-8");
2368
+ console.log(`${chalk15.green("✓")} Claude Code skill installed at ${chalk15.gray(skillPath)}`);
2369
+ console.log(`Run ${chalk15.gray("/dotenc")} in Claude Code to use it.`);
2370
+ };
2371
+ var init_install_claude_code_skill = () => {};
2372
+
2373
+ // src/commands/tools/install-vscode-extension.ts
2374
+ import { exec, execFile } from "node:child_process";
2375
+ import { existsSync as existsSync11 } from "node:fs";
2376
+ import fs17 from "node:fs/promises";
2377
+ import path17 from "node:path";
2378
+ import { promisify } from "node:util";
2379
+ import chalk16 from "chalk";
2380
+ import inquirer11 from "inquirer";
2381
+ async function addToExtensionsJson() {
2382
+ const extensionsJsonPath = path17.join(process.cwd(), ".vscode", "extensions.json");
2383
+ let json = {};
2384
+ if (existsSync11(extensionsJsonPath)) {
2385
+ const content = await fs17.readFile(extensionsJsonPath, "utf-8");
2386
+ try {
2387
+ json = JSON.parse(content);
2388
+ } catch {
2389
+ json = {};
2390
+ }
2391
+ } else {
2392
+ await fs17.mkdir(path17.join(process.cwd(), ".vscode"), { recursive: true });
2393
+ }
2394
+ if (!Array.isArray(json.recommendations)) {
2395
+ json.recommendations = [];
2396
+ }
2397
+ if (!json.recommendations.includes(EXTENSION_ID)) {
2398
+ json.recommendations.push(EXTENSION_ID);
2399
+ await fs17.writeFile(extensionsJsonPath, JSON.stringify(json, null, 2), "utf-8");
2400
+ console.log(`${chalk16.green("✓")} Added dotenc to ${chalk16.gray(".vscode/extensions.json")}`);
2401
+ } else {
2402
+ console.log(`${chalk16.green("✓")} dotenc already in ${chalk16.gray(".vscode/extensions.json")}`);
2403
+ }
2404
+ }
2405
+ async function which(bin) {
2406
+ try {
2407
+ await execFileAsync("which", [bin]);
2408
+ return true;
2409
+ } catch {
2410
+ return false;
2411
+ }
2412
+ }
2413
+ async function detectEditors() {
2414
+ const detected = [];
2415
+ if (existsSync11(path17.join(process.cwd(), ".cursor")))
2416
+ detected.push("cursor");
2417
+ if (existsSync11(path17.join(process.cwd(), ".windsurf")))
2418
+ detected.push("windsurf");
2419
+ if (existsSync11(path17.join(process.cwd(), ".vscode")))
2420
+ detected.push("vscode");
2421
+ const checks = [
2422
+ { key: "cursor", bins: ["cursor"] },
2423
+ { key: "windsurf", bins: ["windsurf"] },
2424
+ { key: "vscode", bins: ["code"] },
2425
+ { key: "vscodium", bins: ["codium", "vscodium"] }
2426
+ ];
2427
+ if (process.platform === "darwin") {
2428
+ const macApps = {
2429
+ cursor: "/Applications/Cursor.app",
2430
+ windsurf: "/Applications/Windsurf.app",
2431
+ vscode: "/Applications/Visual Studio Code.app",
2432
+ vscodium: "/Applications/VSCodium.app"
2433
+ };
2434
+ for (const [key, appPath] of Object.entries(macApps)) {
2435
+ if (!detected.includes(key) && existsSync11(appPath)) {
2436
+ detected.push(key);
2437
+ }
2438
+ }
2439
+ }
2440
+ for (const { key, bins } of checks) {
2441
+ if (detected.includes(key))
2442
+ continue;
2443
+ for (const bin of bins) {
2444
+ if (await which(bin)) {
2445
+ detected.push(key);
2446
+ break;
2447
+ }
2448
+ }
2449
+ }
2450
+ return detected;
2451
+ }
2452
+ async function openUrl(url) {
2453
+ if (process.platform === "darwin") {
2454
+ await execFileAsync("open", [url]);
2455
+ } else if (process.platform === "win32") {
2456
+ await execAsync(`start ${url}`);
2457
+ } else {
2458
+ await execFileAsync("xdg-open", [url]);
2459
+ }
2460
+ }
2461
+ async function _runInstallVscodeExtension(getEditors = detectEditors, _openUrl = openUrl) {
2462
+ const editors = await getEditors();
2463
+ await addToExtensionsJson();
2464
+ if (editors.length === 0) {
2465
+ console.log(`
2466
+ Install the extension in VS Code: ${chalk16.cyan(EDITOR_PROTOCOL_URLS.vscode)}`);
2467
+ return;
2468
+ }
2469
+ if (editors.length === 1) {
2470
+ const editor = editors[0];
2471
+ const url = EDITOR_PROTOCOL_URLS[editor];
2472
+ const name = EDITOR_NAMES[editor];
2473
+ const { open } = await inquirer11.prompt([
2474
+ {
2475
+ type: "confirm",
2476
+ name: "open",
2477
+ message: `Open extension page in ${name} now?`,
2478
+ default: true
2479
+ }
2480
+ ]);
2481
+ if (open) {
2482
+ try {
2483
+ await _openUrl(url);
2484
+ } catch {
2485
+ console.log(`Open manually: ${chalk16.cyan(url)}`);
2486
+ }
2487
+ } else {
2488
+ console.log(`Install manually: ${chalk16.cyan(url)}`);
2489
+ }
2490
+ return;
2491
+ }
2492
+ console.log(`
2493
+ Install the extension in your editor:`);
2494
+ for (const editor of editors) {
2495
+ const name = EDITOR_NAMES[editor] ?? editor;
2496
+ const url = EDITOR_PROTOCOL_URLS[editor];
2497
+ console.log(` ${name}: ${chalk16.cyan(url)}`);
2498
+ }
2499
+ }
2500
+ var execFileAsync, execAsync, EXTENSION_ID = "dotenc.dotenc", EDITOR_PROTOCOL_URLS, EDITOR_NAMES, installVscodeExtensionCommand = async () => {
2501
+ await _runInstallVscodeExtension();
2502
+ };
2503
+ var init_install_vscode_extension = __esm(() => {
2504
+ execFileAsync = promisify(execFile);
2505
+ execAsync = promisify(exec);
2506
+ EDITOR_PROTOCOL_URLS = {
2507
+ vscode: `vscode:extension/${EXTENSION_ID}`,
2508
+ cursor: `cursor:extension/${EXTENSION_ID}`,
2509
+ windsurf: `windsurf:extension/${EXTENSION_ID}`,
2510
+ vscodium: `vscodium:extension/${EXTENSION_ID}`
2511
+ };
2512
+ EDITOR_NAMES = {
2513
+ vscode: "VS Code",
2514
+ cursor: "Cursor",
2515
+ windsurf: "Windsurf",
2516
+ vscodium: "VSCodium"
2517
+ };
2518
+ });
2519
+
2196
2520
  // src/commands/whoami.ts
2197
2521
  var whoamiCommand = async () => {
2198
2522
  const { keys: privateKeys, passphraseProtectedKeys } = await getPrivateKeys();
2199
2523
  const publicKeys = await getPublicKeys();
2200
2524
  const privateFingerprints = new Set(privateKeys.map((k) => k.fingerprint));
2201
- const matchingPublicKey = publicKeys.find((pub) => privateFingerprints.has(pub.fingerprint));
2202
- if (!matchingPublicKey) {
2525
+ const matchingPublicKeys = publicKeys.filter((pub) => privateFingerprints.has(pub.fingerprint));
2526
+ if (matchingPublicKeys.length === 0) {
2203
2527
  if (privateKeys.length === 0 && passphraseProtectedKeys.length > 0) {
2204
2528
  console.error(passphraseProtectedKeyError(passphraseProtectedKeys));
2205
2529
  } else {
@@ -2207,28 +2531,34 @@ var whoamiCommand = async () => {
2207
2531
  }
2208
2532
  process.exit(1);
2209
2533
  }
2210
- const matchingPrivateKey = privateKeys.find((pk) => pk.fingerprint === matchingPublicKey.fingerprint);
2211
- console.log(`Name: ${matchingPublicKey.name}`);
2212
- console.log(`Active SSH key: ${matchingPrivateKey?.name ?? "unknown"}`);
2213
- console.log(`Fingerprint: ${matchingPublicKey.fingerprint}`);
2214
2534
  const environments = await getEnvironments();
2215
- const authorizedEnvironments = [];
2216
- for (const envName of environments) {
2217
- try {
2218
- const environment = await getEnvironmentByName(envName);
2219
- const hasAccess = environment.keys.some((key) => key.fingerprint === matchingPublicKey.fingerprint);
2220
- if (hasAccess) {
2221
- authorizedEnvironments.push(envName);
2535
+ for (let i = 0;i < matchingPublicKeys.length; i++) {
2536
+ const matchingPublicKey = matchingPublicKeys[i];
2537
+ if (i > 0) {
2538
+ console.log("");
2539
+ }
2540
+ const matchingPrivateKey = privateKeys.find((pk) => pk.fingerprint === matchingPublicKey.fingerprint);
2541
+ console.log(`Name: ${matchingPublicKey.name}`);
2542
+ console.log(`Active SSH key: ${matchingPrivateKey?.name ?? "unknown"}`);
2543
+ console.log(`Fingerprint: ${matchingPublicKey.fingerprint}`);
2544
+ const authorizedEnvironments = [];
2545
+ for (const envName of environments) {
2546
+ try {
2547
+ const environment = await getEnvironmentByName(envName);
2548
+ const hasAccess = environment.keys.some((key) => key.fingerprint === matchingPublicKey.fingerprint);
2549
+ if (hasAccess) {
2550
+ authorizedEnvironments.push(envName);
2551
+ }
2552
+ } catch {}
2553
+ }
2554
+ if (authorizedEnvironments.length > 0) {
2555
+ console.log("Authorized environments:");
2556
+ for (const env of authorizedEnvironments) {
2557
+ console.log(` - ${env}`);
2222
2558
  }
2223
- } catch {}
2224
- }
2225
- if (authorizedEnvironments.length > 0) {
2226
- console.log("Authorized environments:");
2227
- for (const env of authorizedEnvironments) {
2228
- console.log(` - ${env}`);
2559
+ } else {
2560
+ console.log("Authorized environments: none");
2229
2561
  }
2230
- } else {
2231
- console.log("Authorized environments: none");
2232
2562
  }
2233
2563
  };
2234
2564
  var init_whoami = __esm(() => {
@@ -2242,7 +2572,7 @@ var init_whoami = __esm(() => {
2242
2572
  // src/program.ts
2243
2573
  var exports_program = {};
2244
2574
  import { Command, Option } from "commander";
2245
- var program, env, auth, key;
2575
+ var program, env, auth, key, tools;
2246
2576
  var init_program = __esm(() => {
2247
2577
  init_package();
2248
2578
  init_grant();
@@ -2262,6 +2592,8 @@ var init_program = __esm(() => {
2262
2592
  init_remove();
2263
2593
  init_run();
2264
2594
  init_textconv();
2595
+ init_install_claude_code_skill();
2596
+ init_install_vscode_extension();
2265
2597
  init_whoami();
2266
2598
  program = new Command;
2267
2599
  program.name("dotenc").description(package_default.description).version(package_default.version);
@@ -2283,6 +2615,9 @@ var init_program = __esm(() => {
2283
2615
  key.command("add").argument("[name]", "the name of the public key in the project").addOption(new Option("--from-ssh <path>", "add a public key derived from an SSH key file")).addOption(new Option("-f, --from-file <file>", "add the key from a PEM file")).addOption(new Option("-s, --from-string <string>", "add a public key from a string")).description("add a public key to the project").action(keyAddCommand);
2284
2616
  key.command("list").description("list all public keys in the project").action(keyListCommand);
2285
2617
  key.command("remove").argument("[name]", "the name of the public key to remove").description("remove a public key from the project").action(keyRemoveCommand);
2618
+ tools = program.command("tools").description("install editor integrations");
2619
+ tools.command("install-claude-code-skill").addOption(new Option("--force", "overwrite existing installation")).description("install the Claude Code skill for this project").action(installClaudeCodeSkillCommand);
2620
+ tools.command("install-vscode-extension").description("add dotenc to VS Code / Cursor / Windsurf extension recommendations").action(installVscodeExtensionCommand);
2286
2621
  program.command("textconv", { hidden: true }).argument("<filepath>", "path to the encrypted environment file").description("decrypt an environment file for git diff").action(textconvCommand);
2287
2622
  program.command("whoami").description("show your identity in this project").action(whoamiCommand);
2288
2623
  program.command("config").argument("<key>", "the key to get or set").argument("[value]", "the value to set the key to").addOption(new Option("-r, --remove", "remove a configuration key")).description("manage global configuration").action(configCommand);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotenc/cli",
3
- "version": "0.4.5",
3
+ "version": "0.5.0",
4
4
  "description": "🔐 Git-native encrypted environments powered by your SSH keys",
5
5
  "author": "Ivan Filho <i@ivanfilho.com>",
6
6
  "license": "MIT",
@@ -14,6 +14,7 @@
14
14
  "scripts": {
15
15
  "dev": "bun src/cli.ts",
16
16
  "start": "node dist/cli.js",
17
+ "typecheck": "tsc --noEmit -p tsconfig.json",
17
18
  "build": "bun build src/cli.ts --outdir dist --target node --packages external && { echo '#!/usr/bin/env node'; cat dist/cli.js; } > dist/cli.tmp && mv dist/cli.tmp dist/cli.js",
18
19
  "build:binary": "bun run build:binary:darwin-arm64 && bun run build:binary:darwin-x64 && bun run build:binary:linux-x64 && bun run build:binary:linux-arm64 && bun run build:binary:windows-x64",
19
20
  "build:binary:darwin-arm64": "bun build src/cli.ts --compile --target=bun-darwin-arm64 --outfile dist/dotenc-darwin-arm64",