@dotenc/cli 0.5.0 → 0.5.2

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 +317 -158
  2. package/package.json +1 -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.5.0",
10
+ version: "0.5.2",
11
11
  description: "🔐 Git-native encrypted environments powered by your SSH keys",
12
12
  author: "Ivan Filho <i@ivanfilho.com>",
13
13
  license: "MIT",
@@ -1011,9 +1011,10 @@ import fs7 from "node:fs/promises";
1011
1011
  import os2 from "node:os";
1012
1012
  import path6 from "node:path";
1013
1013
  import { z as z2 } from "zod";
1014
- 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) => {
1015
1015
  const parsedConfig = homeConfigSchema.parse(config);
1016
1016
  const configPath = getConfigPath();
1017
+ await fs7.mkdir(path6.dirname(configPath), { recursive: true });
1017
1018
  await fs7.writeFile(configPath, JSON.stringify(parsedConfig, null, 2), "utf-8");
1018
1019
  }, getHomeConfig = async () => {
1019
1020
  const configPath = getConfigPath();
@@ -1024,8 +1025,14 @@ var homeConfigSchema, getConfigPath = () => path6.join(os2.homedir(), ".dotenc",
1024
1025
  return {};
1025
1026
  };
1026
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
+ });
1027
1033
  homeConfigSchema = z2.object({
1028
- editor: z2.string().nullish()
1034
+ editor: z2.string().nullish(),
1035
+ update: updateConfigSchema.nullish()
1029
1036
  });
1030
1037
  });
1031
1038
 
@@ -1354,8 +1361,9 @@ var defaultDecryptCommandDeps, ansiPattern, stripAnsi = (value) => value.replace
1354
1361
  try {
1355
1362
  const environment = await deps.getEnvironmentByName(environmentName);
1356
1363
  const plaintext = await deps.decryptEnvironmentData(environment);
1364
+ const grantedUsers = Array.from(new Set(environment.keys.map((key) => key.name.trim()).filter((name) => name.length > 0)));
1357
1365
  if (options.json) {
1358
- writeJson({ ok: true, content: plaintext }, deps);
1366
+ writeJson({ ok: true, content: plaintext, grantedUsers }, deps);
1359
1367
  } else {
1360
1368
  deps.writeStdout(plaintext);
1361
1369
  }
@@ -2092,7 +2100,7 @@ Some useful tips:`);
2092
2100
  const devCmd = chalk13.gray("dotenc dev <command>");
2093
2101
  console.log(`- To run with your encrypted env: ${devCmd}`);
2094
2102
  if (existsSync8(".claude") || existsSync8("CLAUDE.md")) {
2095
- console.log(`- Install the Claude Code skill: ${chalk13.gray("dotenc tools install-claude-code-skill")}`);
2103
+ console.log(`- Install the agent skill: ${chalk13.gray("dotenc tools install-agent-skill")}`);
2096
2104
  }
2097
2105
  if (existsSync8(".vscode") || existsSync8(".cursor") || existsSync8(".windsurf")) {
2098
2106
  console.log(`- Add the editor extension: ${chalk13.gray("dotenc tools install-vscode-extension")}`);
@@ -2218,133 +2226,23 @@ var init_textconv = __esm(() => {
2218
2226
  init_getEnvironmentByPath();
2219
2227
  });
2220
2228
 
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";
2229
+ // src/commands/tools/install-agent-skill.ts
2230
+ import { spawn as spawn2 } from "node:child_process";
2226
2231
  import chalk15 from "chalk";
2227
2232
  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([
2233
+ var SKILL_SOURCE = "ivanfilhoz/dotenc", SKILL_NAME = "dotenc", runNpx = (args) => new Promise((resolve, reject) => {
2234
+ const child = spawn2("npx", args, {
2235
+ stdio: "inherit",
2236
+ shell: process.platform === "win32"
2237
+ });
2238
+ child.on("error", reject);
2239
+ child.on("exit", (code) => resolve(code ?? 1));
2240
+ }), defaultDeps, _runInstallAgentSkillCommand = async (options, depsOverrides = {}) => {
2241
+ const deps = {
2242
+ ...defaultDeps,
2243
+ ...depsOverrides
2244
+ };
2245
+ const { scope } = await deps.prompt([
2348
2246
  {
2349
2247
  type: "list",
2350
2248
  name: "scope",
@@ -2355,48 +2253,68 @@ dotenc auth grant production ci
2355
2253
  ]
2356
2254
  }
2357
2255
  ]);
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);
2256
+ const args = ["skills", "add", SKILL_SOURCE, "--skill", SKILL_NAME];
2257
+ if (scope === "global") {
2258
+ args.push("-g");
2259
+ }
2260
+ if (options.force) {
2261
+ args.push("-y");
2262
+ }
2263
+ const npxCommand = `npx ${args.join(" ")}`;
2264
+ let exitCode = 0;
2265
+ try {
2266
+ exitCode = await deps.runNpx(args);
2267
+ } catch (error) {
2268
+ deps.logError(`${chalk15.red("Error:")} failed to run ${chalk15.gray(npxCommand)}.`);
2269
+ deps.logError(`${chalk15.red("Details:")} ${error instanceof Error ? error.message : String(error)}`);
2270
+ deps.exit(1);
2271
+ }
2272
+ if (exitCode !== 0) {
2273
+ deps.logError(`${chalk15.red("Error:")} skill installation command exited with code ${exitCode}.`);
2274
+ deps.exit(exitCode);
2365
2275
  }
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.`);
2276
+ deps.log(`${chalk15.green("✓")} Agent skill installation completed via ${chalk15.gray(npxCommand)}.`);
2277
+ deps.log(`Run ${chalk15.gray("/dotenc")} in your agent to use it.`);
2278
+ }, installAgentSkillCommand = async (options) => {
2279
+ await _runInstallAgentSkillCommand(options);
2370
2280
  };
