@tolgamorf/env2op-cli 0.1.0 → 0.1.2

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,34 +1,34 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { errors } from "../utils/errors";
3
- import type { EnvVariable, ParseResult } from "./types";
3
+ import type { EnvLine, EnvVariable, ParseResult } from "./types";
4
4
 
5
5
  /**
6
6
  * Parse a value from an environment variable line
7
7
  * Handles quoted strings and inline comments
8
8
  */
9
9
  function parseValue(raw: string): string {
10
- const trimmed = raw.trim();
10
+ const trimmed = raw.trim();
11
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
- }
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
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
- }
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
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();
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
32
  }
33
33
 
34
34
  /**
@@ -39,58 +39,62 @@ function parseValue(raw: string): string {
39
39
  * @throws Env2OpError if file not found
40
40
  */
41
41
  export function parseEnvFile(filePath: string): ParseResult {
42
- if (!existsSync(filePath)) {
43
- throw errors.envFileNotFound(filePath);
44
- }
42
+ if (!existsSync(filePath)) {
43
+ throw errors.envFileNotFound(filePath);
44
+ }
45
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 = "";
46
+ const content = readFileSync(filePath, "utf-8");
47
+ const rawLines = content.split("\n");
48
+ const variables: EnvVariable[] = [];
49
+ const lines: EnvLine[] = [];
50
+ const parseErrors: string[] = [];
51
+ let currentComment = "";
51
52
 
52
- for (let i = 0; i < lines.length; i++) {
53
- const line = lines[i] ?? "";
54
- const trimmed = line.trim();
55
- const lineNumber = i + 1;
53
+ for (let i = 0; i < rawLines.length; i++) {
54
+ const line = rawLines[i] ?? "";
55
+ const trimmed = line.trim();
56
+ const lineNumber = i + 1;
56
57
 
57
- // Skip empty lines (reset comment)
58
- if (!trimmed) {
59
- currentComment = "";
60
- continue;
61
- }
58
+ // Empty lines
59
+ if (!trimmed) {
60
+ lines.push({ type: "empty" });
61
+ currentComment = "";
62
+ continue;
63
+ }
62
64
 
63
- // Capture comments for next variable
64
- if (trimmed.startsWith("#")) {
65
- currentComment = trimmed.slice(1).trim();
66
- continue;
67
- }
65
+ // Comments (preserve original content including #)
66
+ if (trimmed.startsWith("#")) {
67
+ lines.push({ type: "comment", content: line });
68
+ currentComment = trimmed.slice(1).trim();
69
+ continue;
70
+ }
68
71
 
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
+ // Parse KEY=VALUE
73
+ // Key must start with letter or underscore, followed by letters, numbers, or underscores
74
+ const match = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
72
75
 
73
- if (match?.[1]) {
74
- const key = match[1];
75
- const rawValue = match[2] ?? "";
76
- const value = parseValue(rawValue);
76
+ if (match?.[1]) {
77
+ const key = match[1];
78
+ const rawValue = match[2] ?? "";
79
+ const value = parseValue(rawValue);
77
80
 
78
- variables.push({
79
- key,
80
- value,
81
- comment: currentComment || undefined,
82
- line: lineNumber,
83
- });
81
+ variables.push({
82
+ key,
83
+ value,
84
+ comment: currentComment || undefined,
85
+ line: lineNumber,
86
+ });
84
87
 
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
- }
88
+ lines.push({ type: "variable", key, value });
89
+ currentComment = "";
90
+ } else if (trimmed.includes("=")) {
91
+ // Line has = but doesn't match valid key format
92
+ parseErrors.push(`Line ${lineNumber}: Invalid variable name`);
93
+ }
94
+ // Lines without = are silently ignored (could be malformed or intentional)
95
+ }
92
96
 
93
- return { variables, errors: parseErrors };
97
+ return { variables, lines, errors: parseErrors };
94
98
  }
95
99
 
96
100
  /**
@@ -100,11 +104,8 @@ export function parseEnvFile(filePath: string): ParseResult {
100
104
  * @param filePath - Original file path for error message
101
105
  * @throws Env2OpError if no variables found
102
106
  */
103
- export function validateParseResult(
104
- result: ParseResult,
105
- filePath: string,
106
- ): void {
107
- if (result.variables.length === 0) {
108
- throw errors.envFileEmpty(filePath);
109
- }
107
+ export function validateParseResult(result: ParseResult, filePath: string): void {
108
+ if (result.variables.length === 0) {
109
+ throw errors.envFileEmpty(filePath);
110
+ }
110
111
  }
@@ -6,114 +6,119 @@ import type { CreateItemOptions, CreateItemResult } from "./types";
6
6
  * Check if the 1Password CLI is installed
7
7
  */
8
8
  export async function checkOpCli(): Promise<boolean> {
9
- try {
10
- await $`op --version`.quiet();
11
- return true;
12
- } catch {
13
- return false;
14
- }
9
+ try {
10
+ await $`op --version`.quiet();
11
+ return true;
12
+ } catch {
13
+ return false;
14
+ }
15
15
  }
16
16
 
17
17
  /**
18
18
  * Check if user is signed in to 1Password CLI
19
19
  */
20
20
  export async function checkSignedIn(): Promise<boolean> {
21
- try {
22
- await $`op account get`.quiet();
23
- return true;
24
- } catch {
25
- return false;
26
- }
21
+ try {
22
+ await $`op account get`.quiet();
23
+ return true;
24
+ } catch {
25
+ return false;
26
+ }
27
27
  }
28
28
 
29
29
  /**
30
30
  * Check if an item exists in a vault
31
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
- }
32
+ export async function itemExists(vault: string, title: string): Promise<boolean> {
33
+ try {
34
+ await $`op item get ${title} --vault ${vault}`.quiet();
35
+ return true;
36
+ } catch {
37
+ return false;
38
+ }
42
39
  }
43
40
 
44
41
  /**
45
42
  * Delete an item from a vault
46
43
  */
47
44
  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
- }
45
+ try {
46
+ await $`op item delete ${title} --vault ${vault}`.quiet();
47
+ } catch (_error) {
48
+ // Item might not exist, that's fine
49
+ }
53
50
  }
