@tolgamorf/env2op-cli 0.1.1 → 0.1.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/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # env2op
2
2
 
3
- Convert `.env` files to 1Password Secure Notes and generate template files for `op inject` and `op run`.
3
+ Push `.env` files to 1Password and pull them back with two simple commands.
4
+
5
+ ![env2op demo](demo/env2op-demo.gif)
4
6
 
5
7
  ## Installation
6
8
 
@@ -20,7 +22,18 @@ bunx @tolgamorf/env2op-cli .env Personal "MyApp"
20
22
  - [1Password CLI](https://1password.com/downloads/command-line/) installed and signed in
21
23
  - [Bun](https://bun.sh) runtime (for best performance)
22
24
 
23
- ## Usage
25
+ ## Commands
26
+
27
+ This package provides two commands:
28
+
29
+ | Command | Description |
30
+ |---------|-------------|
31
+ | `env2op` | Push `.env` to 1Password, generate `.env.tpl` template |
32
+ | `op2env` | Pull secrets from 1Password using `.env.tpl` template |
33
+
34
+ ## env2op (Push)
35
+
36
+ Push environment variables to 1Password and generate a template file.
24
37
 
25
38
  ```bash
26
39
  env2op <env_file> <vault> <item_name> [options]
@@ -32,6 +45,9 @@ env2op <env_file> <vault> <item_name> [options]
32
45
  # Basic usage - creates a Secure Note and generates .env.tpl
33
46
  env2op .env.production Personal "MyApp - Production"
34
47
 
48
+ # Custom output path for template
49
+ env2op .env Personal "MyApp" -o secrets.tpl
50
+
35
51
  # Preview what would happen without making changes
36
52
  env2op .env.production Personal "MyApp" --dry-run
37
53
 
@@ -39,38 +55,62 @@ env2op .env.production Personal "MyApp" --dry-run
39
55
  env2op .env.production Personal "MyApp" --secret
40
56
 
41
57
  # Skip confirmation prompts (useful for scripts/CI)
42
- env2op .env.production Personal "MyApp" -y
58
+ env2op .env.production Personal "MyApp" -f
43
59
  ```
44
60
 
45
61
  ### Options
46
62
 
47
63
  | Flag | Description |
48
64
  |------|-------------|
65
+ | `-o, --output` | Output template path (default: `<env_file>.tpl`) |
66
+ | `-f, --force` | Skip confirmation prompts |
49
67
  | `--dry-run` | Preview actions without executing |
50
68
  | `--secret` | Store all fields as 'password' type (default: 'text') |
51
- | `-y, --yes` | Skip confirmation prompts (auto-accept) |
52
69
  | `-h, --help` | Show help |
53
70
  | `-v, --version` | Show version |
54
71
 
55
- ### Overwriting Existing Items
72
+ ## op2env (Pull)
56
73
 
57
- If an item with the same name already exists in the vault, env2op will prompt for confirmation before overwriting. Use `-y` or `--yes` to skip the prompt and auto-accept.
74
+ Pull secrets from 1Password to generate a `.env` file.
58
75
 
59
- ## How It Works
76
+ ```bash
77
+ op2env <template_file> [options]
78
+ ```
60
79
 
61
- 1. **Parses** your `.env` file to extract environment variables
62
- 2. **Creates** a 1Password Secure Note with all variables as fields
63
- 3. **Generates** a `.tpl` template file with `op://` references
80
+ ### Examples
64
81
 
65
- ## Using the Generated Template
82
+ ```bash
83
+ # Basic usage - generates .env from .env.tpl
84
+ op2env .env.tpl
66
85
 
67
- After running env2op, you'll have a `.env.tpl` file with 1Password references:
86
+ # Custom output path
87
+ op2env .env.tpl -o .env.local
68
88
 
69
- ```bash
70
- # Inject secrets into a new .env file
71
- op inject -i .env.tpl -o .env
89
+ # Preview without making changes
90
+ op2env .env.tpl --dry-run
91
+
92
+ # Overwrite existing .env without prompting
93
+ op2env .env.tpl -f
94
+ ```
72
95
 
73
- # Run a command with secrets injected
96
+ ### Options
97
+
98
+ | Flag | Description |
99
+ |------|-------------|
100
+ | `-o, --output` | Output .env path (default: template without `.tpl`) |
101
+ | `-f, --force` | Overwrite without prompting |
102
+ | `--dry-run` | Preview actions without executing |
103
+ | `-h, --help` | Show help |
104
+ | `-v, --version` | Show version |
105
+
106
+ ## How It Works
107
+
108
+ 1. **env2op** parses your `.env` file, creates a 1Password Secure Note, and generates a `.tpl` template
109
+ 2. **op2env** reads the template and pulls current values from 1Password to create a `.env` file
110
+
111
+ You can also use the `op run` command to run processes with secrets injected:
112
+
113
+ ```bash
74
114
  op run --env-file .env.tpl -- npm start
75
115
  ```
76
116
 
@@ -99,12 +139,12 @@ Creates a 1Password Secure Note with fields:
99
139
  - `API_KEY` (text)
100
140
  - `DEBUG` (text)
101
141
 
102
- And generates `.env.tpl`:
142
+ And generates `.env.tpl` with UUID-based references (avoids naming conflicts):
103
143
 
104
144
  ```env
105
- DATABASE_URL=op://Personal/MyApp Secrets/DATABASE_URL
106
- API_KEY=op://Personal/MyApp Secrets/API_KEY
107
- DEBUG=op://Personal/MyApp Secrets/DEBUG
145
+ DATABASE_URL=op://abc123vaultid/xyz789itemid/def456fieldid
146
+ API_KEY=op://abc123vaultid/xyz789itemid/ghi012fieldid
147
+ DEBUG=op://abc123vaultid/xyz789itemid/jkl345fieldid
108
148
  ```
109
149
 
110
150
  ## Programmatic Usage
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tolgamorf/env2op-cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Convert .env files to 1Password Secure Notes and generate templates for op inject/run",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -13,7 +13,8 @@
13
13
  }
14
14
  },
15
15
  "bin": {
16
- "env2op": "src/cli.ts"
16
+ "env2op": "src/cli.ts",
17
+ "op2env": "src/op2env-cli.ts"
17
18
  },
18
19
  "files": [
19
20
  "src",
@@ -23,6 +24,8 @@
23
24
  "scripts": {
24
25
  "dev": "bun run src/cli.ts",
25
26
  "test": "bun test",
27
+ "test:watch": "bun test --watch",
28
+ "test:coverage": "bun test --coverage",
26
29
  "typecheck": "tsc --noEmit",
27
30
  "lint": "bunx biome check .",
28
31
  "lint:fix": "bunx biome check . --write",
package/src/cli.ts CHANGED
@@ -9,9 +9,17 @@ const args = process.argv.slice(2);
9
9
  // Parse arguments
10
10
  const flags = new Set<string>();
11
11
  const positional: string[] = [];
12
-
13
- for (const arg of args) {
14
- if (arg.startsWith("--")) {
12
+ const options: Record<string, string> = {};
13
+
14
+ for (let i = 0; i < args.length; i++) {
15
+ const arg = args[i] as string;
16
+ if (arg === "-o" || arg === "--output") {
17
+ const next = args[i + 1];
18
+ if (next && !next.startsWith("-")) {
19
+ options.output = next;
20
+ i++; // skip next arg
21
+ }
22
+ } else if (arg.startsWith("--")) {
15
23
  flags.add(arg.slice(2));
16
24
  } else if (arg.startsWith("-")) {
17
25
  // Handle short flags
@@ -48,9 +56,10 @@ await runConvert({
48
56
  envFile,
49
57
  vault,
50
58
  itemName,
59
+ output: options.output,
51
60
  dryRun: flags.has("dry-run"),
52
61
  secret: flags.has("secret"),
53
- yes: flags.has("y") || flags.has("yes"),
62
+ force: flags.has("f") || flags.has("force"),
54
63
  });
55
64
 
56
65
  function showHelp(): void {
@@ -70,9 +79,10 @@ ${pc.bold("ARGUMENTS")}
70
79
  ${pc.yellow("item_name")} Name for the Secure Note in 1Password
71
80
 
72
81
  ${pc.bold("OPTIONS")}
82
+ ${pc.cyan("-o, --output")} Output template path (default: <env_file>.tpl)
83
+ ${pc.cyan("-f, --force")} Skip confirmation prompts
73
84
  ${pc.cyan("--dry-run")} Preview actions without executing
74
85
  ${pc.cyan("--secret")} Store all fields as password type (hidden)
75
- ${pc.cyan("-y, --yes")} Skip confirmation prompts (auto-accept)
76
86
  ${pc.cyan("-h, --help")} Show this help message
77
87
  ${pc.cyan("-v, --version")} Show version
78
88
 
@@ -80,6 +90,9 @@ ${pc.bold("EXAMPLES")}
80
90
  ${pc.dim("# Basic usage")}
81
91
  ${pc.cyan("$")} env2op .env.production Personal "MyApp - Production"
82
92
 
93
+ ${pc.dim("# Custom output path")}
94
+ ${pc.cyan("$")} env2op .env Personal "MyApp" -o secrets.tpl
95
+
83
96
  ${pc.dim("# Preview without making changes")}
84
97
  ${pc.cyan("$")} env2op .env Personal "MyApp" --dry-run
85
98
 
@@ -87,10 +100,10 @@ ${pc.bold("EXAMPLES")}
87
100
  ${pc.cyan("$")} env2op .env Personal "MyApp" --secret
88
101
 
89
102
  ${pc.dim("# Skip confirmation prompts (for CI/scripts)")}
90
- ${pc.cyan("$")} env2op .env Personal "MyApp" -y
103
+ ${pc.cyan("$")} env2op .env Personal "MyApp" -f
91
104
 
92
105
  ${pc.bold("DOCUMENTATION")}
93
- ${pc.dim("https://github.com/tolgamorf/env2op")}
106
+ ${pc.dim("https://github.com/tolgamorf/env2op-cli")}
94
107
  `);
95
108
  }
