@tolgamorf/env2op-cli 0.1.5 → 0.2.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.
@@ -1,140 +1,161 @@
1
1
  #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
2
3
  var __create = Object.create;
4
+ var __getProtoOf = Object.getPrototypeOf;
3
5
  var __defProp = Object.defineProperty;
4
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __commonJS = (cb, mod) => function __require() {
9
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
10
- };
11
- var __copyProps = (to, from, except, desc) => {
12
- if (from && typeof from === "object" || typeof from === "function") {
13
- for (let key of __getOwnPropNames(from))
14
- if (!__hasOwnProp.call(to, key) && key !== except)
15
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
- }
8
+ var __toESM = (mod, isNodeMode, target) => {
9
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
10
+ const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
11
+ for (let key of __getOwnPropNames(mod))
12
+ if (!__hasOwnProp.call(to, key))
13
+ __defProp(to, key, {
14
+ get: () => mod[key],
15
+ enumerable: true
16
+ });
17
17
  return to;
18
18
  };
19
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
20
- // If the importer is in node compatibility mode or this is not an ESM
21
- // file that has been converted to a CommonJS file using a Babel-
22
- // compatible transform (i.e. "__esModule" has not been set), then set
23
- // "default" to the CommonJS "module.exports" for node compatibility.
24
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
25
- mod
26
- ));
19
+ var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
27
20
 
28
21
  // package.json