54
51
 
55
52
  /**
56
53
  * Create a Secure Note in 1Password with the given fields
57
54
  * Note: Caller should check for existing items and handle confirmation before calling this
58
55
  */
59
- export async function createSecureNote(
60
- options: CreateItemOptions,
61
- ): Promise<CreateItemResult> {
62
- const { vault, title, fields, secret } = options;
56
+ export async function createSecureNote(options: CreateItemOptions): Promise<CreateItemResult> {
57
+ const { vault, title, fields, secret } = options;
63
58
 
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
- );
59
+ // Build field arguments
60
+ // Format: key[type]=value
61
+ const fieldType = secret ? "password" : "text";
62
+ const fieldArgs = fields.map(({ key, value }) => `${key}[${fieldType}]=${value}`);
70
63
 
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
- ];
64
+ try {
65
+ // Build the command arguments array
66
+ const args = [
67
+ "item",
68
+ "create",
69
+ "--category=Secure Note",
70
+ `--vault=${vault}`,
71
+ `--title=${title}`,
72
+ "--format=json",
73
+ ...fieldArgs,
74
+ ];
82
75
 
83
- // Execute op command
84
- const result = await $`op ${args}`.json();
76
+ // Execute op command
77
+ const result = await $`op ${args}`.json();
85
78
 
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
- }
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
+
89
+ return {
90
+ id: result.id,
91
+ title: result.title,
92
+ vault: result.vault?.name ?? vault,
93
+ vaultId: result.vault?.id ?? "",
94
+ fieldIds,
95
+ };
96
+ } catch (error) {
97
+ const message = error instanceof Error ? error.message : String(error);
98
+ throw errors.itemCreateFailed(message);
99
+ }
95
100
  }
