@coffre/cli 0.1.13 → 0.1.14

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.
@@ -0,0 +1,317 @@
1
+ import { d as style, g as yes, h as wrap, m as width, n as edit, p as truncate, s as readKeys, t as Cancelled } from "./tty-BDa14JX2.js";
2
+ import { readFileSync } from "node:fs";
3
+ //#region src/steps.ts
4
+ const SPINNER = [
5
+ "⠋",
6
+ "⠙",
7
+ "⠹",
8
+ "⠸",
9
+ "⠼",
10
+ "⠴",
11
+ "⠦",
12
+ "⠧",
13
+ "⠇",
14
+ "⠏"
15
+ ];
16
+ const FRAME_MS = 80;
17
+ /** A step failed, and its line says why: nothing more to show. */
18
+ var StepFailed = class extends Error {};
19
+ var Steps = class {
20
+ #out;
21
+ #s;
22
+ #steps;
23
+ #keys;
24
+ #describe;
25
+ #frame = 0;
26
+ #drawn = 0;
27
+ /** The first step still redrawn: those above it have settled, and been printed for good. */
28
+ #from = 0;
29
+ #timer = null;
30
+ /** Whether the failed step has been printed, where nothing is redrawn. */
31
+ #reported = false;
32
+ #restoreCursor = () => this.#out.write("\x1B[?25h");
33
+ /** Ctrl-C while a step runs, the terminal in its usual mode: the cursor back, then out, with nothing more to show. */
34
+ #interrupt = () => {
35
+ this.#stop();
36
+ this.end();
37
+ this.#out.write("\n");
38
+ process.exit(130);
39
+ };
40
+ /**
41
+ * `titles`, what each step does while it runs; `keys`, the keyboard for
42
+ * questions; `describe`, an error as it may be shown, a secret it quotes
43
+ * taken out.
44
+ */
45
+ constructor(out, titles, keys, describe) {
46
+ this.#out = out;
47
+ this.#s = style(out);
48
+ this.#keys = keys;
49
+ this.#describe = describe;
50
+ this.#steps = titles.map((text) => ({
51
+ text,
52
+ status: "pending",
53
+ details: [],
54
+ under: [],
55
+ input: null,
56
+ choice: null
57
+ }));
58
+ if (this.#s.ansi) {
59
+ out.write("\x1B[?25l");
60
+ process.once("exit", this.#restoreCursor);
61
+ process.on("SIGINT", this.#interrupt);
62
+ this.#draw();
63
+ }
64
+ }
65
+ /**
66
+ * Run step `i`: its line is the outcome `work` returns, or the error it
67
+ * throws, which comes out as StepFailed, unless it is Cancelled.
68
+ */
69
+ async run(i, work) {
70
+ const state = this.#steps[i];
71
+ state.status = "running";
72
+ this.#tick();
73
+ const step = {
74
+ note: (text) => {
75
+ state.text = text;
76
+ this.#draw();
77
+ },
78
+ under: (lines) => {
79
+ state.under = lines;
80
+ this.#draw();
81
+ },
82
+ ask: (question) => this.#ask(state, question),
83
+ choose: (question, options) => this.#choose(state, question, options),
84
+ paste: (prompt, accept, signal) => this.#paste(state, prompt, accept, signal)
85
+ };
86
+ try {
87
+ const outcome = await work(step);
88
+ Object.assign(state, typeof outcome === "string" ? {
89
+ text: outcome,
90
+ details: []
91
+ } : outcome, {
92
+ status: "done",
93
+ under: [],
94
+ input: null
95
+ });
96
+ } catch (error) {
97
+ Object.assign(state, {
98
+ under: [],
99
+ input: null,
100
+ choice: null
101
+ });
102
+ state.status = "failed";
103
+ state.details = error instanceof Cancelled ? [] : [this.#describe(error)];
104
+ if (error instanceof Cancelled) state.text = `${state.text}: cancelled`;
105
+ this.#stop();
106
+ throw error instanceof Cancelled ? error : new StepFailed(state.details[0]);
107
+ }
108
+ this.#stop();
109
+ if (!this.#s.ansi) this.#out.write(`✓ ${state.text}\n${state.details.map((line) => ` ${line}\n`).join("")}`);
110
+ else if (this.#steps.every((each) => each.status === "done")) this.end();
111
+ }
112
+ /**
113
+ * Lines that stay, such as an address to open, printed whole, so that a
114
+ * terminal can wrap them, and they can be copied or clicked. They go
115
+ * under the steps done, above the one running: those done are printed
116
+ * for good first, and only the rest is redrawn from then on.
117
+ */
118
+ print(text) {
119
+ if (!this.#s.ansi) {
120
+ this.#out.write(`${text}\n`);
121
+ return;
122
+ }
123
+ const running = this.#steps.findIndex((each, i) => i >= this.#from && each.status !== "done");
124
+ const settled = this.#lines(this.#from, running === -1 ? this.#steps.length : running);
125
+ this.#from = running === -1 ? this.#steps.length : running;
126
+ const up = this.#drawn > 0 ? `\x1b[${this.#drawn}F` : "";
127
+ this.#out.write(`${up}\x1b[J${settled.map((line) => `${line}\n`).join("")}${text}\n`);
128
+ this.#drawn = 0;
129
+ this.#draw();
130
+ }
131
+ /** A remark that stays, dim, wrapped to the terminal's width. */
132
+ aside(text) {
133
+ this.print(this.#s.dim(wrap(text, Math.max(20, (this.#out.columns ?? 80) - 3)).map((line) => ` ${line}`).join("\n")));
134
+ }
135
+ /**
136
+ * An address to open, after what it is for: on one line when both fit;
137
+ * otherwise the address on a line of its own, from the first column, so
138
+ * that the terminal wraps it in one piece, to copy whole.
139
+ */
140
+ link(label, address) {
141
+ const fits = width(` ${label} ${address}`) < (this.#out.columns ?? 80);
142
+ this.print(fits ? ` ${this.#s.dim(label)} ${address}` : ` ${this.#s.dim(label)}\n${address}`);
143
+ }
144
+ /** Leave the list as it stands, the cursor below it. */
145
+ end() {
146
+ this.#stop();
147
+ this.#release();
148
+ }
149
+ /** The cursor back, and Ctrl-C the terminal's again. */
150
+ #release() {
151
+ if (this.#s.ansi) {
152
+ this.#restoreCursor();
153
+ process.off("exit", this.#restoreCursor);
154
+ process.off("SIGINT", this.#interrupt);
155
+ }
156
+ }
157
+ async #ask(state, question) {
158
+ const keys = this.#keys();
159
+ if (keys === null) return false;
160
+ this.#stop();
161
+ const before = state.text;
162
+ state.text = `${question} ${this.#s.dim("y/N")}`;
163
+ state.status = "asking";
164
+ this.#draw();
165
+ try {
166
+ return await yes(keys);
167
+ } finally {
168
+ state.text = before;
169
+ state.status = "running";
170
+ this.#tick();
171
+ }
172
+ }
173
+ async #choose(state, question, options) {
174
+ const keys = this.#keys();
175
+ if (keys === null) return 0;
176
+ this.#stop();
177
+ const before = state.text;
178
+ Object.assign(state, {
179
+ text: `${question} ${this.#s.dim("↑↓ then Enter")}`,
180
+ status: "asking",
181
+ choice: {
182
+ options,
183
+ at: 0
184
+ }
185
+ });
186
+ let cancelled = false;
187
+ try {
188
+ const chosen = readKeys(keys, (key) => {
189
+ const choice = state.choice;
190
+ if (key.ctrl && key.name === "c") cancelled = true;
191
+ else if (key.name === "return" || key.name === "enter") return true;
192
+ else if (key.name === "up" || key.name === "k") choice.at = (choice.at - 1 + options.length) % options.length;
193
+ else if (key.name === "down" || key.name === "j" || key.name === "tab") choice.at = (choice.at + 1) % options.length;
194
+ if (cancelled) return true;
195
+ this.#draw();
196
+ return false;
197
+ });
198
+ this.#draw();
199
+ await chosen;
200
+ if (cancelled) throw new Cancelled();
201
+ return state.choice.at;
202
+ } finally {
203
+ Object.assign(state, {
204
+ text: before,
205
+ status: "running",
206
+ choice: null
207
+ });
208
+ this.#tick();
209
+ }
210
+ }
211
+ async #paste(state, prompt, accept, signal) {
212
+ const keys = this.#keys();
213
+ if (keys === null) {
214
+ await new Promise((resolve) => signal.aborted ? resolve() : signal.addEventListener("abort", () => resolve()));
215
+ return null;
216
+ }
217
+ state.input = {
218
+ prompt,
219
+ typed: "",
220
+ error: null
221
+ };
222
+ let taken = null;
223
+ let cancelled = false;
224
+ try {
225
+ const pasted = readKeys(keys, async (key, sequence) => {
226
+ const input = state.input;
227
+ const next = edit(input.typed, key, sequence);
228
+ if (next === "cancel") cancelled = true;
229
+ else if (next === "submit" && input.typed.trim() !== "") {
230
+ const text = input.typed.trim();
231
+ input.typed = "";
232
+ input.error = await accept(text);
233
+ if (input.error === null) taken = text;
234
+ } else if (next !== "submit") {
235
+ input.typed = next;
236
+ input.error = null;
237
+ }
238
+ this.#draw();
239
+ return cancelled || taken !== null;
240
+ }, signal);
241
+ this.#draw();
242
+ await pasted;
243
+ } finally {
244
+ state.input = null;
245
+ this.#draw();
246
+ }
247
+ if (cancelled) throw new Cancelled();
248
+ return taken;
249
+ }
250
+ #tick() {
251
+ this.#draw();
252
+ if (this.#s.ansi && this.#timer === null) this.#timer = setInterval(() => {
253
+ this.#frame = (this.#frame + 1) % SPINNER.length;
254
+ this.#draw();
255
+ }, FRAME_MS).unref();
256
+ }
257
+ #stop() {
258
+ if (this.#timer !== null) clearInterval(this.#timer);
259
+ this.#timer = null;
260
+ this.#draw();
261
+ if (!this.#s.ansi) {
262
+ const failed = this.#steps.find((each) => each.status === "failed");
263
+ if (failed !== void 0 && !this.#reported) {
264
+ this.#reported = true;
265
+ this.#out.write(`✗ ${failed.text}\n${failed.details.map((line) => ` ${line}\n`).join("")}`);
266
+ }
267
+ } else if (this.#steps.some((each) => each.status === "failed")) this.#release();
268
+ }
269
+ #draw() {
270
+ if (!this.#s.ansi) return;
271
+ const lines = this.#lines(this.#from, this.#steps.length);
272
+ const up = this.#drawn > 0 ? `\x1b[${this.#drawn}F` : "";
273
+ this.#out.write(`${up}${lines.map((line) => `${line}\x1b[K\n`).join("")}\x1b[J`);
274
+ this.#drawn = lines.length;
275
+ }
276
+ /** Steps `from` to `to`, as lines cut to the terminal's width. */
277
+ #lines(from, to) {
278
+ const s = this.#s;
279
+ const columns = Math.max(20, (this.#out.columns ?? 80) - 1);
280
+ return this.#steps.slice(from, to).flatMap(({ text, status, details, under, input, choice }) => {
281
+ const icon = {
282
+ pending: s.dim("·"),
283
+ running: s.accent(SPINNER[this.#frame]),
284
+ asking: s.accent("?"),
285
+ done: s.green("✓"),
286
+ failed: s.red("✗")
287
+ }[status];
288
+ const line = status === "pending" ? s.dim(text) : status === "failed" ? s.red(text) : text;
289
+ const below = status === "failed" ? details.flatMap((detail) => wrap(detail, columns - 4)) : details.map(s.dim);
290
+ const live = [
291
+ ...under.map((text) => ` ${s.dim(text)}`),
292
+ ...choice === null ? [] : choice.options.map((option, i) => i === choice.at ? ` ${s.accent("›")} ${s.accent(option)}` : ` ${option}`),
293
+ ...input === null ? [] : [
294
+ ` ${input.prompt}`,
295
+ ` ${s.accent("›")} ${"•".repeat(input.typed.length)}`,
296
+ ...input.error === null ? [] : [` ${s.red(input.error)}`]
297
+ ]
298
+ ];
299
+ return [
300
+ ` ${icon} ${line}`,
301
+ ...below.map((detail) => ` ${detail}`),
302
+ ...live
303
+ ];
304
+ }).map((line) => truncate(line, columns));
305
+ }
306
+ };
307
+ //#endregion
308
+ //#region src/version.ts
309
+ /**
310
+ * This CLI's version: the schema it migrates to, the version of coffre a
311
+ * deployment it makes runs, and the one `coffre migrate` expects to find.
312
+ */
313
+ function cliVersion() {
314
+ return JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
315
+ }
316
+ //#endregion
317
+ export { StepFailed as n, Steps as r, cliVersion as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coffre/cli",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "description": "The coffre command line: secrets in your shell, and `coffre init` for a new deployment.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -28,9 +28,9 @@
28
28
  "pg": "8.16.3",
29
29
  "tsdown": "0.23.0",
30
30
  "typescript": "5.9.3",
31
- "@coffre/db": "0.1.13",
32
- "@coffre/core": "0.1.13",
33
- "@coffre/client": "0.1.13"
31
+ "@coffre/client": "0.1.14",
32
+ "@coffre/db": "0.1.14",
33
+ "@coffre/core": "0.1.14"
34
34
  },
35
35
  "scripts": {
36
36
  "build": "tsdown && node scripts/copy-templates.ts && node scripts/copy-migrations.ts",
@@ -4,8 +4,8 @@ import { D as customType, O as boolean, V as SQL, W as sql, b as text, ct as is,
4
4
  import { r as drizzle } from "./node-postgres-BHgGDfvg.js";
5
5
  import { a as trackCommits, i as relationsOf, n as asPostgresDatabase, r as ACTIVE_SUBJECT } from "./portable-9wWdi5e1.js";
6
6
  import { resolve } from "node:path";
7
- import { AsyncLocalStorage } from "node:async_hooks";
8
7
  import { checkServerIdentity } from "node:tls";
8
+ import { AsyncLocalStorage } from "node:async_hooks";
9
9
  //#region ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudflare+workers-types@5.20260920.1_@libsql+client@0.18.0_@types+pg@8.20.0_pg@8.16.3/node_modules/drizzle-orm/pg-core/checks.js
10
10
  var CheckBuilder = class {
11
11
  constructor(name, value) {
@@ -126,6 +126,39 @@ function uniqueIndex(name) {
126
126
  return new IndexBuilderOn(true, name);
127
127
  }
128
128
  //#endregion
129
+ //#region ../../node_modules/.pnpm/pg-connection-string@2.14.0/node_modules/pg-connection-string/esm/index.mjs
130
+ var import_pg_connection_string = /* @__PURE__ */ __toESM(require_pg_connection_string(), 1);
131
+ import_pg_connection_string.default.parse;
132
+ import_pg_connection_string.default.parse;
133
+ import_pg_connection_string.default.toClientConfig;
134
+ const parseIntoClientConfig = import_pg_connection_string.default.parseIntoClientConfig;
135
+ //#endregion
136
+ //#region ../db/dist/postgres-Bmar8y0q.js
137
+ /** Keep libpq's system-root shortcut from being read as a certificate filename by pg. */
138
+ function postgresConnection(url) {
139
+ let parsed;
140
+ try {
141
+ parsed = new URL(url);
142
+ } catch {
143
+ throw new Error("invalid Postgres URL");
144
+ }
145
+ const parameters = parsed.searchParams;
146
+ if (parameters.getAll("sslrootcert").at(-1) !== "system") return { connectionString: url };
147
+ const mode = parameters.getAll("sslmode").at(-1);
148
+ if (mode !== void 0 && mode !== "verify-full") throw new Error("sslrootcert=system requires sslmode=verify-full");
149
+ parameters.delete("sslrootcert");
150
+ parameters.set("sslmode", "verify-full");
151
+ const config = parseIntoClientConfig(parsed.toString());
152
+ const host = config.host;
153
+ if (!host) throw new Error("sslrootcert=system requires a Postgres hostname");
154
+ config.ssl = {
155
+ ...config.ssl,
156
+ rejectUnauthorized: true,
157
+ checkServerIdentity: (_hostname, certificate) => checkServerIdentity(host, certificate)
158
+ };
159
+ return config;
160
+ }
161
+ //#endregion
129
162
  //#region ../db/dist/rolldown-runtime-D7D4PA-g.js
130
163
  var __defProp = Object.defineProperty;
131
164
  var __exportAll = (all, no_symbols) => {
@@ -562,39 +595,6 @@ function createDatabase(pool) {
562
595
  return trackCommits(db);
563
596
  }
564
597
  //#endregion
565
- //#region ../../node_modules/.pnpm/pg-connection-string@2.14.0/node_modules/pg-connection-string/esm/index.mjs
566
- var import_pg_connection_string = /* @__PURE__ */ __toESM(require_pg_connection_string(), 1);
567
- import_pg_connection_string.default.parse;
568
- import_pg_connection_string.default.parse;
569
- import_pg_connection_string.default.toClientConfig;
570
- const parseIntoClientConfig = import_pg_connection_string.default.parseIntoClientConfig;
571
- //#endregion
572
- //#region ../db/dist/postgres-Bmar8y0q.js
573
- /** Keep libpq's system-root shortcut from being read as a certificate filename by pg. */
574
- function postgresConnection(url) {
575
- let parsed;
576
- try {
577
- parsed = new URL(url);
578
- } catch {
579
- throw new Error("invalid Postgres URL");
580
- }
581
- const parameters = parsed.searchParams;
582
- if (parameters.getAll("sslrootcert").at(-1) !== "system") return { connectionString: url };
583
- const mode = parameters.getAll("sslmode").at(-1);
584
- if (mode !== void 0 && mode !== "verify-full") throw new Error("sslrootcert=system requires sslmode=verify-full");
585
- parameters.delete("sslrootcert");
586
- parameters.set("sslmode", "verify-full");
587
- const config = parseIntoClientConfig(parsed.toString());
588
- const host = config.host;
589
- if (!host) throw new Error("sslrootcert=system requires a Postgres hostname");
590
- config.ssl = {
591
- ...config.ssl,
592
- rejectUnauthorized: true,
593
- checkServerIdentity: (_hostname, certificate) => checkServerIdentity(host, certificate)
594
- };
595
- return config;
596
- }
597
- //#endregion
598
598
  //#region ../db/dist/connect.js
599
599
  /**
600
600
  * Open a database from its URL, on a Node server: