@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.
package/dist/cli.js CHANGED
@@ -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,14 +102,14 @@ var require_package = __commonJS((exports, module) => {
102
102
  });
103
103
 
104
104
  // src/cli.ts
105
- import pc3 from "picocolors";
105
+ import pc5 from "picocolors";
106
106
 
107
107
  // src/commands/convert.ts
108
- import { basename, dirname, join } from "node:path";
109
- import * as p2 from "@clack/prompts";
108
+ import { basename, dirname, join as join2 } from "node:path";
109
+ import * as p4 from "@clack/prompts";
110
110
 
111
- // src/core/env-parser.ts
112
- import { readFile } from "node:fs/promises";
111
+ // src/core/auth.ts
112
+ import * as p from "@clack/prompts";
113
113
 
114
114
  // src/utils/errors.ts
115
115
  class Env2OpError extends Error {
@@ -142,117 +142,17 @@ var errors = {
142
142
  envFileEmpty: (path) => new Env2OpError(`No valid environment variables found in ${path}`, ErrorCodes.ENV_FILE_EMPTY, "Ensure the file contains KEY=value pairs"),
143
143
  opCliNotInstalled: () => new Env2OpError("1Password CLI (op) is not installed", ErrorCodes.OP_CLI_NOT_INSTALLED, "Install it from https://1password.com/downloads/command-line/"),
144
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'),
145
146
  vaultNotFound: (vault) => new Env2OpError(`Vault not found: ${vault}`, ErrorCodes.VAULT_NOT_FOUND, 'Run "op vault list" to see available vaults'),
146
147
  vaultCreateFailed: (message) => new Env2OpError(`Failed to create vault: ${message}`, ErrorCodes.VAULT_CREATE_FAILED),
147
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"),
148
149
  itemCreateFailed: (message) => new Env2OpError(`Failed to create 1Password item: ${message}`, ErrorCodes.ITEM_CREATE_FAILED),
149
150
  itemEditFailed: (message) => new Env2OpError(`Failed to edit 1Password item: ${message}`, ErrorCodes.ITEM_EDIT_FAILED),
150
- 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)
151
154
  };
152
155
 
153
- // src/core/env-parser.ts
154
- var HEADER_SEPARATOR = "# ===========================================================================";
155
- function stripHeaders(content) {
156
- const lines = content.split(`
157
- `);
158
- const result = [];
159
- let inHeader = false;
160
- for (const line of lines) {
161
- const trimmed = line.trim();
162
- if (trimmed === HEADER_SEPARATOR) {
163
- if (!inHeader) {
164
- inHeader = true;
165
- } else {
166
- inHeader = false;
167
- }
168
- continue;
169
- }
170
- if (!inHeader) {
171
- result.push(line);
172
- }
173
- }
174
- while (result.length > 0 && result[0]?.trim() === "") {
175
- result.shift();
176
- }
177
- return result.join(`
178
- `);
179
- }
180
- function parseValue(raw) {
181
- const trimmed = raw.trim();
182
- if (trimmed.startsWith('"')) {
183
- const endQuote = trimmed.indexOf('"', 1);
184
- if (endQuote !== -1) {
185
- return trimmed.slice(1, endQuote);
186
- }
187
- }
188
- if (trimmed.startsWith("'")) {
189
- const endQuote = trimmed.indexOf("'", 1);
190
- if (endQuote !== -1) {
191
- return trimmed.slice(1, endQuote);
192
- }
193
- }
194
- const parts = trimmed.split(/\s+#/);
195
- return (parts[0] ?? trimmed).trim();
196
- }
197
- function stripBom(content) {
198
- if (content.charCodeAt(0) === 65279) {
199
- return content.slice(1);
200
- }
201
- return content;
202
- }
203
- async function parseEnvFile(filePath) {
204
- let rawContent;
205
- try {
206
- rawContent = await readFile(filePath, "utf-8");
207
- } catch {
208
- throw errors.envFileNotFound(filePath);
209
- }
210
- const content = stripHeaders(stripBom(rawContent));
211
- const rawLines = content.split(`
212
- `);
213
- const variables = [];
214
- const lines = [];
215
- const parseErrors = [];
216
- let currentComment = "";
217
- for (let i = 0;i < rawLines.length; i++) {
218
- const line = rawLines[i] ?? "";
219
- const trimmed = line.trim();
220
- const lineNumber = i + 1;
221
- if (!trimmed) {
222
- lines.push({ type: "empty" });
223
- currentComment = "";
224
- continue;
225
- }
226
- if (trimmed.startsWith("#")) {
227
- lines.push({ type: "comment", content: line });
228
- currentComment = trimmed.slice(1).trim();
229
- continue;
230
- }
231
- const match = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
232
- if (match?.[1]) {
233
- const key = match[1];
234
- const rawValue = match[2] ?? "";
235
- const value = parseValue(rawValue);
236
- variables.push({
237
- key,
238
- value,
239
- comment: currentComment || undefined,
240
- line: lineNumber
241
- });
242
- lines.push({ type: "variable", key, value });
243
- currentComment = "";
244
- } else if (trimmed.includes("=")) {
245
- parseErrors.push(`Line ${lineNumber}: Invalid variable name`);
246
- }
247
- }
248
- return { variables, lines, errors: parseErrors };
249
- }
250
- function validateParseResult(result, filePath) {
251
- if (result.variables.length === 0) {
252
- throw errors.envFileEmpty(filePath);
253
- }
254
- }
255
-
256
156
  // src/utils/shell.ts
257
157
  import { spawn } from "node:child_process";
258
158
  import pc from "picocolors";
@@ -262,16 +162,8 @@ function quoteArg(arg) {
262
162
  }
263
163
  return arg;
264
164
  }