96
101
 
97
102
  /**
98
103
  * Check if a vault exists
99
104
  */
100
105
  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
- }
106
+ try {
107
+ await $`op vault get ${vault}`.quiet();
108
+ return true;
109
+ } catch {
110
+ return false;
111
+ }
107
112
  }
108
113
 
109
114
  /**
110
115
  * Create a new vault
111
116
  */
112
117
  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
- }
118
+ try {
119
+ await $`op vault create ${name}`.quiet();
120
+ } catch (error) {
121
+ const message = error instanceof Error ? error.message : String(error);
122
+ throw errors.vaultCreateFailed(message);
123
+ }
119
124
  }
@@ -1,4 +1,5 @@
1
1
  import { writeFileSync } from "node:fs";
2
+ import pkg from "../../package.json";
2
3
  import type { TemplateOptions } from "./types";
3
4
 
4
5
  /**
@@ -10,51 +11,53 @@ import type { TemplateOptions } from "./types";
10
11
  * - `op inject -i template.tpl -o .env`
11
12
  * - `op run --env-file template.tpl -- command`
12
13
  */
13
- export function generateTemplateContent(options: TemplateOptions): string {
14
- const { vault, itemTitle, variables } = options;
14
+ export function generateTemplateContent(options: TemplateOptions, templateFileName: string): string {
15
+ const { vaultId, itemId, lines: envLines, fieldIds } = options;
15
16
 
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
- ];
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
+ ];
25
26
 
