lecoffre 0.2.0 → 0.2.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,233 +0,0 @@
1
- import { execFile as execFileCb } from "node:child_process";
2
- import { promisify } from "node:util";
3
- import { ProjectNotFoundError, Storage, StorageNotInitializedError } from "./storage.ts";
4
-
5
- interface OpItemSummary {
6
- id: string;
7
- title: string;
8
- category: string;
9
- }
10
-
11
- interface OpField {
12
- id: string;
13
- label: string;
14
- value: string;
15
- type: string;
16
- section?: { id: string; label: string };
17
- purpose?: string;
18
- }
19
-
20
- interface OpItemDetail {
21
- id: string;
22
- title: string;
23
- fields: Array<OpField>;
24
- }
25
-
26
- interface ExecError {
27
- stderr?: string;
28
- }
29
-
30
- function hasStderr(error: unknown): error is ExecError {
31
- return typeof error === "object" && error !== null && "stderr" in error;
32
- }
33
-
34
- function getStderr(error: unknown): string {
35
- return hasStderr(error) ? (error.stderr?.trim() ?? "") : "";
36
- }
37
-
38
- function isItemNotFound(error: unknown): boolean {
39
- return /isn't an item/i.test(getStderr(error));
40
- }
41
-
42
- function isVaultNotFound(error: unknown): boolean {
43
- return /isn't a vault/i.test(getStderr(error));
44
- }
45
-
46
- const execFile = promisify(execFileCb);
47
-
48
- async function execOp(...args: Array<string>): Promise<string> {
49
- try {
50
- const { stdout } = await execFile("op", args);
51
- return stdout;
52
- } catch (error) {
53
- if ((error as NodeJS.ErrnoException).code === "ENOENT") {
54
- throw new Error(
55
- "1Password CLI (op) is not installed. See https://developer.1password.com/docs/cli/get-started/",
56
- );
57
- }
58
- throw error;
59
- }
60
- }
61
-
62
- const VAULT = "lecoffre";
63
-
64
- export class OnePasswordStorage extends Storage {
65
- private readonly vault: string;
66
-
67
- constructor(vault: string = VAULT) {
68
- super();
69
- this.vault = vault;
70
- }
71
-
72
- private rethrow(error: unknown): never {
73
- if (isVaultNotFound(error)) {
74
- throw new StorageNotInitializedError(
75
- `Vault "${this.vault}" not found. Run "lecoffre init" to create it.`,
76
- );
77
- }
78
- const stderr = getStderr(error);
79
- if (stderr !== "") {
80
- throw new Error(stderr);
81
- }
82
- throw error;
83
- }
84
-
85
- async init(): Promise<void> {
86
- try {
87
- await execOp("vault", "get", this.vault, "--format", "json");
88
- } catch (error) {
89
- if (isVaultNotFound(error)) {
90
- await execOp("vault", "create", this.vault);
91
- return;
92
- }
93
- throw error;
94
- }
95
- }
96
-
97
- private async getItem(project: string): Promise<OpItemDetail | null> {
98
- try {
99
- const stdout = await execOp(
100
- "item",
101
- "get",
102
- project,
103
- "--vault",
104
- this.vault,
105
- "--format",
106
- "json",
107
- );
108
- return JSON.parse(stdout) as OpItemDetail;
109
- } catch (error) {
110
- if (isItemNotFound(error)) return null;
111
- return this.rethrow(error);
112
- }
113
- }
114
-
115
- /**
116
- * Return only user-defined fields. 1Password items include system fields
117
- * (e.g. "notesPlain") that have a `purpose` property set. User-created
118
- * fields never have `purpose`, so we use that to distinguish them.
119
- */
120
- private getUserFields(fields: Array<OpField>): Array<OpField> {
121
- return fields.filter(
122
- (field) => field.purpose === undefined && field.section?.label !== undefined,
123
- );
124
- }
125
-
126
- async getProjects(): Promise<Array<string>> {
127
- try {
128
- const stdout = await execOp("item", "list", "--vault", this.vault, "--format", "json");
129
- const items = JSON.parse(stdout) as Array<OpItemSummary>;
130
- return items.map((item) => item.title);
131
- } catch (error) {
132
- return this.rethrow(error);
133
- }
134
- }
135
-
136
- async getProject(project: string): Promise<Record<string, Record<string, string>>> {
137
- const item = await this.getItem(project);
138
- if (item === null) {
139
- throw new ProjectNotFoundError(project);
140
- }
141
-
142
- const envs: Record<string, Record<string, string>> = {};
143
- for (const field of this.getUserFields(item.fields)) {
144
- const sectionLabel = field.section?.label;
145
- if (sectionLabel !== undefined) {
146
- envs[sectionLabel] ??= {};
147
- envs[sectionLabel][field.label] = field.value;
148
- }
149
- }
150
- return envs;
151
- }
152
-
153
- // Note: field values are passed as process arguments and are briefly visible
154
- // in /proc/<pid>/cmdline. The 1Password CLI does not support reading field
155
- // values from stdin when spawned as a child process (only shell pipes work).
156
- async setVariables(project: string, env: string, vars: Record<string, string>): Promise<void> {
157
- const item = await this.getItem(project);
158
-
159
- if (item === null) {
160
- const fieldAssignments = Object.entries(vars).map(
161
- ([key, value]) => `${env}.${key}[concealed]=${value}`,
162
- );
163
- await execOp(
164
- "item",
165
- "create",
166
- "--vault",
167
- this.vault,
168
- "--category",
169
- "Secure Note",
170
- "--title",
171
- project,
172
- ...fieldAssignments,
173
- );
174
- return;
175
- }
176
-
177
- const operations: Array<string> = [];
178
-
179
- for (const field of this.getUserFields(item.fields)) {
180
- if (field.section?.label === env) {
181
- operations.push(`${env}.${field.label}[delete]`);
182
- }
183
- }
184
-
185
- for (const [key, value] of Object.entries(vars)) {
186
- operations.push(`${env}.${key}[concealed]=${value}`);
187
- }
188
-
189
- if (operations.length > 0) {
190
- await execOp("item", "edit", project, "--vault", this.vault, ...operations);
191
- }
192
- }
193
-
194
- async deleteEnvironment(project: string, env: string): Promise<void> {
195
- const item = await this.getItem(project);
196
- if (item === null) {
197
- return;
198
- }
199
-
200
- const userFields = this.getUserFields(item.fields);
201
- const sections = new Set<string>();
202
- const fieldsToDelete: Array<string> = [];
203
-
204
- for (const field of userFields) {
205
- if (field.section?.label !== undefined) {
206
- sections.add(field.section.label);
207
- }
208
- if (field.section?.label === env) {
209
- fieldsToDelete.push(`${env}.${field.label}[delete]`);
210
- }
211
- }
212
-
213
- if (fieldsToDelete.length === 0) {
214
- return;
215
- }
216
-
217
- if (sections.size <= 1 && sections.has(env)) {
218
- await execOp("item", "delete", project, "--vault", this.vault);
219
- return;
220
- }
221
-
222
- await execOp("item", "edit", project, "--vault", this.vault, ...fieldsToDelete);
223
- }
224
-
225
- async deleteProject(project: string): Promise<void> {
226
- try {
227
- await execOp("item", "delete", project, "--vault", this.vault);
228
- } catch (error) {
229
- if (isItemNotFound(error)) return;
230
- this.rethrow(error);
231
- }
232
- }
233
- }
@@ -1,108 +0,0 @@
1
- import { parse } from "@bomb.sh/args";
2
- import { ZodError } from "zod";
3
- import type { AnyCommandDefinition } from "./define-command.ts";
4
- import type { OptionDefinition } from "./define-option.ts";
5
- import { isBoolean } from "./zod-utils.ts";
6
-
7
- export class CommandValidationError extends Error {
8
- errors: Array<string>;
9
-
10
- constructor(errors: Array<string>) {
11
- super(errors.join("\n"));
12
- this.errors = errors;
13
- }
14
- }
15
-
16
- export class CommandHelpRequested extends Error {
17
- constructor() {
18
- super("Help requested");
19
- }
20
- }
21
-
22
- export function parseCommand(
23
- argv: Array<string>,
24
- command: AnyCommandDefinition,
25
- ): { options: Record<string, unknown>; args: Array<unknown> } {
26
- const commandOptions = command.options ?? {};
27
- const commandArgs = command.args ?? [];
28
-
29
- const parseOpts = buildParseOptions(commandOptions);
30
- const raw = parse(argv, parseOpts);
31
-
32
- if (raw["help"]) {
33
- throw new CommandHelpRequested();
34
- }
35
-
36
- const errors: Array<string> = [];
37
-
38
- const options: Record<string, unknown> = {};
39
- for (const [key, opt] of Object.entries(commandOptions)) {
40
- const rawValue = raw[opt.name] as unknown;
41
- try {
42
- if (rawValue !== undefined) {
43
- options[key] = opt.schema.parse(rawValue);
44
- } else {
45
- options[key] = opt.schema.parse(undefined);
46
- }
47
- } catch (error) {
48
- if (error instanceof ZodError) {
49
- for (const issue of error.issues) {
50
- errors.push(`option "--${opt.name}": ${issue.message}`);
51
- }
52
- } else {
53
- throw error;
54
- }
55
- }
56
- }
57
-
58
- const args: Array<unknown> = [];
59
- for (let i = 0; i < commandArgs.length; i++) {
60
- const argDef = commandArgs[i]!;
61
- const rawValue = raw._[i];
62
- try {
63
- if (rawValue !== undefined) {
64
- args.push(argDef.schema.parse(String(rawValue)));
65
- } else {
66
- args.push(argDef.schema.parse(undefined));
67
- }
68
- } catch (error) {
69
- if (error instanceof ZodError) {
70
- for (const issue of error.issues) {
71
- errors.push(`argument <${argDef.placeholder}>: ${issue.message}`);
72
- }
73
- } else {
74
- throw error;
75
- }
76
- }
77
- }
78
-
79
- if (errors.length > 0) {
80
- throw new CommandValidationError(errors);
81
- }
82
-
83
- return { options, args };
84
- }
85
-
86
- function buildParseOptions(options: Record<string, OptionDefinition>) {
87
- const boolean: Array<string> = [];
88
- const string: Array<string> = [];
89
- const alias: Record<string, string> = {};
90
-
91
- for (const opt of Object.values(options)) {
92
- if (isBoolean(opt.schema)) {
93
- boolean.push(opt.name);
94
- } else {
95
- string.push(opt.name);
96
- }
97
- if (opt.aliases !== undefined) {
98
- for (const a of opt.aliases) {
99
- alias[a] = opt.name;
100
- }
101
- }
102
- }
103
-
104
- boolean.push("help");
105
- alias["h"] = "help";
106
-
107
- return { boolean, string, alias };
108
- }
package/src/lib/shell.ts DELETED
@@ -1,52 +0,0 @@
1
- import { execFileSync } from "node:child_process";
2
- import { basename } from "node:path";
3
-
4
- const SUPPORTED_SHELLS = ["bash", "zsh", "fish"] as const;
5
-
6
- export type ShellName = (typeof SUPPORTED_SHELLS)[number];
7
-
8
- function isSupportedShell(name: string): name is ShellName {
9
- return (SUPPORTED_SHELLS as ReadonlyArray<string>).includes(name);
10
- }
11
-
12
- export function detectShell(): ShellName {
13
- const name = basename(
14
- execFileSync("ps", ["-p", String(process.ppid), "-o", "comm="], { encoding: "utf-8" }).trim(),
15
- );
16
- if (isSupportedShell(name)) {
17
- return name;
18
- }
19
- throw new Error(`Unsupported shell: ${name}`);
20
- }
21
-
22
- export function formatVariables(shell: ShellName, vars: Record<string, string>): string {
23
- const lines = Object.entries(vars).map(([key, value]) => formatSingleVariable(shell, key, value));
24
- return lines.join("\n");
25
- }
26
-
27
- function formatSingleVariable(shell: ShellName, key: string, value: string): string {
28
- if (shell === "fish") {
29
- return `set -gx ${key} '${escapeSingleQuotes(value, "fish")}'`;
30
- }
31
- return `export ${key}='${escapeSingleQuotes(value, "posix")}'`;
32
- }
33
-
34
- export function formatUnsetVariables(shell: ShellName, keys: Array<string>): string {
35
- const lines = keys.map((key) => formatSingleUnsetVariable(shell, key));
36
- return lines.join("\n");
37
- }
38
-
39
- function formatSingleUnsetVariable(shell: ShellName, key: string): string {
40
- if (shell === "fish") {
41
- return `set -e ${key}`;
42
- }
43
- return `unset ${key}`;
44
- }
45
-
46
- function escapeSingleQuotes(value: string, mode: "posix" | "fish"): string {
47
- if (mode === "fish") {
48
- return value.replaceAll("\\", "\\\\").replaceAll("'", "\\'");
49
- }
50
- // In POSIX shells: end quote, escaped quote, restart quote
51
- return value.replaceAll("'", "'\\''");
52
- }
@@ -1,37 +0,0 @@
1
- export class ProjectNotFoundError extends Error {
2
- readonly project: string;
3
-
4
- constructor(project: string) {
5
- super(`Project not found: ${project}`);
6
- this.name = "ProjectNotFoundError";
7
- this.project = project;
8
- }
9
- }
10
-
11
- export class EnvironmentNotFoundError extends Error {
12
- readonly project: string;
13
- readonly environment: string;
14
-
15
- constructor(project: string, environment: string) {
16
- super(`Environment not found: ${environment}`);
17
- this.name = "EnvironmentNotFoundError";
18
- this.project = project;
19
- this.environment = environment;
20
- }
21
- }
22
-
23
- export class StorageNotInitializedError extends Error {
24
- constructor(message: string) {
25
- super(message);
26
- this.name = "StorageNotInitializedError";
27
- }
28
- }
29
-
30
- export abstract class Storage {
31
- abstract init(): Promise<void>;
32
- abstract getProjects(): Promise<Array<string>>;
33
- abstract getProject(project: string): Promise<Record<string, Record<string, string>>>;
34
- abstract setVariables(project: string, env: string, vars: Record<string, string>): Promise<void>;
35
- abstract deleteEnvironment(project: string, env: string): Promise<void>;
36
- abstract deleteProject(project: string): Promise<void>;
37
- }
@@ -1,50 +0,0 @@
1
- import type { z } from "zod";
2
-
3
- interface ZodDef {
4
- type?: string;
5
- innerType?: z.ZodType;
6
- defaultValue?: unknown;
7
- in?: z.ZodType;
8
- }
9
-
10
- function getDef(schema: z.ZodType): ZodDef {
11
- return (schema as unknown as { _zod: { def: ZodDef } })._zod.def;
12
- }
13
-
14
- export function isBoolean(schema: z.ZodType): boolean {
15
- const def = getDef(schema);
16
- if (def.type === "boolean") return true;
17
- if (def.type === "default" || def.type === "optional" || def.type === "nullable") {
18
- if (def.innerType !== undefined) return isBoolean(def.innerType);
19
- }
20
- return false;
21
- }
22
-
23
- export function isRequired(schema: z.ZodType): boolean {
24
- const def = getDef(schema);
25
- if (def.type === "default" || def.type === "optional" || def.type === "nullable") {
26
- return false;
27
- }
28
- if (def.type === "pipe" && def.in !== undefined) {
29
- return isRequired(def.in);
30
- }
31
- return true;
32
- }
33
-
34
- export function getDefault(schema: z.ZodType): unknown {
35
- let current: z.ZodType | undefined = schema;
36
- while (current !== undefined) {
37
- const def = getDef(current);
38
- if (def.type === "default") return def.defaultValue;
39
- if (def.type === "optional" || def.type === "nullable") {
40
- current = def.innerType;
41
- continue;
42
- }
43
- if (def.type === "pipe") {
44
- current = def.in;
45
- continue;
46
- }
47
- break;
48
- }
49
- return undefined;
50
- }
@@ -1,10 +0,0 @@
1
- import { z } from "zod";
2
- import { defineOption } from "../lib/define-option.ts";
3
-
4
- export const environmentOption = defineOption({
5
- name: "environment",
6
- schema: z.string().default("default"),
7
- description: "Environment name",
8
- aliases: ["e"],
9
- placeholder: "env",
10
- });
@@ -1,13 +0,0 @@
1
- import { z } from "zod";
2
- import { defineOption } from "../lib/define-option.ts";
3
-
4
- export const projectOption = defineOption({
5
- name: "project",
6
- schema: z
7
- .string()
8
- .refine((val) => !val.startsWith("-"), { message: 'must not start with "-"' })
9
- .optional(),
10
- description: "Project name",
11
- aliases: ["p"],
12
- placeholder: "name",
13
- });