@coffre/cli 0.1.4 → 0.1.6

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/README.md CHANGED
@@ -4,7 +4,7 @@ The `coffre` command: secrets in your shell, and a new deployment.
4
4
 
5
5
  ```sh
6
6
  npx @coffre/cli init --workers my-coffre # or --node
7
- npx @coffre/cli setup # its database logins, migrations and keys
7
+ npx @coffre/cli setup # its database, keys and, on Workers, Cloudflare
8
8
  coffre login https://secrets.acme.example
9
9
  coffre run market/prod -- node server.js
10
10
  ```
@@ -1,9 +1,78 @@
1
1
  import { parseArgs } from "node:util";
2
2
  import { spawn } from "node:child_process";
3
- import { openSync } from "node:fs";
3
+ import { existsSync, mkdirSync, openSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
4
+ import { basename, dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
4
6
  import { randomBytes } from "node:crypto";
5
7
  import { emitKeypressEvents } from "node:readline";
6
8
  import tty from "node:tty";
9
+ //#region src/init.ts
10
+ const KINDS = ["workers", "node"];
11
+ /** Where a template lives: in the package when published, else in the repository. */
12
+ function templateDir(kind) {
13
+ const packed = fileURLToPath(new URL(`./templates/${kind}/`, import.meta.url));
14
+ if (existsSync(packed)) return packed;
15
+ return fileURLToPath(new URL(`../../../examples/${kind}/`, import.meta.url));
16
+ }
17
+ /**
18
+ * A template's files, relative and sorted: everything but what its own
19
+ * `.gitignore` names, such as installed packages, local state and secrets.
20
+ * Patterns are file or directory names, `*` standing for any run of
21
+ * characters.
22
+ */
23
+ function templateFiles(dir) {
24
+ const ignoreFile = [join(dir, ".gitignore"), join(dir, "gitignore")].find((path) => existsSync(path));
25
+ const ignored = (ignoreFile === void 0 ? "" : readFileSync(ignoreFile, "utf8")).split("\n").map((line) => line.trim()).filter((line) => line !== "" && !line.startsWith("#")).map((glob) => new RegExp(`^${glob.split("*").map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join(".*")}$`));
26
+ const files = [];
27
+ const walk = (relative) => {
28
+ for (const entry of readdirSync(join(dir, relative), { withFileTypes: true })) {
29
+ if (ignored.some((pattern) => pattern.test(entry.name))) continue;
30
+ const path = relative === "" ? entry.name : `${relative}/${entry.name}`;
31
+ if (entry.isDirectory()) walk(path);
32
+ else files.push(path);
33
+ }
34
+ };
35
+ walk("");
36
+ return files.sort();
37
+ }
38
+ /** The name a template file has in a project. */
39
+ function projectPath(templatePath) {
40
+ return templatePath === "gitignore" ? ".gitignore" : templatePath;
41
+ }
42
+ /** A package name from a directory's, e.g. `Acme Secrets` → `acme-secrets`. */
43
+ function packageName(dir) {
44
+ const name = basename(dir).toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[-._]+|-+$/g, "");
45
+ return name === "" ? "coffre" : name;
46
+ }
47
+ /** The template's package.json, for this project and this version of coffre. */
48
+ function manifest(source, name, version) {
49
+ const pkg = JSON.parse(source);
50
+ pkg.name = name;
51
+ for (const field of ["dependencies", "devDependencies"]) {
52
+ const deps = pkg[field];
53
+ for (const dep of Object.keys(deps ?? {})) if (dep.startsWith("@coffre/")) deps[dep] = version;
54
+ }
55
+ return `${JSON.stringify(pkg, null, 2)}\n`;
56
+ }
57
+ /** Whether a directory holds nothing yet, or nothing but `.git`: a repository made first, then cloned. */
58
+ function isEmpty(dir) {
59
+ return !existsSync(dir) || readdirSync(dir).every((entry) => entry === ".git");
60
+ }
61
+ /** Write a new deployment into `target`, which must be empty (`isEmpty`) or absent. Returns the files written. */
62
+ function init(kind, target, version) {
63
+ if (!isEmpty(target)) throw new Error(`${target} is not empty; give \`coffre init\` a new directory`);
64
+ const source = templateDir(kind);
65
+ const written = [];
66
+ for (const file of templateFiles(source)) {
67
+ const path = projectPath(file);
68
+ const content = readFileSync(join(source, file), "utf8");
69
+ mkdirSync(dirname(join(target, path)), { recursive: true });
70
+ writeFileSync(join(target, path), path === "package.json" ? manifest(content, packageName(target), version) : content);
71
+ written.push(path);
72
+ }
73
+ return written;
74
+ }
75
+ //#endregion
7
76
  //#region src/clipboard.ts
8
77
  const CLEAR_AFTER_MS = 3e4;
9
78
  /** The system's clipboard tool, by platform and display server; null for none known. */
@@ -224,6 +293,10 @@ function paragraph(out, text, indent = 2) {
224
293
  const pad = " ".repeat(indent);
225
294
  return wrap(text, Math.max(20, (out.columns || 80) - indent - 1)).map((line) => `${pad}${line}`).join("\n");
226
295
  }
296
+ /** "a, b or c". */
297
+ function listed(items, last) {
298
+ return items.length < 2 ? items.join("") : `${items.slice(0, -1).join(", ")} ${last} ${items.at(-1)}`;
299
+ }
227
300
  /** A labelled row: `label` in a column of `column`, `text` wrapped beside it with a hanging indent. */
228
301
  function row(out, s, label, text, indent = 4, column = 10) {
229
302
  const lines = wrap(text, Math.max(20, (out.columns || 80) - indent - column - 1));
@@ -257,25 +330,107 @@ function release(keys) {
257
330
  keys.pause();
258
331
  if (keys !== process.stdin) keys.destroy?.();
259
332
  }
260
- /** Every key pressed, in raw mode, until `until` returns true. */
261
- function readKeys(keys, until) {
333
+ /**
334
+ * Every key pressed, in raw mode, until `until` returns true, or `signal`
335
+ * aborts the reading. Raw mode is on when this returns: a prompt shown after
336
+ * it gets every key, none echoed or edited by the terminal first.
337
+ */
338
+ function readKeys(keys, until, signal) {
262
339
  emitKeypressEvents(keys);
263
340
  keys.setRawMode?.(true);
264
341
  keys.resume();
265
342
  return new Promise((resolve, reject) => {
266
343
  let busy = Promise.resolve();
344
+ const stop = () => {
345
+ keys.off("keypress", onKey);
346
+ signal?.removeEventListener("abort", stop);
347
+ keys.setRawMode?.(false);
348
+ keys.pause();
349
+ resolve();
350
+ };
267
351
  const onKey = (sequence, key) => {
268
352
  busy = busy.then(async () => {
269
- if (await until(key ?? { sequence }, sequence ?? key?.sequence ?? "")) {
270
- keys.off("keypress", onKey);
271
- keys.setRawMode?.(false);
272
- keys.pause();
273
- resolve();
274
- }
353
+ if (await until(key ?? { sequence }, sequence ?? key?.sequence ?? "")) stop();
275
354
  }).catch(reject);
276
355
  };
277
356
  keys.on("keypress", onKey);
357
+ if (signal?.aborted) stop();
358
+ else signal?.addEventListener("abort", stop);
359
+ });
360
+ }
361
+ /** What a typed key does to a line: its new text, or `submit`, or `cancel`. */
362
+ function edit(text, key, sequence) {
363
+ if (key.ctrl && key.name === "c") return "cancel";
364
+ if (key.name === "return" || key.name === "enter") return "submit";
365
+ if (key.name === "backspace") return text.slice(0, -1);
366
+ if (key.ctrl && key.name === "u") return "";
367
+ if (!key.ctrl && !key.meta && sequence.length > 0 && !sequence.startsWith("\x1B") && sequence >= " ") return text + sequence;
368
+ return text;
369
+ }
370
+ /**
371
+ * A question answered by typing, shown as typed, `initial` to start from;
372
+ * `note`, dim under it while it is asked; `check` says why an answer will
373
+ * not do, under it. Once given, the question becomes one line: what was
374
+ * asked, and the answer. Ctrl-C cancels.
375
+ */
376
+ async function textLine(keys, out, s, question, hint, options = {}) {
377
+ const columns = Math.max(20, (out.columns || 80) - 1);
378
+ let typed = options.initial ?? "";
379
+ let error = null;
380
+ const draw = () => {
381
+ const below = [...error === null ? [] : [s.red(error)], ...options.note === void 0 ? [] : wrap(options.note, columns - 4).map(s.dim)].map((line) => `\n${truncate(` ${line}`, columns)}`);
382
+ out.write(`\r\x1b[J${below.join("")}${below.length > 0 ? `\x1b[${below.length}A` : ""}\r${truncate(` ${s.accent("›")} ${typed}`, columns)}`);
383
+ };
384
+ let cancelled = false;
385
+ const answered = readKeys(keys, async (key, sequence) => {
386
+ const next = edit(typed, key, sequence);
387
+ if (next === "cancel") cancelled = true;
388
+ else if (next === "submit") {
389
+ if (typed.trim() === "") return false;
390
+ error = await options.check?.(typed.trim()) ?? null;
391
+ if (error === null) return true;
392
+ } else {
393
+ typed = next;
394
+ error = null;
395
+ }
396
+ if (cancelled) return true;
397
+ draw();
398
+ return false;
278
399
  });
400
+ out.write(`${truncate(` ${s.accent("?")} ${s.bold(question)} ${s.dim(hint)}`, columns)}\n`);
401
+ draw();
402
+ await answered;
403
+ out.write("\r\x1B[J\x1B[1A\x1B[2K");
404
+ if (cancelled) throw new Cancelled();
405
+ out.write(`${truncate(` ${s.green("✓")} ${s.dim(question)} ${typed.trim()}`, columns)}\n`);
406
+ return typed.trim();
407
+ }
408
+ /** One of `options`, chosen with the arrows and Enter; then one line, as `textLine` leaves. Ctrl-C cancels. */
409
+ async function select(keys, out, s, question, options) {
410
+ const columns = Math.max(20, (out.columns || 80) - 1);
411
+ let at = 0;
412
+ let drawn = 0;
413
+ const draw = () => {
414
+ const lines = [` ${s.accent("?")} ${s.bold(question)} ${s.dim("↑↓ then Enter")}`, ...options.map((option, i) => i === at ? ` ${s.accent("›")} ${s.accent(option)}` : ` ${option}`)];
415
+ out.write(`${drawn > 0 ? `\x1b[${drawn}F` : ""}${lines.map((line) => `${truncate(line, columns)}\x1b[K\n`).join("")}`);
416
+ drawn = lines.length;
417
+ };
418
+ let cancelled = false;
419
+ const chosen = readKeys(keys, (key) => {
420
+ if (key.ctrl && key.name === "c") cancelled = true;
421
+ else if (key.name === "return" || key.name === "enter") return true;
422
+ else if (key.name === "up" || key.name === "k") at = (at - 1 + options.length) % options.length;
423
+ else if (key.name === "down" || key.name === "j" || key.name === "tab") at = (at + 1) % options.length;
424
+ if (cancelled) return true;
425
+ draw();
426
+ return false;
427
+ });
428
+ draw();
429
+ await chosen;
430
+ out.write(`\x1b[${drawn}F\x1b[J`);
431
+ if (cancelled) throw new Cancelled();
432
+ out.write(`${truncate(` ${s.green("✓")} ${s.dim(question)} ${options[at]}`, columns)}\n`);
433
+ return at;
279
434
  }
280
435
  var Cancelled = class extends Error {
281
436
  constructor() {
@@ -289,24 +444,24 @@ var Cancelled = class extends Error {
289
444
  */
290
445
  async function hiddenLine(keys, out, s, question, hint) {
291
446
  const columns = Math.max(20, (out.columns || 80) - 1);
292
- out.write(`${truncate(` ${s.accent("?")} ${s.bold(question)}`, columns)}\n${truncate(` ${s.dim(hint)}`, columns)}\n`);
293
447
  let typed = "";
294
448
  const draw = () => {
295
449
  const count = typed.length > 0 ? s.dim(` ${typed.length} characters`) : "";
296
450
  out.write(`\r\x1b[2K${truncate(` ${s.accent("›")} ${"•".repeat(typed.length)}`, columns - width(count))}${count}`);
297
451
  };
298
- draw();
299
452
  let cancelled = false;
300
- await readKeys(keys, (key, sequence) => {
301
- if (key.ctrl && key.name === "c") cancelled = true;
302
- else if (key.name === "return" || key.name === "enter") return true;
303
- else if (key.name === "backspace") typed = typed.slice(0, -1);
304
- else if (key.ctrl && key.name === "u") typed = "";
305
- else if (!key.ctrl && !key.meta && sequence.length > 0 && !sequence.startsWith("\x1B") && sequence >= " ") typed += sequence;
453
+ const typedIn = readKeys(keys, (key, sequence) => {
454
+ const next = edit(typed, key, sequence);
455
+ if (next === "cancel") cancelled = true;
456
+ else if (next === "submit") return true;
457
+ else typed = next;
306
458
  if (cancelled) return true;
307
459
  draw();
308
460
  return false;
309
461
  });
462
+ out.write(`${truncate(` ${s.accent("?")} ${s.bold(question)}`, columns)}\n${truncate(` ${s.dim(hint)}`, columns)}\n`);
463
+ draw();
464
+ await typedIn;
310
465
  out.write("\r\x1B[2K\x1B[1A\x1B[2K\x1B[1A\x1B[2K");
311
466
  if (cancelled) throw new Cancelled();
312
467
  return typed.trim();
@@ -389,7 +544,7 @@ function render(screen, state, columns, rows, s) {
389
544
  "",
390
545
  `${title}${" ".repeat(Math.max(1, columns - width(title) - width(right) - 2))}${right}`,
391
546
  rule,
392
- ...state.view === "values" ? wrap("Save each one in your password manager now. They aren't stored anywhere, and won't be shown again.", columns - 4).map((line) => ` ${line}`) : [` ${s.bold("Where these go")}`],
547
+ ...state.view === "values" ? wrap(screen.intro ?? "Save each one in your password manager now. They aren't stored anywhere, and won't be shown again.", columns - 4).map((line) => ` ${line}`) : [` ${s.bold("Where these go")}`],
393
548
  ""
394
549
  ];
395
550
  const body = [];
@@ -723,4 +878,4 @@ async function keys(args) {
723
878
  ].join("\n"));
724
879
  }
725
880
  //#endregion
726
- export { yes as _, keys as a, Cancelled as c, paragraph as d, release as f, wrap as g, truncate as h, keyValues as i, hiddenLine as l, style as m, jsonWarning as n, needsTerminal as o, row as p, keyGuide as r, showSecrets as s, generateKeys as t, openTerminal as u };
881
+ export { yes as C, isEmpty as E, wrap as S, init as T, select as _, keys as a, truncate as b, Cancelled as c, listed as d, openTerminal as f, row as g, release as h, keyValues as i, edit as l, readKeys as m, jsonWarning as n, needsTerminal as o, paragraph as p, keyGuide as r, showSecrets as s, generateKeys as t, hiddenLine as u, style as v, KINDS as w, width as x, textLine as y };
package/dist/main.js CHANGED
@@ -1,75 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import { a as keys } from "./keys-RhB7ayOY.js";
2
+ import { T as init, a as keys, w as KINDS } from "./keys-BCKUEb-Y.js";
3
3
  import { i as isRole, r as assignableToEnvironment, t as ROLES } from "./access-CO1kGjWW.js";
4
4
  import { parseArgs, promisify } from "node:util";
5
5
  import { execFile, spawn } from "node:child_process";
6
- import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
6
+ import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
7
7
  import { homedir, hostname } from "node:os";
8
- import { basename, dirname, join, resolve } from "node:path";
9
- import { fileURLToPath } from "node:url";
10
- //#region src/init.ts
11
- const KINDS = ["workers", "node"];
12
- /** Where a template lives: in the package when published, else in the repository. */
13
- function templateDir(kind) {
14
- const packed = fileURLToPath(new URL(`./templates/${kind}/`, import.meta.url));
15
- if (existsSync(packed)) return packed;
16
- return fileURLToPath(new URL(`../../../examples/${kind}/`, import.meta.url));
17
- }
18
- /**
19
- * A template's files, relative and sorted: everything but what its own
20
- * `.gitignore` names, such as installed packages, local state and secrets.
21
- * Patterns are file or directory names, `*` standing for any run of
22
- * characters.
23
- */
24
- function templateFiles(dir) {
25
- const ignoreFile = [join(dir, ".gitignore"), join(dir, "gitignore")].find((path) => existsSync(path));
26
- const ignored = (ignoreFile === void 0 ? "" : readFileSync(ignoreFile, "utf8")).split("\n").map((line) => line.trim()).filter((line) => line !== "" && !line.startsWith("#")).map((glob) => new RegExp(`^${glob.split("*").map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join(".*")}$`));
27
- const files = [];
28
- const walk = (relative) => {
29
- for (const entry of readdirSync(join(dir, relative), { withFileTypes: true })) {
30
- if (ignored.some((pattern) => pattern.test(entry.name))) continue;
31
- const path = relative === "" ? entry.name : `${relative}/${entry.name}`;
32
- if (entry.isDirectory()) walk(path);
33
- else files.push(path);
34
- }
35
- };
36
- walk("");
37
- return files.sort();
38
- }
39
- /** The name a template file has in a project. */
40
- function projectPath(templatePath) {
41
- return templatePath === "gitignore" ? ".gitignore" : templatePath;
42
- }
43
- /** A package name from a directory's, e.g. `Acme Secrets` → `acme-secrets`. */
44
- function packageName(dir) {
45
- const name = basename(dir).toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[-._]+|-+$/g, "");
46
- return name === "" ? "coffre" : name;
47
- }
48
- /** The template's package.json, for this project and this version of coffre. */
49
- function manifest(source, name, version) {
50
- const pkg = JSON.parse(source);
51
- pkg.name = name;
52
- for (const field of ["dependencies", "devDependencies"]) {
53
- const deps = pkg[field];
54
- for (const dep of Object.keys(deps ?? {})) if (dep.startsWith("@coffre/")) deps[dep] = version;
55
- }
56
- return `${JSON.stringify(pkg, null, 2)}\n`;
57
- }
58
- /** Write a new deployment into `target`, which must be empty or absent. Returns the files written. */
59
- function init(kind, target, version) {
60
- if (existsSync(target) && readdirSync(target).length > 0) throw new Error(`${target} is not empty; give \`coffre init\` a new directory`);
61
- const source = templateDir(kind);
62
- const written = [];
63
- for (const file of templateFiles(source)) {
64
- const path = projectPath(file);
65
- const content = readFileSync(join(source, file), "utf8");
66
- mkdirSync(dirname(join(target, path)), { recursive: true });
67
- writeFileSync(join(target, path), path === "package.json" ? manifest(content, packageName(target), version) : content);
68
- written.push(path);
69
- }
70
- return written;
71
- }
72
- //#endregion
8
+ import { dirname, join, resolve } from "node:path";
73
9
  //#region src/instance.ts
74
10
  const MODES = ["signin", "cloudflare"];
75
11
  function emptyStore() {
@@ -1462,7 +1398,8 @@ const USAGE = `coffre - secrets, with an audit log
1462
1398
  coffre init --node [<dir>] a Node server, and its vault beside it
1463
1399
  coffre setup [--reset-passwords] [--json]
1464
1400
  its database logins, migrations and keys in one go,
1465
- shown once on a screen of their own
1401
+ shown once on a screen of their own; on Workers,
1402
+ Cloudflare too, and in an empty directory, the deployment
1466
1403
  coffre keys [--json] the app key, vault key and vault ID alone, shown the same way
1467
1404
 
1468
1405
  Session
@@ -1516,7 +1453,7 @@ switch (command) {
1516
1453
  await keys(rest);
1517
1454
  break;
1518
1455
  case "setup":
1519
- await (await import("./setup-3OJc_VCA.js")).setup(rest);
1456
+ await (await import("./setup-D3SXt_P-.js")).setup(rest);
1520
1457
  break;
1521
1458
  case "login":
1522
1459
  await login(rest);