@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 +21 -0
- package/README.md +123 -0
- package/package.json +69 -0
- package/src/cli.ts +117 -0
- package/src/commands/convert.ts +182 -0
- package/src/core/env-parser.ts +110 -0
- package/src/core/onepassword.ts +119 -0
- package/src/core/template-generator.ts +60 -0
- package/src/core/types.ts +79 -0
- package/src/index.ts +39 -0
- package/src/utils/errors.ts +95 -0
- package/src/utils/logger.ts +144 -0
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
|
+
};
|