2371
- var init_install_claude_code_skill = () => {};
2281
+ var init_install_agent_skill = __esm(() => {
2282
+ defaultDeps = {
2283
+ prompt: inquirer10.prompt,
2284
+ runNpx,
2285
+ log: console.log,
2286
+ logError: console.error,
2287
+ exit: process.exit
2288
+ };
2289
+ });
2372
2290
 
2373
2291
  // src/commands/tools/install-vscode-extension.ts
2374
2292
  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";
2293
+ import { existsSync as existsSync10 } from "node:fs";
2294
+ import fs16 from "node:fs/promises";
2295
+ import path16 from "node:path";
2378
2296
  import { promisify } from "node:util";
2379
2297
  import chalk16 from "chalk";
2380
2298
  import inquirer11 from "inquirer";
2381
2299
  async function addToExtensionsJson() {
2382
- const extensionsJsonPath = path17.join(process.cwd(), ".vscode", "extensions.json");
2300
+ const extensionsJsonPath = path16.join(process.cwd(), ".vscode", "extensions.json");
2383
2301
  let json = {};
2384
- if (existsSync11(extensionsJsonPath)) {
2385
- const content = await fs17.readFile(extensionsJsonPath, "utf-8");
2302
+ if (existsSync10(extensionsJsonPath)) {
2303
+ const content = await fs16.readFile(extensionsJsonPath, "utf-8");
2386
2304
  try {
2387
2305
  json = JSON.parse(content);
2388
2306
  } catch {
2389
2307
  json = {};
2390
2308
  }
2391
2309
  } else {
2392
- await fs17.mkdir(path17.join(process.cwd(), ".vscode"), { recursive: true });
2310
+ await fs16.mkdir(path16.join(process.cwd(), ".vscode"), { recursive: true });
2393
2311
  }
2394
2312
  if (!Array.isArray(json.recommendations)) {
2395
2313
  json.recommendations = [];
2396
2314
  }
2397
2315
  if (!json.recommendations.includes(EXTENSION_ID)) {
2398
2316
  json.recommendations.push(EXTENSION_ID);
2399
- await fs17.writeFile(extensionsJsonPath, JSON.stringify(json, null, 2), "utf-8");
2317
+ await fs16.writeFile(extensionsJsonPath, JSON.stringify(json, null, 2), "utf-8");
2400
2318
  console.log(`${chalk16.green("✓")} Added dotenc to ${chalk16.gray(".vscode/extensions.json")}`);
2401
2319
  } else {
2402
2320
  console.log(`${chalk16.green("✓")} dotenc already in ${chalk16.gray(".vscode/extensions.json")}`);
@@ -2412,11 +2330,11 @@ async function which(bin) {
2412
2330
  }
2413
2331
  async function detectEditors() {
2414
2332
  const detected = [];
2415
- if (existsSync11(path17.join(process.cwd(), ".cursor")))
2333
+ if (existsSync10(path16.join(process.cwd(), ".cursor")))
2416
2334
  detected.push("cursor");
2417
- if (existsSync11(path17.join(process.cwd(), ".windsurf")))
2335
+ if (existsSync10(path16.join(process.cwd(), ".windsurf")))
2418
2336
  detected.push("windsurf");
2419
- if (existsSync11(path17.join(process.cwd(), ".vscode")))
2337
+ if (existsSync10(path16.join(process.cwd(), ".vscode")))
2420
2338
  detected.push("vscode");
2421
2339
  const checks = [
2422
2340
  { key: "cursor", bins: ["cursor"] },
@@ -2432,7 +2350,7 @@ async function detectEditors() {
2432
2350
  vscodium: "/Applications/VSCodium.app"
2433
2351
  };
2434
2352
  for (const [key, appPath] of Object.entries(macApps)) {
2435
- if (!detected.includes(key) && existsSync11(appPath)) {
2353
+ if (!detected.includes(key) && existsSync10(appPath)) {
2436
2354
  detected.push(key);
2437
2355
  }
2438
2356
  }
@@ -2517,6 +2435,162 @@ var init_install_vscode_extension = __esm(() => {
2517
2435
  };
2518
2436
  });
2519
2437
 
2438
+ // src/helpers/update.ts
2439
+ import { realpathSync } from "node:fs";
2440
+ 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) => {
2441
+ try {
2442
+ return resolveRealPath(filePath);
2443
+ } catch {
2444
+ return filePath;
2445
+ }
2446
+ }, detectInstallMethod = (options = {}) => {
2447
+ const execPath = options.execPath ?? process.execPath;
2448
+ const argv = options.argv ?? process.argv;
2449
+ const platform = options.platform ?? process.platform;
2450
+ const resolveRealPath = options.resolveRealPath ?? realpathSync;
2451
+ const scriptPath = argv[1] ?? "";
2452
+ const resolvedPaths = [execPath, scriptPath].filter(Boolean).map((value) => normalizePath(safeRealPath(resolveRealPath, value)));
2453
+ const allPaths = resolvedPaths.join(" ");
2454
+ const normalizedScriptPath = normalizePath(scriptPath);
2455
+ if (allPaths.includes("/cellar/dotenc/") || allPaths.includes("/homebrew/cellar/dotenc/")) {
2456
+ return "homebrew";
2457
+ }
2458
+ if (allPaths.includes("/scoop/apps/dotenc/") || allPaths.includes("/scoop/shims/")) {
2459
+ return "scoop";
2460
+ }
2461
+ if (allPaths.includes("/node_modules/@dotenc/cli/")) {
2462
+ return "npm";
2463
+ }
2464
+ if (normalizedScriptPath.endsWith("/src/cli.ts") || normalizedScriptPath.endsWith("/dist/cli.js") && !allPaths.includes("/node_modules/@dotenc/cli/")) {
2465
+ return "unknown";
2466
+ }
2467
+ if (platform === "win32" && allPaths.includes("/scoop/")) {
2468
+ return "scoop";
2469
+ }
2470
+ return "binary";
2471
+ }, parseVersionParts = (version) => {
2472
+ const cleaned = version.trim().replace(/^v/i, "").split("-")[0];
2473
+ if (!cleaned)
2474
+ return null;
2475
+ const parts = cleaned.split(".").map((part) => Number.parseInt(part, 10));
2476
+ if (parts.some((part) => Number.isNaN(part))) {
2477
+ return null;
2478
+ }
2479
+ return parts;
2480
+ }, compareVersions = (left, right) => {
2481
+ const leftParts = parseVersionParts(left);
2482
+ const rightParts = parseVersionParts(right);
2483
+ if (!leftParts || !rightParts)
2484
+ return 0;
2485
+ const maxLen = Math.max(leftParts.length, rightParts.length);
2486
+ for (let i = 0;i < maxLen; i += 1) {
2487
+ const leftPart = leftParts[i] ?? 0;
2488
+ const rightPart = rightParts[i] ?? 0;
2489
+ if (leftPart > rightPart)
2490
+ return 1;
2491
+ if (leftPart < rightPart)
2492
+ return -1;
2493
+ }
2494
+ return 0;
2495
+ }, isVersionNewer = (candidate, current) => compareVersions(candidate, current) > 0, fetchLatestVersion = async (options = {}) => {
2496
+ const fetchImpl = options.fetchImpl ?? fetch;
2497
+ const timeoutMs = options.timeoutMs ?? 1500;
2498
+ const controller = new AbortController;
2499
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
2500
+ try {
2501
+ const response = await fetchImpl(NPM_LATEST_URL, {
2502
+ headers: {
2503
+ accept: "application/json"
2504
+ },
2505
+ signal: controller.signal
2506
+ });
2507
+ if (!response.ok) {
2508
+ return null;
2509
+ }
2510
+ const payload = await response.json();
2511
+ return typeof payload.version === "string" ? payload.version : null;
2512
+ } catch {
2513
+ return null;
2514
+ } finally {
2515
+ clearTimeout(timer);
2516
+ }
2517
+ };
2518
+ var init_update = () => {};
2519
+
2520
+ // src/commands/update.ts
2521
+ import { spawn as spawn3 } from "node:child_process";
2522
+ import chalk17 from "chalk";
2523
+ var runPackageManagerCommand = (command, args) => new Promise((resolve, reject) => {
2524
+ const child = spawn3(command, args, {
2525
+ stdio: "inherit",
2526
+ shell: process.platform === "win32"
2527
+ });
2528
+ child.on("error", reject);
2529
+ child.on("exit", (code) => resolve(code ?? 1));
2530
+ }), defaultDeps2, updateCommands, _runUpdateCommand = async (depsOverrides = {}) => {
2531
+ const deps = {
2532
+ ...defaultDeps2,
2533
+ ...depsOverrides
2534
+ };
2535
+ const method = deps.detectInstallMethod();
2536
+ if (method === "binary") {
2537
+ deps.log(`Standalone binary detected. Download the latest release at ${chalk17.cyan(GITHUB_RELEASES_URL)}.`);
2538
+ return;
2539
+ }
2540
+ if (method === "unknown") {
2541
+ deps.log("Could not determine installation method automatically.");
2542
+ deps.log(`Try one of these commands:`);
2543
+ deps.log(` ${chalk17.gray("brew upgrade dotenc")}`);
2544
+ deps.log(` ${chalk17.gray("scoop update dotenc")}`);
2545
+ deps.log(` ${chalk17.gray("npm install -g @dotenc/cli")}`);
2546
+ deps.log(`Or download from ${chalk17.cyan(GITHUB_RELEASES_URL)}.`);
2547
+ return;
2548
+ }
2549
+ const updater = updateCommands[method];
2550
+ deps.log(`Updating dotenc via ${updater.label}...`);
2551
+ let exitCode = 0;
2552
+ try {
2553
+ exitCode = await deps.runPackageManagerCommand(updater.command, updater.args);
2554
+ } catch (error) {
2555
+ deps.logError(`${chalk17.red("Error:")} failed to run ${chalk17.gray([updater.command, ...updater.args].join(" "))}.`);
2556
+ deps.logError(`${chalk17.red("Details:")} ${error instanceof Error ? error.message : String(error)}`);
2557
+ deps.exit(1);
2558
+ }
2559
+ if (exitCode !== 0) {
2560
+ deps.logError(`${chalk17.red("Error:")} update command exited with code ${exitCode}.`);
2561
+ deps.exit(exitCode);
2562
+ }
2563
+ }, updateCommand = async () => {
2564
+ await _runUpdateCommand();
2565
+ };
2566
+ var init_update2 = __esm(() => {
2567
+ init_update();
2568
+ defaultDeps2 = {
2569
+ detectInstallMethod,
2570
+ runPackageManagerCommand,
2571
+ log: console.log,
2572
+ logError: console.error,
2573
+ exit: process.exit
2574
+ };
2575
+ updateCommands = {
2576
+ homebrew: {
2577
+ command: "brew",
2578
+ args: ["upgrade", "dotenc"],
2579
+ label: "Homebrew"
2580
+ },
2581
+ scoop: {
2582
+ command: "scoop",
2583
+ args: ["update", "dotenc"],
2584
+ label: "Scoop"
2585
+ },
2586
+ npm: {
2587
+ command: "npm",
2588
+ args: ["install", "-g", "@dotenc/cli"],
2589
+ label: "npm"
2590
+ }
2591
+ };
2592
+ });
2593
+
2520
2594
  // src/commands/whoami.ts
2521
2595
  var whoamiCommand = async () => {
2522
2596
  const { keys: privateKeys, passphraseProtectedKeys } = await getPrivateKeys();
@@ -2569,11 +2643,92 @@ var init_whoami = __esm(() => {
2569
2643
  init_getPublicKeys();
2570
2644
  });
2571
2645
 
2646
+ // src/helpers/updateNotifier.ts
2647
+ import chalk18 from "chalk";
2648
+ var CHECK_INTERVAL_MS, defaultDeps3, shouldSkipCheck = (args, env) => {
2649
+ if (env.DOTENC_SKIP_UPDATE_CHECK === "1") {
2650
+ return true;
2651
+ }
2652
+ const firstArg = args[0];
2653
+ if (!firstArg)
2654
+ return false;
2655
+ return ["update", "--help", "-h", "--version", "-V", "help"].includes(firstArg);
2656
+ }, parseTimestamp = (value) => {
2657
+ if (!value)
2658
+ return 0;
2659
+ const parsed = Date.parse(value);
2660
+ return Number.isNaN(parsed) ? 0 : parsed;
2661
+ }, persistUpdateState = async (config, updateState, deps) => {
2662
+ try {
2663
+ await deps.setHomeConfig({
2664
+ ...config,
2665
+ update: updateState
2666
+ });
2667
+ } catch {}
2668
+ }, maybeNotifyAboutUpdate = async (depsOverrides = {}) => {
2669
+ const deps = {
2670
+ ...defaultDeps3,
2671
+ ...depsOverrides
2672
+ };
2673
+ if (shouldSkipCheck(deps.args, deps.env)) {
2674
+ return;
2675
+ }
2676
+ let config = {};
2677
+ try {
2678
+ config = await deps.getHomeConfig();
2679
+ } catch {
2680
+ config = {};
2681
+ }
2682
+ let updateState = config.update ?? {};
2683
+ let latestVersion = updateState.latestVersion ?? null;
2684
+ const now = deps.now();
2685
+ const lastCheckedAt = parseTimestamp(updateState.lastCheckedAt);
2686
+ const shouldRefresh = !latestVersion || now - lastCheckedAt >= CHECK_INTERVAL_MS;
2687
+ if (shouldRefresh) {
2688
+ const fetchedVersion = await deps.fetchLatestVersion();
2689
+ updateState = {
2690
+ ...updateState,
2691
+ lastCheckedAt: new Date(now).toISOString(),
2692
+ latestVersion: fetchedVersion ?? latestVersion ?? undefined
2693
+ };
2694
+ latestVersion = updateState.latestVersion ?? null;
2695
+ await persistUpdateState(config, updateState, deps);
2696
+ }
2697
+ if (!latestVersion || !isVersionNewer(latestVersion, deps.currentVersion)) {
2698
+ return;
2699
+ }
2700
+ if (updateState.notifiedVersion === latestVersion) {
2701
+ return;
2702
+ }
2703
+ deps.log(`${chalk18.yellow("Update available:")} ${chalk18.gray(`dotenc ${deps.currentVersion}`)} -> ${chalk18.cyan(`dotenc ${latestVersion}`)}. Run ${chalk18.gray("dotenc update")}.`);
2704
+ updateState = {
2705
+ ...updateState,
2706
+ notifiedVersion: latestVersion
2707
+ };
2708
+ await persistUpdateState(config, updateState, deps);
2709
+ };
2710
+ var init_updateNotifier = __esm(() => {
2711
+ init_package();
2712
+ init_homeConfig();
2713
+ init_update();
2714
+ CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000;
2715
+ defaultDeps3 = {
2716
+ getHomeConfig,
2717
+ setHomeConfig,
2718
+ fetchLatestVersion,
2719
+ currentVersion: package_default.version,
2720
+ now: () => Date.now(),
2721
+ log: console.log,
2722
+ args: process.argv.slice(2),
2723
+ env: process.env
2724
+ };
2725
+ });
2726
+
2572
2727
  // src/program.ts
2573
2728
  var exports_program = {};
2574
2729
  import { Command, Option } from "commander";
2575
2730
  var program, env, auth, key, tools;
2576
- var init_program = __esm(() => {
2731
+ var init_program = __esm(async () => {
2577
2732
  init_package();
2578
2733
  init_grant();
2579
2734
  init_list();
@@ -2592,9 +2747,11 @@ var init_program = __esm(() => {
2592
2747
  init_remove();
2593
2748
  init_run();
2594
2749
  init_textconv();
2595
- init_install_claude_code_skill();
2750
+ init_install_agent_skill();
2596
2751
  init_install_vscode_extension();
2752
+ init_update2();
2597
2753
  init_whoami();
2754
+ init_updateNotifier();
2598
2755
  program = new Command;
2599
2756
  program.name("dotenc").description(package_default.description).version(package_default.version);
2600
2757
  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);
@@ -2616,13 +2773,15 @@ var init_program = __esm(() => {
2616
2773
  key.command("list").description("list all public keys in the project").action(keyListCommand);
2617
2774
  key.command("remove").argument("[name]", "the name of the public key to remove").description("remove a public key from the project").action(keyRemoveCommand);
2618
2775
  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);
2776
+ 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);
2620
2777
  tools.command("install-vscode-extension").description("add dotenc to VS Code / Cursor / Windsurf extension recommendations").action(installVscodeExtensionCommand);
2621
2778
  program.command("textconv", { hidden: true }).argument("<filepath>", "path to the encrypted environment file").description("decrypt an environment file for git diff").action(textconvCommand);
2779
+ program.command("update").description("update dotenc based on your installation method").action(updateCommand);
2622
2780
  program.command("whoami").description("show your identity in this project").action(whoamiCommand);
2623
2781
  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);
2782
+ await maybeNotifyAboutUpdate();
2624
2783
  program.parse();
2625
2784
  });
2626
2785
 
2627
2786
  // src/cli.ts
2628
- Promise.resolve().then(() => init_program());
2787
+ init_program();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotenc/cli",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "🔐 Git-native encrypted environments powered by your SSH keys",
5
5
  "author": "Ivan Filho <i@ivanfilho.com>",
6
6
  "license": "MIT",