@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.
- package/package.json +72 -67
- package/src/cli.ts +30 -30
- package/src/commands/convert.ts +163 -168
- package/src/core/env-parser.ts +71 -70
- package/src/core/onepassword.ts +64 -71
- package/src/core/template-generator.ts +33 -32
- package/src/core/types.ts +56 -44
- package/src/index.ts +20 -22
- package/src/utils/errors.ts +59 -68
- package/src/utils/logger.ts +130 -130
package/package.json
CHANGED
|
@@ -1,69 +1,74 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
2
|
+
"name": "@tolgamorf/env2op-cli",
|
|
3
|
+
"version": "0.1.1",
|
|
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": [
|
|
19
|
+
"src",
|
|
20
|
+
"LICENSE",
|
|
21
|
+
"README.md"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"dev": "bun run src/cli.ts",
|
|
25
|
+
"test": "bun test",
|
|
26
|
+
"typecheck": "tsc --noEmit",
|
|
27
|
+
"lint": "bunx biome check .",
|
|
28
|
+
"lint:fix": "bunx biome check . --write",
|
|
29
|
+
"format": "bunx biome format --write .",
|
|
30
|
+
"format:check": "bunx biome format .",
|
|
31
|
+
"prepublishOnly": "bun run typecheck",
|
|
32
|
+
"release": "bun run scripts/release.ts"
|
|
33
|
+
},
|
|
34
|
+
"keywords": [
|
|
35
|
+
"env",
|
|
36
|
+
"1password",
|
|
37
|
+
"op",
|
|
38
|
+
"cli",
|
|
39
|
+
"secrets",
|
|
40
|
+
"environment-variables",
|
|
41
|
+
"dotenv",
|
|
42
|
+
"secure-notes",
|
|
43
|
+
"bun",
|
|
44
|
+
"op-inject",
|
|
45
|
+
"op-run",
|
|
46
|
+
"template"
|
|
47
|
+
],
|
|
48
|
+
"author": {
|
|
49
|
+
"name": "Tolga O.",
|
|
50
|
+
"url": "https://github.com/tolgamorf"
|
|
51
|
+
},
|
|
52
|
+
"license": "MIT",
|
|
53
|
+
"repository": {
|
|
54
|
+
"type": "git",
|
|
55
|
+
"url": "git+https://github.com/tolgamorf/env2op-cli.git"
|
|
56
|
+
},
|
|
57
|
+
"bugs": {
|
|
58
|
+
"url": "https://github.com/tolgamorf/env2op-cli/issues"
|
|
59
|
+
},
|
|
60
|
+
"homepage": "https://github.com/tolgamorf/env2op-cli#readme",
|
|
61
|
+
"engines": {
|
|
62
|
+
"bun": ">=1.0.0"
|
|
63
|
+
},
|
|
64
|
+
"dependencies": {
|
|
65
|
+
"@clack/prompts": "^0.11.0",
|
|
66
|
+
"picocolors": "^1.1.1"
|
|
67
|
+
},
|
|
68
|
+
"devDependencies": {
|
|
69
|
+
"@types/bun": "^1.3.5",
|
|
70
|
+
"typescript": "^5.9.3",
|
|
71
|
+
"@tsconfig/bun": "^1.0.10",
|
|
72
|
+
"@biomejs/biome": "^2.3.10"
|
|
73
|
+
}
|
|
69
74
|
}
|
package/src/cli.ts
CHANGED
|
@@ -11,16 +11,16 @@ const flags = new Set<string>();
|
|
|
11
11
|
const positional: string[] = [];
|
|
12
12
|
|
|
13
13
|
for (const arg of args) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
24
|
}
|
|
25
25
|
|
|
26
26
|
// Check for help/version flags first
|
|
@@ -28,36 +28,36 @@ const hasHelp = flags.has("h") || flags.has("help");
|
|
|
28
28
|
const hasVersion = flags.has("v") || flags.has("version");
|
|
29
29
|
|
|
30
30
|
if (hasVersion) {
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
console.log(pkg.version);
|
|
32
|
+
process.exit(0);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
if (hasHelp || positional.length === 0) {
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
showHelp();
|
|
37
|
+
process.exit(0);
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
if (positional.length < 3) {
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
showMissingArgsError(positional);
|
|
42
|
+
process.exit(1);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
// All positional args present, run the command
|
|
46
46
|
const [envFile, vault, itemName] = positional as [string, string, string];
|
|
47
47
|
await runConvert({
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
54
|
});
|
|
55
55
|
|
|
56
56
|
function showHelp(): void {
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
const name = pc.bold(pc.cyan("env2op"));
|
|
58
|
+
const version = pc.dim(`v${pkg.version}`);
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
console.log(`
|
|
61
61
|
${name} ${version}
|
|
62
62
|
${pkg.description}
|
|
63
63
|
|
|
@@ -95,13 +95,13 @@ ${pc.bold("DOCUMENTATION")}
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
function showMissingArgsError(provided: string[]): void {
|
|
98
|
-
|
|
98
|
+
const missing: string[] = [];
|
|
99
99
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
103
|
|
|
104
|
-
|
|
104
|
+
console.log(`
|
|
105
105
|
${pc.red(pc.bold("Error:"))} Missing required arguments
|
|
106
106
|
|
|
107
107
|
${pc.bold("Usage:")} env2op ${pc.yellow("<env_file>")} ${pc.yellow("<vault>")} ${pc.yellow("<item_name>")} ${pc.dim("[options]")}
|
package/src/commands/convert.ts
CHANGED
|
@@ -2,19 +2,15 @@ import { basename, dirname, join } from "node:path";
|
|
|
2
2
|
import * as p from "@clack/prompts";
|
|
3
3
|
import { parseEnvFile, validateParseResult } from "../core/env-parser";
|
|
4
4
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
checkOpCli,
|
|
6
|
+
checkSignedIn,
|
|
7
|
+
createSecureNote,
|
|
8
|
+
createVault,
|
|
9
|
+
deleteItem,
|
|
10
|
+
itemExists,
|
|
11
|
+
vaultExists,
|
|
12
12
|
} from "../core/onepassword";
|
|
13
|
-
import {
|
|
14
|
-
generateTemplateContent,
|
|
15
|
-
generateUsageInstructions,
|
|
16
|
-
writeTemplate,
|
|
17
|
-
} from "../core/template-generator";
|
|
13
|
+
import { generateTemplateContent, generateUsageInstructions, writeTemplate } from "../core/template-generator";
|
|
18
14
|
import type { ConvertOptions } from "../core/types";
|
|
19
15
|
import { Env2OpError } from "../utils/errors";
|
|
20
16
|
import { logger } from "../utils/logger";
|
|
@@ -23,160 +19,159 @@ import { logger } from "../utils/logger";
|
|
|
23
19
|
* Execute the convert operation
|
|
24
20
|
*/
|
|
25
21
|
export async function runConvert(options: ConvertOptions): Promise<void> {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}
|
|
22
|
+
const { envFile, vault, itemName, dryRun, secret, yes } = options;
|
|
23
|
+
|
|
24
|
+
// Display intro
|
|
25
|
+
const pkg = await import("../../package.json");
|
|
26
|
+
logger.intro("env2op", pkg.version, dryRun);
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
// Step 1: Parse .env file
|
|
30
|
+
const parseResult = parseEnvFile(envFile);
|
|
31
|
+
validateParseResult(parseResult, envFile);
|
|
32
|
+
|
|
33
|
+
const { variables, lines } = parseResult;
|
|
34
|
+
|
|
35
|
+
logger.success(`Parsed ${basename(envFile)}`);
|
|
36
|
+
logger.message(`Found ${variables.length} environment variable${variables.length === 1 ? "" : "s"}`);
|
|
37
|
+
|
|
38
|
+
// Show parse errors as warnings
|
|
39
|
+
for (const error of parseResult.errors) {
|
|
40
|
+
logger.warn(error);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Step 2: Create 1Password Secure Note
|
|
44
|
+
if (dryRun) {
|
|
45
|
+
logger.warn("Would create Secure Note");
|
|
46
|
+
logger.keyValue("Vault", vault);
|
|
47
|
+
logger.keyValue("Title", itemName);
|
|
48
|
+
logger.keyValue("Type", secret ? "password (hidden)" : "text (visible)");
|
|
49
|
+
logger.keyValue("Fields", logger.formatFields(variables.map((v) => v.key)));
|
|
50
|
+
} else {
|
|
51
|
+
// Check 1Password CLI before attempting
|
|
52
|
+
const opInstalled = await checkOpCli();
|
|
53
|
+
if (!opInstalled) {
|
|
54
|
+
throw new Env2OpError(
|
|
55
|
+
"1Password CLI (op) is not installed",
|
|
56
|
+
"OP_CLI_NOT_INSTALLED",
|
|
57
|
+
"Install from https://1password.com/downloads/command-line/",
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const signedIn = await checkSignedIn();
|
|
62
|
+
if (!signedIn) {
|
|
63
|
+
throw new Env2OpError(
|
|
64
|
+
"Not signed in to 1Password CLI",
|
|
65
|
+
"OP_NOT_SIGNED_IN",
|
|
66
|
+
'Run "op signin" to authenticate',
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Check if vault exists
|
|
71
|
+
const vaultFound = await vaultExists(vault);
|
|
72
|
+
|
|
73
|
+
if (!vaultFound) {
|
|
74
|
+
if (yes) {
|
|
75
|
+
// Auto-create vault
|
|
76
|
+
logger.warn(`Vault "${vault}" not found, creating...`);
|
|
77
|
+
await createVault(vault);
|
|
78
|
+
logger.success(`Created vault "${vault}"`);
|
|
79
|
+
} else {
|
|
80
|
+
// Ask for confirmation to create vault
|
|
81
|
+
const shouldCreate = await p.confirm({
|
|
82
|
+
message: `Vault "${vault}" does not exist. Create it?`,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
if (p.isCancel(shouldCreate) || !shouldCreate) {
|
|
86
|
+
logger.cancel("Operation cancelled");
|
|
87
|
+
logger.info('Run "op vault list" to see available vaults');
|
|
88
|
+
process.exit(0);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const spinner = logger.spinner();
|
|
92
|
+
spinner.start(`Creating vault "${vault}"...`);
|
|
93
|
+
await createVault(vault);
|
|
94
|
+
spinner.stop(`Created vault "${vault}"`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Check if item already exists
|
|
99
|
+
const exists = await itemExists(vault, itemName);
|
|
100
|
+
|
|
101
|
+
if (exists) {
|
|
102
|
+
if (yes) {
|
|
103
|
+
// Auto-accept: delete and recreate
|
|
104
|
+
logger.warn(`Item "${itemName}" already exists, overwriting...`);
|
|
105
|
+
await deleteItem(vault, itemName);
|
|
106
|
+
} else {
|
|
107
|
+
// Ask for confirmation
|
|
108
|
+
const shouldOverwrite = await p.confirm({
|
|
109
|
+
message: `Item "${itemName}" already exists in vault "${vault}". Overwrite?`,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
if (p.isCancel(shouldOverwrite) || !shouldOverwrite) {
|
|
113
|
+
logger.cancel("Operation cancelled");
|
|
114
|
+
process.exit(0);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
await deleteItem(vault, itemName);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const spinner = logger.spinner();
|
|
122
|
+
spinner.start("Creating 1Password Secure Note...");
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const result = await createSecureNote({
|
|
126
|
+
vault,
|
|
127
|
+
title: itemName,
|
|
128
|
+
fields: variables,
|
|
129
|
+
secret,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
spinner.stop(`Created "${result.title}" in vault "${result.vault}"`);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
spinner.stop("Failed to create Secure Note");
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 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
|
+
);
|
|
151
|
+
|
|
152
|
+
if (dryRun) {
|
|
153
|
+
logger.warn(`Would generate template: ${templatePath}`);
|
|
154
|
+
} else {
|
|
155
|
+
writeTemplate(templateContent, templatePath);
|
|
156
|
+
logger.success(`Generated template: ${templatePath}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Step 4: Show usage instructions
|
|
160
|
+
if (dryRun) {
|
|
161
|
+
logger.outro("Dry run complete. No changes made.");
|
|
162
|
+
} else {
|
|
163
|
+
const usage = generateUsageInstructions(templatePath);
|
|
164
|
+
logger.note(usage, "Next steps");
|
|
165
|
+
logger.outro("Done! Your secrets are now in 1Password");
|
|
166
|
+
}
|
|
167
|
+
} catch (error) {
|
|
168
|
+
if (error instanceof Env2OpError) {
|
|
169
|
+
logger.error(error.message);
|
|
170
|
+
if (error.suggestion) {
|
|
171
|
+
logger.info(`Suggestion: ${error.suggestion}`);
|
|
172
|
+
}
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
182
177
|
}
|