@coffre/cli 0.1.13 → 0.1.15

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/main.js CHANGED
@@ -1,12 +1,270 @@
1
1
  #!/usr/bin/env node
2
- import { T as init, _ as select, a as keys, c as Cancelled, f as openTerminal, h as release, u as hiddenLine, v as style, w as KINDS } from "./keys-BCKUEb-Y.js";
2
+ import { n as init, t as KINDS } from "./init-2xGelVG1.js";
3
+ import { a as keys } from "./keys-DDdMwm0N.js";
4
+ import { a as openTerminal, c as release, d as style, r as hiddenLine, t as Cancelled, u as select } from "./tty-BDa14JX2.js";
3
5
  import { i as isRole, r as assignableToEnvironment, t as ROLES } from "./access-CO1kGjWW.js";
4
6
  import { inspect, parseArgs, promisify } from "node:util";
5
7
  import { execFile, spawn } from "node:child_process";
6
- import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
7
- import { homedir, hostname } from "node:os";
8
+ import { appendFileSync, chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
9
+ import { EOL, homedir, hostname } from "node:os";
8
10
  import { dirname, join, resolve } from "node:path";
9
- import { createCipheriv, createDecipheriv, createHash, hkdfSync, randomBytes, timingSafeEqual } from "node:crypto";
11
+ import { createCipheriv, createDecipheriv, createHash, hkdfSync, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
12
+ /**
13
+ * The line to show, or null: nothing pending, or nothing told. An instance
14
+ * older than this notice has no `instance` on `/me` at all.
15
+ */
16
+ function noticeFor(origin, instance) {
17
+ if (instance === null || instance === void 0 || !Array.isArray(instance.migrations?.known)) return null;
18
+ const pending = instance.migrations.known.slice(instance.migrations.applied);
19
+ if (pending.length === 0) return null;
20
+ return `${pending.length === 1 ? "1 migration" : `${pending.length} migrations`} pending on ${origin} (${pending.join(", ")}): run \`coffre migrate\``;
21
+ }
22
+ /**
23
+ * The notice for `origin`, at most once a day: null without asking when it
24
+ * was asked less than a day ago; otherwise `me` is asked, and the time kept,
25
+ * whatever the answer. A failure to ask says nothing, and is not kept.
26
+ */
27
+ async function dailyNotice(origin, now, checked, me) {
28
+ const last = checked.read()[origin];
29
+ if (last !== void 0 && now - last < 864e5) return null;
30
+ let instance;
31
+ try {
32
+ ({instance} = await me());
33
+ } catch {
34
+ return null;
35
+ }
36
+ checked.write({
37
+ ...checked.read(),
38
+ [origin]: now
39
+ });
40
+ return noticeFor(origin, instance);
41
+ }
42
+ //#endregion
43
+ //#region ../core/dist/dotenv.js
44
+ const KEY_RE = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/;
45
+ function parseDotenv(input) {
46
+ const entries = [];
47
+ const problems = [];
48
+ const seen = /* @__PURE__ */ new Set();
49
+ const lines = input.split(/\r\n|\n|\r/);
50
+ for (let index = 0; index < lines.length; index++) {
51
+ const lineNumber = index + 1;
52
+ const trimmed = lines[index].trim();
53
+ if (trimmed === "" || trimmed.startsWith("#")) continue;
54
+ const withoutExport = trimmed.startsWith("export ") ? trimmed.slice(7).trim() : trimmed;
55
+ const equals = withoutExport.indexOf("=");
56
+ if (equals === -1) {
57
+ problems.push({
58
+ line: lineNumber,
59
+ text: trimmed,
60
+ reason: "no \"=\" on this line"
61
+ });
62
+ continue;
63
+ }
64
+ const key = withoutExport.slice(0, equals).trim();
65
+ if (!KEY_RE.test(key)) {
66
+ problems.push({
67
+ line: lineNumber,
68
+ text: key,
69
+ reason: "key must match ^[A-Za-z_][A-Za-z0-9_]*$"
70
+ });
71
+ continue;
72
+ }
73
+ let rest = withoutExport.slice(equals + 1).trim();
74
+ let value;
75
+ if (rest.startsWith("\"")) {
76
+ const closing = findClosingQuote(rest, "\"");
77
+ if (closing === -1) {
78
+ problems.push({
79
+ line: lineNumber,
80
+ text: key,
81
+ reason: "unterminated double quote (multi-line values are not supported)"
82
+ });
83
+ continue;
84
+ }
85
+ value = unescapeDoubleQuoted(rest.slice(1, closing));
86
+ if (trailingIsSignificant(rest.slice(closing + 1))) {
87
+ problems.push({
88
+ line: lineNumber,
89
+ text: key,
90
+ reason: "unexpected text after the closing quote"
91
+ });
92
+ continue;
93
+ }
94
+ } else if (rest.startsWith("'")) {
95
+ const closing = rest.indexOf("'", 1);
96
+ if (closing === -1) {
97
+ problems.push({
98
+ line: lineNumber,
99
+ text: key,
100
+ reason: "unterminated single quote (multi-line values are not supported)"
101
+ });
102
+ continue;
103
+ }
104
+ value = rest.slice(1, closing);
105
+ if (trailingIsSignificant(rest.slice(closing + 1))) {
106
+ problems.push({
107
+ line: lineNumber,
108
+ text: key,
109
+ reason: "unexpected text after the closing quote"
110
+ });
111
+ continue;
112
+ }
113
+ } else {
114
+ const comment = rest.indexOf(" #");
115
+ if (comment !== -1) rest = rest.slice(0, comment);
116
+ value = rest.trim();
117
+ }
118
+ if (value.includes("\0")) {
119
+ problems.push({
120
+ line: lineNumber,
121
+ text: key,
122
+ reason: "value contains a NUL byte, which cannot be passed in an environment"
123
+ });
124
+ continue;
125
+ }
126
+ if (seen.has(key)) {
127
+ problems.push({
128
+ line: lineNumber,
129
+ text: key,
130
+ reason: "duplicate key in this file"
131
+ });
132
+ continue;
133
+ }
134
+ seen.add(key);
135
+ entries.push({
136
+ key,
137
+ value,
138
+ line: lineNumber
139
+ });
140
+ }
141
+ return {
142
+ entries,
143
+ problems
144
+ };
145
+ }
146
+ /** True when what follows a closing quote is neither whitespace nor a comment. */
147
+ function trailingIsSignificant(rest) {
148
+ const trimmed = rest.trim();
149
+ return trimmed !== "" && !trimmed.startsWith("#");
150
+ }
151
+ function findClosingQuote(text, quote) {
152
+ for (let index = 1; index < text.length; index++) {
153
+ if (text[index] === "\\") {
154
+ index++;
155
+ continue;
156
+ }
157
+ if (text[index] === quote) return index;
158
+ }
159
+ return -1;
160
+ }
161
+ /**
162
+ * Expand escapes inside a double-quoted value.
163
+ *
164
+ * Deliberately minimal: only the escapes that are necessary. `\n` and `\r`
165
+ * because multi-line values (PEM keys) cannot otherwise be expressed on one
166
+ * line, and `\\` `\"` `\'` because quoting needs them.
167
+ *
168
+ * EVERYTHING ELSE IS PRESERVED VERBATIM, backslash included. The obvious
169
+ * implementation -- returning the character and dropping the backslash for
170
+ * unknown escapes -- silently mangles credentials: `"C:\path\to\file"` came
171
+ * out as `C:path<TAB>ofile`, and `"\d+\w"` as `d+w`. A secrets store that
172
+ * quietly rewrites what you gave it is worse than one that refuses.
173
+ *
174
+ * A value that should contain no interpretation at all belongs in single
175
+ * quotes, which are literal.
176
+ */
177
+ function unescapeDoubleQuoted(text) {
178
+ return text.replace(/\\(.)/g, (match, character) => {
179
+ switch (character) {
180
+ case "n": return "\n";
181
+ case "r": return "\r";
182
+ case "\\":
183
+ case "\"":
184
+ case "'": return character;
185
+ default: return match;
186
+ }
187
+ });
188
+ }
189
+ /** Values that read the same unquoted: no spaces, quotes, `#`, `$` or backslashes. */
190
+ const PLAIN_VALUE = /^[A-Za-z0-9_./:@+,=%?-]*$/;
191
+ /**
192
+ * Write entries as a .env file that `parseDotenv` reads back exactly.
193
+ *
194
+ * Plain values stay bare; anything else is single-quoted, which is literal;
195
+ * values holding a quote or a line break are double-quoted with the four
196
+ * escapes the parser knows (`\\`, `\"`, `\n`, `\r`).
197
+ */
198
+ function formatDotenv(entries) {
199
+ let out = "";
200
+ for (const [key, value] of entries) {
201
+ assertEnvironmentEntry(key, value);
202
+ out += `${key}=${quoteDotenv(value)}\n`;
203
+ }
204
+ return out;
205
+ }
206
+ /** Reject names and values that cannot be passed intact in an environment. */
207
+ function assertEnvironmentEntry(key, value) {
208
+ if (!KEY_RE.test(key)) throw new Error(`${JSON.stringify(key)} is not a valid variable name`);
209
+ if (value.includes("\0")) throw new Error(`${key} contains a NUL byte`);
210
+ }
211
+ function quoteDotenv(value) {
212
+ if (PLAIN_VALUE.test(value)) return value;
213
+ if (!/['\n\r]/.test(value)) return `'${value}'`;
214
+ return `"${value.replace(/[\\"\n\r]/g, (character) => {
215
+ switch (character) {
216
+ case "\n": return "\\n";
217
+ case "\r": return "\\r";
218
+ default: return `\\${character}`;
219
+ }
220
+ })}"`;
221
+ }
222
+ /**
223
+ * Write entries as POSIX shell `export` statements, for
224
+ * `eval "$(coffre export … --format shell)"`. Single quotes are the only
225
+ * shell quoting with no expansion inside; a single quote itself is written
226
+ * as `'\''`.
227
+ */
228
+ function formatShellExports(entries) {
229
+ let out = "";
230
+ for (const [key, value] of entries) {
231
+ assertEnvironmentEntry(key, value);
232
+ out += `export ${key}='${value.replaceAll("'", "'\\''")}'\n`;
233
+ }
234
+ return out;
235
+ }
236
+ //#endregion
237
+ //#region src/github-env.ts
238
+ /** The runner decodes %, CR and LF in command data, in this escaping order.
239
+ * https://github.com/actions/toolkit/blob/main/packages/core/src/command.ts
240
+ */
241
+ function githubMasks(entries) {
242
+ let out = "";
243
+ for (const [, value] of entries) for (const mask of /* @__PURE__ */ new Set([value, ...value.split(/\r\n|\r|\n/)])) {
244
+ if (mask === "") continue;
245
+ out += `::add-mask::${mask.replaceAll("%", "%25").replaceAll("\r", "%0D").replaceAll("\n", "%0A")}\n`;
246
+ }
247
+ return out;
248
+ }
249
+ /** Multiline-safe environment records, consumed by GitHub after the step.
250
+ * https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-commands#multiline-strings
251
+ * Match the host's framing newline, including on Windows, so a trailing CR
252
+ * in the value is not mistaken for part of that newline.
253
+ */
254
+ function githubEnvironment(entries, nextDelimiter = () => `coffre_${randomUUID()}`, newline = EOL) {
255
+ let out = "";
256
+ for (const [key, value] of entries) {
257
+ assertEnvironmentEntry(key, value);
258
+ if (key.toUpperCase() === "NODE_OPTIONS") throw new Error(`${key} cannot be set through GITHUB_ENV`);
259
+ let delimiter;
260
+ do
261
+ delimiter = nextDelimiter();
262
+ while (value.includes(delimiter));
263
+ out += `${key}<<${delimiter}${newline}${value}${newline}${delimiter}${newline}`;
264
+ }
265
+ return out;
266
+ }
267
+ //#endregion
10
268
  //#region ../client/dist/index.js
11
269
  /** The API's error shape, `{ error, message }`, with the HTTP status. */
12
270
  const COFFRE_ERROR = Symbol.for("@coffre/client:CoffreError");
@@ -1521,199 +1779,6 @@ async function pickCheck() {
1521
1779
  }
1522
1780
  }
1523
1781
  //#endregion
1524
- //#region ../core/dist/dotenv.js
1525
- const KEY_RE = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/;
1526
- function parseDotenv(input) {
1527
- const entries = [];
1528
- const problems = [];
1529
- const seen = /* @__PURE__ */ new Set();
1530
- const lines = input.split(/\r\n|\n|\r/);
1531
- for (let index = 0; index < lines.length; index++) {
1532
- const lineNumber = index + 1;
1533
- const trimmed = lines[index].trim();
1534
- if (trimmed === "" || trimmed.startsWith("#")) continue;
1535
- const withoutExport = trimmed.startsWith("export ") ? trimmed.slice(7).trim() : trimmed;
1536
- const equals = withoutExport.indexOf("=");
1537
- if (equals === -1) {
1538
- problems.push({
1539
- line: lineNumber,
1540
- text: trimmed,
1541
- reason: "no \"=\" on this line"
1542
- });
1543
- continue;
1544
- }
1545
- const key = withoutExport.slice(0, equals).trim();
1546
- if (!KEY_RE.test(key)) {
1547
- problems.push({
1548
- line: lineNumber,
1549
- text: key,
1550
- reason: "key must match ^[A-Za-z_][A-Za-z0-9_]*$"
1551
- });
1552
- continue;
1553
- }
1554
- let rest = withoutExport.slice(equals + 1).trim();
1555
- let value;
1556
- if (rest.startsWith("\"")) {
1557
- const closing = findClosingQuote(rest, "\"");
1558
- if (closing === -1) {
1559
- problems.push({
1560
- line: lineNumber,
1561
- text: key,
1562
- reason: "unterminated double quote (multi-line values are not supported)"
1563
- });
1564
- continue;
1565
- }
1566
- value = unescapeDoubleQuoted(rest.slice(1, closing));
1567
- if (trailingIsSignificant(rest.slice(closing + 1))) {
1568
- problems.push({
1569
- line: lineNumber,
1570
- text: key,
1571
- reason: "unexpected text after the closing quote"
1572
- });
1573
- continue;
1574
- }
1575
- } else if (rest.startsWith("'")) {
1576
- const closing = rest.indexOf("'", 1);
1577
- if (closing === -1) {
1578
- problems.push({
1579
- line: lineNumber,
1580
- text: key,
1581
- reason: "unterminated single quote (multi-line values are not supported)"
1582
- });
1583
- continue;
1584
- }
1585
- value = rest.slice(1, closing);
1586
- if (trailingIsSignificant(rest.slice(closing + 1))) {
1587
- problems.push({
1588
- line: lineNumber,
1589
- text: key,
1590
- reason: "unexpected text after the closing quote"
1591
- });
1592
- continue;
1593
- }
1594
- } else {
1595
- const comment = rest.indexOf(" #");
1596
- if (comment !== -1) rest = rest.slice(0, comment);
1597
- value = rest.trim();
1598
- }
1599
- if (value.includes("\0")) {
1600
- problems.push({
1601
- line: lineNumber,
1602
- text: key,
1603
- reason: "value contains a NUL byte, which cannot be passed in an environment"
1604
- });
1605
- continue;
1606
- }
1607
- if (seen.has(key)) {
1608
- problems.push({
1609
- line: lineNumber,
1610
- text: key,
1611
- reason: "duplicate key in this file"
1612
- });
1613
- continue;
1614
- }
1615
- seen.add(key);
1616
- entries.push({
1617
- key,
1618
- value,
1619
- line: lineNumber
1620
- });
1621
- }
1622
- return {
1623
- entries,
1624
- problems
1625
- };
1626
- }
1627
- /** True when what follows a closing quote is neither whitespace nor a comment. */
1628
- function trailingIsSignificant(rest) {
1629
- const trimmed = rest.trim();
1630
- return trimmed !== "" && !trimmed.startsWith("#");
1631
- }
1632
- function findClosingQuote(text, quote) {
1633
- for (let index = 1; index < text.length; index++) {
1634
- if (text[index] === "\\") {
1635
- index++;
1636
- continue;
1637
- }
1638
- if (text[index] === quote) return index;
1639
- }
1640
- return -1;
1641
- }
1642
- /**
1643
- * Expand escapes inside a double-quoted value.
1644
- *
1645
- * Deliberately minimal: only the escapes that are necessary. `\n` and `\r`
1646
- * because multi-line values (PEM keys) cannot otherwise be expressed on one
1647
- * line, and `\\` `\"` `\'` because quoting needs them.
1648
- *
1649
- * EVERYTHING ELSE IS PRESERVED VERBATIM, backslash included. The obvious
1650
- * implementation -- returning the character and dropping the backslash for
1651
- * unknown escapes -- silently mangles credentials: `"C:\path\to\file"` came
1652
- * out as `C:path<TAB>ofile`, and `"\d+\w"` as `d+w`. A secrets store that
1653
- * quietly rewrites what you gave it is worse than one that refuses.
1654
- *
1655
- * A value that should contain no interpretation at all belongs in single
1656
- * quotes, which are literal.
1657
- */
1658
- function unescapeDoubleQuoted(text) {
1659
- return text.replace(/\\(.)/g, (match, character) => {
1660
- switch (character) {
1661
- case "n": return "\n";
1662
- case "r": return "\r";
1663
- case "\\":
1664
- case "\"":
1665
- case "'": return character;
1666
- default: return match;
1667
- }
1668
- });
1669
- }
1670
- /** Values that read the same unquoted: no spaces, quotes, `#`, `$` or backslashes. */
1671
- const PLAIN_VALUE = /^[A-Za-z0-9_./:@+,=%?-]*$/;
1672
- /**
1673
- * Write entries as a .env file that `parseDotenv` reads back exactly.
1674
- *
1675
- * Plain values stay bare; anything else is single-quoted, which is literal;
1676
- * values holding a quote or a line break are double-quoted with the four
1677
- * escapes the parser knows (`\\`, `\"`, `\n`, `\r`).
1678
- */
1679
- function formatDotenv(entries) {
1680
- let out = "";
1681
- for (const [key, value] of entries) {
1682
- assertWritable(key, value);
1683
- out += `${key}=${quoteDotenv(value)}\n`;
1684
- }
1685
- return out;
1686
- }
1687
- function assertWritable(key, value) {
1688
- if (!KEY_RE.test(key)) throw new Error(`${JSON.stringify(key)} is not a valid variable name`);
1689
- if (value.includes("\0")) throw new Error(`${key} contains a NUL byte`);
1690
- }
1691
- function quoteDotenv(value) {
1692
- if (PLAIN_VALUE.test(value)) return value;
1693
- if (!/['\n\r]/.test(value)) return `'${value}'`;
1694
- return `"${value.replace(/[\\"\n\r]/g, (character) => {
1695
- switch (character) {
1696
- case "\n": return "\\n";
1697
- case "\r": return "\\r";
1698
- default: return `\\${character}`;
1699
- }
1700
- })}"`;
1701
- }
1702
- /**
1703
- * Write entries as POSIX shell `export` statements, for
1704
- * `eval "$(coffre export … --format shell)"`. Single quotes are the only
1705
- * shell quoting with no expansion inside; a single quote itself is written
1706
- * as `'\''`.
1707
- */
1708
- function formatShellExports(entries) {
1709
- let out = "";
1710
- for (const [key, value] of entries) {
1711
- assertWritable(key, value);
1712
- out += `export ${key}='${value.replaceAll("'", "'\\''")}'\n`;
1713
- }
1714
- return out;
1715
- }
1716
- //#endregion
1717
1782
  //#region src/main.ts
1718
1783
  /**
1719
1784
  * coffre CLI.
@@ -1797,8 +1862,59 @@ function client(to = target()) {
1797
1862
  transport: (request) => send(request, to)
1798
1863
  });
1799
1864
  }
1865
+ /** Whether this run has asked about pending migrations yet: once, before its first request. */
1866
+ let noticed = false;
1867
+ /** The commands that say nothing of pending migrations: migrate says as much itself; login has no session yet; verify sends only what it checks. */
1868
+ const QUIET = /* @__PURE__ */ new Set([
1869
+ "migrate",
1870
+ "login",
1871
+ "verify"
1872
+ ]);
1873
+ /**
1874
+ * A line on stderr, at most once a day per instance, when its database is
1875
+ * behind the code it runs (`notice.ts`). Not for a machine's credential:
1876
+ * only owners are told. Whatever goes wrong here, the command goes on.
1877
+ */
1878
+ async function noticePending(to) {
1879
+ if (noticed) return;
1880
+ noticed = true;
1881
+ if (QUIET.has(command ?? "") || process.env.COFFRE_TOKEN?.trim() || to.credential.kind === "access-service-token") return;
1882
+ const line = await dailyNotice(to.origin, Date.now(), checkedFile, async () => {
1883
+ const response = await fetch(`${to.origin}/api/me`, {
1884
+ headers: await headersFor(to),
1885
+ redirect: "manual"
1886
+ });
1887
+ if (!response.ok || !isJsonContentType(response.headers.get("content-type"))) throw new Error("no answer");
1888
+ return await response.json();
1889
+ }).catch(() => null);
1890
+ if (line !== null) {
1891
+ const s = style(process.stderr);
1892
+ process.stderr.write(`${s.dim(`coffre: ${line}`)}\n`);
1893
+ }
1894
+ }
1895
+ const CHECKED_PATH = join(homedir(), ".coffre", "checked.json");
1896
+ /** When each instance was last asked about its migrations: nothing secret, beside the credentials. */
1897
+ const checkedFile = {
1898
+ read() {
1899
+ try {
1900
+ return JSON.parse(readFileSync(CHECKED_PATH, "utf8"));
1901
+ } catch {
1902
+ return {};
1903
+ }
1904
+ },
1905
+ write(checked) {
1906
+ try {
1907
+ mkdirSync(dirname(CHECKED_PATH), {
1908
+ recursive: true,
1909
+ mode: 448
1910
+ });
1911
+ writeFileSync(CHECKED_PATH, `${JSON.stringify(checked)}\n`, { mode: 384 });
1912
+ } catch {}
1913
+ }
1914
+ };
1800
1915
  /** One request; every way it can fail is explained in terms of what to do next. */
1801
1916
  async function send(request, to) {
1917
+ await noticePending(to);
1802
1918
  let response;
1803
1919
  try {
1804
1920
  response = await fetch(request, { redirect: "manual" });
@@ -2108,14 +2224,22 @@ async function exportEnv(args) {
2108
2224
  } },
2109
2225
  allowPositionals: true
2110
2226
  });
2111
- const usage = "usage: coffre export <project>/<environment> [--format dotenv|json|shell]";
2227
+ const usage = "usage: coffre export <project>/<environment> [--format dotenv|json|shell|github]";
2112
2228
  if (!positionals[0]) fail(usage);
2113
2229
  const format = values.format;
2114
- if (format !== "dotenv" && format !== "json" && format !== "shell") fail(usage);
2230
+ if (format !== "dotenv" && format !== "json" && format !== "shell" && format !== "github") fail(usage);
2231
+ const githubEnv = process.env.GITHUB_ENV;
2232
+ if (format === "github" && !githubEnv) fail("--format github requires GITHUB_ENV (run it in a GitHub Actions step)");
2115
2233
  const { project, environment } = parsePath(positionals[0]);
2116
2234
  const revealed = await client().secrets.reveal(`${project}/${environment}`);
2117
2235
  const entries = Object.entries(revealed.values).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
2118
- process.stdout.write(format === "json" ? `${JSON.stringify(Object.fromEntries(entries), null, 2)}\n` : attempt(() => format === "shell" ? formatShellExports(entries) : formatDotenv(entries)));
2236
+ if (format === "github") {
2237
+ process.stdout.write(githubMasks(entries));
2238
+ attempt(() => appendFileSync(githubEnv, githubEnvironment(entries), {
2239
+ encoding: "utf8",
2240
+ mode: 384
2241
+ }));
2242
+ } else process.stdout.write(format === "json" ? `${JSON.stringify(Object.fromEntries(entries), null, 2)}\n` : attempt(() => format === "shell" ? formatShellExports(entries) : formatDotenv(entries)));
2119
2243
  if (process.stderr.isTTY) {
2120
2244
  const count = entries.length;
2121
2245
  process.stderr.write(`coffre: read ${count} secret${count === 1 ? "" : "s"} from ${project}/${environment}; each read is in the audit log under your name\n`);
@@ -2392,6 +2516,30 @@ function initProject(args) {
2392
2516
  const at = dir === process.cwd() ? null : positionals[0] ?? null;
2393
2517
  process.stdout.write(`\nA ${kind === "workers" ? "Cloudflare Workers" : "Node"} deployment of coffre ${version}${at === null ? " here" : ` in ${at}`}. README.md takes it from there:\n\n${at === null ? "" : ` cd ${/\s/.test(at) ? `'${at}'` : at}\n`} pnpm install\n`);
2394
2518
  }
2519
+ /**
2520
+ * The instance `coffre migrate` works on: the current one, or the one
2521
+ * `--url` names, with the session saved for it.
2522
+ */
2523
+ function migrateTarget(url) {
2524
+ if (url !== void 0) process.env.COFFRE_API_URL = url;
2525
+ const to = target();
2526
+ const api = client(to);
2527
+ return {
2528
+ origin: to.origin,
2529
+ me: () => api.me(),
2530
+ ready: async () => {
2531
+ try {
2532
+ return await (await fetch(`${to.origin}/readyz`, { redirect: "manual" })).json();
2533
+ } catch {
2534
+ return {
2535
+ ok: false,
2536
+ heartbeatAgeSeconds: null,
2537
+ checkpointed: false
2538
+ };
2539
+ }
2540
+ }
2541
+ };
2542
+ }
2395
2543
  const USAGE = `coffre - secrets, with an audit log
2396
2544
 
2397
2545
  New deployment
@@ -2403,6 +2551,11 @@ const USAGE = `coffre - secrets, with an audit log
2403
2551
  Cloudflare too, and in an empty directory, the deployment
2404
2552
  coffre keys [--json] the app key, vault key and vault ID alone, shown the same way
2405
2553
 
2554
+ Upgrade
2555
+ coffre update [--yes] this CLI, and in a deployment, its coffre packages
2556
+ coffre migrate [--url <url>] [--yes] the instance's database, to the schema its version ships,
2557
+ with the owner's connection string, asked for hidden
2558
+
2406
2559
  Session
2407
2560
  coffre login [<url>] [--no-browser] sign in, and make <url> the current instance
2408
2561
  coffre logout [<url>]
@@ -2414,7 +2567,7 @@ const USAGE = `coffre - secrets, with an audit log
2414
2567
  coffre get <project>/<environment>/<KEY>
2415
2568
  coffre set <project>/<environment>/<KEY> [value] (reads stdin if omitted)
2416
2569
  coffre run <project>/<environment> -- <command>
2417
- coffre export <project>/<environment> [--format dotenv|json|shell]
2570
+ coffre export <project>/<environment> [--format dotenv|json|shell|github]
2418
2571
  coffre history <project>/<environment>/<KEY>
2419
2572
  coffre rollback <project>/<environment>/<KEY> <version>
2420
2573
  coffre import <project>/<environment> [--file .env] [--apply]
@@ -2453,7 +2606,13 @@ switch (command) {
2453
2606
  await keys(rest);
2454
2607
  break;
2455
2608
  case "setup":
2456
- await (await import("./setup-BMmFAR3N.js")).setup(rest);
2609
+ await (await import("./setup-Bs8q8Ily.js")).setup(rest);
2610
+ break;
2611
+ case "update":
2612
+ await (await import("./update-eYzLTNoL.js")).update(rest);
2613
+ break;
2614
+ case "migrate":
2615
+ await (await import("./migrate-ERNNgYO0.js")).migrate(rest, migrateTarget);
2457
2616
  break;
2458
2617
  case "login":
2459
2618
  await login(rest);