29
- var require_package = __commonJS({
30
- "package.json"(exports, module) {
31
- module.exports = {
32
- name: "@tolgamorf/env2op-cli",
33
- version: "0.1.5",
34
- description: "Convert .env files to 1Password Secure Notes and generate templates for op inject/run",
35
- type: "module",
36
- main: "dist/index.js",
37
- module: "dist/index.js",
38
- types: "dist/index.d.ts",
39
- exports: {
40
- ".": {
41
- import: "./dist/index.js",
42
- types: "./dist/index.d.ts"
43
- }
44
- },
45
- bin: {
46
- env2op: "dist/cli.js",
47
- op2env: "dist/op2env-cli.js"
48
- },
49
- files: [
50
- "dist",
51
- "LICENSE",
52
- "README.md"
53
- ],
54
- scripts: {
55
- dev: "bun run src/cli.ts",
56
- build: "tsup",
57
- test: "bun test",
58
- "test:watch": "bun test --watch",
59
- "test:coverage": "bun test --coverage",
60
- typecheck: "tsc --noEmit",
61
- lint: "bunx biome check .",
62
- "lint:fix": "bunx biome check . --write",
63
- format: "bunx biome format --write .",
64
- "format:check": "bunx biome format .",
65
- prepublishOnly: "bun run build",
66
- release: "bun run scripts/release.ts"
67
- },
68
- keywords: [
69
- "env",
70
- "1password",
71
- "op",
72
- "cli",
73
- "secrets",
74
- "environment-variables",
75
- "dotenv",
76
- "secure-notes",
77
- "bun",
78
- "op-inject",
79
- "op-run",
80
- "template"
81
- ],
82
- author: {
83
- name: "Tolga O.",
84
- url: "https://github.com/tolgamorf"
85
- },
86
- license: "MIT",
87
- repository: {
88
- type: "git",
89
- url: "git+https://github.com/tolgamorf/env2op-cli.git"
90
- },
91
- bugs: {
92
- url: "https://github.com/tolgamorf/env2op-cli/issues"
93
- },
94
- homepage: "https://github.com/tolgamorf/env2op-cli#readme",
95
- engines: {
96
- node: ">=18.0.0"
97
- },
98
- dependencies: {
99
- "@clack/prompts": "^0.11.0",
100
- picocolors: "^1.1.1"
101
- },
102
- devDependencies: {
103
- "@biomejs/biome": "^2.3.10",
104
- "@tsconfig/bun": "^1.0.10",
105
- "@types/bun": "^1.3.5",
106
- tsup: "^8.5.1",
107
- typescript: "^5.9.3"
22
+ var require_package = __commonJS((exports, module) => {
23
+ module.exports = {
24
+ name: "@tolgamorf/env2op-cli",
25
+ version: "0.2.1",
26
+ description: "Convert .env files to 1Password Secure Notes and generate templates for op inject/run",
27
+ type: "module",
28
+ main: "dist/index.js",
29
+ module: "dist/index.js",
30
+ types: "dist/index.d.ts",
31
+ exports: {
32
+ ".": {
33
+ import: "./dist/index.js",
34
+ types: "./dist/index.d.ts"
108
35
  }
109
- };
110
- }
36
+ },
37
+ bin: {
38
+ env2op: "dist/cli.js",
39
+ op2env: "dist/op2env-cli.js"
40
+ },
41
+ files: [
42
+ "dist",
43
+ "LICENSE",
44
+ "README.md"
45
+ ],
46
+ scripts: {
47
+ dev: "bun run src/cli.ts",
48
+ build: "bunup",
49
+ test: "bun test",
50
+ "test:watch": "bun test --watch",
51
+ "test:coverage": "bun test --coverage",
52
+ typecheck: "tsc --noEmit",
53
+ lint: "bunx biome check .",
54
+ "lint:fix": "bunx biome check . --write",
55
+ format: "bunx biome format --write .",
56
+ "format:check": "bunx biome format .",
57
+ prepublishOnly: "bun run build",
58
+ release: "bun run scripts/release.ts"
59
+ },
60
+ keywords: [
61
+ "env",
62
+ "1password",
63
+ "op",
64
+ "cli",
65
+ "secrets",
66
+ "environment-variables",
67
+ "dotenv",
68
+ "secure-notes",
69
+ "bun",
70
+ "op-inject",
71
+ "op-run",
72
+ "template"
73
+ ],
74
+ author: {
75
+ name: "Tolga O.",
76
+ url: "https://github.com/tolgamorf"
77
+ },
78
+ license: "MIT",
79
+ repository: {
80
+ type: "git",
81
+ url: "git+https://github.com/tolgamorf/env2op-cli.git"
82
+ },
83
+ bugs: {
84
+ url: "https://github.com/tolgamorf/env2op-cli/issues"
85
+ },
86
+ homepage: "https://github.com/tolgamorf/env2op-cli#readme",
87
+ engines: {
88
+ node: ">=18.0.0"
89
+ },
90
+ dependencies: {
91
+ "@clack/prompts": "^0.11.0",
92
+ picocolors: "^1.1.1"
93
+ },
94
+ devDependencies: {
95
+ "@biomejs/biome": "^2.3.10",
96
+ "@tsconfig/bun": "^1.0.10",
97
+ "@types/bun": "^1.3.5",
98
+ bunup: "^0.16.17",
99
+ typescript: "^5.9.3"
100
+ }
101
+ };
111
102
  });
112
103
 
113
104
  // src/op2env-cli.ts
114
- import pc2 from "picocolors";
105
+ import pc3 from "picocolors";
115
106
 
116
107
  // src/commands/inject.ts
117
- import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
118
- import { basename } from "path";
108
+ import { existsSync, readFileSync, writeFileSync as writeFileSync2 } from "node:fs";
109
+ import { basename } from "node:path";
119
110
  import * as p2 from "@clack/prompts";
120
111
 
121
112
  // src/core/env-parser.ts
122
- import { existsSync, readFileSync } from "fs";
113
+ import { readFile } from "node:fs/promises";
123
114
 
124
115
  // src/utils/errors.ts
125
- var Env2OpError = class extends Error {
116
+ class Env2OpError extends Error {
117
+ code;
118
+ suggestion;
126
119
  constructor(message, code, suggestion) {
127
120
  super(message);
128
121
  this.code = code;
129
122
  this.suggestion = suggestion;
130
123
  this.name = "Env2OpError";
131
124
  }
125
+ }
126
+ var ErrorCodes = {
127
+ ENV_FILE_NOT_FOUND: "ENV_FILE_NOT_FOUND",
128
+ ENV_FILE_EMPTY: "ENV_FILE_EMPTY",
129
+ OP_CLI_NOT_INSTALLED: "OP_CLI_NOT_INSTALLED",
130
+ OP_NOT_SIGNED_IN: "OP_NOT_SIGNED_IN",
131
+ OP_SIGNIN_FAILED: "OP_SIGNIN_FAILED",
132
+ VAULT_NOT_FOUND: "VAULT_NOT_FOUND",
133
+ VAULT_CREATE_FAILED: "VAULT_CREATE_FAILED",
134
+ ITEM_EXISTS: "ITEM_EXISTS",
135
+ ITEM_CREATE_FAILED: "ITEM_CREATE_FAILED",
136
+ ITEM_EDIT_FAILED: "ITEM_EDIT_FAILED",
137
+ PARSE_ERROR: "PARSE_ERROR",
138
+ TEMPLATE_NOT_FOUND: "TEMPLATE_NOT_FOUND",
139
+ INJECT_FAILED: "INJECT_FAILED"
140
+ };
141
+ var errors = {
142
+ envFileNotFound: (path) => new Env2OpError(`File not found: ${path}`, ErrorCodes.ENV_FILE_NOT_FOUND, "Check that the file path is correct"),
143
+ envFileEmpty: (path) => new Env2OpError(`No valid environment variables found in ${path}`, ErrorCodes.ENV_FILE_EMPTY, "Ensure the file contains KEY=value pairs"),
144
+ opCliNotInstalled: () => new Env2OpError("1Password CLI (op) is not installed", ErrorCodes.OP_CLI_NOT_INSTALLED, "Install it from https://1password.com/downloads/command-line/"),
145
+ opNotSignedIn: () => new Env2OpError("Not signed in to 1Password CLI", ErrorCodes.OP_NOT_SIGNED_IN, 'Run "op signin" to authenticate'),
146
+ vaultNotFound: (vault) => new Env2OpError(`Vault not found: ${vault}`, ErrorCodes.VAULT_NOT_FOUND, 'Run "op vault list" to see available vaults'),
147
+ vaultCreateFailed: (message) => new Env2OpError(`Failed to create vault: ${message}`, ErrorCodes.VAULT_CREATE_FAILED),
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
+ itemCreateFailed: (message) => new Env2OpError(`Failed to create 1Password item: ${message}`, ErrorCodes.ITEM_CREATE_FAILED),
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)
132
152
  };
133
153
 
134
154
  // src/core/env-parser.ts
135
155
  var HEADER_SEPARATOR = "# ===========================================================================";
136
156
  function stripHeaders(content) {
137
- const lines = content.split("\n");
157
+ const lines = content.split(`
158
+ `);
138
159
  const result = [];
139
160
  let inHeader = false;
140
161
  for (const line of lines) {
@@ -154,69 +175,343 @@ function stripHeaders(content) {
154
175
  while (result.length > 0 && result[0]?.trim() === "") {
155
176
  result.shift();
156
177
  }
157
- return result.join("\n");
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
+ }
158
255
  }
159
256
 
160
257
  // src/utils/shell.ts
161
- import { spawn } from "child_process";
162
- async function exec(command, args2 = []) {
163
- return new Promise((resolve, reject) => {
164
- const proc = spawn(command, args2, {
165
- shell: false,
166
- stdio: ["pipe", "pipe", "pipe"]
258
+ import { spawn } from "node:child_process";
259
+ import pc from "picocolors";
260
+ function quoteArg(arg) {
261
+ if (/[ [\]'"\\=]/.test(arg)) {
262
+ return `'${arg.replace(/'/g, "'\\''")}'`;
263
+ }
264
+ return arg;
265
+ }
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
+ }
272
+ return new Promise((resolve) => {
273
+ const proc = spawn(command, args, {
274
+ stdio: ["ignore", "pipe", "pipe"]
167
275
  });
168
- let stdout = "";
169
- let stderr = "";
276
+ const stdoutChunks = [];
277
+ const stderrChunks = [];
170
278
  proc.stdout?.on("data", (data) => {
171
- stdout += data.toString();
279
+ const text = Buffer.isBuffer(data) ? data.toString() : String(data);
280
+ stdoutChunks.push(text);
281
+ if (verbose) {
282
+ process.stdout.write(text);
283
+ }
172
284
  });
173
285
  proc.stderr?.on("data", (data) => {
174
- stderr += data.toString();
286
+ const text = Buffer.isBuffer(data) ? data.toString() : String(data);
287
+ stderrChunks.push(text);
288
+ if (verbose) {
289
+ process.stderr.write(text);
290
+ }
175
291
  });
176
- proc.on("error", (error) => {
177
- reject(error);
292
+ proc.on("close", (code) => {
293
+ resolve({
294
+ stdout: stdoutChunks.join(""),
295
+ stderr: stderrChunks.join(""),
296
+ exitCode: code ?? 1
297
+ });
298
+ });
299
+ proc.on("error", (err) => {
300
+ stderrChunks.push(err.message);
301
+ resolve({
302
+ stdout: stdoutChunks.join(""),
303
+ stderr: stderrChunks.join(""),
304
+ exitCode: 1
305
+ });
306
+ });
307
+ });
308
+ }
309
+ async function execWithStdin(command, args = [], options) {
310
+ const { stdin: stdinContent, verbose = false } = options;
311
+ if (verbose) {
312
+ const fullCommand = `${command} ${args.map(quoteArg).join(" ")}`;
313
+ console.log(pc.dim(`$ echo '...' | ${fullCommand}`));
314
+ }
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);
178
334
  });
179
335
  proc.on("close", (code) => {
180
336
  resolve({
181
- stdout,
182
- stderr,
183
- exitCode: code ?? 0
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
184
348
  });
185
349
  });
186
350
  });
187
351
  }
188
- async function execQuiet(command, args2 = []) {
352
+
353
+ // src/core/onepassword.ts
354
+ async function checkOpCli(options = {}) {
355
+ const result = await exec("op", ["--version"], options);
356
+ return result.exitCode === 0;
357
+ }
358
+ async function checkSignedIn(options = {}) {
359
+ const result = await exec("op", ["whoami", "--format", "json"], options);
360
+ return result.exitCode === 0;
361
+ }
362
+ async function signIn(options = {}) {
363
+ const result = await exec("op", ["signin"], options);
364
+ return result.exitCode === 0;
365
+ }
366
+ async function itemExists(vault, title, options = {}) {
367
+ const result = await exec("op", ["item", "list", "--vault", vault, "--format", "json"], options);
368
+ if (result.exitCode !== 0) {
369
+ return null;
370
+ }
371
+ try {
372
+ const items = JSON.parse(result.stdout);
373
+ const item = items.find((item2) => item2.title === title);
374
+ return item?.id ?? null;
375
+ } catch {
376
+ return null;
377
+ }
378
+ }
379
+ async function vaultExists(vault, options = {}) {
380
+ const result = await exec("op", ["vault", "list", "--format", "json"], options);
381
+ if (result.exitCode !== 0) {
382
+ return false;
383
+ }
189
384
  try {
190
- const result = await exec(command, args2);
191
- return result.exitCode === 0;
385
+ const vaults = JSON.parse(result.stdout);
386
+ return vaults.some((v) => v.name === vault);
192
387
  } catch {
193
388
  return false;
194
389
  }
195
390
  }
196
-
197
- // src/core/onepassword.ts
198
- async function checkOpCli() {
199
- return execQuiet("op", ["--version"]);
391
+ async function createVault(name, options = {}) {
392
+ try {
393
+ await exec("op", ["vault", "create", name], options);
394
+ } catch (error) {
395
+ const message = error instanceof Error ? error.message : String(error);
396
+ throw errors.vaultCreateFailed(message);
397
+ }
200
398
  }
201
- async function checkSignedIn() {
202
- return execQuiet("op", ["account", "get"]);
399
+ function buildItemTemplate(title, vault, fields, secret) {
400
+ const fieldType = secret ? "CONCEALED" : "STRING";
401
+ return {
402
+ title,
403
+ vault: { name: vault },
404
+ category: "SECURE_NOTE",
405
+ fields: fields.map(({ key, value }) => ({
406
+ type: fieldType,
407
+ label: key,
408
+ value
409
+ }))
410
+ };
411
+ }
412
+ async function createSecureNote(options) {
413
+ const { vault, title, fields, secret, verbose } = options;
414
+ const template = buildItemTemplate(title, vault, fields, secret);
415
+ const json = JSON.stringify(template);
416
+ try {
417
+ const result = await execWithStdin("op", ["item", "create", "--format", "json"], { stdin: json, verbose });
418
+ if (result.exitCode !== 0) {
419
+ throw new Error(result.stderr || "Failed to create item");
420
+ }
421
+ const item = JSON.parse(result.stdout);
422
+ const fieldIds = {};
423
+ for (const field of item.fields ?? []) {
424
+ if (field.label && field.id) {
425
+ fieldIds[field.label] = field.id;
426
+ }
427
+ }
428
+ return {
429
+ id: item.id,
430
+ title: item.title,
431
+ vault: item.vault?.name ?? vault,
432
+ vaultId: item.vault?.id ?? "",
433
+ fieldIds
434
+ };
435
+ } catch (error) {
436
+ const message = error instanceof Error ? error.message : String(error);
437
+ throw errors.itemCreateFailed(message);
438
+ }
439
+ }
440
+ async function editSecureNote(options) {
441
+ const { vault, title, fields, secret, verbose, itemId } = options;
442
+ const template = buildItemTemplate(title, vault, fields, secret);
443
+ const json = JSON.stringify(template);
444
+ try {
445
+ const result = await execWithStdin("op", ["item", "edit", itemId, "--format", "json"], {
446
+ stdin: json,
447
+ verbose
448
+ });
449
+ if (result.exitCode !== 0) {
450
+ throw new Error(result.stderr || "Failed to edit item");
451
+ }
452
+ const item = JSON.parse(result.stdout);
453
+ const fieldIds = {};
454
+ for (const field of item.fields ?? []) {
455
+ if (field.label && field.id) {
456
+ fieldIds[field.label] = field.id;
457
+ }
458
+ }
459
+ return {
460
+ id: item.id,
461
+ title: item.title,
462
+ vault: item.vault?.name ?? vault,
463
+ vaultId: item.vault?.id ?? "",
464
+ fieldIds
465
+ };
466
+ } catch (error) {
467
+ const message = error instanceof Error ? error.message : String(error);
468
+ throw errors.itemEditFailed(message);
469
+ }
203
470
  }
204
471
 
205
472
  // src/core/template-generator.ts
206
- var import_package = __toESM(require_package(), 1);
207
- import { writeFileSync } from "fs";
473
+ var import__package = __toESM(require_package(), 1);
474
+ import { writeFileSync } from "node:fs";
208
475
  var SEPARATOR = "# ===========================================================================";
476
+ function deriveEnvFileName(templateFileName) {
477
+ if (templateFileName.endsWith(".tpl")) {
478
+ return templateFileName.slice(0, -4);
479
+ }
480
+ return templateFileName;
481
+ }
209
482
  function deriveTemplateFileName(envFileName) {
210
483
  return `${envFileName}.tpl`;
211
484
  }
212
485
  function formatTimestamp() {
213
- return (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").replace(/\.\d{3}Z$/, " UTC");
486
+ return new Date().toISOString().replace("T", " ").replace(/\.\d{3}Z$/, " UTC");
487
+ }
488
+ function generateTemplateHeader(templateFileName) {
489
+ const envFileName = deriveEnvFileName(templateFileName);
490
+ return [
491
+ SEPARATOR,
492
+ `# ${templateFileName} — 1Password Secret References`,
493
+ "#",
494
+ "# This template contains references to secrets stored in 1Password.",
495
+ "# The actual values are not stored here — only secret references.",
496
+ "#",
497
+ `# To generate ${envFileName} with real values:`,
498
+ `# op2env ${templateFileName}`,
499
+ "#",
500
+ "# To run a command with secrets injected:",
501
+ `# op run --env-file ${templateFileName} -- npm start`,
502
+ "#",
503
+ `# Pushed: ${formatTimestamp()}`,
504
+ `# Generated by env2op v${import__package.default.version}`,
505
+ "# https://github.com/tolgamorf/env2op-cli",
506
+ SEPARATOR,
507
+ ""
508
+ ];
214
509
  }
215
510
  function generateEnvHeader(envFileName) {
216
511
  const templateFileName = deriveTemplateFileName(envFileName);
217
512
  return [
218
513
  SEPARATOR,
219
- `# ${envFileName} \u2014 Environment Variables`,
514
+ `# ${envFileName} Environment Variables`,
220
515
  "#",
221
516
  "# WARNING: This file contains sensitive values. Do not commit to git!",
222
517
  "#",
@@ -224,127 +519,105 @@ function generateEnvHeader(envFileName) {
224
519
  `# env2op ${envFileName} <vault> "<item_name>"`,
225
520
  "#",
226
521
  `# Pulled: ${formatTimestamp()}`,
227
- `# Generated by op2env v${import_package.default.version}`,
522
+ `# Generated by op2env v${import__package.default.version}`,
228
523
  "# https://github.com/tolgamorf/env2op-cli",
229
524
  SEPARATOR,
230
525
  "",
231
526
  ""
232
527
  ];
233
528
  }
529
+ function generateTemplateContent(options, templateFileName) {
530
+ const { vaultId, itemId, lines: envLines, fieldIds } = options;
531
+ const outputLines = generateTemplateHeader(templateFileName);
532
+ for (const line of envLines) {
533
+ switch (line.type) {
534
+ case "empty":
535
+ outputLines.push("");
536
+ break;
537
+ case "comment":
538
+ outputLines.push(line.content);
539
+ break;
540
+ case "variable": {
541
+ const fieldId = fieldIds[line.key] ?? line.key;
542
+ outputLines.push(`${line.key}=op://${vaultId}/${itemId}/${fieldId}`);
543
+ break;
544
+ }
545
+ }
546
+ }
547
+ return `${outputLines.join(`
548
+ `)}
549
+ `;
550
+ }
551
+ function writeTemplate(content, outputPath) {
552
+ writeFileSync(outputPath, content, "utf-8");
553
+ }
554
+ function generateUsageInstructions(templatePath) {
555
+ return ["Usage:", ` op2env ${templatePath}`, ` op run --env-file ${templatePath} -- npm start`].join(`
556
+ `);
557
+ }
234
558
 
235
559
  // src/utils/logger.ts
236
560
  import * as p from "@clack/prompts";
237
- import pc from "picocolors";
561
+ import pc2 from "picocolors";
238
562
  var symbols = {
239
- success: pc.green("\u2713"),
240
- error: pc.red("\u2717"),
241
- warning: pc.yellow("\u26A0"),
242
- info: pc.blue("\u2139"),
243
- arrow: pc.cyan("\u2192"),
244
- bullet: pc.dim("\u2022")
563
+ success: pc2.green(""),
564
+ error: pc2.red(""),
565
+ warning: pc2.yellow(""),
566
+ info: pc2.blue(""),
567
+ arrow: pc2.cyan(""),
568
+ bullet: pc2.dim("")
245
569
  };
246
570
  var logger = {
247
- /**
248
- * Display CLI intro banner
249
- */
250
571
  intro(name, version, dryRun = false) {
251
- const label = dryRun ? pc.bgYellow(pc.black(` ${name} v${version} [DRY RUN] `)) : pc.bgCyan(pc.black(` ${name} v${version} `));
572
+ const label = dryRun ? pc2.bgYellow(pc2.black(` ${name} v${version} [DRY RUN] `)) : pc2.bgCyan(pc2.black(` ${name} v${version} `));
252
573
  p.intro(label);
253
574
  },
254
- /**
255
- * Display section header
256
- */
257
575
  section(title) {
258
576
  console.log(`
259
- ${pc.bold(pc.underline(title))}`);
577
+ ${pc2.bold(pc2.underline(title))}`);
260
578
  },
261
- /**
262
- * Success message
263
- */
264
579
  success(message) {
265
580
  p.log.success(message);
266
581
  },
267
- /**
268
- * Error message
269
- */
270
582
  error(message) {
271
583
  p.log.error(message);
272
584
  },
273
- /**
274
- * Warning message
275
- */
276
585
  warn(message) {
277
586
  p.log.warn(message);
278
587
  },
279
- /**
280
- * Info message
281
- */
282
588
  info(message) {
283
589
  p.log.info(message);
284
590
  },
285
- /**
286
- * Step in a process
287
- */
288
591
  step(message) {
289
592
  p.log.step(message);
290
593
  },
291
- /**
292
- * Message (neutral)
293
- */
294
594
  message(message) {
295
595
  p.log.message(message);
296
596
  },
297
- /**
298
- * Display key-value pair
299
- */
300
597
  keyValue(key, value, indent = 2) {
301
- console.log(`${" ".repeat(indent)}${pc.dim(key)}: ${pc.cyan(value)}`);
598
+ console.log(`${" ".repeat(indent)}${pc2.dim(key)}: ${pc2.cyan(value)}`);
302
599
  },
303
- /**
304
- * Display list item
305
- */
306
600
  listItem(item, indent = 2) {
307
601
  console.log(`${" ".repeat(indent)}${symbols.bullet} ${item}`);
308
602
  },
309
- /**
310
- * Display arrow item
311
- */
312
603
  arrowItem(item, indent = 2) {
313
604
  console.log(`${" ".repeat(indent)}${symbols.arrow} ${item}`);
314
605
  },
315
- /**
316
- * Display dry run indicator
317
- */
318
606
  dryRun(message) {
319
- console.log(`${pc.yellow("[DRY RUN]")} ${message}`);
607
+ console.log(`${pc2.yellow("[DRY RUN]")} ${message}`);
320
608
  },
321
- /**
322
- * Create a spinner for async operations
323
- */
324
609
  spinner() {
325
610
  return p.spinner();
326
611
  },
327
- /**
328
- * Display outro message
329
- */
330
612
  outro(message) {
331
- p.outro(pc.green(message));
613
+ p.outro(pc2.green(message));
332
614
  },
333
- /**
334
- * Display cancellation message
335
- */
336
615
  cancel(message) {
337
616
  p.cancel(message);
338
617
  },
339
- /**
340
- * Display a note block
341
- */
342
618
  note(message, title) {
343
619
  p.note(message, title);
344
620
  },
345
- /**
346
- * Format a field list for display
347
- */
348
621
  formatFields(fields, max = 3) {
349
622
  if (fields.length <= max) {
350
623
  return fields.join(", ");
@@ -353,6 +626,14 @@ ${pc.bold(pc.underline(title))}`);
353
626
  }
354
627
  };
355
628
 
629
+ // src/utils/timing.ts
630
+ import { setTimeout } from "node:timers/promises";
631
+ var MIN_SPINNER_TIME = 500;
632
+ async function withMinTime(promise, minTime = MIN_SPINNER_TIME) {
633
+ const [result] = await Promise.all([promise, setTimeout(minTime)]);
634
+ return result;
635
+ }
636
+
356
637
  // src/commands/inject.ts
357
638
  function deriveOutputPath(templatePath) {
358
639
  if (templatePath.endsWith(".tpl")) {
@@ -360,39 +641,41 @@ function deriveOutputPath(templatePath) {
360
641
  }
361
642
  return `${templatePath}.env`;
362
643
  }
363
- async function runInject(options2) {
364
- const { templateFile: templateFile2, output, dryRun, force } = options2;
365
- const outputPath = output ?? deriveOutputPath(templateFile2);
366
- const pkg3 = await Promise.resolve().then(() => __toESM(require_package(), 1));
367
- logger.intro("op2env", pkg3.version, dryRun);
644
+ async function runInject(options) {
645
+ const { templateFile, output, dryRun, force, verbose } = options;
646
+ const outputPath = output ?? deriveOutputPath(templateFile);
647
+ const pkg2 = await Promise.resolve().then(() => __toESM(require_package(), 1));
648
+ logger.intro("op2env", pkg2.version, dryRun);
368
649
  try {
369
- if (!existsSync2(templateFile2)) {
370
- throw new Env2OpError(
371
- `Template file not found: ${templateFile2}`,
372
- "TEMPLATE_NOT_FOUND",
373
- "Ensure the file exists and the path is correct"
374
- );
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");
375
652
  }
376
- logger.success(`Found template: ${basename(templateFile2)}`);
653
+ logger.success(`Found template: ${basename(templateFile)}`);
377
654
  if (!dryRun) {
378
- const opInstalled = await checkOpCli();
655
+ const authSpinner = p2.spinner();
656
+ authSpinner.start("Checking 1Password CLI...");
657
+ const opInstalled = await checkOpCli({ verbose });
379
658
  if (!opInstalled) {
380
- throw new Env2OpError(
381
- "1Password CLI (op) is not installed",
382
- "OP_CLI_NOT_INSTALLED",
383
- "Install from https://1password.com/downloads/command-line/"
384
- );
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/");
385
661
  }
386
- const signedIn = await checkSignedIn();
662
+ let signedIn = await checkSignedIn({ verbose });
387
663
  if (!signedIn) {
388
- throw new Env2OpError(
389
- "Not signed in to 1Password CLI",
390
- "OP_NOT_SIGNED_IN",
391
- 'Run "op signin" to authenticate'
392
- );
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
+ }
393
675
  }
676
+ authSpinner.stop("1Password CLI ready");
394
677
  }
395
- const outputExists = existsSync2(outputPath);
678
+ const outputExists = existsSync(outputPath);
396
679
  if (dryRun) {
397
680
  if (outputExists) {
398
681
  logger.warn(`Would overwrite: ${outputPath}`);
@@ -411,23 +694,31 @@ async function runInject(options2) {
411
694
  process.exit(0);
412
695
  }
413
696
  }
414
- const spinner2 = logger.spinner();
415
- spinner2.start("Injecting secrets from 1Password...");
697
+ const spinner3 = verbose ? null : logger.spinner();
698
+ spinner3?.start("Pulling secrets from 1Password...");
416
699
  try {
417
- const result = await exec("op", ["inject", "-i", templateFile2, "-o", outputPath, "-f"]);
700
+ const result = await withMinTime(exec("op", ["inject", "-i", templateFile, "-o", outputPath, "-f"], { verbose }));
418
701
  if (result.exitCode !== 0) {
419
702
  throw new Error(result.stderr);
420
703
  }
421
- const rawContent = readFileSync2(outputPath, "utf-8");
704
+ const rawContent = readFileSync(outputPath, "utf-8");
422
705
  const envContent = stripHeaders(rawContent);
423
- const header = generateEnvHeader(basename(outputPath)).join("\n");
706
+ const header = generateEnvHeader(basename(outputPath)).join(`
707
+ `);
424
708
  writeFileSync2(outputPath, header + envContent, "utf-8");
425
- spinner2.stop(`Generated: ${outputPath}`);
709
+ const varCount = envContent.split(`
710
+ `).filter((line) => line.trim() && !line.trim().startsWith("#")).length;
711
+ const stopMessage = `Generated ${basename(outputPath)} — ${varCount} variable${varCount === 1 ? "" : "s"}`;
712
+ if (spinner3) {
713
+ spinner3.stop(stopMessage);
714
+ } else {
715
+ logger.success(stopMessage);
716
+ }
426
717
  } catch (error) {
427
- spinner2.stop("Failed to inject secrets");
718
+ spinner3?.stop("Failed to pull secrets");
428
719
  const stderr = error?.stderr;
429
720
  const message = stderr || (error instanceof Error ? error.message : String(error));
430
- throw new Env2OpError("Failed to inject secrets from 1Password", "INJECT_FAILED", message);
721
+ throw new Env2OpError("Failed to pull secrets from 1Password", "INJECT_FAILED", message);
431
722
  }
432
723
  logger.outro("Done! Your .env file is ready");
433
724
  } catch (error) {
@@ -445,10 +736,10 @@ async function runInject(options2) {
445
736
  // src/op2env-cli.ts
446
737
  var pkg2 = await Promise.resolve().then(() => __toESM(require_package(), 1));
447
738
  var args = process.argv.slice(2);
448
- var flags = /* @__PURE__ */ new Set();
739
+ var flags = new Set;
449
740
  var positional = [];
450
741
  var options = {};
451
- for (let i = 0; i < args.length; i++) {
742
+ for (let i = 0;i < args.length; i++) {
452
743
  const arg = args[i];
453
744
  if (arg === "-o" || arg === "--output") {
454
745
  const next = args[i + 1];
@@ -481,42 +772,44 @@ await runInject({
481
772
  templateFile,
482
773
  output: options.output,
483
774
  dryRun: flags.has("dry-run"),
484
- force: flags.has("f") || flags.has("force")
775
+ force: flags.has("f") || flags.has("force"),
776
+ verbose: flags.has("verbose")
485
777
  });
486
778
  function showHelp() {
487
- const name = pc2.bold(pc2.cyan("op2env"));
488
- const version = pc2.dim(`v${pkg2.version}`);
779
+ const name = pc3.bold(pc3.cyan("op2env"));
780
+ const version = pc3.dim(`v${pkg2.version}`);
489
781
  console.log(`
490
782
  ${name} ${version}
491
783
  Pull secrets from 1Password to generate .env files
492
784
 
493
- ${pc2.bold("USAGE")}
494
- ${pc2.cyan("$")} op2env ${pc2.yellow("<template_file>")} ${pc2.dim("[options]")}
785
+ ${pc3.bold("USAGE")}
786
+ ${pc3.cyan("$")} op2env ${pc3.yellow("<template_file>")} ${pc3.dim("[options]")}
495
787
 
496
- ${pc2.bold("ARGUMENTS")}
497
- ${pc2.yellow("template_file")} Path to .env.tpl template file
788
+ ${pc3.bold("ARGUMENTS")}
789
+ ${pc3.yellow("template_file")} Path to .env.tpl template file
498
790
 
499
- ${pc2.bold("OPTIONS")}
500
- ${pc2.cyan("-o, --output")} Output .env path (default: template without .tpl)
501
- ${pc2.cyan("-f, --force")} Overwrite without prompting
502
- ${pc2.cyan("--dry-run")} Preview actions without executing
503
- ${pc2.cyan("-h, --help")} Show this help message
504
- ${pc2.cyan("-v, --version")} Show version
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
505
798
 
506
- ${pc2.bold("EXAMPLES")}
507
- ${pc2.dim("# Basic usage - generates .env from .env.tpl")}
508
- ${pc2.cyan("$")} op2env .env.tpl
799
+ ${pc3.bold("EXAMPLES")}
800
+ ${pc3.dim("# Basic usage - generates .env from .env.tpl")}
801
+ ${pc3.cyan("$")} op2env .env.tpl
509
802
 
510
- ${pc2.dim("# Custom output path")}
511
- ${pc2.cyan("$")} op2env .env.tpl -o .env.local
803
+ ${pc3.dim("# Custom output path")}
804
+ ${pc3.cyan("$")} op2env .env.tpl -o .env.local
512
805
 
513
- ${pc2.dim("# Preview without making changes")}
514
- ${pc2.cyan("$")} op2env .env.tpl --dry-run
806
+ ${pc3.dim("# Preview without making changes")}
807
+ ${pc3.cyan("$")} op2env .env.tpl --dry-run
515
808
 
516
- ${pc2.dim("# Overwrite existing .env without prompting")}
517
- ${pc2.cyan("$")} op2env .env.tpl -f
809
+ ${pc3.dim("# Overwrite existing .env without prompting")}
810
+ ${pc3.cyan("$")} op2env .env.tpl -f
518
811
 
519
- ${pc2.bold("DOCUMENTATION")}
520
- ${pc2.dim("https://github.com/tolgamorf/env2op-cli")}
812
+ ${pc3.bold("DOCUMENTATION")}
813
+ ${pc3.dim("https://github.com/tolgamorf/env2op-cli")}
521
814
  `);
522
815
  }