96
109
 
@@ -11,7 +11,7 @@ import {
11
11
  vaultExists,
12
12
  } from "../core/onepassword";
13
13
  import { generateTemplateContent, generateUsageInstructions, writeTemplate } from "../core/template-generator";
14
- import type { ConvertOptions } from "../core/types";
14
+ import type { ConvertOptions, CreateItemResult } from "../core/types";
15
15
  import { Env2OpError } from "../utils/errors";
16
16
  import { logger } from "../utils/logger";
17
17
 
@@ -19,7 +19,7 @@ import { logger } from "../utils/logger";
19
19
  * Execute the convert operation
20
20
  */
21
21
  export async function runConvert(options: ConvertOptions): Promise<void> {
22
- const { envFile, vault, itemName, dryRun, secret, yes } = options;
22
+ const { envFile, vault, itemName, output, dryRun, secret, force } = options;
23
23
 
24
24
  // Display intro
25
25
  const pkg = await import("../../package.json");
@@ -41,6 +41,8 @@ export async function runConvert(options: ConvertOptions): Promise<void> {
41
41
  }
42
42
 
43
43
  // Step 2: Create 1Password Secure Note
44
+ let itemResult: CreateItemResult | null = null;
45
+
44
46
  if (dryRun) {
45
47
  logger.warn("Would create Secure Note");
46
48
  logger.keyValue("Vault", vault);
@@ -70,8 +72,10 @@ export async function runConvert(options: ConvertOptions): Promise<void> {
70
72
  // Check if vault exists
71
73
  const vaultFound = await vaultExists(vault);
72
74
 
73
- if (!vaultFound) {
74
- if (yes) {
75
+ if (vaultFound) {
76
+ logger.success(`Vault "${vault}" found`);
77
+ } else {
78
+ if (force) {
75
79
  // Auto-create vault
76
80
  logger.warn(`Vault "${vault}" not found, creating...`);
77
81
  await createVault(vault);
@@ -99,7 +103,7 @@ export async function runConvert(options: ConvertOptions): Promise<void> {
99
103
  const exists = await itemExists(vault, itemName);
100
104
 
101
105
  if (exists) {
102
- if (yes) {
106
+ if (force) {
103
107
  // Auto-accept: delete and recreate
104
108
  logger.warn(`Item "${itemName}" already exists, overwriting...`);
105
109
  await deleteItem(vault, itemName);
@@ -122,14 +126,14 @@ export async function runConvert(options: ConvertOptions): Promise<void> {
122
126
  spinner.start("Creating 1Password Secure Note...");
123
127
 
124
128
  try {
125
- const result = await createSecureNote({
129
+ itemResult = await createSecureNote({
126
130
  vault,
127
131
  title: itemName,
128
132
  fields: variables,
129
133
  secret,
130
134
  });
131
135
 
132
- spinner.stop(`Created "${result.title}" in vault "${result.vault}"`);
136
+ spinner.stop(`Created "${itemResult.title}" in vault "${itemResult.vault}"`);
133
137
  } catch (error) {
134
138
  spinner.stop("Failed to create Secure Note");
135
139
  throw error;
@@ -137,21 +141,22 @@ export async function runConvert(options: ConvertOptions): Promise<void> {
137
141
  }
138
142
 
139
143
  // Step 3: Generate template file
140
- const templateFileName = `${basename(envFile)}.tpl`;
141
- const templatePath = join(dirname(envFile), templateFileName);
142
- const templateContent = generateTemplateContent(
143
- {
144
- vault,
145
- itemTitle: itemName,
146
- variables,
147
- lines,
148
- },
149
- templateFileName,
150
- );
144
+ const templatePath = output ?? join(dirname(envFile), `${basename(envFile)}.tpl`);
145
+ const templateFileName = basename(templatePath);
151
146
 
152
147
  if (dryRun) {
153
148
  logger.warn(`Would generate template: ${templatePath}`);
154
- } else {
149
+ } else if (itemResult) {
150
+ const templateContent = generateTemplateContent(
151
+ {
152
+ vaultId: itemResult.vaultId,
153
+ itemId: itemResult.id,
154
+ variables,
155
+ lines,
156
+ fieldIds: itemResult.fieldIds,
157
+ },
158
+ templateFileName,
159
+ );
155
160
  writeTemplate(templateContent, templatePath);
156
161
  logger.success(`Generated template: ${templatePath}`);
157
162
  }
@@ -0,0 +1,130 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { basename } from "node:path";
3
+ import * as p from "@clack/prompts";
4
+ import { $ } from "bun";
5
+ import { stripHeaders } from "../core/env-parser";
6
+ import { checkOpCli, checkSignedIn } from "../core/onepassword";
7
+ import { generateEnvHeader } from "../core/template-generator";
8
+ import type { InjectOptions } from "../core/types";
9
+ import { Env2OpError } from "../utils/errors";
10
+ import { logger } from "../utils/logger";
11
+
12
+ /**
13
+ * Derive output path from template path
14
+ * .env.tpl -> .env
15
+ * .env.local.tpl -> .env.local
16
+ * secrets.tpl -> secrets
17
+ */
18
+ function deriveOutputPath(templatePath: string): string {
19
+ if (templatePath.endsWith(".tpl")) {
20
+ return templatePath.slice(0, -4);
21
+ }
22
+ return `${templatePath}.env`;
23
+ }
24
+
25
+ /**
26
+ * Execute the inject operation (op2env)
27
+ */
28
+ export async function runInject(options: InjectOptions): Promise<void> {
29
+ const { templateFile, output, dryRun, force } = options;
30
+ const outputPath = output ?? deriveOutputPath(templateFile);
31
+
32
+ // Display intro
33
+ const pkg = await import("../../package.json");
34
+ logger.intro("op2env", pkg.version, dryRun);
35
+
36
+ try {
37
+ // Step 1: Check template file exists
38
+ if (!existsSync(templateFile)) {
39
+ throw new Env2OpError(
40
+ `Template file not found: ${templateFile}`,
41
+ "TEMPLATE_NOT_FOUND",
42
+ "Ensure the file exists and the path is correct",
43
+ );
44
+ }
45
+
46
+ logger.success(`Found template: ${basename(templateFile)}`);
47
+
48
+ // Step 2: Check 1Password CLI
49
+ if (!dryRun) {
50
+ const opInstalled = await checkOpCli();
51
+ if (!opInstalled) {
52
+ throw new Env2OpError(
53
+ "1Password CLI (op) is not installed",
54
+ "OP_CLI_NOT_INSTALLED",
55
+ "Install from https://1password.com/downloads/command-line/",
56
+ );
57
+ }
58
+
59
+ const signedIn = await checkSignedIn();
60
+ if (!signedIn) {
61
+ throw new Env2OpError(
62
+ "Not signed in to 1Password CLI",
63
+ "OP_NOT_SIGNED_IN",
64
+ 'Run "op signin" to authenticate',
65
+ );
66
+ }
67
+ }
68
+
69
+ // Step 3: Check if output file exists
70
+ const outputExists = existsSync(outputPath);
71
+
72
+ if (dryRun) {
73
+ if (outputExists) {
74
+ logger.warn(`Would overwrite: ${outputPath}`);
75
+ } else {
76
+ logger.warn(`Would create: ${outputPath}`);
77
+ }
78
+ logger.outro("Dry run complete. No changes made.");
79
+ return;
80
+ }
81
+
82
+ if (outputExists && !force) {
83
+ const shouldOverwrite = await p.confirm({
84
+ message: `File "${outputPath}" already exists. Overwrite?`,
85
+ });
86
+
87
+ if (p.isCancel(shouldOverwrite) || !shouldOverwrite) {
88
+ logger.cancel("Operation cancelled");
89
+ process.exit(0);
90
+ }
91
+ }
92
+
93
+ // Step 4: Run op inject
94
+ const spinner = logger.spinner();
95
+ spinner.start("Injecting secrets from 1Password...");
96
+
97
+ try {
98
+ const result = await $`op inject -i ${templateFile} -o ${outputPath} -f`.quiet();
99
+
100
+ if (result.exitCode !== 0) {
101
+ throw new Error(result.stderr.toString());
102
+ }
103
+
104
+ // Strip any existing headers and prepend fresh .env header
105
+ const rawContent = readFileSync(outputPath, "utf-8");
106
+ const envContent = stripHeaders(rawContent);
107
+ const header = generateEnvHeader(basename(outputPath)).join("\n");
108
+ writeFileSync(outputPath, header + envContent, "utf-8");
109
+
110
+ spinner.stop(`Generated: ${outputPath}`);
111
+ } catch (error) {
112
+ spinner.stop("Failed to inject secrets");
113
+ // Extract stderr from Bun shell error
114
+ const stderr = (error as { stderr?: Buffer })?.stderr?.toString?.();
115
+ const message = stderr || (error instanceof Error ? error.message : String(error));
116
+ throw new Env2OpError("Failed to inject secrets from 1Password", "INJECT_FAILED", message);
117
+ }
118
+
119
+ logger.outro("Done! Your .env file is ready");
120
+ } catch (error) {
121
+ if (error instanceof Env2OpError) {
122
+ logger.error(error.message);
123
+ if (error.suggestion) {
124
+ logger.info(`Suggestion: ${error.suggestion}`);
125
+ }
126
+ process.exit(1);
127
+ }
128
+ throw error;
129
+ }
130
+ }
@@ -2,6 +2,44 @@ import { existsSync, readFileSync } from "node:fs";
2
2
  import { errors } from "../utils/errors";
3
3
  import type { EnvLine, EnvVariable, ParseResult } from "./types";
4
4
 
5
+ const HEADER_SEPARATOR = "# ===========================================================================";
6
+
7
+ /**
8
+ * Strip env2op/op2env header blocks from content
9
+ * Headers are delimited by separator lines
10
+ */
11
+ export function stripHeaders(content: string): string {
12
+ const lines = content.split("\n");
13
+ const result: string[] = [];
14
+ let inHeader = false;
15
+
16
+ for (const line of lines) {
17
+ const trimmed = line.trim();
18
+
19
+ if (trimmed === HEADER_SEPARATOR) {
20
+ if (!inHeader) {
21
+ // Starting a header block
22
+ inHeader = true;
23
+ } else {
24
+ // Ending a header block
25
+ inHeader = false;
26
+ }
27
+ continue;
28
+ }
29
+
30
+ if (!inHeader) {
31
+ result.push(line);
32
+ }
33
+ }
34
+
35
+ // Remove leading empty lines left after stripping header
36
+ while (result.length > 0 && result[0]?.trim() === "") {
37
+ result.shift();
38
+ }
39
+
40
+ return result.join("\n");
41
+ }
42
+
5
43
  /**
6
44
  * Parse a value from an environment variable line
7
45
  * Handles quoted strings and inline comments
@@ -43,7 +81,8 @@ export function parseEnvFile(filePath: string): ParseResult {
43
81
  throw errors.envFileNotFound(filePath);
44
82
  }
45
83
 
46
- const content = readFileSync(filePath, "utf-8");
84
+ const rawContent = readFileSync(filePath, "utf-8");
85
+ const content = stripHeaders(rawContent);
47
86
  const rawLines = content.split("\n");
48
87
  const variables: EnvVariable[] = [];
49
88
  const lines: EnvLine[] = [];
@@ -76,10 +76,22 @@ export async function createSecureNote(options: CreateItemOptions): Promise<Crea
76
76
  // Execute op command
77
77
  const result = await $`op ${args}`.json();
78
78
 
79
+ // Extract field IDs mapped by label
80
+ const fieldIds: Record<string, string> = {};
81
+ if (Array.isArray(result.fields)) {
82
+ for (const field of result.fields) {
83
+ if (field.label && field.id) {
84
+ fieldIds[field.label] = field.id;
85
+ }
86
+ }
87
+ }
88
+
79
89
  return {
80
90
  id: result.id,
81
91
  title: result.title,
82
92
  vault: result.vault?.name ?? vault,
93
+ vaultId: result.vault?.id ?? "",
94
+ fieldIds,
83
95
  };
84
96
  } catch (error) {
85
97
  const message = error instanceof Error ? error.message : String(error);
@@ -2,27 +2,89 @@ import { writeFileSync } from "node:fs";
2
2
  import pkg from "../../package.json";
3
3
  import type { TemplateOptions } from "./types";
4
4
 
5
+ const SEPARATOR = "# ===========================================================================";
6
+
7
+ /**
8
+ * Derive .env filename from template filename
9
+ * .env.tpl -> .env
10
+ * .env.local.tpl -> .env.local
11
+ */
12
+ function deriveEnvFileName(templateFileName: string): string {
13
+ if (templateFileName.endsWith(".tpl")) {
14
+ return templateFileName.slice(0, -4);
15
+ }
16
+ return templateFileName;
17
+ }
18
+
19
+ /**
20
+ * Derive template filename from .env filename
21
+ * .env -> .env.tpl
22
+ * .env.local -> .env.local.tpl
23
+ */
24
+ function deriveTemplateFileName(envFileName: string): string {
25
+ return `${envFileName}.tpl`;
26
+ }
27
+
28
+ /**
29
+ * Generate header for .env.tpl template files
30
+ */
31
+ export function generateTemplateHeader(templateFileName: string): string[] {
32
+ const envFileName = deriveEnvFileName(templateFileName);
33
+ return [
34
+ SEPARATOR,
35
+ `# ${templateFileName} — 1Password Secret References`,
36
+ "#",
37
+ "# This template contains references to secrets stored in 1Password.",
38
+ "# The actual values are not stored here — only secret references.",
39
+ "#",
40
+ `# To generate ${envFileName} with real values:`,
41
+ `# op2env ${templateFileName}`,
42
+ "#",
43
+ "# To run a command with secrets injected:",
44
+ `# op run --env-file ${templateFileName} -- npm start`,
45
+ "#",
46
+ `# Generated by env2op v${pkg.version}`,
47
+ "# https://github.com/tolgamorf/env2op-cli",
48
+ SEPARATOR,
49
+ "",
50
+ ];
51
+ }
52
+
53
+ /**
54
+ * Generate header for .env files (after pulling from 1Password)
55
+ */
56
+ export function generateEnvHeader(envFileName: string): string[] {
57
+ const templateFileName = deriveTemplateFileName(envFileName);
58
+ return [
59
+ SEPARATOR,
60
+ `# ${envFileName} — Environment Variables`,
61
+ "#",
62
+ "# WARNING: This file contains sensitive values. Do not commit to git!",
63
+ "#",
64
+ `# To push updates to 1Password and generate ${templateFileName}:`,
65
+ `# env2op ${envFileName} <vault> "<item_name>"`,
66
+ "#",
67
+ `# Pulled from 1Password by op2env v${pkg.version}`,
68
+ "# https://github.com/tolgamorf/env2op-cli",
69
+ SEPARATOR,
70
+ "",
71
+ "",
72
+ ];
73
+ }
74
+
5
75
  /**
6
76
  * Generate op:// reference template content
7
77
  *
8
78
  * Format: KEY=op://vault/item/field
9
79
  *
10
80
  * This template can be used with:
11
- * - `op inject -i template.tpl -o .env`
81
+ * - `op2env template.tpl` to generate .env
12
82
  * - `op run --env-file template.tpl -- command`
13
83
  */
14
84
  export function generateTemplateContent(options: TemplateOptions, templateFileName: string): string {
15
- const { vault, itemTitle, lines: envLines } = options;
85
+ const { vaultId, itemId, lines: envLines, fieldIds } = options;
16
86
 
17
- const outputLines: string[] = [
18
- `# Generated by env2op v${pkg.version}`,
19
- "# https://github.com/tolgamorf/env2op-cli#README",
20
- "#",
21
- "# Usage:",
22
- `# op inject -i ${templateFileName} -o .env`,
23
- `# op run --env-file ${templateFileName} -- npm start`,
24
- "",
25
- ];
87
+ const outputLines: string[] = generateTemplateHeader(templateFileName);
26
88
 
27
89
  for (const line of envLines) {
28
90
  switch (line.type) {
@@ -32,9 +94,11 @@ export function generateTemplateContent(options: TemplateOptions, templateFileNa
32
94
  case "comment":
33
95
  outputLines.push(line.content);
34
96
  break;
35
- case "variable":
36
- outputLines.push(`${line.key}=op://${vault}/${itemTitle}/${line.key}`);
97
+ case "variable": {
98
+ const fieldId = fieldIds[line.key] ?? line.key;
99
+ outputLines.push(`${line.key}=op://${vaultId}/${itemId}/${fieldId}`);
37
100
  break;
101
+ }
38
102
  }
39
103
  }
40
104
 
@@ -52,10 +116,5 @@ export function writeTemplate(content: string, outputPath: string): void {
52
116
  * Generate usage instructions for display
53
117
  */
54
118
  export function generateUsageInstructions(templatePath: string): string {
55
- return [
56
- "",
57
- "Usage:",
58
- ` op inject -i ${templatePath} -o .env`,
59
- ` op run --env-file ${templatePath} -- npm start`,
60
- ].join("\n");
119
+ return ["", "Usage:", ` op2env ${templatePath}`, ` op run --env-file ${templatePath} -- npm start`].join("\n");
61
120
  }
package/src/core/types.ts CHANGED
@@ -56,10 +56,14 @@ export interface CreateItemResult {
56
56
  title: string;
57
57
  /** Vault name */
58
58
  vault: string;
59
+ /** Vault ID */
60
+ vaultId: string;
61
+ /** Field IDs mapped by field label */
62
+ fieldIds: Record<string, string>;
59
63
  }
60
64
 
61
65
  /**
62
- * Options for the convert command
66
+ * Options for the convert command (env2op)
63
67
  */
64
68
  export interface ConvertOptions {
65
69
  /** Path to .env file */
@@ -68,24 +72,42 @@ export interface ConvertOptions {
68
72
  vault: string;
69
73
  /** Secure Note title */
70
74
  itemName: string;
75
+ /** Custom output path for template file */
76
+ output?: string;
71
77
  /** Preview mode - don't make changes */
72
78
  dryRun: boolean;
73
79
  /** Store all fields as password type */
74
80
  secret: boolean;
75
- /** Skip confirmation prompts (auto-accept) */
76
- yes: boolean;
81
+ /** Skip confirmation prompts */
82
+ force: boolean;
83
+ }
84
+
85
+ /**
86
+ * Options for the inject command (op2env)
87
+ */
88
+ export interface InjectOptions {
89
+ /** Path to template file */
90
+ templateFile: string;
91
+ /** Custom output path for .env file */
92
+ output?: string;
93
+ /** Preview mode - don't make changes */
94
+ dryRun: boolean;
95
+ /** Skip confirmation prompts */
96
+ force: boolean;
77
97
  }
78
98
 
79
99
  /**
80
100
  * Options for template generation
81
101
  */
82
102
  export interface TemplateOptions {
83
- /** Vault name */
84
- vault: string;
85
- /** Item title in 1Password */
86
- itemTitle: string;
103
+ /** Vault ID */
104
+ vaultId: string;
105
+ /** Item ID in 1Password */
106
+ itemId: string;
87
107
  /** Variables to include */
88
108
  variables: EnvVariable[];
89
109
  /** All lines preserving structure */
90
110
  lines: EnvLine[];
111
+ /** Field IDs mapped by field label */
112
+ fieldIds: Record<string, string>;
91
113
  }
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import pc from "picocolors";
4
+ import { runInject } from "./commands/inject";
5
+
6
+ const pkg = await import("../package.json");
7
+ const args = process.argv.slice(2);
8
+
9
+ // Parse arguments
10
+ const flags = new Set<string>();
11
+ const positional: string[] = [];
12
+ const options: Record<string, string> = {};
13
+
14
+ for (let i = 0; i < args.length; i++) {
15
+ const arg = args[i] as string;
16
+ if (arg === "-o" || arg === "--output") {
17
+ const next = args[i + 1];
18
+ if (next && !next.startsWith("-")) {
19
+ options.output = next;
20
+ i++; // skip next arg
21
+ }
22
+ } else if (arg.startsWith("--")) {
23
+ flags.add(arg.slice(2));
24
+ } else if (arg.startsWith("-")) {
25
+ // Handle short flags
26
+ for (const char of arg.slice(1)) {
27
+ flags.add(char);
28
+ }
29
+ } else {
30
+ positional.push(arg);
31
+ }
32
+ }
33
+
34
+ // Check for help/version flags first
35
+ const hasHelp = flags.has("h") || flags.has("help");
36
+ const hasVersion = flags.has("v") || flags.has("version");
37
+
38
+ if (hasVersion) {
39
+ console.log(pkg.version);
40
+ process.exit(0);
41
+ }
42
+
43
+ if (hasHelp || positional.length === 0) {
44
+ showHelp();
45
+ process.exit(0);
46
+ }
47
+
48
+ // Run the inject command
49
+ const [templateFile] = positional as [string];
50
+ await runInject({
51
+ templateFile,
52
+ output: options.output,
53
+ dryRun: flags.has("dry-run"),
54
+ force: flags.has("f") || flags.has("force"),
55
+ });
56
+
57
+ function showHelp(): void {
58
+ const name = pc.bold(pc.cyan("op2env"));
59
+ const version = pc.dim(`v${pkg.version}`);
60
+
61
+ console.log(`
62
+ ${name} ${version}
63
+ Pull secrets from 1Password to generate .env files
64
+
65
+ ${pc.bold("USAGE")}
66
+ ${pc.cyan("$")} op2env ${pc.yellow("<template_file>")} ${pc.dim("[options]")}
67
+
68
+ ${pc.bold("ARGUMENTS")}
69
+ ${pc.yellow("template_file")} Path to .env.tpl template file
70
+
71
+ ${pc.bold("OPTIONS")}
72
+ ${pc.cyan("-o, --output")} Output .env path (default: template without .tpl)
73
+ ${pc.cyan("-f, --force")} Overwrite without prompting
74
+ ${pc.cyan("--dry-run")} Preview actions without executing
75
+ ${pc.cyan("-h, --help")} Show this help message
76
+ ${pc.cyan("-v, --version")} Show version
77
+
78
+ ${pc.bold("EXAMPLES")}
79
+ ${pc.dim("# Basic usage - generates .env from .env.tpl")}
80
+ ${pc.cyan("$")} op2env .env.tpl
81
+
82
+ ${pc.dim("# Custom output path")}
83
+ ${pc.cyan("$")} op2env .env.tpl -o .env.local
84
+
85
+ ${pc.dim("# Preview without making changes")}
86
+ ${pc.cyan("$")} op2env .env.tpl --dry-run
87
+
88
+ ${pc.dim("# Overwrite existing .env without prompting")}
89
+ ${pc.cyan("$")} op2env .env.tpl -f
90
+
91
+ ${pc.bold("DOCUMENTATION")}
92
+ ${pc.dim("https://github.com/tolgamorf/env2op-cli")}
93
+ `);
94
+ }
@@ -25,6 +25,8 @@ export const ErrorCodes = {
25
25
  ITEM_EXISTS: "ITEM_EXISTS",
26
26
  ITEM_CREATE_FAILED: "ITEM_CREATE_FAILED",
27
27
  PARSE_ERROR: "PARSE_ERROR",
28
+ TEMPLATE_NOT_FOUND: "TEMPLATE_NOT_FOUND",
29
+ INJECT_FAILED: "INJECT_FAILED",
28
30
  } as const;
29
31
 
30
32
  export type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];