@tolgamorf/env2op-cli 0.2.1 → 0.2.3

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.
@@ -22,7 +22,7 @@ var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports,
22
22
  var require_package = __commonJS((exports, module) => {
23
23
  module.exports = {
24
24
  name: "@tolgamorf/env2op-cli",
25
- version: "0.2.1",
25
+ version: "0.2.3",
26
26
  description: "Convert .env files to 1Password Secure Notes and generate templates for op inject/run",
27
27
  type: "module",
28
28
  main: "dist/index.js",
@@ -102,15 +102,14 @@ var require_package = __commonJS((exports, module) => {
102
102
  });
103
103
 
104
104
  // src/op2env-cli.ts
105
- import pc3 from "picocolors";
105
+ import pc5 from "picocolors";
106
106
 
107
107
  // src/commands/inject.ts
108
- import { existsSync, readFileSync, writeFileSync as writeFileSync2 } from "node:fs";
108
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "node:fs";
109
109
  import { basename } from "node:path";
110
- import * as p2 from "@clack/prompts";
111
110
 
112
- // src/core/env-parser.ts
113
- import { readFile } from "node:fs/promises";
111
+ // src/core/auth.ts
112
+ import * as p from "@clack/prompts";
114
113
 
115
114
  // src/utils/errors.ts
116
115
  class Env2OpError extends Error {
@@ -143,117 +142,17 @@ var errors = {
143
142
  envFileEmpty: (path) => new Env2OpError(`No valid environment variables found in ${path}`, ErrorCodes.ENV_FILE_EMPTY, "Ensure the file contains KEY=value pairs"),
144
143
  opCliNotInstalled: () => new Env2OpError("1Password CLI (op) is not installed", ErrorCodes.OP_CLI_NOT_INSTALLED, "Install it from https://1password.com/downloads/command-line/"),
145
144
  opNotSignedIn: () => new Env2OpError("Not signed in to 1Password CLI", ErrorCodes.OP_NOT_SIGNED_IN, 'Run "op signin" to authenticate'),
145
+ opSigninFailed: () => new Env2OpError("Failed to sign in to 1Password CLI", ErrorCodes.OP_SIGNIN_FAILED, 'Try running "op signin" manually'),
146
146
  vaultNotFound: (vault) => new Env2OpError(`Vault not found: ${vault}`, ErrorCodes.VAULT_NOT_FOUND, 'Run "op vault list" to see available vaults'),
147
147
  vaultCreateFailed: (message) => new Env2OpError(`Failed to create vault: ${message}`, ErrorCodes.VAULT_CREATE_FAILED),
148
148
  itemExists: (title, vault) => new Env2OpError(`Item "${title}" already exists in vault "${vault}"`, ErrorCodes.ITEM_EXISTS, "Use default behavior (overwrites) or choose a different item name"),
149
149
  itemCreateFailed: (message) => new Env2OpError(`Failed to create 1Password item: ${message}`, ErrorCodes.ITEM_CREATE_FAILED),
150
150
  itemEditFailed: (message) => new Env2OpError(`Failed to edit 1Password item: ${message}`, ErrorCodes.ITEM_EDIT_FAILED),
151
- parseError: (line, message) => new Env2OpError(`Parse error at line ${line}: ${message}`, ErrorCodes.PARSE_ERROR)
151
+ parseError: (line, message) => new Env2OpError(`Parse error at line ${line}: ${message}`, ErrorCodes.PARSE_ERROR),
152
+ templateNotFound: (path) => new Env2OpError(`Template file not found: ${path}`, ErrorCodes.TEMPLATE_NOT_FOUND, "Ensure the file exists and the path is correct"),
153
+ injectFailed: (message) => new Env2OpError("Failed to pull secrets from 1Password", ErrorCodes.INJECT_FAILED, message)
152
154
  };
153
155
 
154
- // src/core/env-parser.ts
155
- var HEADER_SEPARATOR = "# ===========================================================================";
156
- function stripHeaders(content) {
157
- const lines = content.split(`
158
- `);
159
- const result = [];
160
- let inHeader = false;
161
- for (const line of lines) {
162
- const trimmed = line.trim();
163
- if (trimmed === HEADER_SEPARATOR) {
164
- if (!inHeader) {
165
- inHeader = true;
166
- } else {
167
- inHeader = false;
168
- }
169
- continue;
170
- }
171
- if (!inHeader) {
172
- result.push(line);
173
- }
174
- }
175
- while (result.length > 0 && result[0]?.trim() === "") {
176
- result.shift();
177
- }
178
- return result.join(`
179
- `);
180
- }
181
- function parseValue(raw) {
182
- const trimmed = raw.trim();
183
- if (trimmed.startsWith('"')) {
184
- const endQuote = trimmed.indexOf('"', 1);
185
- if (endQuote !== -1) {
186
- return trimmed.slice(1, endQuote);
187
- }
188
- }
189
- if (trimmed.startsWith("'")) {
190
- const endQuote = trimmed.indexOf("'", 1);
191
- if (endQuote !== -1) {
192
- return trimmed.slice(1, endQuote);
193
- }
194
- }
195
- const parts = trimmed.split(/\s+#/);
196
- return (parts[0] ?? trimmed).trim();
197
- }
198
- function stripBom(content) {
199
- if (content.charCodeAt(0) === 65279) {
200
- return content.slice(1);
201
- }
202
- return content;
203
- }
204
- async function parseEnvFile(filePath) {
205
- let rawContent;
206
- try {
207
- rawContent = await readFile(filePath, "utf-8");
208
- } catch {
209
- throw errors.envFileNotFound(filePath);
210
- }
211
- const content = stripHeaders(stripBom(rawContent));
212
- const rawLines = content.split(`
213
- `);
214
- const variables = [];
215
- const lines = [];
216
- const parseErrors = [];
217
- let currentComment = "";
218
- for (let i = 0;i < rawLines.length; i++) {
219
- const line = rawLines[i] ?? "";
220
- const trimmed = line.trim();
221
- const lineNumber = i + 1;
222
- if (!trimmed) {
223
- lines.push({ type: "empty" });
224
- currentComment = "";
225
- continue;
226
- }
227
- if (trimmed.startsWith("#")) {
228
- lines.push({ type: "comment", content: line });
229
- currentComment = trimmed.slice(1).trim();
230
- continue;
231
- }
232
- const match = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
233
- if (match?.[1]) {
234
- const key = match[1];
235
- const rawValue = match[2] ?? "";
236
- const value = parseValue(rawValue);
237
- variables.push({
238
- key,
239
- value,
240
- comment: currentComment || undefined,
241
- line: lineNumber
242
- });
243
- lines.push({ type: "variable", key, value });
244
- currentComment = "";
245
- } else if (trimmed.includes("=")) {
246
- parseErrors.push(`Line ${lineNumber}: Invalid variable name`);
247
- }
248
- }
249
- return { variables, lines, errors: parseErrors };
250
- }
251
- function validateParseResult(result, filePath) {
252
- if (result.variables.length === 0) {
253
- throw errors.envFileEmpty(filePath);
254
- }
255
- }
256
-
257
156
  // src/utils/shell.ts
258
157
  import { spawn } from "node:child_process";
259
158
  import pc from "picocolors";
@@ -263,16 +162,8 @@ function quoteArg(arg) {
263
162
  }
264
163
  return arg;
265
164
  }
266
- async function exec(command, args = [], options = {}) {
267
- const { verbose = false } = options;
268
- const fullCommand = `${command} ${args.map(quoteArg).join(" ")}`;
269
- if (verbose) {
270
- console.log(pc.dim(`$ ${fullCommand}`));
271
- }
165
+ function collectOutput(proc, verbose) {
272
166
  return new Promise((resolve) => {
273
- const proc = spawn(command, args, {
274
- stdio: ["ignore", "pipe", "pipe"]
275
- });
276
167
  const stdoutChunks = [];
277
168
  const stderrChunks = [];
278
169
  proc.stdout?.on("data", (data) => {
@@ -306,48 +197,29 @@ async function exec(command, args = [], options = {}) {
306
197
  });
307
198
  });
308
199
  }
200
+ async function exec(command, args = [], options = {}) {
201
+ const { verbose = false } = options;
202
+ const fullCommand = `${command} ${args.map(quoteArg).join(" ")}`;
203
+ if (verbose) {
204
+ console.log(pc.dim(`$ ${fullCommand}`));
205
+ }
206
+ const proc = spawn(command, args, {
207
+ stdio: ["ignore", "pipe", "pipe"]
208
+ });
209
+ return collectOutput(proc, verbose);
210
+ }
309
211
  async function execWithStdin(command, args = [], options) {
310
212
  const { stdin: stdinContent, verbose = false } = options;
311
213
  if (verbose) {
312
214
  const fullCommand = `${command} ${args.map(quoteArg).join(" ")}`;
313
215
  console.log(pc.dim(`$ echo '...' | ${fullCommand}`));
314
216
  }
315
- return new Promise((resolve) => {
316
- const proc = spawn(command, args, {
317
- stdio: ["pipe", "pipe", "pipe"]
318
- });
319
- const stdoutChunks = [];
320
- const stderrChunks = [];
321
- proc.stdin?.write(stdinContent);
322
- proc.stdin?.end();
323
- proc.stdout?.on("data", (data) => {
324
- const text = Buffer.isBuffer(data) ? data.toString() : String(data);
325
- stdoutChunks.push(text);
326
- if (verbose)
327
- process.stdout.write(text);
328
- });
329
- proc.stderr?.on("data", (data) => {
330
- const text = Buffer.isBuffer(data) ? data.toString() : String(data);
331
- stderrChunks.push(text);
332
- if (verbose)
333
- process.stderr.write(text);
334
- });
335
- proc.on("close", (code) => {
336
- resolve({
337
- stdout: stdoutChunks.join(""),
338
- stderr: stderrChunks.join(""),
339
- exitCode: code ?? 1
340
- });
341
- });
342
- proc.on("error", (err) => {
343
- stderrChunks.push(err.message);
344
- resolve({
345
- stdout: stdoutChunks.join(""),
346
- stderr: stderrChunks.join(""),
347
- exitCode: 1
348
- });
349
- });
217
+ const proc = spawn(command, args, {
218
+ stdio: ["pipe", "pipe", "pipe"]
350
219
  });
220
+ proc.stdin?.write(stdinContent);
221
+ proc.stdin?.end();
222
+ return collectOutput(proc, verbose);
351
223
  }
352
224
 
353
225
  // src/core/onepassword.ts
@@ -469,10 +341,332 @@ async function editSecureNote(options) {
469
341
  }
470
342
  }
471
343
 
344
+ // src/core/auth.ts
345
+ async function ensureOpAuthenticated(options) {
346
+ const { verbose } = options;
347
+ const authSpinner = p.spinner();
348
+ authSpinner.start("Checking 1Password CLI...");
349
+ const opInstalled = await checkOpCli({ verbose });
350
+ if (!opInstalled) {
351
+ authSpinner.stop("1Password CLI not found");
352
+ throw errors.opCliNotInstalled();
353
+ }
354
+ let signedIn = await checkSignedIn({ verbose });
355
+ if (!signedIn) {
356
+ authSpinner.message("Signing in to 1Password...");
357
+ const signInSuccess = await signIn({ verbose });
358
+ if (!signInSuccess) {
359
+ authSpinner.stop();
360
+ throw errors.opSigninFailed();
361
+ }
362
+ signedIn = await checkSignedIn({ verbose });
363
+ if (!signedIn) {
364
+ authSpinner.stop();
365
+ throw errors.opNotSignedIn();
366
+ }
367
+ }
368
+ authSpinner.stop("1Password CLI ready");
369
+ }
370
+
371
+ // src/core/env-parser.ts
372
+ import { readFile } from "node:fs/promises";
373
+
374
+ // src/core/constants.ts
375
+ var HEADER_SEPARATOR = `# ${"=".repeat(75)}`;
376
+
377
+ // src/core/env-parser.ts
378
+ function stripHeaders(content) {
379
+ const lines = content.split(`
380
+ `);
381
+ const result = [];
382
+ let inHeader = false;
383
+ for (const line of lines) {
384
+ const trimmed = line.trim();
385
+ if (trimmed === HEADER_SEPARATOR) {
386
+ if (!inHeader) {
387
+ inHeader = true;
388
+ } else {
389
+ inHeader = false;
390
+ }
391
+ continue;
392
+ }
393
+ if (!inHeader) {
394
+ result.push(line);
395
+ }
396
+ }
397
+ while (result.length > 0 && result[0]?.trim() === "") {
398
+ result.shift();
399
+ }
400
+ return result.join(`
401
+ `);
402
+ }
403
+ function parseValue(raw) {
404
+ const trimmed = raw.trim();
405
+ if (trimmed.startsWith('"')) {
406
+ const endQuote = trimmed.indexOf('"', 1);
407
+ if (endQuote !== -1) {
408
+ return trimmed.slice(1, endQuote);
409
+ }
410
+ }
411
+ if (trimmed.startsWith("'")) {
412
+ const endQuote = trimmed.indexOf("'", 1);
413
+ if (endQuote !== -1) {
414
+ return trimmed.slice(1, endQuote);
415
+ }
416
+ }
417
+ const parts = trimmed.split(/\s+#/);
418
+ return (parts[0] ?? trimmed).trim();
419
+ }
420
+ function stripBom(content) {
421
+ if (content.charCodeAt(0) === 65279) {
422
+ return content.slice(1);
423
+ }
424
+ return content;
425
+ }
426
+ async function parseEnvFile(filePath) {
427
+ let rawContent;
428
+ try {
429
+ rawContent = await readFile(filePath, "utf-8");
430
+ } catch {
431
+ throw errors.envFileNotFound(filePath);
432
+ }
433
+ const content = stripHeaders(stripBom(rawContent));
434
+ const rawLines = content.split(`
435
+ `);
436
+ const variables = [];
437
+ const lines = [];
438
+ const parseErrors = [];
439
+ let currentComment = "";
440
+ for (let i = 0;i < rawLines.length; i++) {
441
+ const line = rawLines[i] ?? "";
442
+ const trimmed = line.trim();
443
+ const lineNumber = i + 1;
444
+ if (!trimmed) {
445
+ lines.push({ type: "empty" });
446
+ currentComment = "";
447
+ continue;
448
+ }
449
+ if (trimmed.startsWith("#")) {
450
+ lines.push({ type: "comment", content: line });
451
+ currentComment = trimmed.slice(1).trim();
452
+ continue;
453
+ }
454
+ const match = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
455
+ if (match?.[1]) {
456
+ const key = match[1];
457
+ const rawValue = match[2] ?? "";
458
+ const value = parseValue(rawValue);
459
+ variables.push({
460
+ key,
461
+ value,
462
+ comment: currentComment || undefined,
463
+ line: lineNumber
464
+ });
465
+ lines.push({ type: "variable", key, value });
466
+ currentComment = "";
467
+ } else if (trimmed.includes("=")) {
468
+ parseErrors.push(`Line ${lineNumber}: Invalid variable name`);
469
+ }
470
+ }
471
+ return { variables, lines, errors: parseErrors };
472
+ }
473
+ function validateParseResult(result, filePath) {
474
+ if (result.variables.length === 0) {
475
+ throw errors.envFileEmpty(filePath);
476
+ }
477
+ }
478
+
479
+ // src/core/template-generator.ts
480
+ import { writeFileSync as writeFileSync2 } from "node:fs";
481
+
482
+ // src/lib/update.ts
483
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
484
+ import { homedir } from "node:os";
485
+ import { join } from "node:path";
486
+
487
+ // src/lib/package-manager.ts
488
+ var UPDATE_COMMANDS = {
489
+ homebrew: "brew upgrade tolgamorf/tap/env2op-cli",
490
+ npm: "npm update -g @tolgamorf/env2op-cli",
491
+ bun: "bun update -g @tolgamorf/env2op-cli",
492
+ pnpm: "pnpm update -g @tolgamorf/env2op-cli",
493
+ unknown: "npm update -g @tolgamorf/env2op-cli"
494
+ };
495
+ var DISPLAY_NAMES = {
496
+ homebrew: "Homebrew",
497
+ npm: "npm",
498
+ bun: "Bun",
499
+ pnpm: "pnpm",
500
+ unknown: "npm (default)"
501
+ };
502
+ function detectFromPath() {
503
+ const binPath = process.argv[1] ?? "";
504
+ if (binPath.includes("/Cellar/") || binPath.includes("/homebrew/") || binPath.includes("/opt/homebrew/") || binPath.includes("/home/linuxbrew/")) {
505
+ return "homebrew";
506
+ }
507
+ if (binPath.includes("/.bun/")) {
508
+ return "bun";
509
+ }
510
+ if (binPath.includes("/pnpm/") || binPath.includes("/.pnpm/")) {
511
+ return "pnpm";
512
+ }
513
+ if (binPath.includes("/node_modules/")) {
514
+ return "npm";
515
+ }
516
+ return null;
517
+ }
518
+ async function detectFromCommands() {
519
+ const brewResult = await exec("brew", ["list", "env2op-cli"], { verbose: false });
520
+ if (brewResult.exitCode === 0) {
521
+ return "homebrew";
522
+ }
523
+ return "npm";
524
+ }
525
+ async function detectPackageManager() {
526
+ const fromPath = detectFromPath();
527
+ if (fromPath) {
528
+ return {
529
+ type: fromPath,
530
+ updateCommand: UPDATE_COMMANDS[fromPath],
531
+ displayName: DISPLAY_NAMES[fromPath]
532
+ };
533
+ }
534
+ const fromCommands = await detectFromCommands();
535
+ return {
536
+ type: fromCommands,
537
+ updateCommand: UPDATE_COMMANDS[fromCommands],
538
+ displayName: DISPLAY_NAMES[fromCommands]
539
+ };
540
+ }
541
+
542
+ // src/lib/update.ts
543
+ var CACHE_DIR = join(homedir(), ".env2op");
544
+ var CACHE_FILE = join(CACHE_DIR, "update-check.json");
545
+ var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
546
+ function getCliVersion() {
547
+ try {
548
+ const pkg = require_package();
549
+ return pkg.version ?? "0.0.0";
550
+ } catch {
551
+ return "0.0.0";
552
+ }
553
+ }
554
+ function loadCache() {
555
+ try {
556
+ if (existsSync(CACHE_FILE)) {
557
+ const content = readFileSync(CACHE_FILE, "utf-8");
558
+ return JSON.parse(content);
559
+ }
560
+ } catch {}
561
+ return { lastCheck: 0, latestVersion: null };
562
+ }
563
+ function saveCache(cache) {
564
+ try {
565
+ if (!existsSync(CACHE_DIR)) {
566
+ mkdirSync(CACHE_DIR, { recursive: true });
567
+ }
568
+ writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2));
569
+ } catch {}
570
+ }
571
+ function shouldCheckForUpdate(cache) {
572
+ const now = Date.now();
573
+ return now - cache.lastCheck > CHECK_INTERVAL_MS;
574
+ }
575
+ async function fetchLatestVersion() {
576
+ try {
577
+ const response = await fetch("https://registry.npmjs.org/@tolgamorf/env2op-cli/latest");
578
+ if (!response.ok)
579
+ return null;
580
+ const data = await response.json();
581
+ return data.version ?? null;
582
+ } catch {
583
+ return null;
584
+ }
585
+ }
586
+ function compareVersions(v1, v2) {
587
+ const parts1 = v1.split(".").map(Number);
588
+ const parts2 = v2.split(".").map(Number);
589
+ for (let i = 0;i < 3; i++) {
590
+ const p1 = parts1[i] || 0;
591
+ const p2 = parts2[i] || 0;
592
+ if (p1 < p2)
593
+ return -1;
594
+ if (p1 > p2)
595
+ return 1;
596
+ }
597
+ return 0;
598
+ }
599
+ async function checkForUpdate(forceCheck = false) {
600
+ const currentVersion = getCliVersion();
601
+ const cache = loadCache();
602
+ if (!forceCheck && !shouldCheckForUpdate(cache) && cache.latestVersion) {
603
+ const updateAvailable2 = compareVersions(currentVersion, cache.latestVersion) < 0;
604
+ const isSkipped2 = cache.skipVersion === cache.latestVersion;
605
+ return {
606
+ currentVersion,
607
+ latestVersion: cache.latestVersion,
608
+ updateAvailable: updateAvailable2,
609
+ isSkipped: isSkipped2,
610
+ fromCache: true
611
+ };
612
+ }
613
+ const latestVersion = await fetchLatestVersion();
614
+ saveCache({
615
+ ...cache,
616
+ lastCheck: Date.now(),
617
+ latestVersion
618
+ });
619
+ if (!latestVersion) {
620
+ return {
621
+ currentVersion,
622
+ latestVersion: null,
623
+ updateAvailable: false,
624
+ isSkipped: false,
625
+ fromCache: false
626
+ };
627
+ }
628
+ const updateAvailable = compareVersions(currentVersion, latestVersion) < 0;
629
+ const isSkipped = cache.skipVersion === latestVersion;
630
+ return {
631
+ currentVersion,
632
+ latestVersion,
633
+ updateAvailable,
634
+ isSkipped,
635
+ fromCache: false
636
+ };
637
+ }
638
+ async function performUpdate(pm) {
639
+ const packageManager = pm ?? await detectPackageManager();
640
+ try {
641
+ const [command, ...args] = packageManager.updateCommand.split(" ");
642
+ const result = await exec(command, args, { verbose: false });
643
+ if (result.exitCode !== 0) {
644
+ return {
645
+ success: false,
646
+ error: result.stderr || `Command exited with code ${result.exitCode}`
647
+ };
648
+ }
649
+ return { success: true };
650
+ } catch (error) {
651
+ const message = error instanceof Error ? error.message : String(error);
652
+ return { success: false, error: message };
653
+ }
654
+ }
655
+ function skipVersion(version) {
656
+ const cache = loadCache();
657
+ cache.skipVersion = version;
658
+ saveCache(cache);
659
+ }
660
+ async function maybeShowUpdateNotification(cliName, showNotification) {
661
+ try {
662
+ const result = await checkForUpdate();
663
+ if (result.updateAvailable && !result.isSkipped) {
664
+ showNotification(result, cliName);
665
+ }
666
+ } catch {}
667
+ }
668
+
472
669
  // src/core/template-generator.ts
473
- var import__package = __toESM(require_package(), 1);
474
- import { writeFileSync } from "node:fs";
475
- var SEPARATOR = "# ===========================================================================";
476
670
  function deriveEnvFileName(templateFileName) {
477
671
  if (templateFileName.endsWith(".tpl")) {
478
672
  return templateFileName.slice(0, -4);
@@ -488,7 +682,7 @@ function formatTimestamp() {
488
682
  function generateTemplateHeader(templateFileName) {
489
683
  const envFileName = deriveEnvFileName(templateFileName);
490
684
  return [
491
- SEPARATOR,
685
+ HEADER_SEPARATOR,
492
686
  `# ${templateFileName} — 1Password Secret References`,
493
687
  "#",
494
688
  "# This template contains references to secrets stored in 1Password.",
@@ -501,27 +695,27 @@ function generateTemplateHeader(templateFileName) {
501
695
  `# op run --env-file ${templateFileName} -- npm start`,
502
696
  "#",
503
697
  `# Pushed: ${formatTimestamp()}`,
504
- `# Generated by env2op v${import__package.default.version}`,
698
+ `# Generated by env2op v${getCliVersion()}`,
505
699
  "# https://github.com/tolgamorf/env2op-cli",
506
- SEPARATOR,
700
+ HEADER_SEPARATOR,
507
701
  ""
508
702
  ];
509
703
  }
510
704
  function generateEnvHeader(envFileName) {
511
705
  const templateFileName = deriveTemplateFileName(envFileName);
512
706
  return [
513
- SEPARATOR,
707
+ HEADER_SEPARATOR,
514
708
  `# ${envFileName} — Environment Variables`,
515
709
  "#",
516
710
  "# WARNING: This file contains sensitive values. Do not commit to git!",
517
711
  "#",
518
712
  `# To push updates to 1Password and generate ${templateFileName}:`,
519
- `# env2op ${envFileName} <vault> "<item_name>"`,
713
+ `# env2op ${envFileName} "<vault>" "<item_name>"`,
520
714
  "#",
521
715
  `# Pulled: ${formatTimestamp()}`,
522
- `# Generated by op2env v${import__package.default.version}`,
716
+ `# Generated by op2env v${getCliVersion()}`,
523
717
  "# https://github.com/tolgamorf/env2op-cli",
524
- SEPARATOR,
718
+ HEADER_SEPARATOR,
525
719
  "",
526
720
  ""
527
721
  ];
@@ -549,7 +743,7 @@ function generateTemplateContent(options, templateFileName) {
549
743
  `;
550
744
  }
551
745
  function writeTemplate(content, outputPath) {
552
- writeFileSync(outputPath, content, "utf-8");
746
+ writeFileSync2(outputPath, content, "utf-8");
553
747
  }
554
748
  function generateUsageInstructions(templatePath) {
555
749
  return ["Usage:", ` op2env ${templatePath}`, ` op run --env-file ${templatePath} -- npm start`].join(`
@@ -557,7 +751,7 @@ function generateUsageInstructions(templatePath) {
557
751
  }
558
752
 
559
753
  // src/utils/logger.ts
560
- import * as p from "@clack/prompts";
754
+ import * as p2 from "@clack/prompts";
561
755
  import pc2 from "picocolors";
562
756
  var symbols = {
563
757
  success: pc2.green("✓"),
@@ -570,29 +764,29 @@ var symbols = {
570
764
  var logger = {
571
765
  intro(name, version, dryRun = false) {
572
766
  const label = dryRun ? pc2.bgYellow(pc2.black(` ${name} v${version} [DRY RUN] `)) : pc2.bgCyan(pc2.black(` ${name} v${version} `));
573
- p.intro(label);
767
+ p2.intro(label);
574
768
  },
575
769
  section(title) {
576
770
  console.log(`
577
771
  ${pc2.bold(pc2.underline(title))}`);
578
772
  },
579
773
  success(message) {
580
- p.log.success(message);
774
+ p2.log.success(message);
581
775
  },
582
776
  error(message) {
583
- p.log.error(message);
777
+ p2.log.error(message);
584
778
  },
585
779
  warn(message) {
586
- p.log.warn(message);
780
+ p2.log.warn(message);
587
781
  },
588
782
  info(message) {
589
- p.log.info(message);
783
+ p2.log.info(message);
590
784
  },
591
785
  step(message) {
592
- p.log.step(message);
786
+ p2.log.step(message);
593
787
  },
594
788
  message(message) {
595
- p.log.message(message);
789
+ p2.log.message(message);
596
790
  },
597
791
  keyValue(key, value, indent = 2) {
598
792
  console.log(`${" ".repeat(indent)}${pc2.dim(key)}: ${pc2.cyan(value)}`);
@@ -607,16 +801,16 @@ ${pc2.bold(pc2.underline(title))}`);
607
801
  console.log(`${pc2.yellow("[DRY RUN]")} ${message}`);
608
802
  },
609
803
  spinner() {
610
- return p.spinner();
804
+ return p2.spinner();
611
805
  },
612
806
  outro(message) {
613
- p.outro(pc2.green(message));
807
+ p2.outro(pc2.green(message));
614
808
  },
615
809
  cancel(message) {
616
- p.cancel(message);
810
+ p2.cancel(message);
617
811
  },
618
812
  note(message, title) {
619
- p.note(message, title);
813
+ p2.note(message, title);
620
814
  },
621
815
  formatFields(fields, max = 3) {
622
816
  if (fields.length <= max) {
@@ -626,6 +820,28 @@ ${pc2.bold(pc2.underline(title))}`);
626
820
  }
627
821
  };
628
822
 
823
+ // src/utils/error-handler.ts
824
+ function handleCommandError(error) {
825
+ if (error instanceof Env2OpError) {
826
+ logger.error(error.message);
827
+ if (error.suggestion) {
828
+ logger.info(`Suggestion: ${error.suggestion}`);
829
+ }
830
+ process.exit(1);
831
+ }
832
+ throw error;
833
+ }
834
+
835
+ // src/utils/prompts.ts
836
+ import * as p3 from "@clack/prompts";
837
+ async function confirmOrExit(message) {
838
+ const confirmed = await p3.confirm({ message });
839
+ if (p3.isCancel(confirmed) || !confirmed) {
840
+ logger.cancel("Operation cancelled");
841
+ process.exit(0);
842
+ }
843
+ }
844
+
629
845
  // src/utils/timing.ts
630
846
  import { setTimeout } from "node:timers/promises";
631
847
  var MIN_SPINNER_TIME = 500;
@@ -644,38 +860,16 @@ function deriveOutputPath(templatePath) {
644
860
  async function runInject(options) {
645
861
  const { templateFile, output, dryRun, force, verbose } = options;
646
862
  const outputPath = output ?? deriveOutputPath(templateFile);
647
- const pkg2 = await Promise.resolve().then(() => __toESM(require_package(), 1));
648
- logger.intro("op2env", pkg2.version, dryRun);
863
+ logger.intro("op2env", getCliVersion(), dryRun);
649
864
  try {
650
- if (!existsSync(templateFile)) {
651
- throw new Env2OpError(`Template file not found: ${templateFile}`, "TEMPLATE_NOT_FOUND", "Ensure the file exists and the path is correct");
865
+ if (!existsSync2(templateFile)) {
866
+ throw errors.templateNotFound(templateFile);
652
867
  }
653
868
  logger.success(`Found template: ${basename(templateFile)}`);
654
869
  if (!dryRun) {
655
- const authSpinner = p2.spinner();
656
- authSpinner.start("Checking 1Password CLI...");
657
- const opInstalled = await checkOpCli({ verbose });
658
- if (!opInstalled) {
659
- authSpinner.stop("1Password CLI not found");
660
- throw new Env2OpError("1Password CLI (op) is not installed", "OP_CLI_NOT_INSTALLED", "Install from https://1password.com/downloads/command-line/");
661
- }
662
- let signedIn = await checkSignedIn({ verbose });
663
- if (!signedIn) {
664
- authSpinner.message("Signing in to 1Password...");
665
- const signInSuccess = await signIn({ verbose });
666
- if (!signInSuccess) {
667
- authSpinner.stop();
668
- throw new Env2OpError("Failed to sign in to 1Password CLI", "OP_SIGNIN_FAILED", 'Try running "op signin" manually');
669
- }
670
- signedIn = await checkSignedIn({ verbose });
671
- if (!signedIn) {
672
- authSpinner.stop();
673
- throw new Env2OpError("Not signed in to 1Password CLI", "OP_NOT_SIGNED_IN", 'Run "op signin" to authenticate');
674
- }
675
- }
676
- authSpinner.stop("1Password CLI ready");
870
+ await ensureOpAuthenticated({ verbose });
677
871
  }
678
- const outputExists = existsSync(outputPath);
872
+ const outputExists = existsSync2(outputPath);
679
873
  if (dryRun) {
680
874
  if (outputExists) {
681
875
  logger.warn(`Would overwrite: ${outputPath}`);
@@ -686,13 +880,7 @@ async function runInject(options) {
686
880
  return;
687
881
  }
688
882
  if (outputExists && !force) {
689
- const shouldOverwrite = await p2.confirm({
690
- message: `File "${outputPath}" already exists. Overwrite?`
691
- });
692
- if (p2.isCancel(shouldOverwrite) || !shouldOverwrite) {
693
- logger.cancel("Operation cancelled");
694
- process.exit(0);
695
- }
883
+ await confirmOrExit(`File "${outputPath}" already exists. Overwrite?`);
696
884
  }
697
885
  const spinner3 = verbose ? null : logger.spinner();
698
886
  spinner3?.start("Pulling secrets from 1Password...");
@@ -701,11 +889,11 @@ async function runInject(options) {
701
889
  if (result.exitCode !== 0) {
702
890
  throw new Error(result.stderr);
703
891
  }
704
- const rawContent = readFileSync(outputPath, "utf-8");
892
+ const rawContent = readFileSync2(outputPath, "utf-8");
705
893
  const envContent = stripHeaders(rawContent);
706
894
  const header = generateEnvHeader(basename(outputPath)).join(`
707
895
  `);
708
- writeFileSync2(outputPath, header + envContent, "utf-8");
896
+ writeFileSync3(outputPath, header + envContent, "utf-8");
709
897
  const varCount = envContent.split(`
710
898
  `).filter((line) => line.trim() && !line.trim().startsWith("#")).length;
711
899
  const stopMessage = `Generated ${basename(outputPath)} — ${varCount} variable${varCount === 1 ? "" : "s"}`;
@@ -718,49 +906,146 @@ async function runInject(options) {
718
906
  spinner3?.stop("Failed to pull secrets");
719
907
  const stderr = error?.stderr;
720
908
  const message = stderr || (error instanceof Error ? error.message : String(error));
721
- throw new Env2OpError("Failed to pull secrets from 1Password", "INJECT_FAILED", message);
909
+ throw errors.injectFailed(message);
722
910
  }
723
911
  logger.outro("Done! Your .env file is ready");
724
912
  } catch (error) {
725
- if (error instanceof Env2OpError) {
726
- logger.error(error.message);
727
- if (error.suggestion) {
728
- logger.info(`Suggestion: ${error.suggestion}`);
729
- }
730
- process.exit(1);
731
- }
732
- throw error;
913
+ handleCommandError(error);
733
914
  }
734
915
  }
735
916
 
736
- // src/op2env-cli.ts
737
- var pkg2 = await Promise.resolve().then(() => __toESM(require_package(), 1));
738
- var args = process.argv.slice(2);
739
- var flags = new Set;
740
- var positional = [];
741
- var options = {};
742
- for (let i = 0;i < args.length; i++) {
743
- const arg = args[i];
744
- if (arg === "-o" || arg === "--output") {
745
- const next = args[i + 1];
746
- if (next && !next.startsWith("-")) {
747
- options.output = next;
748
- i++;
917
+ // src/commands/update.ts
918
+ import * as p5 from "@clack/prompts";
919
+ import pc4 from "picocolors";
920
+
921
+ // src/lib/update-prompts.ts
922
+ import * as p4 from "@clack/prompts";
923
+ import pc3 from "picocolors";
924
+ var S_BAR_START = "┌";
925
+ var S_BAR_END = "";
926
+ function showUpdateNotification(result, cliName = "env2op") {
927
+ console.log();
928
+ console.log(`${pc3.gray(S_BAR_START)}${pc3.gray("─")} ${pc3.yellow("Update available:")} ${pc3.dim(result.currentVersion)} ${pc3.dim("→")} ${pc3.green(result.latestVersion)}`);
929
+ console.log(`${pc3.gray(S_BAR_END)}${pc3.gray("─")} Run ${pc3.cyan(`'${cliName} update'`)} to update`);
930
+ }
931
+ async function askToUpdate(result) {
932
+ const response = await p4.select({
933
+ message: "Would you like to update?",
934
+ options: [
935
+ { value: "update", label: "Update now", hint: "Download and install the latest version" },
936
+ { value: "later", label: "Remind me later", hint: "Ask again next time" },
937
+ { value: "skip", label: "Skip this version", hint: `Don't ask about ${result.latestVersion} again` }
938
+ ]
939
+ });
940
+ if (p4.isCancel(response)) {
941
+ return "later";
942
+ }
943
+ return response;
944
+ }
945
+ function showUpdateAvailable(result) {
946
+ p4.log.success(`Update available: ${pc3.dim(result.currentVersion)} ${pc3.dim("→")} ${pc3.green(result.latestVersion)}`);
947
+ }
948
+ function showUpToDate(currentVersion) {
949
+ p4.log.success(`You're on the latest version ${pc3.green(`(${currentVersion})`)}`);
950
+ }
951
+ function showPackageManagerInfo(pm) {
952
+ console.log();
953
+ console.log(` ${pc3.dim("Detected:")} ${pm.displayName} installation`);
954
+ console.log(` ${pc3.dim("Command:")} ${pc3.cyan(pm.updateCommand)}`);
955
+ console.log();
956
+ }
957
+ function showUpdateSuccess(newVersion) {
958
+ p4.log.success(`Updated to version ${pc3.green(newVersion)}`);
959
+ p4.log.info("Please restart to use the new version.");
960
+ }
961
+ function showUpdateError(error, pm) {
962
+ if (error) {
963
+ p4.log.error(pc3.dim(error));
964
+ }
965
+ p4.log.info(`Try running manually: ${pc3.cyan(pm.updateCommand)}`);
966
+ }
967
+
968
+ // src/commands/update.ts
969
+ async function runUpdate(options) {
970
+ const { force = false, cliName = "env2op" } = options;
971
+ p5.intro(pc4.bgCyan(pc4.black(` ${cliName} update `)));
972
+ const spinner4 = p5.spinner();
973
+ spinner4.start("Checking for updates...");
974
+ const result = await checkForUpdate(true);
975
+ spinner4.stop("Checked for updates");
976
+ if (!result.updateAvailable || !result.latestVersion) {
977
+ showUpToDate(result.currentVersion);
978
+ return;
979
+ }
980
+ showUpdateAvailable(result);
981
+ const pm = await detectPackageManager();
982
+ showPackageManagerInfo(pm);
983
+ if (!force) {
984
+ const choice = await askToUpdate(result);
985
+ if (choice === "skip") {
986
+ skipVersion(result.latestVersion);
987
+ p5.log.info(`Skipped version ${result.latestVersion}`);
988
+ return;
749
989
  }
750
- } else if (arg.startsWith("--")) {
751
- flags.add(arg.slice(2));
752
- } else if (arg.startsWith("-")) {
753
- for (const char of arg.slice(1)) {
754
- flags.add(char);
990
+ if (choice === "later") {
991
+ p5.log.info("Update postponed");
992
+ return;
755
993
  }
994
+ }
995
+ const updateSpinner = p5.spinner();
996
+ updateSpinner.start(`Updating to ${result.latestVersion}...`);
997
+ const updateResult = await performUpdate(pm);
998
+ if (updateResult.success) {
999
+ updateSpinner.stop("Update completed");
1000
+ showUpdateSuccess(result.latestVersion);
756
1001
  } else {
757
- positional.push(arg);
1002
+ updateSpinner.stop("Update failed");
1003
+ showUpdateError(updateResult.error, pm);
1004
+ process.exit(1);
758
1005
  }
759
1006
  }
1007
+
1008
+ // src/utils/args.ts
1009
+ function parseArgs(args) {
1010
+ const flags = new Set;
1011
+ const positional = [];
1012
+ const options = {};
1013
+ for (let i = 0;i < args.length; i++) {
1014
+ const arg = args[i];
1015
+ if (arg === "-o" || arg === "--output") {
1016
+ const next = args[i + 1];
1017
+ if (next && !next.startsWith("-")) {
1018
+ options.output = next;
1019
+ i++;
1020
+ }
1021
+ } else if (arg.startsWith("--")) {
1022
+ flags.add(arg.slice(2));
1023
+ } else if (arg.startsWith("-")) {
1024
+ for (const char of arg.slice(1)) {
1025
+ flags.add(char);
1026
+ }
1027
+ } else {
1028
+ positional.push(arg);
1029
+ }
1030
+ }
1031
+ return { flags, positional, options };
1032
+ }
1033
+
1034
+ // src/op2env-cli.ts
1035
+ var { flags, positional, options } = parseArgs(process.argv.slice(2));
760
1036
  var hasHelp = flags.has("h") || flags.has("help");
761
1037
  var hasVersion = flags.has("v") || flags.has("version");
1038
+ var hasUpdate = flags.has("update");
762
1039
  if (hasVersion) {
763
- console.log(pkg2.version);
1040
+ console.log(getCliVersion());
1041
+ process.exit(0);
1042
+ }
1043
+ if (hasUpdate) {
1044
+ await runUpdate({
1045
+ force: flags.has("f") || flags.has("force"),
1046
+ verbose: flags.has("verbose"),
1047
+ cliName: "op2env"
1048
+ });
764
1049
  process.exit(0);
765
1050
  }
766
1051
  if (hasHelp || positional.length === 0) {
@@ -775,41 +1060,43 @@ await runInject({
775
1060
  force: flags.has("f") || flags.has("force"),
776
1061
  verbose: flags.has("verbose")
777
1062
  });
1063
+ await maybeShowUpdateNotification("op2env", showUpdateNotification);
778
1064
  function showHelp() {
779
- const name = pc3.bold(pc3.cyan("op2env"));
780
- const version = pc3.dim(`v${pkg2.version}`);
1065
+ const name = pc5.bold(pc5.cyan("op2env"));
1066
+ const version = pc5.dim(`v${getCliVersion()}`);
781
1067
  console.log(`
782
1068
  ${name} ${version}
783
1069
  Pull secrets from 1Password to generate .env files
784
1070
 
785
- ${pc3.bold("USAGE")}
786
- ${pc3.cyan("$")} op2env ${pc3.yellow("<template_file>")} ${pc3.dim("[options]")}
1071
+ ${pc5.bold("USAGE")}
1072
+ ${pc5.cyan("$")} op2env ${pc5.yellow("<template_file>")} ${pc5.dim("[options]")}
787
1073
 
788
- ${pc3.bold("ARGUMENTS")}
789
- ${pc3.yellow("template_file")} Path to .env.tpl template file
1074
+ ${pc5.bold("ARGUMENTS")}
1075
+ ${pc5.yellow("template_file")} Path to .env.tpl template file
790
1076
 
791
- ${pc3.bold("OPTIONS")}
792
- ${pc3.cyan("-o, --output")} Output .env path (default: template without .tpl)
793
- ${pc3.cyan("-f, --force")} Overwrite without prompting
794
- ${pc3.cyan("--dry-run")} Preview actions without executing
795
- ${pc3.cyan("--verbose")} Show op CLI output
796
- ${pc3.cyan("-h, --help")} Show this help message
797
- ${pc3.cyan("-v, --version")} Show version
1077
+ ${pc5.bold("OPTIONS")}
1078
+ ${pc5.cyan("-o, --output")} Output .env path (default: template without .tpl)
1079
+ ${pc5.cyan("-f, --force")} Overwrite without prompting
1080
+ ${pc5.cyan(" --dry-run")} Preview actions without executing
1081
+ ${pc5.cyan(" --verbose")} Show op CLI output
1082
+ ${pc5.cyan(" --update")} Check for and install updates
1083
+ ${pc5.cyan("-v, --version")} Show version
1084
+ ${pc5.cyan("-h, --help")} Show this help message
798
1085
 
799
- ${pc3.bold("EXAMPLES")}
800
- ${pc3.dim("# Basic usage - generates .env from .env.tpl")}
801
- ${pc3.cyan("$")} op2env .env.tpl
1086
+ ${pc5.bold("EXAMPLES")}
1087
+ ${pc5.dim("# Basic usage - generates .env from .env.tpl")}
1088
+ ${pc5.cyan("$")} op2env .env.tpl
802
1089
 
803
- ${pc3.dim("# Custom output path")}
804
- ${pc3.cyan("$")} op2env .env.tpl -o .env.local
1090
+ ${pc5.dim("# Custom output path")}
1091
+ ${pc5.cyan("$")} op2env .env.tpl -o .env.local
805
1092
 
806
- ${pc3.dim("# Preview without making changes")}
807
- ${pc3.cyan("$")} op2env .env.tpl --dry-run
1093
+ ${pc5.dim("# Preview without making changes")}
1094
+ ${pc5.cyan("$")} op2env .env.tpl --dry-run
808
1095
 
809
- ${pc3.dim("# Overwrite existing .env without prompting")}
810
- ${pc3.cyan("$")} op2env .env.tpl -f
1096
+ ${pc5.dim("# Overwrite existing .env without prompting")}
1097
+ ${pc5.cyan("$")} op2env .env.tpl -f
811
1098
 
812
- ${pc3.bold("DOCUMENTATION")}
813
- ${pc3.dim("https://github.com/tolgamorf/env2op-cli")}
1099
+ ${pc5.bold("DOCUMENTATION")}
1100
+ ${pc5.dim("https://github.com/tolgamorf/env2op-cli")}
814
1101
  `);
815
1102
  }