26
- for (const { key, comment } of variables) {
27
- if (comment) {
28
- lines.push(`# ${comment}`);
29
- }
30
- lines.push(`${key}=op://${vault}/${itemTitle}/${key}`);
31
- }
27
+ for (const line of envLines) {
28
+ switch (line.type) {
29
+ case "empty":
30
+ outputLines.push("");
31
+ break;
32
+ case "comment":
33
+ outputLines.push(line.content);
34
+ break;
35
+ case "variable": {
36
+ const fieldId = fieldIds[line.key] ?? line.key;
37
+ outputLines.push(`${line.key}=op://${vaultId}/${itemId}/${fieldId}`);
38
+ break;
39
+ }
40
+ }
41
+ }
32
42
 
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`;
43
+ return `${outputLines.join("\n")}\n`;
41
44
  }
42
45
 
43
46
  /**
44
47
  * Write template to file
45
48
  */
46
49
  export function writeTemplate(content: string, outputPath: string): void {
47
- writeFileSync(outputPath, content, "utf-8");
50
+ writeFileSync(outputPath, content, "utf-8");
48
51
  }
49
52
 
50
53
  /**
51
54
  * Generate usage instructions for display
52
55
  */
53
56
  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");
57
+ return [
58
+ "",
59
+ "Usage:",
60
+ ` op inject -i ${templatePath} -o .env`,
61
+ ` op run --env-file ${templatePath} -- npm start`,
62
+ ].join("\n");
60
63
  }
package/src/core/types.ts CHANGED
@@ -2,78 +2,96 @@
2
2
  * Represents a single environment variable parsed from a .env file
3
3
  */
4
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;
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
13
  }
14
14
 
15
+ /**
16
+ * Represents a line in the .env file (preserves structure)
17
+ */
18
+ export type EnvLine =
19
+ | { type: "comment"; content: string }
20
+ | { type: "empty" }
21
+ | { type: "variable"; key: string; value: string };
22
+
15
23
  /**
16
24
  * Result of parsing an .env file
17
25
  */
18
26
  export interface ParseResult {
19
- /** Successfully parsed variables */
20
- variables: EnvVariable[];
21
- /** Any parse errors encountered */
22
- errors: string[];
27
+ /** Successfully parsed variables */
28
+ variables: EnvVariable[];
29
+ /** All lines preserving structure */
30
+ lines: EnvLine[];
31
+ /** Any parse errors encountered */
32
+ errors: string[];
23
33
  }
24
34
 
25
35
  /**
26
36
  * Options for creating a 1Password Secure Note
27
37
  */
28
38
  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;
39
+ /** Vault name */
40
+ vault: string;
41
+ /** Item title */
42
+ title: string;
43
+ /** Fields to store */
44
+ fields: EnvVariable[];
45
+ /** Store as password type (hidden) instead of text (visible) */
46
+ secret: boolean;
37
47
  }
38
48
 
39
49
  /**
40
50
  * Result of creating a 1Password item
41
51
  */
42
52
  export interface CreateItemResult {
43
- /** 1Password item ID */
44
- id: string;
45
- /** Item title */
46
- title: string;
47
- /** Vault name */
48
- vault: string;
53
+ /** 1Password item ID */
54
+ id: string;
55
+ /** Item title */
56
+ title: string;
57
+ /** Vault name */
58
+ vault: string;
59
+ /** Vault ID */
60
+ vaultId: string;
61
+ /** Field IDs mapped by field label */
62
+ fieldIds: Record<string, string>;
49
63
  }
50
64
 
51
65
  /**
52
66
  * Options for the convert command
53
67
  */
54
68
  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;
69
+ /** Path to .env file */
70
+ envFile: string;
71
+ /** 1Password vault name */
72
+ vault: string;
73
+ /** Secure Note title */
74
+ itemName: string;
75
+ /** Preview mode - don't make changes */
76
+ dryRun: boolean;
77
+ /** Store all fields as password type */
78
+ secret: boolean;
79
+ /** Skip confirmation prompts (auto-accept) */
80
+ yes: boolean;
67
81
  }
68
82
 
69
83
  /**
70
84
  * Options for template generation
71
85
  */
72
86
  export interface TemplateOptions {
73
- /** Vault name */
74
- vault: string;
75
- /** Item title in 1Password */
76
- itemTitle: string;
77
- /** Variables to include */
78
- variables: EnvVariable[];
87
+ /** Vault ID */
88
+ vaultId: string;
89
+ /** Item ID in 1Password */
90
+ itemId: string;
91
+ /** Variables to include */
92
+ variables: EnvVariable[];
93
+ /** All lines preserving structure */
94
+ lines: EnvLine[];
95
+ /** Field IDs mapped by field label */
96
+ fieldIds: Record<string, string>;
79
97
  }
package/src/index.ts CHANGED
@@ -4,36 +4,34 @@
4
4
  * This module exports the core functionality for programmatic use.
5
5
  */
6
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
7
  // Env parsing
18
8
  export { parseEnvFile, validateParseResult } from "./core/env-parser";
19
-
20
9
  // 1Password integration
21
10
  export {
22
- checkOpCli,
23
- checkSignedIn,
24
- itemExists,
25
- deleteItem,
26
- createSecureNote,
27
- vaultExists,
28
- createVault,
11
+ checkOpCli,
12
+ checkSignedIn,
13
+ createSecureNote,
14
+ createVault,
15
+ deleteItem,
16
+ itemExists,
17
+ vaultExists,
29
18
  } from "./core/onepassword";
30
-
31
19
  // Template generation
32
20
  export {
33
- generateTemplateContent,
34
- writeTemplate,
35
- generateUsageInstructions,
21
+ generateTemplateContent,
22
+ generateUsageInstructions,
23
+ writeTemplate,
36
24
  } from "./core/template-generator";
25
+ // Core types
26
+ export type {
27
+ ConvertOptions,
28
+ CreateItemOptions,
29
+ CreateItemResult,
30
+ EnvLine,
31
+ EnvVariable,
32
+ ParseResult,
33
+ TemplateOptions,
34
+ } from "./core/types";
37
35
 
38
36
  // Errors
39
37
  export { Env2OpError, ErrorCodes, errors } from "./utils/errors";