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.
- package/dist/lecoffre.js +856 -0
- package/package.json +5 -4
- package/bin/lecoffre.ts +0 -63
- package/src/commands/import.command.ts +0 -101
- package/src/commands/init.command.ts +0 -11
- package/src/commands/list.command.ts +0 -47
- package/src/commands/load.command.ts +0 -32
- package/src/commands/unload.command.ts +0 -32
- package/src/lib/define-argument.ts +0 -13
- package/src/lib/define-command.ts +0 -43
- package/src/lib/define-option.ts +0 -15
- package/src/lib/format.ts +0 -108
- package/src/lib/get-storage.ts +0 -11
- package/src/lib/json-storage.ts +0 -74
- package/src/lib/one-password-storage.ts +0 -233
- package/src/lib/parse-command.ts +0 -108
- package/src/lib/shell.ts +0 -52
- package/src/lib/storage.ts +0 -37
- package/src/lib/zod-utils.ts +0 -50
- package/src/options/environment.option.ts +0 -10
- package/src/options/project.option.ts +0 -13
package/dist/lecoffre.js
ADDED
|
@@ -0,0 +1,856 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// bin/lecoffre.ts
|
|
4
|
+
import { parse as parse2 } from "@bomb.sh/args";
|
|
5
|
+
|
|
6
|
+
// package.json
|
|
7
|
+
var package_default = {
|
|
8
|
+
name: "lecoffre",
|
|
9
|
+
version: "0.2.1",
|
|
10
|
+
description: "Work in progress CLI project.",
|
|
11
|
+
repository: {
|
|
12
|
+
type: "git",
|
|
13
|
+
url: "https://github.com/hsablonniere/lecoffre"
|
|
14
|
+
},
|
|
15
|
+
bin: {
|
|
16
|
+
lecoffre: "./dist/lecoffre.js"
|
|
17
|
+
},
|
|
18
|
+
files: [
|
|
19
|
+
"dist",
|
|
20
|
+
"README.md",
|
|
21
|
+
"LICENSE"
|
|
22
|
+
],
|
|
23
|
+
type: "module",
|
|
24
|
+
scripts: {
|
|
25
|
+
dev: "node bin/lecoffre.ts",
|
|
26
|
+
test: "vitest run",
|
|
27
|
+
lint: "oxlint . --vitest-plugin --deny-warnings",
|
|
28
|
+
"lint:fix": "oxlint . --vitest-plugin --deny-warnings --fix",
|
|
29
|
+
format: "oxfmt . --write",
|
|
30
|
+
"format:check": "oxfmt . --check",
|
|
31
|
+
typecheck: "tsgo --noEmit",
|
|
32
|
+
build: "esbuild bin/lecoffre.ts --bundle --platform=node --format=esm --packages=external --outfile=dist/lecoffre.js",
|
|
33
|
+
check: "pnpm lint && pnpm format:check && pnpm typecheck && pnpm test",
|
|
34
|
+
changeset: "changeset",
|
|
35
|
+
"version-packages": "changeset version",
|
|
36
|
+
prepack: "pnpm build",
|
|
37
|
+
release: "changeset publish"
|
|
38
|
+
},
|
|
39
|
+
dependencies: {
|
|
40
|
+
"@bomb.sh/args": "^0.3.1",
|
|
41
|
+
zod: "^4.3.6"
|
|
42
|
+
},
|
|
43
|
+
devDependencies: {
|
|
44
|
+
"@changesets/changelog-github": "^0.5.2",
|
|
45
|
+
"@changesets/cli": "^2.29.8",
|
|
46
|
+
"@types/node": "^25.2.3",
|
|
47
|
+
"@typescript/native-preview": "^7.0.0-dev.20260217.1",
|
|
48
|
+
esbuild: "^0.27.4",
|
|
49
|
+
execa: "^9.6.1",
|
|
50
|
+
oxfmt: "^0.33.0",
|
|
51
|
+
oxlint: "^1.48.0",
|
|
52
|
+
vitest: "^4.0.18"
|
|
53
|
+
},
|
|
54
|
+
engines: {
|
|
55
|
+
node: ">=24"
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// src/commands/import.command.ts
|
|
60
|
+
import { basename } from "node:path";
|
|
61
|
+
import { realpath } from "node:fs/promises";
|
|
62
|
+
import { parseEnv } from "node:util";
|
|
63
|
+
import { z as z3 } from "zod";
|
|
64
|
+
|
|
65
|
+
// src/lib/define-command.ts
|
|
66
|
+
function defineCommand(definition) {
|
|
67
|
+
return definition;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/lib/define-option.ts
|
|
71
|
+
function defineOption(definition) {
|
|
72
|
+
return definition;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/lib/json-storage.ts
|
|
76
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
77
|
+
|
|
78
|
+
// src/lib/storage.ts
|
|
79
|
+
var ProjectNotFoundError = class extends Error {
|
|
80
|
+
project;
|
|
81
|
+
constructor(project) {
|
|
82
|
+
super(`Project not found: ${project}`);
|
|
83
|
+
this.name = "ProjectNotFoundError";
|
|
84
|
+
this.project = project;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
var EnvironmentNotFoundError = class extends Error {
|
|
88
|
+
project;
|
|
89
|
+
environment;
|
|
90
|
+
constructor(project, environment) {
|
|
91
|
+
super(`Environment not found: ${environment}`);
|
|
92
|
+
this.name = "EnvironmentNotFoundError";
|
|
93
|
+
this.project = project;
|
|
94
|
+
this.environment = environment;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
var StorageNotInitializedError = class extends Error {
|
|
98
|
+
constructor(message) {
|
|
99
|
+
super(message);
|
|
100
|
+
this.name = "StorageNotInitializedError";
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
var Storage = class {
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// src/lib/json-storage.ts
|
|
107
|
+
var JsonStorage = class extends Storage {
|
|
108
|
+
filePath;
|
|
109
|
+
constructor(filePath) {
|
|
110
|
+
super();
|
|
111
|
+
this.filePath = filePath;
|
|
112
|
+
}
|
|
113
|
+
async read() {
|
|
114
|
+
try {
|
|
115
|
+
const content = await readFile(this.filePath, "utf-8");
|
|
116
|
+
return JSON.parse(content);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
if (error.code === "ENOENT") {
|
|
119
|
+
return {};
|
|
120
|
+
}
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async write(data) {
|
|
125
|
+
await writeFile(this.filePath, JSON.stringify(data, null, 2) + "\n");
|
|
126
|
+
}
|
|
127
|
+
async getProjects() {
|
|
128
|
+
const data = await this.read();
|
|
129
|
+
return Object.keys(data);
|
|
130
|
+
}
|
|
131
|
+
async getProject(project) {
|
|
132
|
+
const data = await this.read();
|
|
133
|
+
const projectData = data[project];
|
|
134
|
+
if (projectData === void 0) {
|
|
135
|
+
throw new ProjectNotFoundError(project);
|
|
136
|
+
}
|
|
137
|
+
return Object.fromEntries(Object.entries(projectData).map(([env, vars]) => [env, { ...vars }]));
|
|
138
|
+
}
|
|
139
|
+
async init() {
|
|
140
|
+
}
|
|
141
|
+
async setVariables(project, env, vars) {
|
|
142
|
+
const data = await this.read();
|
|
143
|
+
if (data[project] === void 0) {
|
|
144
|
+
data[project] = {};
|
|
145
|
+
}
|
|
146
|
+
data[project][env] = vars;
|
|
147
|
+
await this.write(data);
|
|
148
|
+
}
|
|
149
|
+
async deleteEnvironment(project, env) {
|
|
150
|
+
const data = await this.read();
|
|
151
|
+
const projectData = data[project];
|
|
152
|
+
if (projectData !== void 0) {
|
|
153
|
+
delete projectData[env];
|
|
154
|
+
if (Object.keys(projectData).length === 0) {
|
|
155
|
+
delete data[project];
|
|
156
|
+
}
|
|
157
|
+
await this.write(data);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
async deleteProject(project) {
|
|
161
|
+
const data = await this.read();
|
|
162
|
+
delete data[project];
|
|
163
|
+
await this.write(data);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// src/lib/one-password-storage.ts
|
|
168
|
+
import { execFile as execFileCb } from "node:child_process";
|
|
169
|
+
import { promisify } from "node:util";
|
|
170
|
+
function hasStderr(error) {
|
|
171
|
+
return typeof error === "object" && error !== null && "stderr" in error;
|
|
172
|
+
}
|
|
173
|
+
function getStderr(error) {
|
|
174
|
+
return hasStderr(error) ? error.stderr?.trim() ?? "" : "";
|
|
175
|
+
}
|
|
176
|
+
function isItemNotFound(error) {
|
|
177
|
+
return /isn't an item/i.test(getStderr(error));
|
|
178
|
+
}
|
|
179
|
+
function isVaultNotFound(error) {
|
|
180
|
+
return /isn't a vault/i.test(getStderr(error));
|
|
181
|
+
}
|
|
182
|
+
var execFile = promisify(execFileCb);
|
|
183
|
+
async function execOp(...args) {
|
|
184
|
+
try {
|
|
185
|
+
const { stdout } = await execFile("op", args);
|
|
186
|
+
return stdout;
|
|
187
|
+
} catch (error) {
|
|
188
|
+
if (error.code === "ENOENT") {
|
|
189
|
+
throw new Error(
|
|
190
|
+
"1Password CLI (op) is not installed. See https://developer.1password.com/docs/cli/get-started/"
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
var VAULT = "lecoffre";
|
|
197
|
+
var OnePasswordStorage = class extends Storage {
|
|
198
|
+
vault;
|
|
199
|
+
constructor(vault = VAULT) {
|
|
200
|
+
super();
|
|
201
|
+
this.vault = vault;
|
|
202
|
+
}
|
|
203
|
+
rethrow(error) {
|
|
204
|
+
if (isVaultNotFound(error)) {
|
|
205
|
+
throw new StorageNotInitializedError(
|
|
206
|
+
`Vault "${this.vault}" not found. Run "lecoffre init" to create it.`
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
const stderr = getStderr(error);
|
|
210
|
+
if (stderr !== "") {
|
|
211
|
+
throw new Error(stderr);
|
|
212
|
+
}
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
async init() {
|
|
216
|
+
try {
|
|
217
|
+
await execOp("vault", "get", this.vault, "--format", "json");
|
|
218
|
+
} catch (error) {
|
|
219
|
+
if (isVaultNotFound(error)) {
|
|
220
|
+
await execOp("vault", "create", this.vault);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
async getItem(project) {
|
|
227
|
+
try {
|
|
228
|
+
const stdout = await execOp(
|
|
229
|
+
"item",
|
|
230
|
+
"get",
|
|
231
|
+
project,
|
|
232
|
+
"--vault",
|
|
233
|
+
this.vault,
|
|
234
|
+
"--format",
|
|
235
|
+
"json"
|
|
236
|
+
);
|
|
237
|
+
return JSON.parse(stdout);
|
|
238
|
+
} catch (error) {
|
|
239
|
+
if (isItemNotFound(error)) return null;
|
|
240
|
+
return this.rethrow(error);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Return only user-defined fields. 1Password items include system fields
|
|
245
|
+
* (e.g. "notesPlain") that have a `purpose` property set. User-created
|
|
246
|
+
* fields never have `purpose`, so we use that to distinguish them.
|
|
247
|
+
*/
|
|
248
|
+
getUserFields(fields) {
|
|
249
|
+
return fields.filter(
|
|
250
|
+
(field) => field.purpose === void 0 && field.section?.label !== void 0
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
async getProjects() {
|
|
254
|
+
try {
|
|
255
|
+
const stdout = await execOp("item", "list", "--vault", this.vault, "--format", "json");
|
|
256
|
+
const items = JSON.parse(stdout);
|
|
257
|
+
return items.map((item) => item.title);
|
|
258
|
+
} catch (error) {
|
|
259
|
+
return this.rethrow(error);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
async getProject(project) {
|
|
263
|
+
const item = await this.getItem(project);
|
|
264
|
+
if (item === null) {
|
|
265
|
+
throw new ProjectNotFoundError(project);
|
|
266
|
+
}
|
|
267
|
+
const envs = {};
|
|
268
|
+
for (const field of this.getUserFields(item.fields)) {
|
|
269
|
+
const sectionLabel = field.section?.label;
|
|
270
|
+
if (sectionLabel !== void 0) {
|
|
271
|
+
envs[sectionLabel] ??= {};
|
|
272
|
+
envs[sectionLabel][field.label] = field.value;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return envs;
|
|
276
|
+
}
|
|
277
|
+
// Note: field values are passed as process arguments and are briefly visible
|
|
278
|
+
// in /proc/<pid>/cmdline. The 1Password CLI does not support reading field
|
|
279
|
+
// values from stdin when spawned as a child process (only shell pipes work).
|
|
280
|
+
async setVariables(project, env, vars) {
|
|
281
|
+
const item = await this.getItem(project);
|
|
282
|
+
if (item === null) {
|
|
283
|
+
const fieldAssignments = Object.entries(vars).map(
|
|
284
|
+
([key, value]) => `${env}.${key}[concealed]=${value}`
|
|
285
|
+
);
|
|
286
|
+
await execOp(
|
|
287
|
+
"item",
|
|
288
|
+
"create",
|
|
289
|
+
"--vault",
|
|
290
|
+
this.vault,
|
|
291
|
+
"--category",
|
|
292
|
+
"Secure Note",
|
|
293
|
+
"--title",
|
|
294
|
+
project,
|
|
295
|
+
...fieldAssignments
|
|
296
|
+
);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
const operations = [];
|
|
300
|
+
for (const field of this.getUserFields(item.fields)) {
|
|
301
|
+
if (field.section?.label === env) {
|
|
302
|
+
operations.push(`${env}.${field.label}[delete]`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
306
|
+
operations.push(`${env}.${key}[concealed]=${value}`);
|
|
307
|
+
}
|
|
308
|
+
if (operations.length > 0) {
|
|
309
|
+
await execOp("item", "edit", project, "--vault", this.vault, ...operations);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
async deleteEnvironment(project, env) {
|
|
313
|
+
const item = await this.getItem(project);
|
|
314
|
+
if (item === null) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const userFields = this.getUserFields(item.fields);
|
|
318
|
+
const sections = /* @__PURE__ */ new Set();
|
|
319
|
+
const fieldsToDelete = [];
|
|
320
|
+
for (const field of userFields) {
|
|
321
|
+
if (field.section?.label !== void 0) {
|
|
322
|
+
sections.add(field.section.label);
|
|
323
|
+
}
|
|
324
|
+
if (field.section?.label === env) {
|
|
325
|
+
fieldsToDelete.push(`${env}.${field.label}[delete]`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (fieldsToDelete.length === 0) {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
if (sections.size <= 1 && sections.has(env)) {
|
|
332
|
+
await execOp("item", "delete", project, "--vault", this.vault);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
await execOp("item", "edit", project, "--vault", this.vault, ...fieldsToDelete);
|
|
336
|
+
}
|
|
337
|
+
async deleteProject(project) {
|
|
338
|
+
try {
|
|
339
|
+
await execOp("item", "delete", project, "--vault", this.vault);
|
|
340
|
+
} catch (error) {
|
|
341
|
+
if (isItemNotFound(error)) return;
|
|
342
|
+
this.rethrow(error);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
// src/lib/get-storage.ts
|
|
348
|
+
function getStorage() {
|
|
349
|
+
const storagePath = process.env.LECOFFRE_STORAGE_PATH;
|
|
350
|
+
if (storagePath !== void 0) {
|
|
351
|
+
return new JsonStorage(storagePath);
|
|
352
|
+
}
|
|
353
|
+
return new OnePasswordStorage();
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// src/options/environment.option.ts
|
|
357
|
+
import { z } from "zod";
|
|
358
|
+
var environmentOption = defineOption({
|
|
359
|
+
name: "environment",
|
|
360
|
+
schema: z.string().default("default"),
|
|
361
|
+
description: "Environment name",
|
|
362
|
+
aliases: ["e"],
|
|
363
|
+
placeholder: "env"
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// src/options/project.option.ts
|
|
367
|
+
import { z as z2 } from "zod";
|
|
368
|
+
var projectOption = defineOption({
|
|
369
|
+
name: "project",
|
|
370
|
+
schema: z2.string().refine((val) => !val.startsWith("-"), { message: 'must not start with "-"' }).optional(),
|
|
371
|
+
description: "Project name",
|
|
372
|
+
aliases: ["p"],
|
|
373
|
+
placeholder: "name"
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
// src/commands/import.command.ts
|
|
377
|
+
var importCommand = defineCommand({
|
|
378
|
+
description: "Import variables from stdin (.env format)",
|
|
379
|
+
options: {
|
|
380
|
+
project: projectOption,
|
|
381
|
+
environment: environmentOption,
|
|
382
|
+
merge: defineOption({
|
|
383
|
+
name: "merge",
|
|
384
|
+
schema: z3.boolean().default(false),
|
|
385
|
+
description: "Merge with existing variables instead of replacing",
|
|
386
|
+
aliases: ["m"]
|
|
387
|
+
})
|
|
388
|
+
},
|
|
389
|
+
async handler(options) {
|
|
390
|
+
const storage = getStorage();
|
|
391
|
+
const project = options.project ?? basename(await realpath(process.cwd()));
|
|
392
|
+
const input = await readStdin();
|
|
393
|
+
const newVars = parseEnv(input);
|
|
394
|
+
let existingVars;
|
|
395
|
+
try {
|
|
396
|
+
const projectData = await storage.getProject(project);
|
|
397
|
+
existingVars = projectData[options.environment] ?? {};
|
|
398
|
+
} catch (error) {
|
|
399
|
+
if (error instanceof ProjectNotFoundError) {
|
|
400
|
+
existingVars = {};
|
|
401
|
+
} else {
|
|
402
|
+
throw error;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
const added = [];
|
|
406
|
+
const updated = [];
|
|
407
|
+
const removed = [];
|
|
408
|
+
if (options.merge) {
|
|
409
|
+
for (const key of Object.keys(newVars)) {
|
|
410
|
+
if (key in existingVars) {
|
|
411
|
+
if (existingVars[key] !== newVars[key]) {
|
|
412
|
+
updated.push(key);
|
|
413
|
+
}
|
|
414
|
+
} else {
|
|
415
|
+
added.push(key);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
await storage.setVariables(project, options.environment, { ...existingVars, ...newVars });
|
|
419
|
+
} else {
|
|
420
|
+
for (const key of Object.keys(newVars)) {
|
|
421
|
+
if (key in existingVars) {
|
|
422
|
+
if (existingVars[key] !== newVars[key]) {
|
|
423
|
+
updated.push(key);
|
|
424
|
+
}
|
|
425
|
+
} else {
|
|
426
|
+
added.push(key);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
for (const key of Object.keys(existingVars)) {
|
|
430
|
+
if (!(key in newVars)) {
|
|
431
|
+
removed.push(key);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
await storage.setVariables(project, options.environment, newVars);
|
|
435
|
+
}
|
|
436
|
+
for (const key of added) {
|
|
437
|
+
console.error(`+ ${key} (added)`);
|
|
438
|
+
}
|
|
439
|
+
for (const key of updated) {
|
|
440
|
+
console.error(`~ ${key} (updated)`);
|
|
441
|
+
}
|
|
442
|
+
for (const key of removed) {
|
|
443
|
+
console.error(`- ${key} (removed)`);
|
|
444
|
+
}
|
|
445
|
+
const totalVars = Object.keys(newVars).length;
|
|
446
|
+
console.error(
|
|
447
|
+
`Imported ${totalVars} variable${totalVars !== 1 ? "s" : ""} into ${project} [${options.environment}]`
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
async function readStdin() {
|
|
452
|
+
const chunks = [];
|
|
453
|
+
for await (const chunk of process.stdin) {
|
|
454
|
+
chunks.push(chunk);
|
|
455
|
+
}
|
|
456
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// src/commands/init.command.ts
|
|
460
|
+
var initCommand = defineCommand({
|
|
461
|
+
description: "Initialize the storage backend",
|
|
462
|
+
async handler() {
|
|
463
|
+
const storage = getStorage();
|
|
464
|
+
await storage.init();
|
|
465
|
+
console.log("Storage initialized.");
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
// src/commands/list.command.ts
|
|
470
|
+
var listCommand = defineCommand({
|
|
471
|
+
description: "List projects and their environments",
|
|
472
|
+
options: {
|
|
473
|
+
project: projectOption
|
|
474
|
+
},
|
|
475
|
+
async handler(options) {
|
|
476
|
+
const storage = getStorage();
|
|
477
|
+
if (options.project !== void 0) {
|
|
478
|
+
let projectData;
|
|
479
|
+
try {
|
|
480
|
+
projectData = await storage.getProject(options.project);
|
|
481
|
+
} catch (error) {
|
|
482
|
+
if (error instanceof ProjectNotFoundError) {
|
|
483
|
+
throw new Error(`Project not found: ${options.project}`);
|
|
484
|
+
}
|
|
485
|
+
throw error;
|
|
486
|
+
}
|
|
487
|
+
for (const [env, vars] of Object.entries(projectData)) {
|
|
488
|
+
const count = Object.keys(vars).length;
|
|
489
|
+
console.log(`${env} (${count} variable${count !== 1 ? "s" : ""})`);
|
|
490
|
+
}
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
const projects = await storage.getProjects();
|
|
494
|
+
if (projects.length === 0) {
|
|
495
|
+
console.log("No projects found.");
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
for (const p of projects) {
|
|
499
|
+
console.log(p);
|
|
500
|
+
const projectData = await storage.getProject(p);
|
|
501
|
+
for (const [env, vars] of Object.entries(projectData)) {
|
|
502
|
+
const count = Object.keys(vars).length;
|
|
503
|
+
console.log(` ${env} (${count} variable${count !== 1 ? "s" : ""})`);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
// src/commands/load.command.ts
|
|
510
|
+
import { basename as basename3 } from "node:path";
|
|
511
|
+
import { realpath as realpath2 } from "node:fs/promises";
|
|
512
|
+
|
|
513
|
+
// src/lib/shell.ts
|
|
514
|
+
import { execFileSync } from "node:child_process";
|
|
515
|
+
import { basename as basename2 } from "node:path";
|
|
516
|
+
var SUPPORTED_SHELLS = ["bash", "zsh", "fish"];
|
|
517
|
+
function isSupportedShell(name2) {
|
|
518
|
+
return SUPPORTED_SHELLS.includes(name2);
|
|
519
|
+
}
|
|
520
|
+
function detectShell() {
|
|
521
|
+
const name2 = basename2(
|
|
522
|
+
execFileSync("ps", ["-p", String(process.ppid), "-o", "comm="], { encoding: "utf-8" }).trim()
|
|
523
|
+
);
|
|
524
|
+
if (isSupportedShell(name2)) {
|
|
525
|
+
return name2;
|
|
526
|
+
}
|
|
527
|
+
throw new Error(`Unsupported shell: ${name2}`);
|
|
528
|
+
}
|
|
529
|
+
function formatVariables(shell, vars) {
|
|
530
|
+
const lines = Object.entries(vars).map(([key, value]) => formatSingleVariable(shell, key, value));
|
|
531
|
+
return lines.join("\n");
|
|
532
|
+
}
|
|
533
|
+
function formatSingleVariable(shell, key, value) {
|
|
534
|
+
if (shell === "fish") {
|
|
535
|
+
return `set -gx ${key} '${escapeSingleQuotes(value, "fish")}'`;
|
|
536
|
+
}
|
|
537
|
+
return `export ${key}='${escapeSingleQuotes(value, "posix")}'`;
|
|
538
|
+
}
|
|
539
|
+
function formatUnsetVariables(shell, keys) {
|
|
540
|
+
const lines = keys.map((key) => formatSingleUnsetVariable(shell, key));
|
|
541
|
+
return lines.join("\n");
|
|
542
|
+
}
|
|
543
|
+
function formatSingleUnsetVariable(shell, key) {
|
|
544
|
+
if (shell === "fish") {
|
|
545
|
+
return `set -e ${key}`;
|
|
546
|
+
}
|
|
547
|
+
return `unset ${key}`;
|
|
548
|
+
}
|
|
549
|
+
function escapeSingleQuotes(value, mode) {
|
|
550
|
+
if (mode === "fish") {
|
|
551
|
+
return value.replaceAll("\\", "\\\\").replaceAll("'", "\\'");
|
|
552
|
+
}
|
|
553
|
+
return value.replaceAll("'", "'\\''");
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// src/commands/load.command.ts
|
|
557
|
+
var loadCommand = defineCommand({
|
|
558
|
+
description: "Load variables into the current shell environment",
|
|
559
|
+
options: {
|
|
560
|
+
project: projectOption,
|
|
561
|
+
environment: environmentOption
|
|
562
|
+
},
|
|
563
|
+
async handler(options) {
|
|
564
|
+
const storage = getStorage();
|
|
565
|
+
const project = options.project ?? basename3(await realpath2(process.cwd()));
|
|
566
|
+
const projectData = await storage.getProject(project);
|
|
567
|
+
const vars = projectData[options.environment];
|
|
568
|
+
if (vars === void 0) {
|
|
569
|
+
throw new EnvironmentNotFoundError(project, options.environment);
|
|
570
|
+
}
|
|
571
|
+
const shell = detectShell();
|
|
572
|
+
const output = formatVariables(shell, vars);
|
|
573
|
+
if (output !== "") {
|
|
574
|
+
console.log(output);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
// src/commands/unload.command.ts
|
|
580
|
+
import { basename as basename4 } from "node:path";
|
|
581
|
+
import { realpath as realpath3 } from "node:fs/promises";
|
|
582
|
+
var unloadCommand = defineCommand({
|
|
583
|
+
description: "Unload variables from the current shell environment",
|
|
584
|
+
options: {
|
|
585
|
+
project: projectOption,
|
|
586
|
+
environment: environmentOption
|
|
587
|
+
},
|
|
588
|
+
async handler(options) {
|
|
589
|
+
const storage = getStorage();
|
|
590
|
+
const project = options.project ?? basename4(await realpath3(process.cwd()));
|
|
591
|
+
const projectData = await storage.getProject(project);
|
|
592
|
+
const vars = projectData[options.environment];
|
|
593
|
+
if (vars === void 0) {
|
|
594
|
+
throw new EnvironmentNotFoundError(project, options.environment);
|
|
595
|
+
}
|
|
596
|
+
const shell = detectShell();
|
|
597
|
+
const output = formatUnsetVariables(shell, Object.keys(vars));
|
|
598
|
+
if (output !== "") {
|
|
599
|
+
console.log(output);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
// src/lib/format.ts
|
|
605
|
+
import { styleText } from "node:util";
|
|
606
|
+
|
|
607
|
+
// src/lib/zod-utils.ts
|
|
608
|
+
function getDef(schema) {
|
|
609
|
+
return schema._zod.def;
|
|
610
|
+
}
|
|
611
|
+
function isBoolean(schema) {
|
|
612
|
+
const def = getDef(schema);
|
|
613
|
+
if (def.type === "boolean") return true;
|
|
614
|
+
if (def.type === "default" || def.type === "optional" || def.type === "nullable") {
|
|
615
|
+
if (def.innerType !== void 0) return isBoolean(def.innerType);
|
|
616
|
+
}
|
|
617
|
+
return false;
|
|
618
|
+
}
|
|
619
|
+
function isRequired(schema) {
|
|
620
|
+
const def = getDef(schema);
|
|
621
|
+
if (def.type === "default" || def.type === "optional" || def.type === "nullable") {
|
|
622
|
+
return false;
|
|
623
|
+
}
|
|
624
|
+
if (def.type === "pipe" && def.in !== void 0) {
|
|
625
|
+
return isRequired(def.in);
|
|
626
|
+
}
|
|
627
|
+
return true;
|
|
628
|
+
}
|
|
629
|
+
function getDefault(schema) {
|
|
630
|
+
let current = schema;
|
|
631
|
+
while (current !== void 0) {
|
|
632
|
+
const def = getDef(current);
|
|
633
|
+
if (def.type === "default") return def.defaultValue;
|
|
634
|
+
if (def.type === "optional" || def.type === "nullable") {
|
|
635
|
+
current = def.innerType;
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
if (def.type === "pipe") {
|
|
639
|
+
current = def.in;
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
break;
|
|
643
|
+
}
|
|
644
|
+
return void 0;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// src/lib/format.ts
|
|
648
|
+
function formatGlobalHelp(toolName, commands2) {
|
|
649
|
+
const names = Object.keys(commands2);
|
|
650
|
+
const sections = [styleText("bold", "USAGE"), ` ${toolName} <command> [options]`];
|
|
651
|
+
if (names.length > 0) {
|
|
652
|
+
const longest = Math.max(...names.map((n) => n.length));
|
|
653
|
+
const commandList = names.map((name2) => ` ${name2.padEnd(longest)} ${commands2[name2].description}`).join("\n");
|
|
654
|
+
sections.push("", styleText("bold", "COMMANDS"), commandList);
|
|
655
|
+
}
|
|
656
|
+
return sections.join("\n");
|
|
657
|
+
}
|
|
658
|
+
function formatCommandHelp(toolName, commandName2, command2) {
|
|
659
|
+
const sections = [
|
|
660
|
+
styleText("bold", "USAGE"),
|
|
661
|
+
formatUsageLine(toolName, commandName2, command2)
|
|
662
|
+
];
|
|
663
|
+
const argList = formatArgList(command2.args ?? []);
|
|
664
|
+
if (argList !== null) {
|
|
665
|
+
sections.push("", styleText("bold", "ARGUMENTS"), argList);
|
|
666
|
+
}
|
|
667
|
+
const optionList = formatOptionList(command2.options ?? {});
|
|
668
|
+
if (optionList !== null) {
|
|
669
|
+
sections.push("", styleText("bold", "OPTIONS"), optionList);
|
|
670
|
+
}
|
|
671
|
+
return sections.join("\n");
|
|
672
|
+
}
|
|
673
|
+
function formatUsageLine(toolName, commandName2, command2) {
|
|
674
|
+
const argPlaceholders = (command2.args ?? []).map((a) => `<${a.placeholder}>`).join(" ");
|
|
675
|
+
const parts = [toolName, commandName2];
|
|
676
|
+
if (argPlaceholders !== "") parts.push(argPlaceholders);
|
|
677
|
+
parts.push("[options]");
|
|
678
|
+
return ` ${parts.join(" ")}`;
|
|
679
|
+
}
|
|
680
|
+
function formatArgList(args) {
|
|
681
|
+
if (args.length === 0) return null;
|
|
682
|
+
const lines = args.map((arg) => ({
|
|
683
|
+
left: ` ${arg.placeholder}`,
|
|
684
|
+
description: formatArgDescription(arg.description, arg.schema)
|
|
685
|
+
}));
|
|
686
|
+
const longest = Math.max(...lines.map((l) => l.left.length));
|
|
687
|
+
return lines.map((l) => `${l.left.padEnd(longest)} ${l.description}`).join("\n");
|
|
688
|
+
}
|
|
689
|
+
function formatOptionList(options) {
|
|
690
|
+
const entries = Object.values(options);
|
|
691
|
+
if (entries.length === 0) return null;
|
|
692
|
+
const aliasPrefixes = entries.map((opt) => {
|
|
693
|
+
const aliases = opt.aliases ?? [];
|
|
694
|
+
return aliases.length > 0 ? aliases.map((a) => `-${a}`).join(", ") + ", " : "";
|
|
695
|
+
});
|
|
696
|
+
const longestAlias = Math.max(...aliasPrefixes.map((p) => p.length));
|
|
697
|
+
const lines = entries.map((opt, i) => {
|
|
698
|
+
const aliasPrefix = aliasPrefixes[i].padStart(longestAlias);
|
|
699
|
+
const placeholder = opt.placeholder ?? opt.name;
|
|
700
|
+
const flag = isBoolean(opt.schema) ? `--${opt.name}` : `--${opt.name} <${placeholder}>`;
|
|
701
|
+
return {
|
|
702
|
+
left: ` ${aliasPrefix}${flag}`,
|
|
703
|
+
description: formatDescription(opt.description, opt.schema)
|
|
704
|
+
};
|
|
705
|
+
});
|
|
706
|
+
const longest = Math.max(...lines.map((l) => l.left.length));
|
|
707
|
+
return lines.map((l) => `${l.left.padEnd(longest)} ${l.description}`).join("\n");
|
|
708
|
+
}
|
|
709
|
+
function formatArgDescription(description, schema) {
|
|
710
|
+
const defaultValue = getDefault(schema);
|
|
711
|
+
if (defaultValue !== void 0) return `${description} (default: ${String(defaultValue)})`;
|
|
712
|
+
return isRequired(schema) ? description : `${description} (optional)`;
|
|
713
|
+
}
|
|
714
|
+
function formatDescription(description, schema) {
|
|
715
|
+
if (isRequired(schema)) return `${description} (required)`;
|
|
716
|
+
const defaultValue = getDefault(schema);
|
|
717
|
+
return defaultValue === void 0 ? description : `${description} (default: ${String(defaultValue)})`;
|
|
718
|
+
}
|
|
719
|
+
function formatErrors(errors) {
|
|
720
|
+
const errorLines = errors.map((e) => ` ${e}`).join("\n");
|
|
721
|
+
return `${styleText("bold", "ERRORS")}
|
|
722
|
+
${errorLines}`;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// src/lib/parse-command.ts
|
|
726
|
+
import { parse } from "@bomb.sh/args";
|
|
727
|
+
import { ZodError } from "zod";
|
|
728
|
+
var CommandValidationError = class extends Error {
|
|
729
|
+
errors;
|
|
730
|
+
constructor(errors) {
|
|
731
|
+
super(errors.join("\n"));
|
|
732
|
+
this.errors = errors;
|
|
733
|
+
}
|
|
734
|
+
};
|
|
735
|
+
var CommandHelpRequested = class extends Error {
|
|
736
|
+
constructor() {
|
|
737
|
+
super("Help requested");
|
|
738
|
+
}
|
|
739
|
+
};
|
|
740
|
+
function parseCommand(argv, command2) {
|
|
741
|
+
const commandOptions = command2.options ?? {};
|
|
742
|
+
const commandArgs = command2.args ?? [];
|
|
743
|
+
const parseOpts = buildParseOptions(commandOptions);
|
|
744
|
+
const raw = parse(argv, parseOpts);
|
|
745
|
+
if (raw["help"]) {
|
|
746
|
+
throw new CommandHelpRequested();
|
|
747
|
+
}
|
|
748
|
+
const errors = [];
|
|
749
|
+
const options = {};
|
|
750
|
+
for (const [key, opt] of Object.entries(commandOptions)) {
|
|
751
|
+
const rawValue = raw[opt.name];
|
|
752
|
+
try {
|
|
753
|
+
if (rawValue !== void 0) {
|
|
754
|
+
options[key] = opt.schema.parse(rawValue);
|
|
755
|
+
} else {
|
|
756
|
+
options[key] = opt.schema.parse(void 0);
|
|
757
|
+
}
|
|
758
|
+
} catch (error) {
|
|
759
|
+
if (error instanceof ZodError) {
|
|
760
|
+
for (const issue of error.issues) {
|
|
761
|
+
errors.push(`option "--${opt.name}": ${issue.message}`);
|
|
762
|
+
}
|
|
763
|
+
} else {
|
|
764
|
+
throw error;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
const args = [];
|
|
769
|
+
for (let i = 0; i < commandArgs.length; i++) {
|
|
770
|
+
const argDef = commandArgs[i];
|
|
771
|
+
const rawValue = raw._[i];
|
|
772
|
+
try {
|
|
773
|
+
if (rawValue !== void 0) {
|
|
774
|
+
args.push(argDef.schema.parse(String(rawValue)));
|
|
775
|
+
} else {
|
|
776
|
+
args.push(argDef.schema.parse(void 0));
|
|
777
|
+
}
|
|
778
|
+
} catch (error) {
|
|
779
|
+
if (error instanceof ZodError) {
|
|
780
|
+
for (const issue of error.issues) {
|
|
781
|
+
errors.push(`argument <${argDef.placeholder}>: ${issue.message}`);
|
|
782
|
+
}
|
|
783
|
+
} else {
|
|
784
|
+
throw error;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
if (errors.length > 0) {
|
|
789
|
+
throw new CommandValidationError(errors);
|
|
790
|
+
}
|
|
791
|
+
return { options, args };
|
|
792
|
+
}
|
|
793
|
+
function buildParseOptions(options) {
|
|
794
|
+
const boolean = [];
|
|
795
|
+
const string = [];
|
|
796
|
+
const alias = {};
|
|
797
|
+
for (const opt of Object.values(options)) {
|
|
798
|
+
if (isBoolean(opt.schema)) {
|
|
799
|
+
boolean.push(opt.name);
|
|
800
|
+
} else {
|
|
801
|
+
string.push(opt.name);
|
|
802
|
+
}
|
|
803
|
+
if (opt.aliases !== void 0) {
|
|
804
|
+
for (const a of opt.aliases) {
|
|
805
|
+
alias[a] = opt.name;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
boolean.push("help");
|
|
810
|
+
alias["h"] = "help";
|
|
811
|
+
return { boolean, string, alias };
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// bin/lecoffre.ts
|
|
815
|
+
var { name } = package_default;
|
|
816
|
+
var commands = {
|
|
817
|
+
init: initCommand,
|
|
818
|
+
list: listCommand,
|
|
819
|
+
load: loadCommand,
|
|
820
|
+
unload: unloadCommand,
|
|
821
|
+
import: importCommand
|
|
822
|
+
};
|
|
823
|
+
var initial = parse2(process.argv.slice(2));
|
|
824
|
+
var [commandNameRaw] = initial._;
|
|
825
|
+
if (commandNameRaw === void 0) {
|
|
826
|
+
console.log(formatGlobalHelp(name, commands));
|
|
827
|
+
process.exit(0);
|
|
828
|
+
}
|
|
829
|
+
var commandName = String(commandNameRaw);
|
|
830
|
+
var command = commands[commandName];
|
|
831
|
+
if (command === void 0) {
|
|
832
|
+
console.error(`Unknown command "${commandName}" for "${name}"
|
|
833
|
+
`);
|
|
834
|
+
console.error(formatGlobalHelp(name, commands));
|
|
835
|
+
process.exit(1);
|
|
836
|
+
}
|
|
837
|
+
try {
|
|
838
|
+
const { options, args } = parseCommand(process.argv.slice(3), command);
|
|
839
|
+
await command.handler(options, ...args);
|
|
840
|
+
} catch (error) {
|
|
841
|
+
if (error instanceof CommandHelpRequested) {
|
|
842
|
+
console.log(formatCommandHelp(name, commandName, command));
|
|
843
|
+
process.exit(0);
|
|
844
|
+
}
|
|
845
|
+
if (error instanceof CommandValidationError) {
|
|
846
|
+
console.error(formatErrors(error.errors) + "\n");
|
|
847
|
+
console.error(formatCommandHelp(name, commandName, command));
|
|
848
|
+
process.exit(1);
|
|
849
|
+
}
|
|
850
|
+
if (error instanceof Error) {
|
|
851
|
+
console.error(error.message);
|
|
852
|
+
process.exit(1);
|
|
853
|
+
}
|
|
854
|
+
console.error("An unexpected error occurred");
|
|
855
|
+
process.exit(1);
|
|
856
|
+
}
|