@dotenc/cli 0.5.0 → 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 +315 -157
  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.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",
@@ -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
 
@@ -2092,7 +2099,7 @@ Some useful tips:`);
2092
2099
  const devCmd = chalk13.gray("dotenc dev <command>");
2093
2100
  console.log(`- To run with your encrypted env: ${devCmd}`);
2094
2101
  if (existsSync8(".claude") || existsSync8("CLAUDE.md")) {
2095
- console.log(`- Install the Claude Code skill: ${chalk13.gray("dotenc tools install-claude-code-skill")}`);
2102
+ console.log(`- Install the agent skill: ${chalk13.gray("dotenc tools install-agent-skill")}`);
2096
2103
  }
2097
2104
  if (existsSync8(".vscode") || existsSync8(".cursor") || existsSync8(".windsurf")) {
2098
2105
  console.log(`- Add the editor extension: ${chalk13.gray("dotenc tools install-vscode-extension")}`);
@@ -2218,133 +2225,23 @@ var init_textconv = __esm(() => {
2218
2225
  init_getEnvironmentByPath();
2219
2226
  });
2220
2227
 
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";
2228
+ // src/commands/tools/install-agent-skill.ts
2229
+ import { spawn as spawn2 } from "node:child_process";
2226
2230
  import chalk15 from "chalk";
2227
2231
  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([
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([
2348
2245
  {
2349
2246
  type: "list",
2350
2247
  name: "scope",
@@ -2355,48 +2252,68 @@ dotenc auth grant production ci
2355
2252
  ]
2356
2253
  }
2357
2254
  ]);
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);
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);
2365
2274
  }
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.`);
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);
2370
2279
  };
2371
- var init_install_claude_code_skill = () => {};
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
+ });
2372
2289
 
2373
2290
  // src/commands/tools/install-vscode-extension.ts
2374
2291
  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";
2292
+ import { existsSync as existsSync10 } from "node:fs";
2293
+ import fs16 from "node:fs/promises";
2294
+ import path16 from "node:path";
2378
2295
  import { promisify } from "node:util";
2379
2296
  import chalk16 from "chalk";
2380
2297
  import inquirer11 from "inquirer";
2381
2298
  async function addToExtensionsJson() {
2382
- const extensionsJsonPath = path17.join(process.cwd(), ".vscode", "extensions.json");
2299
+ const extensionsJsonPath = path16.join(process.cwd(), ".vscode", "extensions.json");
2383
2300
  let json = {};
2384
- if (existsSync11(extensionsJsonPath)) {
2385
- const content = await fs17.readFile(extensionsJsonPath, "utf-8");
2301
+ if (existsSync10(extensionsJsonPath)) {
2302
+ const content = await fs16.readFile(extensionsJsonPath, "utf-8");
2386
2303
  try {
2387
2304
  json = JSON.parse(content);
2388
2305
  } catch {
2389
2306
  json = {};
2390
2307
  }
2391
2308
  } else {
2392
- await fs17.mkdir(path17.join(process.cwd(), ".vscode"), { recursive: true });
2309
+ await fs16.mkdir(path16.join(process.cwd(), ".vscode"), { recursive: true });
2393
2310
  }
2394
2311
  if (!Array.isArray(json.recommendations)) {
2395
2312
  json.recommendations = [];
2396
2313
  }
2397
2314
  if (!json.recommendations.includes(EXTENSION_ID)) {
2398
2315
  json.recommendations.push(EXTENSION_ID);
2399
- await fs17.writeFile(extensionsJsonPath, JSON.stringify(json, null, 2), "utf-8");
2316
+ await fs16.writeFile(extensionsJsonPath, JSON.stringify(json, null, 2), "utf-8");
2400
2317
  console.log(`${chalk16.green("✓")} Added dotenc to ${chalk16.gray(".vscode/extensions.json")}`);
2401
2318
  } else {
2402
2319
  console.log(`${chalk16.green("✓")} dotenc already in ${chalk16.gray(".vscode/extensions.json")}`);
@@ -2412,11 +2329,11 @@ async function which(bin) {
2412
2329
  }
2413
2330
  async function detectEditors() {
2414
2331
  const detected = [];
2415
- if (existsSync11(path17.join(process.cwd(), ".cursor")))
2332
+ if (existsSync10(path16.join(process.cwd(), ".cursor")))
2416
2333
  detected.push("cursor");
2417
- if (existsSync11(path17.join(process.cwd(), ".windsurf")))
2334
+ if (existsSync10(path16.join(process.cwd(), ".windsurf")))
2418
2335
  detected.push("windsurf");
2419
- if (existsSync11(path17.join(process.cwd(), ".vscode")))
2336
+ if (existsSync10(path16.join(process.cwd(), ".vscode")))
2420
2337
  detected.push("vscode");
2421
2338
  const checks = [
2422
2339
  { key: "cursor", bins: ["cursor"] },
@@ -2432,7 +2349,7 @@ async function detectEditors() {
2432
2349
  vscodium: "/Applications/VSCodium.app"
2433
2350
  };
2434
2351
  for (const [key, appPath] of Object.entries(macApps)) {
2435
- if (!detected.includes(key) && existsSync11(appPath)) {
2352
+ if (!detected.includes(key) && existsSync10(appPath)) {
2436
2353
  detected.push(key);
2437
2354
  }
2438
2355
  }
@@ -2517,6 +2434,162 @@ var init_install_vscode_extension = __esm(() => {
2517
2434
  };
2518
2435
  });
2519
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
+
2520
2593
  // src/commands/whoami.ts
2521
2594
  var whoamiCommand = async () => {
2522
2595
  const { keys: privateKeys, passphraseProtectedKeys } = await getPrivateKeys();
@@ -2569,11 +2642,92 @@ var init_whoami = __esm(() => {
2569
2642
  init_getPublicKeys();
2570
2643
  });
2571
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
+
2572
2726
  // src/program.ts
2573
2727
  var exports_program = {};
2574
2728
  import { Command, Option } from "commander";
2575
2729
  var program, env, auth, key, tools;
2576
- var init_program = __esm(() => {
2730
+ var init_program = __esm(async () => {
2577
2731
  init_package();
2578
2732
  init_grant();
2579
2733
  init_list();
@@ -2592,9 +2746,11 @@ var init_program = __esm(() => {
2592
2746
  init_remove();
2593
2747
  init_run();
2594
2748
  init_textconv();
2595
- init_install_claude_code_skill();
2749
+ init_install_agent_skill();
2596
2750
  init_install_vscode_extension();
2751
+ init_update2();
2597
2752
  init_whoami();
2753
+ init_updateNotifier();
2598
2754
  program = new Command;
2599
2755
  program.name("dotenc").description(package_default.description).version(package_default.version);
2600
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);
@@ -2616,13 +2772,15 @@ var init_program = __esm(() => {
2616
2772
  key.command("list").description("list all public keys in the project").action(keyListCommand);
2617
2773
  key.command("remove").argument("[name]", "the name of the public key to remove").description("remove a public key from the project").action(keyRemoveCommand);
2618
2774
  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);
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);
2620
2776
  tools.command("install-vscode-extension").description("add dotenc to VS Code / Cursor / Windsurf extension recommendations").action(installVscodeExtensionCommand);
2621
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);
2622
2779
  program.command("whoami").description("show your identity in this project").action(whoamiCommand);
2623
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();
2624
2782
  program.parse();
2625
2783
  });
2626
2784
 
2627
2785
  // src/cli.ts
2628
- 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.5.0",
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",