265
- async function exec(command, args = [], options = {}) {
266
- const { verbose = false } = options;
267
- const fullCommand = `${command} ${args.map(quoteArg).join(" ")}`;
268
- if (verbose) {
269
- console.log(pc.dim(`$ ${fullCommand}`));
270
- }
165
+ function collectOutput(proc, verbose) {
271
166
  return new Promise((resolve) => {
272
- const proc = spawn(command, args, {
273
- stdio: ["ignore", "pipe", "pipe"]
274
- });
275
167
  const stdoutChunks = [];
276
168
  const stderrChunks = [];
277
169
  proc.stdout?.on("data", (data) => {
@@ -305,48 +197,29 @@ async function exec(command, args = [], options = {}) {
305
197
  });
306
198
  });
307
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
+ }
308
211
  async function execWithStdin(command, args = [], options) {
309
212
  const { stdin: stdinContent, verbose = false } = options;
310
213
  if (verbose) {
311
214
  const fullCommand = `${command} ${args.map(quoteArg).join(" ")}`;
312
215
  console.log(pc.dim(`$ echo '...' | ${fullCommand}`));
313
216
  }
314
- return new Promise((resolve) => {
315
- const proc = spawn(command, args, {
316
- stdio: ["pipe", "pipe", "pipe"]
317
- });
318
- const stdoutChunks = [];
319
- const stderrChunks = [];
320
- proc.stdin?.write(stdinContent);
321
- proc.stdin?.end();
322
- proc.stdout?.on("data", (data) => {
323
- const text = Buffer.isBuffer(data) ? data.toString() : String(data);
324
- stdoutChunks.push(text);
325
- if (verbose)
326
- process.stdout.write(text);
327
- });
328
- proc.stderr?.on("data", (data) => {
329
- const text = Buffer.isBuffer(data) ? data.toString() : String(data);
330
- stderrChunks.push(text);
331
- if (verbose)
332
- process.stderr.write(text);
333
- });
334
- proc.on("close", (code) => {
335
- resolve({
336
- stdout: stdoutChunks.join(""),
337
- stderr: stderrChunks.join(""),
338
- exitCode: code ?? 1
339
- });
340
- });
341
- proc.on("error", (err) => {
342
- stderrChunks.push(err.message);
343
- resolve({
344
- stdout: stdoutChunks.join(""),
345
- stderr: stderrChunks.join(""),
346
- exitCode: 1
347
- });
348
- });
217
+ const proc = spawn(command, args, {
218
+ stdio: ["pipe", "pipe", "pipe"]
349
219
  });
220
+ proc.stdin?.write(stdinContent);
221
+ proc.stdin?.end();
222
+ return collectOutput(proc, verbose);
350
223
  }
351
224
 
352
225
  // src/core/onepassword.ts
@@ -468,10 +341,332 @@ async function editSecureNote(options) {
468
341
  }
469
342
  }
470
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
+
471
669
  // src/core/template-generator.ts
472
- var import__package = __toESM(require_package(), 1);
473
- import { writeFileSync } from "node:fs";
474
- var SEPARATOR = "# ===========================================================================";
475
670
  function deriveEnvFileName(templateFileName) {
476
671
  if (templateFileName.endsWith(".tpl")) {
477
672
  return templateFileName.slice(0, -4);
@@ -487,7 +682,7 @@ function formatTimestamp() {
487
682
  function generateTemplateHeader(templateFileName) {
488
683
  const envFileName = deriveEnvFileName(templateFileName);
489
684
  return [
490
- SEPARATOR,
685
+ HEADER_SEPARATOR,
491
686
  `# ${templateFileName} — 1Password Secret References`,
492
687
  "#",
493
688
  "# This template contains references to secrets stored in 1Password.",
@@ -500,27 +695,27 @@ function generateTemplateHeader(templateFileName) {
500
695
  `# op run --env-file ${templateFileName} -- npm start`,
501
696
  "#",
502
697
  `# Pushed: ${formatTimestamp()}`,
503
- `# Generated by env2op v${import__package.default.version}`,
698
+ `# Generated by env2op v${getCliVersion()}`,
504
699
  "# https://github.com/tolgamorf/env2op-cli",
505
- SEPARATOR,
700
+ HEADER_SEPARATOR,
506
701
  ""
507
702
  ];
508
703
  }
509
704
  function generateEnvHeader(envFileName) {
510
705
  const templateFileName = deriveTemplateFileName(envFileName);
511
706
  return [
512
- SEPARATOR,
707
+ HEADER_SEPARATOR,
513
708
  `# ${envFileName} — Environment Variables`,
514
709
  "#",
515
710
  "# WARNING: This file contains sensitive values. Do not commit to git!",
516
711
  "#",
517
712
  `# To push updates to 1Password and generate ${templateFileName}:`,
518
- `# env2op ${envFileName} <vault> "<item_name>"`,
713
+ `# env2op ${envFileName} "<vault>" "<item_name>"`,
519
714
  "#",
520
715
  `# Pulled: ${formatTimestamp()}`,
521
- `# Generated by op2env v${import__package.default.version}`,
716
+ `# Generated by op2env v${getCliVersion()}`,
522
717
  "# https://github.com/tolgamorf/env2op-cli",
523
- SEPARATOR,
718
+ HEADER_SEPARATOR,
524
719
  "",
525
720
  ""
526
721
  ];
@@ -548,7 +743,7 @@ function generateTemplateContent(options, templateFileName) {
548
743
  `;
549
744
  }
550
745
  function writeTemplate(content, outputPath) {
551
- writeFileSync(outputPath, content, "utf-8");
746
+ writeFileSync2(outputPath, content, "utf-8");
552
747
  }
553
748
  function generateUsageInstructions(templatePath) {
554
749
  return ["Usage:", ` op2env ${templatePath}`, ` op run --env-file ${templatePath} -- npm start`].join(`
@@ -556,7 +751,7 @@ function generateUsageInstructions(templatePath) {
556
751
  }
557
752
 
558
753
  // src/utils/logger.ts
559
- import * as p from "@clack/prompts";
754
+ import * as p2 from "@clack/prompts";
560
755
  import pc2 from "picocolors";
561
756
  var symbols = {
562
757
  success: pc2.green("✓"),
@@ -569,29 +764,29 @@ var symbols = {
569
764
  var logger = {
570
765
  intro(name, version, dryRun = false) {
571
766
  const label = dryRun ? pc2.bgYellow(pc2.black(` ${name} v${version} [DRY RUN] `)) : pc2.bgCyan(pc2.black(` ${name} v${version} `));
572
- p.intro(label);
767
+ p2.intro(label);
573
768
  },
574
769
  section(title) {
575
770
  console.log(`
576
771
  ${pc2.bold(pc2.underline(title))}`);
577
772
  },
578
773
  success(message) {
579
- p.log.success(message);
774
+ p2.log.success(message);
580
775
  },
581
776
  error(message) {
582
- p.log.error(message);
777
+ p2.log.error(message);
583
778
  },
584
779
  warn(message) {
585
- p.log.warn(message);
780
+ p2.log.warn(message);
586
781
  },
587
782
  info(message) {
588
- p.log.info(message);
783
+ p2.log.info(message);
589
784
  },
590
785
  step(message) {
591
- p.log.step(message);
786
+ p2.log.step(message);
592
787
  },
593
788
  message(message) {
594
- p.log.message(message);
789
+ p2.log.message(message);
595
790
  },
596
791
  keyValue(key, value, indent = 2) {
597
792
  console.log(`${" ".repeat(indent)}${pc2.dim(key)}: ${pc2.cyan(value)}`);
@@ -606,16 +801,16 @@ ${pc2.bold(pc2.underline(title))}`);
606
801
  console.log(`${pc2.yellow("[DRY RUN]")} ${message}`);
607
802
  },
608
803
  spinner() {
609
- return p.spinner();
804
+ return p2.spinner();
610
805
  },
611
806
  outro(message) {
612
- p.outro(pc2.green(message));
807
+ p2.outro(pc2.green(message));
613
808
  },
614
809
  cancel(message) {
615
- p.cancel(message);
810
+ p2.cancel(message);
616
811
  },
617
812
  note(message, title) {
618
- p.note(message, title);
813
+ p2.note(message, title);
619
814
  },
620
815
  formatFields(fields, max = 3) {
621
816
  if (fields.length <= max) {
@@ -625,6 +820,28 @@ ${pc2.bold(pc2.underline(title))}`);
625
820
  }
626
821
  };
627
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
+
628
845
  // src/utils/timing.ts
629
846
  import { setTimeout } from "node:timers/promises";
630
847
  var MIN_SPINNER_TIME = 500;
@@ -636,8 +853,7 @@ async function withMinTime(promise, minTime = MIN_SPINNER_TIME) {
636
853
  // src/commands/convert.ts
637
854
  async function runConvert(options) {
638
855
  const { envFile, vault, itemName, output, dryRun, secret, force, verbose } = options;
639
- const pkg2 = await Promise.resolve().then(() => __toESM(require_package(), 1));
640
- logger.intro("env2op", pkg2.version, dryRun);
856
+ logger.intro("env2op", getCliVersion(), dryRun);
641
857
  try {
642
858
  const parseResult = await parseEnvFile(envFile);
643
859
  validateParseResult(parseResult, envFile);
@@ -662,29 +878,8 @@ async function runConvert(options) {
662
878
  logger.keyValue("Type", secret ? "password (hidden)" : "text (visible)");
663
879
  logger.keyValue("Fields", logger.formatFields(variables.map((v) => v.key)));
664
880
  } else {
665
- const authSpinner = p2.spinner();
666
- authSpinner.start("Checking 1Password CLI...");
667
- const opInstalled = await checkOpCli({ verbose });
668
- if (!opInstalled) {
669
- authSpinner.stop("1Password CLI not found");
670
- throw new Env2OpError("1Password CLI (op) is not installed", "OP_CLI_NOT_INSTALLED", "Install from https://1password.com/downloads/command-line/");
671
- }
672
- let signedIn = await checkSignedIn({ verbose });
673
- if (!signedIn) {
674
- authSpinner.message("Signing in to 1Password...");
675
- const signInSuccess = await signIn({ verbose });
676
- if (!signInSuccess) {
677
- authSpinner.stop();
678
- throw new Env2OpError("Failed to sign in to 1Password CLI", "OP_SIGNIN_FAILED", 'Try running "op signin" manually');
679
- }
680
- signedIn = await checkSignedIn({ verbose });
681
- if (!signedIn) {
682
- authSpinner.stop();
683
- throw new Env2OpError("Not signed in to 1Password CLI", "OP_NOT_SIGNED_IN", 'Run "op signin" to authenticate');
684
- }
685
- }
686
- authSpinner.stop("1Password CLI ready");
687
- const vaultSpinner = p2.spinner();
881
+ await ensureOpAuthenticated({ verbose });
882
+ const vaultSpinner = p4.spinner();
688
883
  vaultSpinner.start(`Checking for vault "${vault}"...`);
689
884
  const vaultFound = await withMinTime(vaultExists(vault, { verbose }));
690
885
  if (vaultFound) {
@@ -696,38 +891,32 @@ async function runConvert(options) {
696
891
  vaultSpinner.stop(`Created vault "${vault}"`);
697
892
  } else {
698
893
  vaultSpinner.stop(`Vault "${vault}" not found`);
699
- const shouldCreate = await p2.confirm({
894
+ const shouldCreate = await p4.confirm({
700
895
  message: `Vault "${vault}" does not exist. Create it?`
701
896
  });
702
- if (p2.isCancel(shouldCreate) || !shouldCreate) {
897
+ if (p4.isCancel(shouldCreate) || !shouldCreate) {
703
898
  logger.cancel("Operation cancelled");
704
899
  logger.info('Run "op vault list" to see available vaults');
705
900
  process.exit(0);
706
901
  }
707
- const createSpinner = p2.spinner();
902
+ const createSpinner = p4.spinner();
708
903
  createSpinner.start(`Creating vault "${vault}"...`);
709
904
  await createVault(vault, { verbose });
710
905
  createSpinner.stop(`Created vault "${vault}"`);
711
906
  }
712
907
  }
713
- const itemSpinner = p2.spinner();
908
+ const itemSpinner = p4.spinner();
714
909
  itemSpinner.start(`Checking for item "${itemName}"...`);
715
910
  const existingItemId = await withMinTime(itemExists(vault, itemName, { verbose }));
716
911
  if (existingItemId) {
717
912
  itemSpinner.stop(`Item "${itemName}" found`);
718
913
  if (!force) {
719
- const shouldOverwrite = await p2.confirm({
720
- message: `Item "${itemName}" already exists in vault "${vault}". Update it?`
721
- });
722
- if (p2.isCancel(shouldOverwrite) || !shouldOverwrite) {
723
- logger.cancel("Operation cancelled");
724
- process.exit(0);
725
- }
914
+ await confirmOrExit(`Item "${itemName}" already exists in vault "${vault}". Update it?`);
726
915
  }
727
916
  } else {
728
917
  itemSpinner.stop(`Item "${itemName}" not found`);
729
918
  }
730
- const pushSpinner = p2.spinner();
919
+ const pushSpinner = p4.spinner();
731
920
  pushSpinner.start("Pushing environment variables...");
732
921
  try {
733
922
  if (existingItemId) {
@@ -754,7 +943,7 @@ async function runConvert(options) {
754
943
  throw error;
755
944
  }
756
945
  }
757
- const templatePath = output ?? join(dirname(envFile), `${basename(envFile)}.tpl`);
946
+ const templatePath = output ?? join2(dirname(envFile), `${basename(envFile)}.tpl`);
758
947
  const templateFileName = basename(templatePath);
759
948
  if (dryRun) {
760
949
  logger.warn(`Would generate template: ${templatePath}`);
@@ -777,45 +966,143 @@ async function runConvert(options) {
777
966
  logger.outro("Done! Your secrets are now in 1Password");
778
967
  }
779
968
  } catch (error) {
780
- if (error instanceof Env2OpError) {
781
- logger.error(error.message);
782
- if (error.suggestion) {
783
- logger.info(`Suggestion: ${error.suggestion}`);
784
- }
785
- process.exit(1);
786
- }
787
- throw error;
969
+ handleCommandError(error);
788
970
  }
789
971
  }
790
972
 
791
- // src/cli.ts
792
- var pkg2 = await Promise.resolve().then(() => __toESM(require_package(), 1));
793
- var args = process.argv.slice(2);
794
- var flags = new Set;
795
- var positional = [];
796
- var options = {};
797
- for (let i = 0;i < args.length; i++) {
798
- const arg = args[i];
799
- if (arg === "-o" || arg === "--output") {
800
- const next = args[i + 1];
801
- if (next && !next.startsWith("-")) {
802
- options.output = next;
803
- i++;
973
+ // src/commands/update.ts
974
+ import * as p6 from "@clack/prompts";
975
+ import pc4 from "picocolors";
976
+
977
+ // src/lib/update-prompts.ts
978
+ import * as p5 from "@clack/prompts";
979
+ import pc3 from "picocolors";
980
+ var S_BAR_START = "┌";
981
+ var S_BAR_END = "";
982
+ function showUpdateNotification(result, cliName = "env2op") {
983
+ console.log();
984
+ console.log(`${pc3.gray(S_BAR_START)}${pc3.gray("─")} ${pc3.yellow("Update available:")} ${pc3.dim(result.currentVersion)} ${pc3.dim("→")} ${pc3.green(result.latestVersion)}`);
985
+ console.log(`${pc3.gray(S_BAR_END)}${pc3.gray("─")} Run ${pc3.cyan(`'${cliName} update'`)} to update`);
986
+ }
987
+ async function askToUpdate(result) {
988
+ const response = await p5.select({
989
+ message: "Would you like to update?",
990
+ options: [
991
+ { value: "update", label: "Update now", hint: "Download and install the latest version" },
992
+ { value: "later", label: "Remind me later", hint: "Ask again next time" },
993
+ { value: "skip", label: "Skip this version", hint: `Don't ask about ${result.latestVersion} again` }
994
+ ]
995
+ });
996
+ if (p5.isCancel(response)) {
997
+ return "later";
998
+ }
999
+ return response;
1000
+ }
1001
+ function showUpdateAvailable(result) {
1002
+ p5.log.success(`Update available: ${pc3.dim(result.currentVersion)} ${pc3.dim("→")} ${pc3.green(result.latestVersion)}`);
1003
+ }
1004
+ function showUpToDate(currentVersion) {
1005
+ p5.log.success(`You're on the latest version ${pc3.green(`(${currentVersion})`)}`);
1006
+ }
1007
+ function showPackageManagerInfo(pm) {
1008
+ console.log();
1009
+ console.log(` ${pc3.dim("Detected:")} ${pm.displayName} installation`);
1010
+ console.log(` ${pc3.dim("Command:")} ${pc3.cyan(pm.updateCommand)}`);
1011
+ console.log();
1012
+ }
1013
+ function showUpdateSuccess(newVersion) {
1014
+ p5.log.success(`Updated to version ${pc3.green(newVersion)}`);
1015
+ p5.log.info("Please restart to use the new version.");
1016
+ }
1017
+ function showUpdateError(error, pm) {
1018
+ if (error) {
1019
+ p5.log.error(pc3.dim(error));
1020
+ }
1021
+ p5.log.info(`Try running manually: ${pc3.cyan(pm.updateCommand)}`);
1022
+ }
1023
+
1024
+ // src/commands/update.ts
1025
+ async function runUpdate(options) {
1026
+ const { force = false, cliName = "env2op" } = options;
1027
+ p6.intro(pc4.bgCyan(pc4.black(` ${cliName} update `)));
1028
+ const spinner5 = p6.spinner();
1029
+ spinner5.start("Checking for updates...");
1030
+ const result = await checkForUpdate(true);
1031
+ spinner5.stop("Checked for updates");
1032
+ if (!result.updateAvailable || !result.latestVersion) {
1033
+ showUpToDate(result.currentVersion);
1034
+ return;
1035
+ }
1036
+ showUpdateAvailable(result);
1037
+ const pm = await detectPackageManager();
1038
+ showPackageManagerInfo(pm);
1039
+ if (!force) {
1040
+ const choice = await askToUpdate(result);
1041
+ if (choice === "skip") {
1042
+ skipVersion(result.latestVersion);
1043
+ p6.log.info(`Skipped version ${result.latestVersion}`);
1044
+ return;
804
1045
  }
805
- } else if (arg.startsWith("--")) {
806
- flags.add(arg.slice(2));
807
- } else if (arg.startsWith("-")) {
808
- for (const char of arg.slice(1)) {
809
- flags.add(char);
1046
+ if (choice === "later") {
1047
+ p6.log.info("Update postponed");
1048
+ return;
810
1049
  }
1050
+ }
1051
+ const updateSpinner = p6.spinner();
1052
+ updateSpinner.start(`Updating to ${result.latestVersion}...`);
1053
+ const updateResult = await performUpdate(pm);
1054
+ if (updateResult.success) {
1055
+ updateSpinner.stop("Update completed");
1056
+ showUpdateSuccess(result.latestVersion);
811
1057
  } else {
812
- positional.push(arg);
1058
+ updateSpinner.stop("Update failed");
1059
+ showUpdateError(updateResult.error, pm);
1060
+ process.exit(1);
813
1061
  }
814
1062
  }
1063
+
1064
+ // src/utils/args.ts
1065
+ function parseArgs(args) {
1066
+ const flags = new Set;
1067
+ const positional = [];
1068
+ const options = {};
1069
+ for (let i = 0;i < args.length; i++) {
1070
+ const arg = args[i];
1071
+ if (arg === "-o" || arg === "--output") {
1072
+ const next = args[i + 1];
1073
+ if (next && !next.startsWith("-")) {
1074
+ options.output = next;
1075
+ i++;
1076
+ }
1077
+ } else if (arg.startsWith("--")) {
1078
+ flags.add(arg.slice(2));
1079
+ } else if (arg.startsWith("-")) {
1080
+ for (const char of arg.slice(1)) {
1081
+ flags.add(char);
1082
+ }
1083
+ } else {
1084
+ positional.push(arg);
1085
+ }
1086
+ }
1087
+ return { flags, positional, options };
1088
+ }
1089
+
1090
+ // src/cli.ts
1091
+ var pkg = await Promise.resolve().then(() => __toESM(require_package(), 1));
1092
+ var { flags, positional, options } = parseArgs(process.argv.slice(2));
815
1093
  var hasHelp = flags.has("h") || flags.has("help");
816
1094
  var hasVersion = flags.has("v") || flags.has("version");
1095
+ var hasUpdate = flags.has("update");
817
1096
  if (hasVersion) {
818
- console.log(pkg2.version);
1097
+ console.log(getCliVersion());
1098
+ process.exit(0);
1099
+ }
1100
+ if (hasUpdate) {
1101
+ await runUpdate({
1102
+ force: flags.has("f") || flags.has("force"),
1103
+ verbose: flags.has("verbose"),
1104
+ cliName: "env2op"
1105
+ });
819
1106
  process.exit(0);
820
1107
  }
821
1108
  if (hasHelp || positional.length === 0) {
@@ -837,48 +1124,50 @@ await runConvert({
837
1124
  force: flags.has("f") || flags.has("force"),
838
1125
  verbose: flags.has("verbose")
839
1126
  });
1127
+ await maybeShowUpdateNotification("env2op", showUpdateNotification);
840
1128
  function showHelp() {
841
- const name = pc3.bold(pc3.cyan("env2op"));
842
- const version = pc3.dim(`v${pkg2.version}`);
1129
+ const name = pc5.bold(pc5.cyan("env2op"));
1130
+ const version = pc5.dim(`v${getCliVersion()}`);
843
1131
  console.log(`
844
1132
  ${name} ${version}
845
- ${pkg2.description}
1133
+ ${pkg.description}
846
1134
 
847
- ${pc3.bold("USAGE")}
848
- ${pc3.cyan("$")} env2op ${pc3.yellow("<env_file>")} ${pc3.yellow("<vault>")} ${pc3.yellow("<item_name>")} ${pc3.dim("[options]")}
1135
+ ${pc5.bold("USAGE")}
1136
+ ${pc5.cyan("$")} env2op ${pc5.yellow("<env_file>")} ${pc5.yellow("<vault>")} ${pc5.yellow("<item_name>")} ${pc5.dim("[options]")}
849
1137
 
850
- ${pc3.bold("ARGUMENTS")}
851
- ${pc3.yellow("env_file")} Path to .env file
852
- ${pc3.yellow("vault")} 1Password vault name
853
- ${pc3.yellow("item_name")} Name for the Secure Note in 1Password
1138
+ ${pc5.bold("ARGUMENTS")}
1139
+ ${pc5.yellow("env_file")} Path to .env file
1140
+ ${pc5.yellow("vault")} 1Password vault name
1141
+ ${pc5.yellow("item_name")} Name for the Secure Note in 1Password
854
1142
 
855
- ${pc3.bold("OPTIONS")}
856
- ${pc3.cyan("-o, --output")} Output template path (default: <env_file>.tpl)
857
- ${pc3.cyan("-f, --force")} Skip confirmation prompts
858
- ${pc3.cyan("--dry-run")} Preview actions without executing
859
- ${pc3.cyan("--secret")} Store all fields as password type (hidden)
860
- ${pc3.cyan("--verbose")} Show op CLI output
861
- ${pc3.cyan("-h, --help")} Show this help message
862
- ${pc3.cyan("-v, --version")} Show version
1143
+ ${pc5.bold("OPTIONS")}
1144
+ ${pc5.cyan("-o, --output")} Output template path (default: <env_file>.tpl)
1145
+ ${pc5.cyan("-f, --force")} Skip confirmation prompts
1146
+ ${pc5.cyan(" --dry-run")} Preview actions without executing
1147
+ ${pc5.cyan(" --secret")} Store all fields as password type (hidden)
1148
+ ${pc5.cyan(" --verbose")} Show op CLI output
1149
+ ${pc5.cyan(" --update")} Check for and install updates
1150
+ ${pc5.cyan("-v, --version")} Show version
1151
+ ${pc5.cyan("-h, --help")} Show this help message
863
1152
 
864
- ${pc3.bold("EXAMPLES")}
865
- ${pc3.dim("# Basic usage")}
866
- ${pc3.cyan("$")} env2op .env.production Personal "MyApp - Production"
1153
+ ${pc5.bold("EXAMPLES")}
1154
+ ${pc5.dim("# Basic usage")}
1155
+ ${pc5.cyan("$")} env2op .env.production Personal "MyApp - Production"
867
1156
 
868
- ${pc3.dim("# Custom output path")}
869
- ${pc3.cyan("$")} env2op .env Personal "MyApp" -o secrets.tpl
1157
+ ${pc5.dim("# Custom output path")}
1158
+ ${pc5.cyan("$")} env2op .env Personal "MyApp" -o secrets.tpl
870
1159
 
871
- ${pc3.dim("# Preview without making changes")}
872
- ${pc3.cyan("$")} env2op .env Personal "MyApp" --dry-run
1160
+ ${pc5.dim("# Preview without making changes")}
1161
+ ${pc5.cyan("$")} env2op .env Personal "MyApp" --dry-run
873
1162
 
874
- ${pc3.dim("# Store as hidden password fields")}
875
- ${pc3.cyan("$")} env2op .env Personal "MyApp" --secret
1163
+ ${pc5.dim("# Store as hidden password fields")}
1164
+ ${pc5.cyan("$")} env2op .env Personal "MyApp" --secret
876
1165
 
877
- ${pc3.dim("# Skip confirmation prompts (for CI/scripts)")}
878
- ${pc3.cyan("$")} env2op .env Personal "MyApp" -f
1166
+ ${pc5.dim("# Skip confirmation prompts (for CI/scripts)")}
1167
+ ${pc5.cyan("$")} env2op .env Personal "MyApp" -f
879
1168
 
880
- ${pc3.bold("DOCUMENTATION")}
881
- ${pc3.dim("https://github.com/tolgamorf/env2op-cli")}
1169
+ ${pc5.bold("DOCUMENTATION")}
1170
+ ${pc5.dim("https://github.com/tolgamorf/env2op-cli")}
882
1171
  `);
883
1172
  }
884
1173
  function showMissingArgsError(provided) {
@@ -890,17 +1179,17 @@ function showMissingArgsError(provided) {
890
1179
  if (provided.length < 3)
891
1180
  missing.push("item_name");
892
1181
  console.log(`
893
- ${pc3.red(pc3.bold("Error:"))} Missing required arguments
1182
+ ${pc5.red(pc5.bold("Error:"))} Missing required arguments
894
1183
 
895
- ${pc3.bold("Usage:")} env2op ${pc3.yellow("<env_file>")} ${pc3.yellow("<vault>")} ${pc3.yellow("<item_name>")} ${pc3.dim("[options]")}
1184
+ ${pc5.bold("Usage:")} env2op ${pc5.yellow("<env_file>")} ${pc5.yellow("<vault>")} ${pc5.yellow("<item_name>")} ${pc5.dim("[options]")}
896
1185
 
897
- ${pc3.bold("Missing:")}
898
- ${missing.map((arg) => ` ${pc3.red("•")} ${pc3.yellow(arg)}`).join(`
1186
+ ${pc5.bold("Missing:")}
1187
+ ${missing.map((arg) => ` ${pc5.red("•")} ${pc5.yellow(arg)}`).join(`
899
1188
  `)}
900
1189
 
901
- ${pc3.bold("Example:")}
902
- ${pc3.cyan("$")} env2op .env.production Personal "MyApp - Production"
1190
+ ${pc5.bold("Example:")}
1191
+ ${pc5.cyan("$")} env2op .env.production Personal "MyApp - Production"
903
1192
 
904
- Run ${pc3.cyan("env2op --help")} for more information.
1193
+ Run ${pc5.cyan("env2op --help")} for more information.
905
1194
  `);
906
1195
  }