@tolgamorf/env2op-cli 0.1.0 → 0.1.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,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,107 @@ 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
+ return {
80
+ id: result.id,
81
+ title: result.title,
82
+ vault: result.vault?.name ?? vault,
83
+ };
84
+ } catch (error) {
85
+ const message = error instanceof Error ? error.message : String(error);
86
+ throw errors.itemCreateFailed(message);
87
+ }
95
88
  }
96
89
 
97
90
  /**
98
91
  * Check if a vault exists
99
92
  */
100
93
  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
- }
94
+ try {
95
+ await $`op vault get ${vault}`.quiet();
96
+ return true;
97
+ } catch {
98
+ return false;
99
+ }
107
100
  }
108
101
 
109
102
  /**
110
103
  * Create a new vault
111
104
  */
112
105
  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
- }
106
+ try {
107
+ await $`op vault create ${name}`.quiet();
108
+ } catch (error) {
109
+ const message = error instanceof Error ? error.message : String(error);
110
+ throw errors.vaultCreateFailed(message);
111
+ }
119
112
  }
@@ -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,51 @@ 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 { vault, itemTitle, lines: envLines } = 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
+ outputLines.push(`${line.key}=op://${vault}/${itemTitle}/${line.key}`);
37
+ break;
38
+ }
39
+ }
32
40
 
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
+ return `${outputLines.join("\n")}\n`;
41
42
  }
42
43
 
43
44
  /**
44
45
  * Write template to file
45
46
  */
46
47
  export function writeTemplate(content: string, outputPath: string): void {
47
- writeFileSync(outputPath, content, "utf-8");
48
+ writeFileSync(outputPath, content, "utf-8");
48
49
  }
49
50
 
50
51
  /**
51
52
  * Generate usage instructions for display
52
53
  */
53
54
  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");
55
+ return [
56
+ "",
57
+ "Usage:",
58
+ ` op inject -i ${templatePath} -o .env`,
59
+ ` op run --env-file ${templatePath} -- npm start`,
60
+ ].join("\n");
60
61
  }
package/src/core/types.ts CHANGED
@@ -2,78 +2,90 @@
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;
49
59
  }
50
60
 
51
61
  /**
52
62
  * Options for the convert command
53
63
  */
54
64
  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;
65
+ /** Path to .env file */
66
+ envFile: string;
67
+ /** 1Password vault name */
68
+ vault: string;
69
+ /** Secure Note title */
70
+ itemName: string;
71
+ /** Preview mode - don't make changes */
72
+ dryRun: boolean;
73
+ /** Store all fields as password type */
74
+ secret: boolean;
75
+ /** Skip confirmation prompts (auto-accept) */
76
+ yes: boolean;
67
77
  }
68
78
 
69
79
  /**
70
80
  * Options for template generation
71
81
  */
72
82
  export interface TemplateOptions {
73
- /** Vault name */
74
- vault: string;
75
- /** Item title in 1Password */
76
- itemTitle: string;
77
- /** Variables to include */
78
- variables: EnvVariable[];
83
+ /** Vault name */
84
+ vault: string;
85
+ /** Item title in 1Password */
86
+ itemTitle: string;
87
+ /** Variables to include */
88
+ variables: EnvVariable[];
89
+ /** All lines preserving structure */
90
+ lines: EnvLine[];
79
91
  }
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";