@dotenc/cli 0.4.6 → 0.5.1

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 +539 -46
  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.6",
10
+ version: "0.5.1",
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",
@@ -1010,9 +1011,10 @@ import fs7 from "node:fs/promises";
1010
1011
  import os2 from "node:os";
1011
1012
  import path6 from "node:path";
1012
1013
  import { z as z2 } from "zod";
1013
- var homeConfigSchema, getConfigPath = () => path6.join(os2.homedir(), ".dotenc", "config.json"), setHomeConfig = async (config) => {
1014
+ var updateConfigSchema, homeConfigSchema, getConfigPath = () => path6.join(os2.homedir(), ".dotenc", "config.json"), setHomeConfig = async (config) => {
1014
1015
  const parsedConfig = homeConfigSchema.parse(config);
1015
1016
  const configPath = getConfigPath();
1017
+ await fs7.mkdir(path6.dirname(configPath), { recursive: true });
1016
1018
  await fs7.writeFile(configPath, JSON.stringify(parsedConfig, null, 2), "utf-8");
1017
1019
  }, getHomeConfig = async () => {
1018
1020
  const configPath = getConfigPath();
@@ -1023,8 +1025,14 @@ var homeConfigSchema, getConfigPath = () => path6.join(os2.homedir(), ".dotenc",
1023
1025
  return {};
1024
1026
  };
1025
1027
  var init_homeConfig = __esm(() => {
1028
+ updateConfigSchema = z2.object({
1029
+ lastCheckedAt: z2.string().nullish(),
1030
+ latestVersion: z2.string().nullish(),
1031
+ notifiedVersion: z2.string().nullish()
1032
+ });
1026
1033
  homeConfigSchema = z2.object({
1027
- editor: z2.string().nullish()
1034
+ editor: z2.string().nullish(),
1035
+ update: updateConfigSchema.nullish()
1028
1036
  });
1029
1037
  });
1030
1038
 
@@ -1052,8 +1060,8 @@ var getCurrentKeyName = async (deps = { getPrivateKeys, getPublicKeys }) => {
1052
1060
  const { keys: privateKeys } = await deps.getPrivateKeys();
1053
1061
  const publicKeys = await deps.getPublicKeys();
1054
1062
  const privateFingerprints = new Set(privateKeys.map((k) => k.fingerprint));
1055
- const match = publicKeys.find((pub) => privateFingerprints.has(pub.fingerprint));
1056
- return match?.name;
1063
+ const matches = publicKeys.filter((pub) => privateFingerprints.has(pub.fingerprint));
1064
+ return matches.map((m) => m.name);
1057
1065
  };
1058
1066
  var init_getCurrentKeyName = __esm(() => {
1059
1067
  init_getPrivateKeys();
@@ -1150,12 +1158,29 @@ var init_run = __esm(() => {
1150
1158
 
1151
1159
  // src/commands/dev.ts
1152
1160
  import chalk8 from "chalk";
1153
- var defaultDevCommandDeps, devCommand = async (command, args, deps = defaultDevCommandDeps) => {
1154
- const keyName = await deps.getCurrentKeyName();
1155
- if (!keyName) {
1161
+ import inquirer3 from "inquirer";
1162
+ var defaultSelect = async (message, choices) => {
1163
+ const { selected } = await inquirer3.prompt([
1164
+ {
1165
+ type: "list",
1166
+ name: "selected",
1167
+ message,
1168
+ choices
1169
+ }
1170
+ ]);
1171
+ return selected;
1172
+ }, defaultDevCommandDeps, devCommand = async (command, args, deps = defaultDevCommandDeps) => {
1173
+ const keyNames = await deps.getCurrentKeyName();
1174
+ if (keyNames.length === 0) {
1156
1175
  deps.logError(`${chalk8.red("Error:")} could not resolve your identity. Run ${chalk8.gray("dotenc init")} first.`);
1157
1176
  deps.exit(1);
1158
1177
  }
1178
+ let keyName;
1179
+ if (keyNames.length === 1) {
1180
+ keyName = keyNames[0];
1181
+ } else {
1182
+ keyName = await deps.select("Multiple identities found. Which one do you want to use?", keyNames.map((name) => ({ name, value: name })));
1183
+ }
1159
1184
  await deps.runCommand(command, args, { env: `development,${keyName}` });
1160
1185
  };
1161
1186
  var init_dev = __esm(() => {
@@ -1165,7 +1190,8 @@ var init_dev = __esm(() => {
1165
1190
  getCurrentKeyName,
1166
1191
  runCommand,
1167
1192
  logError: console.error,
1168
- exit: process.exit
1193
+ exit: process.exit,
1194
+ select: defaultSelect
1169
1195
  };
1170
1196
  });
1171
1197
 
@@ -1207,9 +1233,9 @@ var init_projectConfig = __esm(() => {
1207
1233
  });
1208
1234
 
1209
1235
  // src/prompts/createEnvironment.ts
1210
- import inquirer3 from "inquirer";
1236
+ import inquirer4 from "inquirer";
1211
1237
  var createEnvironmentPrompt = async (message, defaultValue) => {
1212
- const result = await inquirer3.prompt([
1238
+ const result = await inquirer4.prompt([
1213
1239
  {
1214
1240
  type: "input",
1215
1241
  name: "environment",
@@ -1704,9 +1730,9 @@ var setupGitDiff = () => {
1704
1730
  var init_setupGitDiff = () => {};
1705
1731
 
1706
1732
  // src/prompts/inputName.ts
1707
- import inquirer4 from "inquirer";
1733
+ import inquirer5 from "inquirer";
1708
1734
  var inputNamePrompt = async (message, defaultValue) => {
1709
- const result = await inquirer4.prompt([
1735
+ const result = await inquirer5.prompt([
1710
1736
  {
1711
1737
  type: "input",
1712
1738
  name: "name",
@@ -1791,9 +1817,9 @@ function validatePublicKey(key) {
1791
1817
  }
1792
1818
 
1793
1819
  // src/prompts/inputKey.ts
1794
- import inquirer5 from "inquirer";
1820
+ import inquirer6 from "inquirer";
1795
1821
  var inputKeyPrompt = async (message, defaultValue) => {
1796
- const result = await inquirer5.prompt([
1822
+ const result = await inquirer6.prompt([
1797
1823
  {
1798
1824
  type: "password",
1799
1825
  name: "key",
@@ -1813,7 +1839,7 @@ import fs12 from "node:fs/promises";
1813
1839
  import os4 from "node:os";
1814
1840
  import path12 from "node:path";
1815
1841
  import chalk12 from "chalk";
1816
- import inquirer6 from "inquirer";
1842
+ import inquirer7 from "inquirer";
1817
1843
  var keyAddCommand = async (nameArg, options) => {
1818
1844
  const { projectId } = await getProjectConfig();
1819
1845
  if (!projectId) {
@@ -1911,7 +1937,7 @@ ${passphraseProtectedKeys.map((k) => ` - ${k}`).join(`
1911
1937
  name: `${key.name} (${key.algorithm})`,
1912
1938
  value: key.name
1913
1939
  }));
1914
- const modePrompt = await inquirer6.prompt({
1940
+ const modePrompt = await inquirer7.prompt({
1915
1941
  type: "list",
1916
1942
  name: "mode",
1917
1943
  message: "Would you like to add one of your SSH keys or paste a public key?",
@@ -1934,7 +1960,7 @@ ${passphraseProtectedKeys.map((k) => ` - ${k}`).join(`
1934
1960
  process.exit(1);
1935
1961
  }
1936
1962
  } else {
1937
- const keyPrompt = await inquirer6.prompt({
1963
+ const keyPrompt = await inquirer7.prompt({
1938
1964
  type: "list",
1939
1965
  name: "key",
1940
1966
  message: "Which SSH key do you want to add?",
@@ -1994,7 +2020,7 @@ import fs13 from "node:fs/promises";
1994
2020
  import os5 from "node:os";
1995
2021
  import path13 from "node:path";
1996
2022
  import chalk13 from "chalk";
1997
- import inquirer7 from "inquirer";
2023
+ import inquirer8 from "inquirer";
1998
2024
  var initCommand = async (options) => {
1999
2025
  const { keys: privateKeys, passphraseProtectedKeys } = await getPrivateKeys();
2000
2026
  if (!privateKeys.length) {
@@ -2025,7 +2051,7 @@ var initCommand = async (options) => {
2025
2051
  if (privateKeys.length === 1) {
2026
2052
  keyToAdd = privateKeys[0].name;
2027
2053
  } else {
2028
- const result = await inquirer7.prompt([
2054
+ const result = await inquirer8.prompt([
2029
2055
  {
2030
2056
  type: "list",
2031
2057
  name: "key",
@@ -2072,6 +2098,12 @@ Some useful tips:`);
2072
2098
  console.log(`- To edit your personal environment: ${editCmd}`);
2073
2099
  const devCmd = chalk13.gray("dotenc dev <command>");
2074
2100
  console.log(`- To run with your encrypted env: ${devCmd}`);
2101
+ if (existsSync8(".claude") || existsSync8("CLAUDE.md")) {
2102
+ console.log(`- Install the agent skill: ${chalk13.gray("dotenc tools install-agent-skill")}`);
2103
+ }
2104
+ if (existsSync8(".vscode") || existsSync8(".cursor") || existsSync8(".windsurf")) {
2105
+ console.log(`- Add the editor extension: ${chalk13.gray("dotenc tools install-vscode-extension")}`);
2106
+ }
2075
2107
  };
2076
2108
  var init_init = __esm(() => {
2077
2109
  init_createProject();
@@ -2099,9 +2131,9 @@ var init_list3 = __esm(() => {
2099
2131
  });
2100
2132
 
2101
2133
  // src/prompts/confirm.ts
2102
- import inquirer8 from "inquirer";
2134
+ import inquirer9 from "inquirer";
2103
2135
  var confirmPrompt = async (message) => {
2104
- const result = await inquirer8.prompt([
2136
+ const result = await inquirer9.prompt([
2105
2137
  {
2106
2138
  type: "confirm",
2107
2139
  name: "confirm",
@@ -2193,13 +2225,378 @@ var init_textconv = __esm(() => {
2193
2225
  init_getEnvironmentByPath();
2194
2226
  });
2195
2227
 
2228
+ // src/commands/tools/install-agent-skill.ts
2229
+ import { spawn as spawn2 } from "node:child_process";
2230
+ import chalk15 from "chalk";
2231
+ import inquirer10 from "inquirer";
2232
+ var SKILL_SOURCE = "ivanfilhoz/dotenc", SKILL_NAME = "dotenc", runNpx = (args) => new Promise((resolve, reject) => {
2233
+ const child = spawn2("npx", args, {
2234
+ stdio: "inherit",
2235
+ shell: process.platform === "win32"
2236
+ });
2237
+ child.on("error", reject);
2238
+ child.on("exit", (code) => resolve(code ?? 1));
2239
+ }), defaultDeps, _runInstallAgentSkillCommand = async (options, depsOverrides = {}) => {
2240
+ const deps = {
2241
+ ...defaultDeps,
2242
+ ...depsOverrides
2243
+ };
2244
+ const { scope } = await deps.prompt([
2245
+ {
2246
+ type: "list",
2247
+ name: "scope",
2248
+ message: "Install locally or globally?",
2249
+ choices: [
2250
+ { name: "Locally (this project)", value: "local" },
2251
+ { name: "Globally (all projects)", value: "global" }
2252
+ ]
2253
+ }
2254
+ ]);
2255
+ const args = ["skills", "add", SKILL_SOURCE, "--skill", SKILL_NAME];
2256
+ if (scope === "global") {
2257
+ args.push("-g");
2258
+ }
2259
+ if (options.force) {
2260
+ args.push("-y");
2261
+ }
2262
+ const npxCommand = `npx ${args.join(" ")}`;
2263
+ let exitCode = 0;
2264
+ try {
2265
+ exitCode = await deps.runNpx(args);
2266
+ } catch (error) {
2267
+ deps.logError(`${chalk15.red("Error:")} failed to run ${chalk15.gray(npxCommand)}.`);
2268
+ deps.logError(`${chalk15.red("Details:")} ${error instanceof Error ? error.message : String(error)}`);
2269
+ deps.exit(1);
2270
+ }
2271
+ if (exitCode !== 0) {
2272
+ deps.logError(`${chalk15.red("Error:")} skill installation command exited with code ${exitCode}.`);
2273
+ deps.exit(exitCode);
2274
+ }
2275
+ deps.log(`${chalk15.green("✓")} Agent skill installation completed via ${chalk15.gray(npxCommand)}.`);
2276
+ deps.log(`Run ${chalk15.gray("/dotenc")} in your agent to use it.`);
2277
+ }, installAgentSkillCommand = async (options) => {
2278
+ await _runInstallAgentSkillCommand(options);
2279
+ };
2280
+ var init_install_agent_skill = __esm(() => {
2281
+ defaultDeps = {
2282
+ prompt: inquirer10.prompt,
2283
+ runNpx,
2284
+ log: console.log,
2285
+ logError: console.error,
2286
+ exit: process.exit
2287
+ };
2288
+ });
2289
+
2290
+ // src/commands/tools/install-vscode-extension.ts
2291
+ import { exec, execFile } from "node:child_process";
2292
+ import { existsSync as existsSync10 } from "node:fs";
2293
+ import fs16 from "node:fs/promises";
2294
+ import path16 from "node:path";
2295
+ import { promisify } from "node:util";
2296
+ import chalk16 from "chalk";
2297
+ import inquirer11 from "inquirer";
2298
+ async function addToExtensionsJson() {
2299
+ const extensionsJsonPath = path16.join(process.cwd(), ".vscode", "extensions.json");
2300
+ let json = {};
2301
+ if (existsSync10(extensionsJsonPath)) {
2302
+ const content = await fs16.readFile(extensionsJsonPath, "utf-8");
2303
+ try {
2304
+ json = JSON.parse(content);
2305
+ } catch {
2306
+ json = {};
2307
+ }
2308
+ } else {
2309
+ await fs16.mkdir(path16.join(process.cwd(), ".vscode"), { recursive: true });
2310
+ }
2311
+ if (!Array.isArray(json.recommendations)) {
2312
+ json.recommendations = [];
2313
+ }
2314
+ if (!json.recommendations.includes(EXTENSION_ID)) {
2315
+ json.recommendations.push(EXTENSION_ID);
2316
+ await fs16.writeFile(extensionsJsonPath, JSON.stringify(json, null, 2), "utf-8");
2317
+ console.log(`${chalk16.green("✓")} Added dotenc to ${chalk16.gray(".vscode/extensions.json")}`);
2318
+ } else {
2319
+ console.log(`${chalk16.green("✓")} dotenc already in ${chalk16.gray(".vscode/extensions.json")}`);
2320
+ }
2321
+ }
2322
+ async function which(bin) {
2323
+ try {
2324
+ await execFileAsync("which", [bin]);
2325
+ return true;
2326
+ } catch {
2327
+ return false;
2328
+ }
2329
+ }
2330
+ async function detectEditors() {
2331
+ const detected = [];
2332
+ if (existsSync10(path16.join(process.cwd(), ".cursor")))
2333
+ detected.push("cursor");
2334
+ if (existsSync10(path16.join(process.cwd(), ".windsurf")))
2335
+ detected.push("windsurf");
2336
+ if (existsSync10(path16.join(process.cwd(), ".vscode")))
2337
+ detected.push("vscode");
2338
+ const checks = [
2339
+ { key: "cursor", bins: ["cursor"] },
2340
+ { key: "windsurf", bins: ["windsurf"] },
2341
+ { key: "vscode", bins: ["code"] },
2342
+ { key: "vscodium", bins: ["codium", "vscodium"] }
2343
+ ];
2344
+ if (process.platform === "darwin") {
2345
+ const macApps = {
2346
+ cursor: "/Applications/Cursor.app",
2347
+ windsurf: "/Applications/Windsurf.app",
2348
+ vscode: "/Applications/Visual Studio Code.app",
2349
+ vscodium: "/Applications/VSCodium.app"
2350
+ };
2351
+ for (const [key, appPath] of Object.entries(macApps)) {
2352
+ if (!detected.includes(key) && existsSync10(appPath)) {
2353
+ detected.push(key);
2354
+ }
2355
+ }
2356
+ }
2357
+ for (const { key, bins } of checks) {
2358
+ if (detected.includes(key))
2359
+ continue;
2360
+ for (const bin of bins) {
2361
+ if (await which(bin)) {
2362
+ detected.push(key);
2363
+ break;
2364
+ }
2365
+ }
2366
+ }
2367
+ return detected;
2368
+ }
2369
+ async function openUrl(url) {
2370
+ if (process.platform === "darwin") {
2371
+ await execFileAsync("open", [url]);
2372
+ } else if (process.platform === "win32") {
2373
+ await execAsync(`start ${url}`);
2374
+ } else {
2375
+ await execFileAsync("xdg-open", [url]);
2376
+ }
2377
+ }
2378
+ async function _runInstallVscodeExtension(getEditors = detectEditors, _openUrl = openUrl) {
2379
+ const editors = await getEditors();
2380
+ await addToExtensionsJson();
2381
+ if (editors.length === 0) {
2382
+ console.log(`
2383
+ Install the extension in VS Code: ${chalk16.cyan(EDITOR_PROTOCOL_URLS.vscode)}`);
2384
+ return;
2385
+ }
2386
+ if (editors.length === 1) {
2387
+ const editor = editors[0];
2388
+ const url = EDITOR_PROTOCOL_URLS[editor];
2389
+ const name = EDITOR_NAMES[editor];
2390
+ const { open } = await inquirer11.prompt([
2391
+ {
2392
+ type: "confirm",
2393
+ name: "open",
2394
+ message: `Open extension page in ${name} now?`,
2395
+ default: true
2396
+ }
2397
+ ]);
2398
+ if (open) {
2399
+ try {
2400
+ await _openUrl(url);
2401
+ } catch {
2402
+ console.log(`Open manually: ${chalk16.cyan(url)}`);
2403
+ }
2404
+ } else {
2405
+ console.log(`Install manually: ${chalk16.cyan(url)}`);
2406
+ }
2407
+ return;
2408
+ }
2409
+ console.log(`
2410
+ Install the extension in your editor:`);
2411
+ for (const editor of editors) {
2412
+ const name = EDITOR_NAMES[editor] ?? editor;
2413
+ const url = EDITOR_PROTOCOL_URLS[editor];
2414
+ console.log(` ${name}: ${chalk16.cyan(url)}`);
2415
+ }
2416
+ }
2417
+ var execFileAsync, execAsync, EXTENSION_ID = "dotenc.dotenc", EDITOR_PROTOCOL_URLS, EDITOR_NAMES, installVscodeExtensionCommand = async () => {
2418
+ await _runInstallVscodeExtension();
2419
+ };
2420
+ var init_install_vscode_extension = __esm(() => {
2421
+ execFileAsync = promisify(execFile);
2422
+ execAsync = promisify(exec);
2423
+ EDITOR_PROTOCOL_URLS = {
2424
+ vscode: `vscode:extension/${EXTENSION_ID}`,
2425
+ cursor: `cursor:extension/${EXTENSION_ID}`,
2426
+ windsurf: `windsurf:extension/${EXTENSION_ID}`,
2427
+ vscodium: `vscodium:extension/${EXTENSION_ID}`
2428
+ };
2429
+ EDITOR_NAMES = {
2430
+ vscode: "VS Code",
2431
+ cursor: "Cursor",
2432
+ windsurf: "Windsurf",
2433
+ vscodium: "VSCodium"
2434
+ };
2435
+ });
2436
+
2437
+ // src/helpers/update.ts
2438
+ import { realpathSync } from "node:fs";
2439
+ var NPM_LATEST_URL = "https://registry.npmjs.org/@dotenc%2fcli/latest", GITHUB_RELEASES_URL = "https://github.com/ivanfilhoz/dotenc/releases", normalizePath = (value) => value.replace(/\\/g, "/").toLowerCase(), safeRealPath = (resolveRealPath, filePath) => {
2440
+ try {
2441
+ return resolveRealPath(filePath);
2442
+ } catch {
2443
+ return filePath;
2444
+ }
2445
+ }, detectInstallMethod = (options = {}) => {
2446
+ const execPath = options.execPath ?? process.execPath;
2447
+ const argv = options.argv ?? process.argv;
2448
+ const platform = options.platform ?? process.platform;
2449
+ const resolveRealPath = options.resolveRealPath ?? realpathSync;
2450
+ const scriptPath = argv[1] ?? "";
2451
+ const resolvedPaths = [execPath, scriptPath].filter(Boolean).map((value) => normalizePath(safeRealPath(resolveRealPath, value)));
2452
+ const allPaths = resolvedPaths.join(" ");
2453
+ const normalizedScriptPath = normalizePath(scriptPath);
2454
+ if (allPaths.includes("/cellar/dotenc/") || allPaths.includes("/homebrew/cellar/dotenc/")) {
2455
+ return "homebrew";
2456
+ }
2457
+ if (allPaths.includes("/scoop/apps/dotenc/") || allPaths.includes("/scoop/shims/")) {
2458
+ return "scoop";
2459
+ }
2460
+ if (allPaths.includes("/node_modules/@dotenc/cli/")) {
2461
+ return "npm";
2462
+ }
2463
+ if (normalizedScriptPath.endsWith("/src/cli.ts") || normalizedScriptPath.endsWith("/dist/cli.js") && !allPaths.includes("/node_modules/@dotenc/cli/")) {
2464
+ return "unknown";
2465
+ }
2466
+ if (platform === "win32" && allPaths.includes("/scoop/")) {
2467
+ return "scoop";
2468
+ }
2469
+ return "binary";
2470
+ }, parseVersionParts = (version) => {
2471
+ const cleaned = version.trim().replace(/^v/i, "").split("-")[0];
2472
+ if (!cleaned)
2473
+ return null;
2474
+ const parts = cleaned.split(".").map((part) => Number.parseInt(part, 10));
2475
+ if (parts.some((part) => Number.isNaN(part))) {
2476
+ return null;
2477
+ }
2478
+ return parts;
2479
+ }, compareVersions = (left, right) => {
2480
+ const leftParts = parseVersionParts(left);
2481
+ const rightParts = parseVersionParts(right);
2482
+ if (!leftParts || !rightParts)
2483
+ return 0;
2484
+ const maxLen = Math.max(leftParts.length, rightParts.length);
2485
+ for (let i = 0;i < maxLen; i += 1) {
2486
+ const leftPart = leftParts[i] ?? 0;
2487
+ const rightPart = rightParts[i] ?? 0;
2488
+ if (leftPart > rightPart)
2489
+ return 1;
2490
+ if (leftPart < rightPart)
2491
+ return -1;
2492
+ }
2493
+ return 0;
2494
+ }, isVersionNewer = (candidate, current) => compareVersions(candidate, current) > 0, fetchLatestVersion = async (options = {}) => {
2495
+ const fetchImpl = options.fetchImpl ?? fetch;
2496
+ const timeoutMs = options.timeoutMs ?? 1500;
2497
+ const controller = new AbortController;
2498
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
2499
+ try {
2500
+ const response = await fetchImpl(NPM_LATEST_URL, {
2501
+ headers: {
2502
+ accept: "application/json"
2503
+ },
2504
+ signal: controller.signal
2505
+ });
2506
+ if (!response.ok) {
2507
+ return null;
2508
+ }
2509
+ const payload = await response.json();
2510
+ return typeof payload.version === "string" ? payload.version : null;
2511
+ } catch {
2512
+ return null;
2513
+ } finally {
2514
+ clearTimeout(timer);
2515
+ }
2516
+ };
2517
+ var init_update = () => {};
2518
+
2519
+ // src/commands/update.ts
2520
+ import { spawn as spawn3 } from "node:child_process";
2521
+ import chalk17 from "chalk";
2522
+ var runPackageManagerCommand = (command, args) => new Promise((resolve, reject) => {
2523
+ const child = spawn3(command, args, {
2524
+ stdio: "inherit",
2525
+ shell: process.platform === "win32"
2526
+ });
2527
+ child.on("error", reject);
2528
+ child.on("exit", (code) => resolve(code ?? 1));
2529
+ }), defaultDeps2, updateCommands, _runUpdateCommand = async (depsOverrides = {}) => {
2530
+ const deps = {
2531
+ ...defaultDeps2,
2532
+ ...depsOverrides
2533
+ };
2534
+ const method = deps.detectInstallMethod();
2535
+ if (method === "binary") {
2536
+ deps.log(`Standalone binary detected. Download the latest release at ${chalk17.cyan(GITHUB_RELEASES_URL)}.`);
2537
+ return;
2538
+ }
2539
+ if (method === "unknown") {
2540
+ deps.log("Could not determine installation method automatically.");
2541
+ deps.log(`Try one of these commands:`);
2542
+ deps.log(` ${chalk17.gray("brew upgrade dotenc")}`);
2543
+ deps.log(` ${chalk17.gray("scoop update dotenc")}`);
2544
+ deps.log(` ${chalk17.gray("npm install -g @dotenc/cli")}`);
2545
+ deps.log(`Or download from ${chalk17.cyan(GITHUB_RELEASES_URL)}.`);
2546
+ return;
2547
+ }
2548
+ const updater = updateCommands[method];
2549
+ deps.log(`Updating dotenc via ${updater.label}...`);
2550
+ let exitCode = 0;
2551
+ try {
2552
+ exitCode = await deps.runPackageManagerCommand(updater.command, updater.args);
2553
+ } catch (error) {
2554
+ deps.logError(`${chalk17.red("Error:")} failed to run ${chalk17.gray([updater.command, ...updater.args].join(" "))}.`);
2555
+ deps.logError(`${chalk17.red("Details:")} ${error instanceof Error ? error.message : String(error)}`);
2556
+ deps.exit(1);
2557
+ }
2558
+ if (exitCode !== 0) {
2559
+ deps.logError(`${chalk17.red("Error:")} update command exited with code ${exitCode}.`);
2560
+ deps.exit(exitCode);
2561
+ }
2562
+ }, updateCommand = async () => {
2563
+ await _runUpdateCommand();
2564
+ };
2565
+ var init_update2 = __esm(() => {
2566
+ init_update();
2567
+ defaultDeps2 = {
2568
+ detectInstallMethod,
2569
+ runPackageManagerCommand,
2570
+ log: console.log,
2571
+ logError: console.error,
2572
+ exit: process.exit
2573
+ };
2574
+ updateCommands = {
2575
+ homebrew: {
2576
+ command: "brew",
2577
+ args: ["upgrade", "dotenc"],
2578
+ label: "Homebrew"
2579
+ },
2580
+ scoop: {
2581
+ command: "scoop",
2582
+ args: ["update", "dotenc"],
2583
+ label: "Scoop"
2584
+ },
2585
+ npm: {
2586
+ command: "npm",
2587
+ args: ["install", "-g", "@dotenc/cli"],
2588
+ label: "npm"
2589
+ }
2590
+ };
2591
+ });
2592
+
2196
2593
  // src/commands/whoami.ts
2197
2594
  var whoamiCommand = async () => {
2198
2595
  const { keys: privateKeys, passphraseProtectedKeys } = await getPrivateKeys();
2199
2596
  const publicKeys = await getPublicKeys();
2200
2597
  const privateFingerprints = new Set(privateKeys.map((k) => k.fingerprint));
2201
- const matchingPublicKey = publicKeys.find((pub) => privateFingerprints.has(pub.fingerprint));
2202
- if (!matchingPublicKey) {
2598
+ const matchingPublicKeys = publicKeys.filter((pub) => privateFingerprints.has(pub.fingerprint));
2599
+ if (matchingPublicKeys.length === 0) {
2203
2600
  if (privateKeys.length === 0 && passphraseProtectedKeys.length > 0) {
2204
2601
  console.error(passphraseProtectedKeyError(passphraseProtectedKeys));
2205
2602
  } else {
@@ -2207,28 +2604,34 @@ var whoamiCommand = async () => {
2207
2604
  }
2208
2605
  process.exit(1);
2209
2606
  }
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
2607
  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);
2608
+ for (let i = 0;i < matchingPublicKeys.length; i++) {
2609
+ const matchingPublicKey = matchingPublicKeys[i];
2610
+ if (i > 0) {
2611
+ console.log("");
2612
+ }
2613
+ const matchingPrivateKey = privateKeys.find((pk) => pk.fingerprint === matchingPublicKey.fingerprint);
2614
+ console.log(`Name: ${matchingPublicKey.name}`);
2615
+ console.log(`Active SSH key: ${matchingPrivateKey?.name ?? "unknown"}`);
2616
+ console.log(`Fingerprint: ${matchingPublicKey.fingerprint}`);
2617
+ const authorizedEnvironments = [];
2618
+ for (const envName of environments) {
2619
+ try {
2620
+ const environment = await getEnvironmentByName(envName);
2621
+ const hasAccess = environment.keys.some((key) => key.fingerprint === matchingPublicKey.fingerprint);
2622
+ if (hasAccess) {
2623
+ authorizedEnvironments.push(envName);
2624
+ }
2625
+ } catch {}
2626
+ }
2627
+ if (authorizedEnvironments.length > 0) {
2628
+ console.log("Authorized environments:");
2629
+ for (const env of authorizedEnvironments) {
2630
+ console.log(` - ${env}`);
2222
2631
  }
2223
- } catch {}
2224
- }
2225
- if (authorizedEnvironments.length > 0) {
2226
- console.log("Authorized environments:");
2227
- for (const env of authorizedEnvironments) {
2228
- console.log(` - ${env}`);
2632
+ } else {
2633
+ console.log("Authorized environments: none");
2229
2634
  }
2230
- } else {
2231
- console.log("Authorized environments: none");
2232
2635
  }
2233
2636
  };
2234
2637
  var init_whoami = __esm(() => {
@@ -2239,11 +2642,92 @@ var init_whoami = __esm(() => {
2239
2642
  init_getPublicKeys();
2240
2643
  });
2241
2644
 
2645
+ // src/helpers/updateNotifier.ts
2646
+ import chalk18 from "chalk";
2647
+ var CHECK_INTERVAL_MS, defaultDeps3, shouldSkipCheck = (args, env) => {
2648
+ if (env.DOTENC_SKIP_UPDATE_CHECK === "1") {
2649
+ return true;
2650
+ }
2651
+ const firstArg = args[0];
2652
+ if (!firstArg)
2653
+ return false;
2654
+ return ["update", "--help", "-h", "--version", "-V", "help"].includes(firstArg);
2655
+ }, parseTimestamp = (value) => {
2656
+ if (!value)
2657
+ return 0;
2658
+ const parsed = Date.parse(value);
2659
+ return Number.isNaN(parsed) ? 0 : parsed;
2660
+ }, persistUpdateState = async (config, updateState, deps) => {
2661
+ try {
2662
+ await deps.setHomeConfig({
2663
+ ...config,
2664
+ update: updateState
2665
+ });
2666
+ } catch {}
2667
+ }, maybeNotifyAboutUpdate = async (depsOverrides = {}) => {
2668
+ const deps = {
2669
+ ...defaultDeps3,
2670
+ ...depsOverrides
2671
+ };
2672
+ if (shouldSkipCheck(deps.args, deps.env)) {
2673
+ return;
2674
+ }
2675
+ let config = {};
2676
+ try {
2677
+ config = await deps.getHomeConfig();
2678
+ } catch {
2679
+ config = {};
2680
+ }
2681
+ let updateState = config.update ?? {};
2682
+ let latestVersion = updateState.latestVersion ?? null;
2683
+ const now = deps.now();
2684
+ const lastCheckedAt = parseTimestamp(updateState.lastCheckedAt);
2685
+ const shouldRefresh = !latestVersion || now - lastCheckedAt >= CHECK_INTERVAL_MS;
2686
+ if (shouldRefresh) {
2687
+ const fetchedVersion = await deps.fetchLatestVersion();
2688
+ updateState = {
2689
+ ...updateState,
2690
+ lastCheckedAt: new Date(now).toISOString(),
2691
+ latestVersion: fetchedVersion ?? latestVersion ?? undefined
2692
+ };
2693
+ latestVersion = updateState.latestVersion ?? null;
2694
+ await persistUpdateState(config, updateState, deps);
2695
+ }
2696
+ if (!latestVersion || !isVersionNewer(latestVersion, deps.currentVersion)) {
2697
+ return;
2698
+ }
2699
+ if (updateState.notifiedVersion === latestVersion) {
2700
+ return;
2701
+ }
2702
+ deps.log(`${chalk18.yellow("Update available:")} ${chalk18.gray(`dotenc ${deps.currentVersion}`)} -> ${chalk18.cyan(`dotenc ${latestVersion}`)}. Run ${chalk18.gray("dotenc update")}.`);
2703
+ updateState = {
2704
+ ...updateState,
2705
+ notifiedVersion: latestVersion
2706
+ };
2707
+ await persistUpdateState(config, updateState, deps);
2708
+ };
2709
+ var init_updateNotifier = __esm(() => {
2710
+ init_package();
2711
+ init_homeConfig();
2712
+ init_update();
2713
+ CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000;
2714
+ defaultDeps3 = {
2715
+ getHomeConfig,
2716
+ setHomeConfig,
2717
+ fetchLatestVersion,
2718
+ currentVersion: package_default.version,
2719
+ now: () => Date.now(),
2720
+ log: console.log,
2721
+ args: process.argv.slice(2),
2722
+ env: process.env
2723
+ };
2724
+ });
2725
+
2242
2726
  // src/program.ts
2243
2727
  var exports_program = {};
2244
2728
  import { Command, Option } from "commander";
2245
- var program, env, auth, key;
2246
- var init_program = __esm(() => {
2729
+ var program, env, auth, key, tools;
2730
+ var init_program = __esm(async () => {
2247
2731
  init_package();
2248
2732
  init_grant();
2249
2733
  init_list();
@@ -2262,7 +2746,11 @@ var init_program = __esm(() => {
2262
2746
  init_remove();
2263
2747
  init_run();
2264
2748
  init_textconv();
2749
+ init_install_agent_skill();
2750
+ init_install_vscode_extension();
2751
+ init_update2();
2265
2752
  init_whoami();
2753
+ init_updateNotifier();
2266
2754
  program = new Command;
2267
2755
  program.name("dotenc").description(package_default.description).version(package_default.version);
2268
2756
  program.command("init").addOption(new Option("-n, --name <name>", "your username for the project")).description("initialize a dotenc project in the current directory").action(initCommand);
@@ -2283,11 +2771,16 @@ var init_program = __esm(() => {
2283
2771
  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
2772
  key.command("list").description("list all public keys in the project").action(keyListCommand);
2285
2773
  key.command("remove").argument("[name]", "the name of the public key to remove").description("remove a public key from the project").action(keyRemoveCommand);
2774
+ tools = program.command("tools").description("install editor integrations");
2775
+ tools.command("install-agent-skill").addOption(new Option("--force", "run npx skills in non-interactive mode (-y)")).description("install the agent skill for this project").action(installAgentSkillCommand);
2776
+ tools.command("install-vscode-extension").description("add dotenc to VS Code / Cursor / Windsurf extension recommendations").action(installVscodeExtensionCommand);
2286
2777
  program.command("textconv", { hidden: true }).argument("<filepath>", "path to the encrypted environment file").description("decrypt an environment file for git diff").action(textconvCommand);
2778
+ program.command("update").description("update dotenc based on your installation method").action(updateCommand);
2287
2779
  program.command("whoami").description("show your identity in this project").action(whoamiCommand);
2288
2780
  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);
2781
+ await maybeNotifyAboutUpdate();
2289
2782
  program.parse();
2290
2783
  });
2291
2784
 
2292
2785
  // src/cli.ts
2293
- Promise.resolve().then(() => init_program());
2786
+ init_program();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotenc/cli",
3
- "version": "0.4.6",
3
+ "version": "0.5.1",
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",