@tolgamorf/env2op-cli 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Tolga O.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,123 @@
1
+ # env2op
2
+
3
+ Convert `.env` files to 1Password Secure Notes and generate template files for `op inject` and `op run`.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ # Using bun
9
+ bun add -g @tolgamorf/env2op-cli
10
+
11
+ # Using npm
12
+ npm install -g @tolgamorf/env2op-cli
13
+
14
+ # Or run directly with bunx/npx
15
+ bunx @tolgamorf/env2op-cli .env Personal "MyApp"
16
+ ```
17
+
18
+ ## Prerequisites
19
+
20
+ - [1Password CLI](https://1password.com/downloads/command-line/) installed and signed in
21
+ - [Bun](https://bun.sh) runtime (for best performance)
22
+
23
+ ## Usage
24
+
25
+ ```bash
26
+ env2op <env_file> <vault> <item_name> [options]
27
+ ```
28
+
29
+ ### Examples
30
+
31
+ ```bash
32
+ # Basic usage - creates a Secure Note and generates .env.tpl
33
+ env2op .env.production Personal "MyApp - Production"
34
+
35
+ # Preview what would happen without making changes
36
+ env2op .env.production Personal "MyApp" --dry-run
37
+
38
+ # Store all fields as password type (hidden in 1Password)
39
+ env2op .env.production Personal "MyApp" --secret
40
+
41
+ # Skip confirmation prompts (useful for scripts/CI)
42
+ env2op .env.production Personal "MyApp" -y
43
+ ```
44
+
45
+ ### Options
46
+
47
+ | Flag | Description |
48
+ |------|-------------|
49
+ | `--dry-run` | Preview actions without executing |
50
+ | `--secret` | Store all fields as 'password' type (default: 'text') |
51
+ | `-y, --yes` | Skip confirmation prompts (auto-accept) |
52
+ | `-h, --help` | Show help |
53
+ | `-v, --version` | Show version |
54
+
55
+ ### Overwriting Existing Items
56
+
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.
58
+
59
+ ## How It Works
60
+
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
64
+
65
+ ## Using the Generated Template
66
+
67
+ After running env2op, you'll have a `.env.tpl` file with 1Password references:
68
+
69
+ ```bash
70
+ # Inject secrets into a new .env file
71
+ op inject -i .env.tpl -o .env
72
+
73
+ # Run a command with secrets injected
74
+ op run --env-file .env.tpl -- npm start
75
+ ```
76
+
77
+ ## Field Types
78
+
79
+ By default, all fields are stored as `text` type (visible in 1Password). Use `--secret` to store them as `password` type (hidden by default, revealed on click).
80
+
81
+ ## Example
82
+
83
+ Given this `.env` file:
84
+
85
+ ```env
86
+ DATABASE_URL=postgres://localhost/myapp
87
+ API_KEY=sk-1234567890
88
+ DEBUG=true
89
+ ```
90
+
91
+ Running:
92
+
93
+ ```bash
94
+ env2op .env Personal "MyApp Secrets"
95
+ ```
96
+
97
+ Creates a 1Password Secure Note with fields:
98
+ - `DATABASE_URL` (text)
99
+ - `API_KEY` (text)
100
+ - `DEBUG` (text)
101
+
102
+ And generates `.env.tpl`:
103
+
104
+ ```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
108
+ ```
109
+
110
+ ## Programmatic Usage
111
+
112
+ You can also use env2op as a library:
113
+
114
+ ```typescript
115
+ import { parseEnvFile, createSecureNote, generateTemplateContent } from "@tolgamorf/env2op-cli";
116
+
117
+ const result = parseEnvFile(".env");
118
+ console.log(result.variables);
119
+ ```
120
+
121
+ ## License
122
+
123
+ MIT
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "@tolgamorf/env2op-cli",
3
+ "version": "0.1.0",
4
+ "description": "Convert .env files to 1Password Secure Notes and generate templates for op inject/run",
5
+ "type": "module",
6
+ "main": "src/index.ts",
7
+ "module": "src/index.ts",
8
+ "types": "src/index.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./src/index.ts",
12
+ "types": "./src/index.ts"
13
+ }
14
+ },
15
+ "bin": {
16
+ "env2op": "src/cli.ts"
17
+ },
18
+ "files": ["src", "LICENSE", "README.md"],
19
+ "scripts": {
20
+ "dev": "bun run src/cli.ts",
21
+ "test": "bun test",
22
+ "typecheck": "tsc --noEmit",
23
+ "lint": "bunx biome check .",
24
+ "lint:fix": "bunx biome check . --write",
25
+ "format": "bunx biome format --write .",
26
+ "format:check": "bunx biome format .",
27
+ "prepublishOnly": "bun run typecheck"
28
+ },
29
+ "keywords": [
30
+ "env",
31
+ "1password",
32
+ "op",
33
+ "cli",
34
+ "secrets",
35
+ "environment-variables",
36
+ "dotenv",
37
+ "secure-notes",
38
+ "bun",
39
+ "op-inject",
40
+ "op-run",
41
+ "template"
42
+ ],
43
+ "author": {
44
+ "name": "Tolga O.",
45
+ "url": "https://github.com/tolgamorf"
46
+ },
47
+ "license": "MIT",
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "git+https://github.com/tolgamorf/env2op-cli.git"
51
+ },
52
+ "bugs": {
53
+ "url": "https://github.com/tolgamorf/env2op-cli/issues"
54
+ },
55
+ "homepage": "https://github.com/tolgamorf/env2op-cli#readme",
56
+ "engines": {
57
+ "bun": ">=1.0.0"
58
+ },
59
+ "dependencies": {
60
+ "@clack/prompts": "^0.11.0",
61
+ "picocolors": "^1.1.1"
62
+ },
63
+ "devDependencies": {
64
+ "@types/bun": "latest",
65
+ "typescript": "^5.9.3",
66
+ "@tsconfig/bun": "^1.0.10",
67
+ "@biomejs/biome": "^1.9.4"
68
+ }
69
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import pc from "picocolors";
4
+ import { runConvert } from "./commands/convert";
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
+
13
+ for (const arg of args) {
14
+ if (arg.startsWith("--")) {
15
+ flags.add(arg.slice(2));
16
+ } else if (arg.startsWith("-")) {
17
+ // Handle short flags
18
+ for (const char of arg.slice(1)) {
19
+ flags.add(char);
20
+ }
21
+ } else {
22
+ positional.push(arg);
23
+ }
24
+ }
25
+
26
+ // Check for help/version flags first
27
+ const hasHelp = flags.has("h") || flags.has("help");
28
+ const hasVersion = flags.has("v") || flags.has("version");
29
+
30
+ if (hasVersion) {
31
+ console.log(pkg.version);
32
+ process.exit(0);
33
+ }
34
+
35
+ if (hasHelp || positional.length === 0) {
36
+ showHelp();
37
+ process.exit(0);
38
+ }
39
+
40
+ if (positional.length < 3) {
41
+ showMissingArgsError(positional);
42
+ process.exit(1);
43
+ }
44
+
45
+ // All positional args present, run the command
46
+ const [envFile, vault, itemName] = positional as [string, string, string];
47
+ await runConvert({
48
+ envFile,
49
+ vault,
50
+ itemName,
51
+ dryRun: flags.has("dry-run"),
52
+ secret: flags.has("secret"),
53
+ yes: flags.has("y") || flags.has("yes"),
54
+ });
55
+
56
+ function showHelp(): void {
57
+ const name = pc.bold(pc.cyan("env2op"));
58
+ const version = pc.dim(`v${pkg.version}`);
59
+
60
+ console.log(`
61
+ ${name} ${version}
62
+ ${pkg.description}
63
+
64
+ ${pc.bold("USAGE")}
65
+ ${pc.cyan("$")} env2op ${pc.yellow("<env_file>")} ${pc.yellow("<vault>")} ${pc.yellow("<item_name>")} ${pc.dim("[options]")}
66
+
67
+ ${pc.bold("ARGUMENTS")}
68
+ ${pc.yellow("env_file")} Path to .env file
69
+ ${pc.yellow("vault")} 1Password vault name
70
+ ${pc.yellow("item_name")} Name for the Secure Note in 1Password
71
+
72
+ ${pc.bold("OPTIONS")}
73
+ ${pc.cyan("--dry-run")} Preview actions without executing
74
+ ${pc.cyan("--secret")} Store all fields as password type (hidden)
75
+ ${pc.cyan("-y, --yes")} Skip confirmation prompts (auto-accept)
76
+ ${pc.cyan("-h, --help")} Show this help message
77
+ ${pc.cyan("-v, --version")} Show version
78
+
79
+ ${pc.bold("EXAMPLES")}
80
+ ${pc.dim("# Basic usage")}
81
+ ${pc.cyan("$")} env2op .env.production Personal "MyApp - Production"
82
+
83
+ ${pc.dim("# Preview without making changes")}
84
+ ${pc.cyan("$")} env2op .env Personal "MyApp" --dry-run
85
+
86
+ ${pc.dim("# Store as hidden password fields")}
87
+ ${pc.cyan("$")} env2op .env Personal "MyApp" --secret
88
+
89
+ ${pc.dim("# Skip confirmation prompts (for CI/scripts)")}
90
+ ${pc.cyan("$")} env2op .env Personal "MyApp" -y
91
+
92
+ ${pc.bold("DOCUMENTATION")}
93
+ ${pc.dim("https://github.com/tolgamorf/env2op")}
94
+ `);
95
+ }
96
+
97
+ function showMissingArgsError(provided: string[]): void {
98
+ const missing: string[] = [];
99
+
100
+ if (provided.length < 1) missing.push("env_file");
101
+ if (provided.length < 2) missing.push("vault");
102
+ if (provided.length < 3) missing.push("item_name");
103
+
104
+ console.log(`
105
+ ${pc.red(pc.bold("Error:"))} Missing required arguments
106
+
107
+ ${pc.bold("Usage:")} env2op ${pc.yellow("<env_file>")} ${pc.yellow("<vault>")} ${pc.yellow("<item_name>")} ${pc.dim("[options]")}
108
+
109
+ ${pc.bold("Missing:")}
110
+ ${missing.map((arg) => ` ${pc.red("•")} ${pc.yellow(arg)}`).join("\n")}
111
+
112
+ ${pc.bold("Example:")}
113
+ ${pc.cyan("$")} env2op .env.production Personal "MyApp - Production"
114
+
115
+ Run ${pc.cyan("env2op --help")} for more information.
116
+ `);
117
+ }
@@ -0,0 +1,182 @@
1
+ import { basename, dirname, join } from "node:path";
2
+ import * as p from "@clack/prompts";
3
+ import { parseEnvFile, validateParseResult } from "../core/env-parser";
4
+ import {
5
+ checkOpCli,
6
+ checkSignedIn,
7
+ createSecureNote,
8
+ createVault,
9
+ deleteItem,
10
+ itemExists,
11
+ vaultExists,
12
+ } from "../core/onepassword";
13
+ import {
14
+ generateTemplateContent,
15
+ generateUsageInstructions,
16
+ writeTemplate,
17
+ } from "../core/template-generator";
18
+ import type { ConvertOptions } from "../core/types";
19
+ import { Env2OpError } from "../utils/errors";
20
+ import { logger } from "../utils/logger";
21
+
22
+ /**
23
+ * Execute the convert operation
24
+ */
25
+ export async function runConvert(options: ConvertOptions): Promise<void> {
26
+ const { envFile, vault, itemName, dryRun, secret, yes } = options;
27
+
28
+ // Display intro
29
+ const pkg = await import("../../package.json");
30
+ logger.intro("env2op", pkg.version, dryRun);
31
+
32
+ try {
33
+ // Step 1: Parse .env file
34
+ const parseResult = parseEnvFile(envFile);
35
+ validateParseResult(parseResult, envFile);
36
+
37
+ const { variables } = parseResult;
38
+
39
+ logger.success(`Parsed ${basename(envFile)}`);
40
+ logger.message(
41
+ `Found ${variables.length} environment variable${variables.length === 1 ? "" : "s"}`,
42
+ );
43
+
44
+ // Show parse errors as warnings
45
+ for (const error of parseResult.errors) {
46
+ logger.warn(error);
47
+ }
48
+
49
+ // Step 2: Create 1Password Secure Note
50
+ if (dryRun) {
51
+ logger.warn("Would create Secure Note");
52
+ logger.keyValue("Vault", vault);
53
+ logger.keyValue("Title", itemName);
54
+ logger.keyValue("Type", secret ? "password (hidden)" : "text (visible)");
55
+ logger.keyValue(
56
+ "Fields",
57
+ logger.formatFields(variables.map((v) => v.key)),
58
+ );
59
+ } else {
60
+ // Check 1Password CLI before attempting
61
+ const opInstalled = await checkOpCli();
62
+ if (!opInstalled) {
63
+ throw new Env2OpError(
64
+ "1Password CLI (op) is not installed",
65
+ "OP_CLI_NOT_INSTALLED",
66
+ "Install from https://1password.com/downloads/command-line/",
67
+ );
68
+ }
69
+
70
+ const signedIn = await checkSignedIn();
71
+ if (!signedIn) {
72
+ throw new Env2OpError(
73
+ "Not signed in to 1Password CLI",
74
+ "OP_NOT_SIGNED_IN",
75
+ 'Run "op signin" to authenticate',
76
+ );
77
+ }
78
+
79
+ // Check if vault exists
80
+ const vaultFound = await vaultExists(vault);
81
+
82
+ if (!vaultFound) {
83
+ if (yes) {
84
+ // Auto-create vault
85
+ logger.warn(`Vault "${vault}" not found, creating...`);
86
+ await createVault(vault);
87
+ logger.success(`Created vault "${vault}"`);
88
+ } else {
89
+ // Ask for confirmation to create vault
90
+ const shouldCreate = await p.confirm({
91
+ message: `Vault "${vault}" does not exist. Create it?`,
92
+ });
93
+
94
+ if (p.isCancel(shouldCreate) || !shouldCreate) {
95
+ logger.cancel("Operation cancelled");
96
+ logger.info('Run "op vault list" to see available vaults');
97
+ process.exit(0);
98
+ }
99
+
100
+ const spinner = logger.spinner();
101
+ spinner.start(`Creating vault "${vault}"...`);
102
+ await createVault(vault);
103
+ spinner.stop(`Created vault "${vault}"`);
104
+ }
105
+ }
106
+
107
+ // Check if item already exists
108
+ const exists = await itemExists(vault, itemName);
109
+
110
+ if (exists) {
111
+ if (yes) {
112
+ // Auto-accept: delete and recreate
113
+ logger.warn(`Item "${itemName}" already exists, overwriting...`);
114
+ await deleteItem(vault, itemName);
115
+ } else {
116
+ // Ask for confirmation
117
+ const shouldOverwrite = await p.confirm({
118
+ message: `Item "${itemName}" already exists in vault "${vault}". Overwrite?`,
119
+ });
120
+
121
+ if (p.isCancel(shouldOverwrite) || !shouldOverwrite) {
122
+ logger.cancel("Operation cancelled");
123
+ process.exit(0);
124
+ }
125
+
126
+ await deleteItem(vault, itemName);
127
+ }
128
+ }
129
+
130
+ const spinner = logger.spinner();
131
+ spinner.start("Creating 1Password Secure Note...");
132
+
133
+ try {
134
+ const result = await createSecureNote({
135
+ vault,
136
+ title: itemName,
137
+ fields: variables,
138
+ secret,
139
+ });
140
+
141
+ spinner.stop(`Created "${result.title}" in vault "${result.vault}"`);
142
+ } catch (error) {
143
+ spinner.stop("Failed to create Secure Note");
144
+ throw error;
145
+ }
146
+ }
147
+
148
+ // Step 3: Generate template file
149
+ const templateContent = generateTemplateContent({
150
+ vault,
151
+ itemTitle: itemName,
152
+ variables,
153
+ });
154
+
155
+ const templatePath = join(dirname(envFile), `${basename(envFile)}.tpl`);
156
+
157
+ if (dryRun) {
158
+ logger.warn(`Would generate template: ${templatePath}`);
159
+ } else {
160
+ writeTemplate(templateContent, templatePath);
161
+ logger.success(`Generated template: ${templatePath}`);
162
+ }
163
+
164
+ // Step 4: Show usage instructions
165
+ if (dryRun) {
166
+ logger.outro("Dry run complete. No changes made.");
167
+ } else {
168
+ const usage = generateUsageInstructions(templatePath);
169
+ logger.note(usage, "Next steps");
170
+ logger.outro("Done! Your secrets are now in 1Password");
171
+ }
172
+ } catch (error) {
173
+ if (error instanceof Env2OpError) {
174
+ logger.error(error.message);
175
+ if (error.suggestion) {
176
+ logger.info(`Suggestion: ${error.suggestion}`);
177
+ }
178
+ process.exit(1);
179
+ }
180
+ throw error;
181
+ }
182
+ }
@@ -0,0 +1,110 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { errors } from "../utils/errors";
3
+ import type { EnvVariable, ParseResult } from "./types";
4
+
5
+ /**
6
+ * Parse a value from an environment variable line
7
+ * Handles quoted strings and inline comments
8
+ */
9
+ function parseValue(raw: string): string {
10
+ const trimmed = raw.trim();
11
+
12
+ // Handle double-quoted values
13
+ if (trimmed.startsWith('"')) {
14
+ const endQuote = trimmed.indexOf('"', 1);
15
+ if (endQuote !== -1) {
16
+ return trimmed.slice(1, endQuote);
17
+ }
18
+ }
19
+
20
+ // Handle single-quoted values
21
+ if (trimmed.startsWith("'")) {
22
+ const endQuote = trimmed.indexOf("'", 1);
23
+ if (endQuote !== -1) {
24
+ return trimmed.slice(1, endQuote);
25
+ }
26
+ }
27
+
28
+ // Handle unquoted values with potential inline comments
29
+ // Only treat # as comment if preceded by whitespace
30
+ const parts = trimmed.split(/\s+#/);
31
+ return (parts[0] ?? trimmed).trim();
32
+ }
33
+
34
+ /**
35
+ * Parse an .env file and extract environment variables
36
+ *
37
+ * @param filePath - Path to the .env file
38
+ * @returns ParseResult containing variables and any errors
39
+ * @throws Env2OpError if file not found
40
+ */
41
+ export function parseEnvFile(filePath: string): ParseResult {
42
+ if (!existsSync(filePath)) {
43
+ throw errors.envFileNotFound(filePath);
44
+ }
45
+
46
+ const content = readFileSync(filePath, "utf-8");
47
+ const lines = content.split("\n");
48
+ const variables: EnvVariable[] = [];
49
+ const parseErrors: string[] = [];
50
+ let currentComment = "";
51
+
52
+ for (let i = 0; i < lines.length; i++) {
53
+ const line = lines[i] ?? "";
54
+ const trimmed = line.trim();
55
+ const lineNumber = i + 1;
56
+
57
+ // Skip empty lines (reset comment)
58
+ if (!trimmed) {
59
+ currentComment = "";
60
+ continue;
61
+ }
62
+
63
+ // Capture comments for next variable
64
+ if (trimmed.startsWith("#")) {
65
+ currentComment = trimmed.slice(1).trim();
66
+ continue;
67
+ }
68
+
69
+ // Parse KEY=VALUE
70
+ // Key must start with letter or underscore, followed by letters, numbers, or underscores
71
+ const match = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
72
+
73
+ if (match?.[1]) {
74
+ const key = match[1];
75
+ const rawValue = match[2] ?? "";
76
+ const value = parseValue(rawValue);
77
+
78
+ variables.push({
79
+ key,
80
+ value,
81
+ comment: currentComment || undefined,
82
+ line: lineNumber,
83
+ });
84
+
85
+ currentComment = "";
86
+ } else if (trimmed.includes("=")) {
87
+ // Line has = but doesn't match valid key format
88
+ parseErrors.push(`Line ${lineNumber}: Invalid variable name`);
89
+ }
90
+ // Lines without = are silently ignored (could be malformed or intentional)
91
+ }
92
+
93
+ return { variables, errors: parseErrors };
94
+ }
95
+
96
+ /**
97
+ * Validate that the parsed result has variables
98
+ *
99
+ * @param result - ParseResult from parseEnvFile
100
+ * @param filePath - Original file path for error message
101
+ * @throws Env2OpError if no variables found
102
+ */
103
+ export function validateParseResult(
104
+ result: ParseResult,
105
+ filePath: string,
106
+ ): void {
107
+ if (result.variables.length === 0) {
108
+ throw errors.envFileEmpty(filePath);
109
+ }
110
+ }
@@ -0,0 +1,119 @@
1
+ import { $ } from "bun";
2
+ import { errors } from "../utils/errors";
3
+ import type { CreateItemOptions, CreateItemResult } from "./types";
4
+
5
+ /**
6
+ * Check if the 1Password CLI is installed
7
+ */
8
+ export async function checkOpCli(): Promise<boolean> {
9
+ try {
10
+ await $`op --version`.quiet();
11
+ return true;
12
+ } catch {
13
+ return false;
14
+ }
15
+ }
16
+
17
+ /**
18
+ * Check if user is signed in to 1Password CLI
19
+ */
20
+ export async function checkSignedIn(): Promise<boolean> {
21
+ try {
22
+ await $`op account get`.quiet();
23
+ return true;
24
+ } catch {
25
+ return false;
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Check if an item exists in a vault
31
+ */
32
+ export async function itemExists(
33
+ vault: string,
34
+ title: string,
35
+ ): Promise<boolean> {
36
+ try {
37
+ await $`op item get ${title} --vault ${vault}`.quiet();
38
+ return true;
39
+ } catch {
40
+ return false;
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Delete an item from a vault
46
+ */
47
+ export async function deleteItem(vault: string, title: string): Promise<void> {
48
+ try {
49
+ await $`op item delete ${title} --vault ${vault}`.quiet();
50
+ } catch (error) {
51
+ // Item might not exist, that's fine
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Create a Secure Note in 1Password with the given fields
57
+ * Note: Caller should check for existing items and handle confirmation before calling this
58
+ */
59
+ export async function createSecureNote(
60
+ options: CreateItemOptions,
61
+ ): Promise<CreateItemResult> {
62
+ const { vault, title, fields, secret } = options;
63
+
64
+ // Build field arguments
65
+ // Format: key[type]=value
66
+ const fieldType = secret ? "password" : "text";
67
+ const fieldArgs = fields.map(
68
+ ({ key, value }) => `${key}[${fieldType}]=${value}`,
69
+ );
70
+
71
+ try {
72
+ // Build the command arguments array
73
+ const args = [
74
+ "item",
75
+ "create",
76
+ "--category=Secure Note",
77
+ `--vault=${vault}`,
78
+ `--title=${title}`,
79
+ "--format=json",
80
+ ...fieldArgs,
81
+ ];
82
+
83
+ // Execute op command
84
+ const result = await $`op ${args}`.json();
85
+
86
+ return {
87
+ id: result.id,
88
+ title: result.title,
89
+ vault: result.vault?.name ?? vault,
90
+ };
91
+ } catch (error) {
92
+ const message = error instanceof Error ? error.message : String(error);
93
+ throw errors.itemCreateFailed(message);
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Check if a vault exists
99
+ */
100
+ export async function vaultExists(vault: string): Promise<boolean> {
101
+ try {
102
+ await $`op vault get ${vault}`.quiet();
103
+ return true;
104
+ } catch {
105
+ return false;
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Create a new vault
111
+ */
112
+ export async function createVault(name: string): Promise<void> {
113
+ try {
114
+ await $`op vault create ${name}`.quiet();
115
+ } catch (error) {
116
+ const message = error instanceof Error ? error.message : String(error);
117
+ throw errors.vaultCreateFailed(message);
118
+ }
119
+ }
@@ -0,0 +1,60 @@
1
+ import { writeFileSync } from "node:fs";
2
+ import type { TemplateOptions } from "./types";
3
+
4
+ /**
5
+ * Generate op:// reference template content
6
+ *
7
+ * Format: KEY=op://vault/item/field
8
+ *
9
+ * This template can be used with:
10
+ * - `op inject -i template.tpl -o .env`
11
+ * - `op run --env-file template.tpl -- command`
12
+ */
13
+ export function generateTemplateContent(options: TemplateOptions): string {
14
+ const { vault, itemTitle, variables } = options;
15
+
16
+ const lines: string[] = [
17
+ "# Generated by env2op",
18
+ "# https://github.com/tolgamorf/env2op",
19
+ "#",
20
+ "# Usage:",
21
+ `# op inject -i ${getTemplateName(options)} -o .env`,
22
+ `# op run --env-file ${getTemplateName(options)} -- npm start`,
23
+ "",
24
+ ];
25
+
26
+ for (const { key, comment } of variables) {
27
+ if (comment) {
28
+ lines.push(`# ${comment}`);
29
+ }
30
+ lines.push(`${key}=op://${vault}/${itemTitle}/${key}`);
31
+ }
32
+
33
+ return `${lines.join("\n")}\n`;
34
+ }
35
+
36
+ /**
37
+ * Get the template filename based on the source file
38
+ */
39
+ function getTemplateName(options: TemplateOptions): string {
40
+ return `${options.itemTitle.toLowerCase().replace(/\s+/g, "-")}.tpl`;
41
+ }
42
+
43
+ /**
44
+ * Write template to file
45
+ */
46
+ export function writeTemplate(content: string, outputPath: string): void {
47
+ writeFileSync(outputPath, content, "utf-8");
48
+ }
49
+
50
+ /**
51
+ * Generate usage instructions for display
52
+ */
53
+ export function generateUsageInstructions(templatePath: string): string {
54
+ return [
55
+ "",
56
+ "Usage:",
57
+ ` op inject -i ${templatePath} -o .env`,
58
+ ` op run --env-file ${templatePath} -- npm start`,
59
+ ].join("\n");
60
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Represents a single environment variable parsed from a .env file
3
+ */
4
+ export interface EnvVariable {
5
+ /** The variable name/key */
6
+ key: string;
7
+ /** The variable value */
8
+ value: string;
9
+ /** Optional comment from preceding line */
10
+ comment?: string;
11
+ /** Line number in source file */
12
+ line: number;
13
+ }
14
+
15
+ /**
16
+ * Result of parsing an .env file
17
+ */
18
+ export interface ParseResult {
19
+ /** Successfully parsed variables */
20
+ variables: EnvVariable[];
21
+ /** Any parse errors encountered */
22
+ errors: string[];
23
+ }
24
+
25
+ /**
26
+ * Options for creating a 1Password Secure Note
27
+ */
28
+ export interface CreateItemOptions {
29
+ /** Vault name */
30
+ vault: string;
31
+ /** Item title */
32
+ title: string;
33
+ /** Fields to store */
34
+ fields: EnvVariable[];
35
+ /** Store as password type (hidden) instead of text (visible) */
36
+ secret: boolean;
37
+ }
38
+
39
+ /**
40
+ * Result of creating a 1Password item
41
+ */
42
+ export interface CreateItemResult {
43
+ /** 1Password item ID */
44
+ id: string;
45
+ /** Item title */
46
+ title: string;
47
+ /** Vault name */
48
+ vault: string;
49
+ }
50
+
51
+ /**
52
+ * Options for the convert command
53
+ */
54
+ export interface ConvertOptions {
55
+ /** Path to .env file */
56
+ envFile: string;
57
+ /** 1Password vault name */
58
+ vault: string;
59
+ /** Secure Note title */
60
+ itemName: string;
61
+ /** Preview mode - don't make changes */
62
+ dryRun: boolean;
63
+ /** Store all fields as password type */
64
+ secret: boolean;
65
+ /** Skip confirmation prompts (auto-accept) */
66
+ yes: boolean;
67
+ }
68
+
69
+ /**
70
+ * Options for template generation
71
+ */
72
+ export interface TemplateOptions {
73
+ /** Vault name */
74
+ vault: string;
75
+ /** Item title in 1Password */
76
+ itemTitle: string;
77
+ /** Variables to include */
78
+ variables: EnvVariable[];
79
+ }
package/src/index.ts ADDED
@@ -0,0 +1,39 @@
1
+ /**
2
+ * env2op - Convert .env files to 1Password Secure Notes
3
+ *
4
+ * This module exports the core functionality for programmatic use.
5
+ */
6
+
7
+ // Core types
8
+ export type {
9
+ EnvVariable,
10
+ ParseResult,
11
+ CreateItemOptions,
12
+ CreateItemResult,
13
+ ConvertOptions,
14
+ TemplateOptions,
15
+ } from "./core/types";
16
+
17
+ // Env parsing
18
+ export { parseEnvFile, validateParseResult } from "./core/env-parser";
19
+
20
+ // 1Password integration
21
+ export {
22
+ checkOpCli,
23
+ checkSignedIn,
24
+ itemExists,
25
+ deleteItem,
26
+ createSecureNote,
27
+ vaultExists,
28
+ createVault,
29
+ } from "./core/onepassword";
30
+
31
+ // Template generation
32
+ export {
33
+ generateTemplateContent,
34
+ writeTemplate,
35
+ generateUsageInstructions,
36
+ } from "./core/template-generator";
37
+
38
+ // Errors
39
+ export { Env2OpError, ErrorCodes, errors } from "./utils/errors";
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Custom error class for env2op with error codes and suggestions
3
+ */
4
+ export class Env2OpError extends Error {
5
+ constructor(
6
+ message: string,
7
+ public code: ErrorCode,
8
+ public suggestion?: string,
9
+ ) {
10
+ super(message);
11
+ this.name = "Env2OpError";
12
+ }
13
+ }
14
+
15
+ /**
16
+ * Error codes for different failure scenarios
17
+ */
18
+ export const ErrorCodes = {
19
+ ENV_FILE_NOT_FOUND: "ENV_FILE_NOT_FOUND",
20
+ ENV_FILE_EMPTY: "ENV_FILE_EMPTY",
21
+ OP_CLI_NOT_INSTALLED: "OP_CLI_NOT_INSTALLED",
22
+ OP_NOT_SIGNED_IN: "OP_NOT_SIGNED_IN",
23
+ VAULT_NOT_FOUND: "VAULT_NOT_FOUND",
24
+ VAULT_CREATE_FAILED: "VAULT_CREATE_FAILED",
25
+ ITEM_EXISTS: "ITEM_EXISTS",
26
+ ITEM_CREATE_FAILED: "ITEM_CREATE_FAILED",
27
+ PARSE_ERROR: "PARSE_ERROR",
28
+ } as const;
29
+
30
+ export type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];
31
+
32
+ /**
33
+ * Error factory functions for common scenarios
34
+ */
35
+ export const errors = {
36
+ envFileNotFound: (path: string) =>
37
+ new Env2OpError(
38
+ `File not found: ${path}`,
39
+ ErrorCodes.ENV_FILE_NOT_FOUND,
40
+ "Check that the file path is correct",
41
+ ),
42
+
43
+ envFileEmpty: (path: string) =>
44
+ new Env2OpError(
45
+ `No valid environment variables found in ${path}`,
46
+ ErrorCodes.ENV_FILE_EMPTY,
47
+ "Ensure the file contains KEY=value pairs",
48
+ ),
49
+
50
+ opCliNotInstalled: () =>
51
+ new Env2OpError(
52
+ "1Password CLI (op) is not installed",
53
+ ErrorCodes.OP_CLI_NOT_INSTALLED,
54
+ "Install it from https://1password.com/downloads/command-line/",
55
+ ),
56
+
57
+ opNotSignedIn: () =>
58
+ new Env2OpError(
59
+ "Not signed in to 1Password CLI",
60
+ ErrorCodes.OP_NOT_SIGNED_IN,
61
+ 'Run "op signin" to authenticate',
62
+ ),
63
+
64
+ vaultNotFound: (vault: string) =>
65
+ new Env2OpError(
66
+ `Vault not found: ${vault}`,
67
+ ErrorCodes.VAULT_NOT_FOUND,
68
+ 'Run "op vault list" to see available vaults',
69
+ ),
70
+
71
+ vaultCreateFailed: (message: string) =>
72
+ new Env2OpError(
73
+ `Failed to create vault: ${message}`,
74
+ ErrorCodes.VAULT_CREATE_FAILED,
75
+ ),
76
+
77
+ itemExists: (title: string, vault: string) =>
78
+ new Env2OpError(
79
+ `Item "${title}" already exists in vault "${vault}"`,
80
+ ErrorCodes.ITEM_EXISTS,
81
+ "Use default behavior (overwrites) or choose a different item name",
82
+ ),
83
+
84
+ itemCreateFailed: (message: string) =>
85
+ new Env2OpError(
86
+ `Failed to create 1Password item: ${message}`,
87
+ ErrorCodes.ITEM_CREATE_FAILED,
88
+ ),
89
+
90
+ parseError: (line: number, message: string) =>
91
+ new Env2OpError(
92
+ `Parse error at line ${line}: ${message}`,
93
+ ErrorCodes.PARSE_ERROR,
94
+ ),
95
+ };
@@ -0,0 +1,144 @@
1
+ import * as p from "@clack/prompts";
2
+ import pc from "picocolors";
3
+
4
+ /**
5
+ * Unicode symbols for different message types
6
+ */
7
+ const symbols = {
8
+ success: pc.green("\u2713"),
9
+ error: pc.red("\u2717"),
10
+ warning: pc.yellow("\u26A0"),
11
+ info: pc.blue("\u2139"),
12
+ arrow: pc.cyan("\u2192"),
13
+ bullet: pc.dim("\u2022"),
14
+ };
15
+
16
+ /**
17
+ * Logger utility for formatted CLI output using @clack/prompts
18
+ */
19
+ export const logger = {
20
+ /**
21
+ * Display CLI intro banner
22
+ */
23
+ intro(name: string, version: string, dryRun = false) {
24
+ const label = dryRun
25
+ ? pc.bgYellow(pc.black(` ${name} v${version} [DRY RUN] `))
26
+ : pc.bgCyan(pc.black(` ${name} v${version} `));
27
+ p.intro(label);
28
+ },
29
+
30
+ /**
31
+ * Display section header
32
+ */
33
+ section(title: string) {
34
+ console.log(`\n${pc.bold(pc.underline(title))}`);
35
+ },
36
+
37
+ /**
38
+ * Success message
39
+ */
40
+ success(message: string) {
41
+ p.log.success(message);
42
+ },
43
+
44
+ /**
45
+ * Error message
46
+ */
47
+ error(message: string) {
48
+ p.log.error(message);
49
+ },
50
+
51
+ /**
52
+ * Warning message
53
+ */
54
+ warn(message: string) {
55
+ p.log.warn(message);
56
+ },
57
+
58
+ /**
59
+ * Info message
60
+ */
61
+ info(message: string) {
62
+ p.log.info(message);
63
+ },
64
+
65
+ /**
66
+ * Step in a process
67
+ */
68
+ step(message: string) {
69
+ p.log.step(message);
70
+ },
71
+
72
+ /**
73
+ * Message (neutral)
74
+ */
75
+ message(message: string) {
76
+ p.log.message(message);
77
+ },
78
+
79
+ /**
80
+ * Display key-value pair
81
+ */
82
+ keyValue(key: string, value: string, indent = 2) {
83
+ console.log(`${" ".repeat(indent)}${pc.dim(key)}: ${pc.cyan(value)}`);
84
+ },
85
+
86
+ /**
87
+ * Display list item
88
+ */
89
+ listItem(item: string, indent = 2) {
90
+ console.log(`${" ".repeat(indent)}${symbols.bullet} ${item}`);
91
+ },
92
+
93
+ /**
94
+ * Display arrow item
95
+ */
96
+ arrowItem(item: string, indent = 2) {
97
+ console.log(`${" ".repeat(indent)}${symbols.arrow} ${item}`);
98
+ },
99
+
100
+ /**
101
+ * Display dry run indicator
102
+ */
103
+ dryRun(message: string) {
104
+ console.log(`${pc.yellow("[DRY RUN]")} ${message}`);
105
+ },
106
+
107
+ /**
108
+ * Create a spinner for async operations
109
+ */
110
+ spinner() {
111
+ return p.spinner();
112
+ },
113
+
114
+ /**
115
+ * Display outro message
116
+ */
117
+ outro(message: string) {
118
+ p.outro(pc.green(message));
119
+ },
120
+
121
+ /**
122
+ * Display cancellation message
123
+ */
124
+ cancel(message: string) {
125
+ p.cancel(message);
126
+ },
127
+
128
+ /**
129
+ * Display a note block
130
+ */
131
+ note(message: string, title?: string) {
132
+ p.note(message, title);
133
+ },
134
+
135
+ /**
136
+ * Format a field list for display
137
+ */
138
+ formatFields(fields: string[], max = 3): string {
139
+ if (fields.length <= max) {
140
+ return fields.join(", ");
141
+ }
142
+ return `${fields.slice(0, max).join(", ")}, ... and ${fields.length - max} more`;
143
+ },
144
+ };