@coffre/cli 0.1.14 → 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/README.md +3 -0
- package/dist/{deployment-S8za5nUu.js → deployment-Di8BG9qY.js} +90 -3
- package/dist/{init-mO8V087p.js → init-2xGelVG1.js} +1 -1
- package/dist/main.js +243 -203
- package/dist/{setup-qGoLPykX.js → setup-Bs8q8Ily.js} +2 -2
- package/dist/templates/node/package.json +4 -3
- package/dist/templates/workers/package.json +5 -4
- package/dist/{update-DwGbG-eM.js → update-eYzLTNoL.js} +80 -9
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
The `coffre` command: secrets in your shell, and a new deployment.
|
|
4
4
|
|
|
5
|
+
The published CLI runs on Node 20 or newer. Developing coffre from its
|
|
6
|
+
TypeScript sources and running a deployment require Node 24.
|
|
7
|
+
|
|
5
8
|
```sh
|
|
6
9
|
npx @coffre/cli init --workers my-coffre # or --node
|
|
7
10
|
npx @coffre/cli setup # its database, keys and, on Workers, Cloudflare
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { r as isEmpty } from "./init-
|
|
1
|
+
import { r as isEmpty } from "./init-2xGelVG1.js";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
@@ -1285,7 +1285,10 @@ function install(dir) {
|
|
|
1285
1285
|
return (async () => {
|
|
1286
1286
|
const ran = await attempt("pnpm", ["install"]) ?? await attempt("corepack", ["pnpm", "install"]);
|
|
1287
1287
|
if (ran === null) throw new Error("pnpm is not installed: corepack enable, or npm install -g pnpm, then run setup again");
|
|
1288
|
-
if (ran.code !== 0)
|
|
1288
|
+
if (ran.code !== 0) {
|
|
1289
|
+
const held = heldBack(ran.output);
|
|
1290
|
+
throw held.length > 0 ? new HeldBack(held, installFailure(ran.output)) : new Error(installFailure(ran.output));
|
|
1291
|
+
}
|
|
1289
1292
|
})();
|
|
1290
1293
|
}
|
|
1291
1294
|
/** A deployment's `@coffre/*` pins, as its package.json has them. */
|
|
@@ -1306,6 +1309,90 @@ function bumpPins(dir, version) {
|
|
|
1306
1309
|
for (const field of ["dependencies", "devDependencies"]) for (const name of Object.keys(manifest[field] ?? {})) if (name.startsWith("@coffre/")) manifest[field][name] = version;
|
|
1307
1310
|
writeFileSync(path, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
1308
1311
|
}
|
|
1312
|
+
/** pnpm install refused packages younger than the deployment's minimumReleaseAge. */
|
|
1313
|
+
var HeldBack = class extends Error {
|
|
1314
|
+
held;
|
|
1315
|
+
constructor(held, message) {
|
|
1316
|
+
super(message);
|
|
1317
|
+
this.held = held;
|
|
1318
|
+
}
|
|
1319
|
+
};
|
|
1320
|
+
/** The packages pnpm held back, as its error names them. */
|
|
1321
|
+
function heldBack(output) {
|
|
1322
|
+
if (!output.includes("MINIMUM_RELEASE_AGE")) return [];
|
|
1323
|
+
return [...output.matchAll(/^\s*(\S+@\d\S*) was published at (\S+?),?\s/gm)].map((match) => ({
|
|
1324
|
+
spec: match[1],
|
|
1325
|
+
publishedAt: new Date(match[2])
|
|
1326
|
+
}));
|
|
1327
|
+
}
|
|
1328
|
+
/** The deployment's minimumReleaseAge, in minutes, as its pnpm-workspace.yaml sets it; pnpm's own default, none, without one. */
|
|
1329
|
+
function minimumReleaseAge(dir) {
|
|
1330
|
+
const path = join(dir, "pnpm-workspace.yaml");
|
|
1331
|
+
const match = existsSync(path) ? /^minimumReleaseAge:\s*(\d+)/m.exec(readFileSync(path, "utf8")) : null;
|
|
1332
|
+
return match === null ? 0 : Number(match[1]);
|
|
1333
|
+
}
|
|
1334
|
+
/** How every temporary exclusion `coffre update` writes ends: with when it lapses. */
|
|
1335
|
+
const EXCLUDED = /^\s*- '([^']+)' # coffre update: until (\S+)/;
|
|
1336
|
+
/**
|
|
1337
|
+
* Exclude `held` from minimumReleaseAge, each until it clears, named in the
|
|
1338
|
+
* deployment's pnpm-workspace.yaml with that date: never silently, and
|
|
1339
|
+
* removed by `removeCleared` once the date has passed.
|
|
1340
|
+
*/
|
|
1341
|
+
function excludeUntil(dir, held, reason) {
|
|
1342
|
+
const path = join(dir, "pnpm-workspace.yaml");
|
|
1343
|
+
const lines = (existsSync(path) ? readFileSync(path, "utf8") : "").replace(/\n*$/, "").split("\n");
|
|
1344
|
+
const entries = held.map(({ spec, clears }) => ` - '${spec}' # coffre update: until ${clears.toISOString()}, ${reason}`);
|
|
1345
|
+
const key = lines.findIndex((line) => /^minimumReleaseAgeExclude:/.test(line));
|
|
1346
|
+
if (key === -1) lines.push("minimumReleaseAgeExclude:", ...entries);
|
|
1347
|
+
else {
|
|
1348
|
+
let end = key + 1;
|
|
1349
|
+
while (end < lines.length && /^\s+(-|#)/.test(lines[end])) end++;
|
|
1350
|
+
lines.splice(end, 0, ...entries);
|
|
1351
|
+
}
|
|
1352
|
+
writeFileSync(path, `${lines.join("\n")}\n`);
|
|
1353
|
+
}
|
|
1354
|
+
/** Remove the exclusions `excludeUntil` wrote whose date has passed; the packages they named. */
|
|
1355
|
+
function removeCleared(dir, now) {
|
|
1356
|
+
const path = join(dir, "pnpm-workspace.yaml");
|
|
1357
|
+
if (!existsSync(path)) return [];
|
|
1358
|
+
const removed = [];
|
|
1359
|
+
const kept = readFileSync(path, "utf8").split("\n").filter((line) => {
|
|
1360
|
+
const match = EXCLUDED.exec(line);
|
|
1361
|
+
if (match === null || new Date(match[2].replace(/,$/, "")) > now) return true;
|
|
1362
|
+
removed.push(match[1]);
|
|
1363
|
+
return false;
|
|
1364
|
+
});
|
|
1365
|
+
if (removed.length > 0) writeFileSync(path, kept.join("\n"));
|
|
1366
|
+
return removed;
|
|
1367
|
+
}
|
|
1368
|
+
/**
|
|
1369
|
+
* The pnpm a deployment runs, `packageManager` in its package.json, set to
|
|
1370
|
+
* `wanted`, coffre's: then a laptop, CI and Workers Builds all install with
|
|
1371
|
+
* one pnpm, which holds minimumReleaseAge alike. The value it had, when it
|
|
1372
|
+
* changed; null when it was right already.
|
|
1373
|
+
*/
|
|
1374
|
+
function pinPackageManager(dir, wanted) {
|
|
1375
|
+
const path = join(dir, "package.json");
|
|
1376
|
+
const manifest = JSON.parse(readFileSync(path, "utf8"));
|
|
1377
|
+
if (manifest.packageManager === wanted) return null;
|
|
1378
|
+
const was = manifest.packageManager;
|
|
1379
|
+
let next;
|
|
1380
|
+
if (was !== void 0) next = {
|
|
1381
|
+
...manifest,
|
|
1382
|
+
packageManager: wanted
|
|
1383
|
+
};
|
|
1384
|
+
else {
|
|
1385
|
+
const after = "private" in manifest ? "private" : "name";
|
|
1386
|
+
next = {};
|
|
1387
|
+
for (const [field, value] of Object.entries(manifest)) {
|
|
1388
|
+
next[field] = value;
|
|
1389
|
+
if (field === after) next.packageManager = wanted;
|
|
1390
|
+
}
|
|
1391
|
+
next.packageManager ??= wanted;
|
|
1392
|
+
}
|
|
1393
|
+
writeFileSync(path, `${JSON.stringify(next, null, 2)}\n`);
|
|
1394
|
+
return was;
|
|
1395
|
+
}
|
|
1309
1396
|
/**
|
|
1310
1397
|
* Why pnpm install failed, in a sentence. The deployment's minimumReleaseAge
|
|
1311
1398
|
* holds back every package published within the week, coffre's own
|
|
@@ -1366,4 +1453,4 @@ function editWorker(dir, path, changes) {
|
|
|
1366
1453
|
return changed;
|
|
1367
1454
|
}
|
|
1368
1455
|
//#endregion
|
|
1369
|
-
export {
|
|
1456
|
+
export { editWorker as a, minimumReleaseAge as c, readWorker as d, removeCleared as f, deploymentKind as i, pinPackageManager as l, bumpPins as n, excludeUntil as o, coffrePins as r, install as s, HeldBack as t, placeholder as u };
|
package/dist/main.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { n as init, t as KINDS } from "./init-
|
|
2
|
+
import { n as init, t as KINDS } from "./init-2xGelVG1.js";
|
|
3
3
|
import { a as keys } from "./keys-DDdMwm0N.js";
|
|
4
4
|
import { a as openTerminal, c as release, d as style, r as hiddenLine, t as Cancelled, u as select } from "./tty-BDa14JX2.js";
|
|
5
5
|
import { i as isRole, r as assignableToEnvironment, t as ROLES } from "./access-CO1kGjWW.js";
|
|
6
6
|
import { inspect, parseArgs, promisify } from "node:util";
|
|
7
7
|
import { execFile, spawn } from "node:child_process";
|
|
8
|
-
import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
9
|
-
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";
|
|
10
10
|
import { dirname, join, resolve } from "node:path";
|
|
11
|
-
import { createCipheriv, createDecipheriv, createHash, hkdfSync, randomBytes, timingSafeEqual } from "node:crypto";
|
|
11
|
+
import { createCipheriv, createDecipheriv, createHash, hkdfSync, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
12
12
|
/**
|
|
13
13
|
* The line to show, or null: nothing pending, or nothing told. An instance
|
|
14
14
|
* older than this notice has no `instance` on `/me` at all.
|
|
@@ -40,6 +40,231 @@ async function dailyNotice(origin, now, checked, me) {
|
|
|
40
40
|
return noticeFor(origin, instance);
|
|
41
41
|
}
|
|
42
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
|
|
43
268
|
//#region ../client/dist/index.js
|
|
44
269
|
/** The API's error shape, `{ error, message }`, with the HTTP status. */
|
|
45
270
|
const COFFRE_ERROR = Symbol.for("@coffre/client:CoffreError");
|
|
@@ -1554,199 +1779,6 @@ async function pickCheck() {
|
|
|
1554
1779
|
}
|
|
1555
1780
|
}
|
|
1556
1781
|
//#endregion
|
|
1557
|
-
//#region ../core/dist/dotenv.js
|
|
1558
|
-
const KEY_RE = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/;
|
|
1559
|
-
function parseDotenv(input) {
|
|
1560
|
-
const entries = [];
|
|
1561
|
-
const problems = [];
|
|
1562
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1563
|
-
const lines = input.split(/\r\n|\n|\r/);
|
|
1564
|
-
for (let index = 0; index < lines.length; index++) {
|
|
1565
|
-
const lineNumber = index + 1;
|
|
1566
|
-
const trimmed = lines[index].trim();
|
|
1567
|
-
if (trimmed === "" || trimmed.startsWith("#")) continue;
|
|
1568
|
-
const withoutExport = trimmed.startsWith("export ") ? trimmed.slice(7).trim() : trimmed;
|
|
1569
|
-
const equals = withoutExport.indexOf("=");
|
|
1570
|
-
if (equals === -1) {
|
|
1571
|
-
problems.push({
|
|
1572
|
-
line: lineNumber,
|
|
1573
|
-
text: trimmed,
|
|
1574
|
-
reason: "no \"=\" on this line"
|
|
1575
|
-
});
|
|
1576
|
-
continue;
|
|
1577
|
-
}
|
|
1578
|
-
const key = withoutExport.slice(0, equals).trim();
|
|
1579
|
-
if (!KEY_RE.test(key)) {
|
|
1580
|
-
problems.push({
|
|
1581
|
-
line: lineNumber,
|
|
1582
|
-
text: key,
|
|
1583
|
-
reason: "key must match ^[A-Za-z_][A-Za-z0-9_]*$"
|
|
1584
|
-
});
|
|
1585
|
-
continue;
|
|
1586
|
-
}
|
|
1587
|
-
let rest = withoutExport.slice(equals + 1).trim();
|
|
1588
|
-
let value;
|
|
1589
|
-
if (rest.startsWith("\"")) {
|
|
1590
|
-
const closing = findClosingQuote(rest, "\"");
|
|
1591
|
-
if (closing === -1) {
|
|
1592
|
-
problems.push({
|
|
1593
|
-
line: lineNumber,
|
|
1594
|
-
text: key,
|
|
1595
|
-
reason: "unterminated double quote (multi-line values are not supported)"
|
|
1596
|
-
});
|
|
1597
|
-
continue;
|
|
1598
|
-
}
|
|
1599
|
-
value = unescapeDoubleQuoted(rest.slice(1, closing));
|
|
1600
|
-
if (trailingIsSignificant(rest.slice(closing + 1))) {
|
|
1601
|
-
problems.push({
|
|
1602
|
-
line: lineNumber,
|
|
1603
|
-
text: key,
|
|
1604
|
-
reason: "unexpected text after the closing quote"
|
|
1605
|
-
});
|
|
1606
|
-
continue;
|
|
1607
|
-
}
|
|
1608
|
-
} else if (rest.startsWith("'")) {
|
|
1609
|
-
const closing = rest.indexOf("'", 1);
|
|
1610
|
-
if (closing === -1) {
|
|
1611
|
-
problems.push({
|
|
1612
|
-
line: lineNumber,
|
|
1613
|
-
text: key,
|
|
1614
|
-
reason: "unterminated single quote (multi-line values are not supported)"
|
|
1615
|
-
});
|
|
1616
|
-
continue;
|
|
1617
|
-
}
|
|
1618
|
-
value = rest.slice(1, closing);
|
|
1619
|
-
if (trailingIsSignificant(rest.slice(closing + 1))) {
|
|
1620
|
-
problems.push({
|
|
1621
|
-
line: lineNumber,
|
|
1622
|
-
text: key,
|
|
1623
|
-
reason: "unexpected text after the closing quote"
|
|
1624
|
-
});
|
|
1625
|
-
continue;
|
|
1626
|
-
}
|
|
1627
|
-
} else {
|
|
1628
|
-
const comment = rest.indexOf(" #");
|
|
1629
|
-
if (comment !== -1) rest = rest.slice(0, comment);
|
|
1630
|
-
value = rest.trim();
|
|
1631
|
-
}
|
|
1632
|
-
if (value.includes("\0")) {
|
|
1633
|
-
problems.push({
|
|
1634
|
-
line: lineNumber,
|
|
1635
|
-
text: key,
|
|
1636
|
-
reason: "value contains a NUL byte, which cannot be passed in an environment"
|
|
1637
|
-
});
|
|
1638
|
-
continue;
|
|
1639
|
-
}
|
|
1640
|
-
if (seen.has(key)) {
|
|
1641
|
-
problems.push({
|
|
1642
|
-
line: lineNumber,
|
|
1643
|
-
text: key,
|
|
1644
|
-
reason: "duplicate key in this file"
|
|
1645
|
-
});
|
|
1646
|
-
continue;
|
|
1647
|
-
}
|
|
1648
|
-
seen.add(key);
|
|
1649
|
-
entries.push({
|
|
1650
|
-
key,
|
|
1651
|
-
value,
|
|
1652
|
-
line: lineNumber
|
|
1653
|
-
});
|
|
1654
|
-
}
|
|
1655
|
-
return {
|
|
1656
|
-
entries,
|
|
1657
|
-
problems
|
|
1658
|
-
};
|
|
1659
|
-
}
|
|
1660
|
-
/** True when what follows a closing quote is neither whitespace nor a comment. */
|
|
1661
|
-
function trailingIsSignificant(rest) {
|
|
1662
|
-
const trimmed = rest.trim();
|
|
1663
|
-
return trimmed !== "" && !trimmed.startsWith("#");
|
|
1664
|
-
}
|
|
1665
|
-
function findClosingQuote(text, quote) {
|
|
1666
|
-
for (let index = 1; index < text.length; index++) {
|
|
1667
|
-
if (text[index] === "\\") {
|
|
1668
|
-
index++;
|
|
1669
|
-
continue;
|
|
1670
|
-
}
|
|
1671
|
-
if (text[index] === quote) return index;
|
|
1672
|
-
}
|
|
1673
|
-
return -1;
|
|
1674
|
-
}
|
|
1675
|
-
/**
|
|
1676
|
-
* Expand escapes inside a double-quoted value.
|
|
1677
|
-
*
|
|
1678
|
-
* Deliberately minimal: only the escapes that are necessary. `\n` and `\r`
|
|
1679
|
-
* because multi-line values (PEM keys) cannot otherwise be expressed on one
|
|
1680
|
-
* line, and `\\` `\"` `\'` because quoting needs them.
|
|
1681
|
-
*
|
|
1682
|
-
* EVERYTHING ELSE IS PRESERVED VERBATIM, backslash included. The obvious
|
|
1683
|
-
* implementation -- returning the character and dropping the backslash for
|
|
1684
|
-
* unknown escapes -- silently mangles credentials: `"C:\path\to\file"` came
|
|
1685
|
-
* out as `C:path<TAB>ofile`, and `"\d+\w"` as `d+w`. A secrets store that
|
|
1686
|
-
* quietly rewrites what you gave it is worse than one that refuses.
|
|
1687
|
-
*
|
|
1688
|
-
* A value that should contain no interpretation at all belongs in single
|
|
1689
|
-
* quotes, which are literal.
|
|
1690
|
-
*/
|
|
1691
|
-
function unescapeDoubleQuoted(text) {
|
|
1692
|
-
return text.replace(/\\(.)/g, (match, character) => {
|
|
1693
|
-
switch (character) {
|
|
1694
|
-
case "n": return "\n";
|
|
1695
|
-
case "r": return "\r";
|
|
1696
|
-
case "\\":
|
|
1697
|
-
case "\"":
|
|
1698
|
-
case "'": return character;
|
|
1699
|
-
default: return match;
|
|
1700
|
-
}
|
|
1701
|
-
});
|
|
1702
|
-
}
|
|
1703
|
-
/** Values that read the same unquoted: no spaces, quotes, `#`, `$` or backslashes. */
|
|
1704
|
-
const PLAIN_VALUE = /^[A-Za-z0-9_./:@+,=%?-]*$/;
|
|
1705
|
-
/**
|
|
1706
|
-
* Write entries as a .env file that `parseDotenv` reads back exactly.
|
|
1707
|
-
*
|
|
1708
|
-
* Plain values stay bare; anything else is single-quoted, which is literal;
|
|
1709
|
-
* values holding a quote or a line break are double-quoted with the four
|
|
1710
|
-
* escapes the parser knows (`\\`, `\"`, `\n`, `\r`).
|
|
1711
|
-
*/
|
|
1712
|
-
function formatDotenv(entries) {
|
|
1713
|
-
let out = "";
|
|
1714
|
-
for (const [key, value] of entries) {
|
|
1715
|
-
assertWritable(key, value);
|
|
1716
|
-
out += `${key}=${quoteDotenv(value)}\n`;
|
|
1717
|
-
}
|
|
1718
|
-
return out;
|
|
1719
|
-
}
|
|
1720
|
-
function assertWritable(key, value) {
|
|
1721
|
-
if (!KEY_RE.test(key)) throw new Error(`${JSON.stringify(key)} is not a valid variable name`);
|
|
1722
|
-
if (value.includes("\0")) throw new Error(`${key} contains a NUL byte`);
|
|
1723
|
-
}
|
|
1724
|
-
function quoteDotenv(value) {
|
|
1725
|
-
if (PLAIN_VALUE.test(value)) return value;
|
|
1726
|
-
if (!/['\n\r]/.test(value)) return `'${value}'`;
|
|
1727
|
-
return `"${value.replace(/[\\"\n\r]/g, (character) => {
|
|
1728
|
-
switch (character) {
|
|
1729
|
-
case "\n": return "\\n";
|
|
1730
|
-
case "\r": return "\\r";
|
|
1731
|
-
default: return `\\${character}`;
|
|
1732
|
-
}
|
|
1733
|
-
})}"`;
|
|
1734
|
-
}
|
|
1735
|
-
/**
|
|
1736
|
-
* Write entries as POSIX shell `export` statements, for
|
|
1737
|
-
* `eval "$(coffre export … --format shell)"`. Single quotes are the only
|
|
1738
|
-
* shell quoting with no expansion inside; a single quote itself is written
|
|
1739
|
-
* as `'\''`.
|
|
1740
|
-
*/
|
|
1741
|
-
function formatShellExports(entries) {
|
|
1742
|
-
let out = "";
|
|
1743
|
-
for (const [key, value] of entries) {
|
|
1744
|
-
assertWritable(key, value);
|
|
1745
|
-
out += `export ${key}='${value.replaceAll("'", "'\\''")}'\n`;
|
|
1746
|
-
}
|
|
1747
|
-
return out;
|
|
1748
|
-
}
|
|
1749
|
-
//#endregion
|
|
1750
1782
|
//#region src/main.ts
|
|
1751
1783
|
/**
|
|
1752
1784
|
* coffre CLI.
|
|
@@ -2192,14 +2224,22 @@ async function exportEnv(args) {
|
|
|
2192
2224
|
} },
|
|
2193
2225
|
allowPositionals: true
|
|
2194
2226
|
});
|
|
2195
|
-
const usage = "usage: coffre export <project>/<environment> [--format dotenv|json|shell]";
|
|
2227
|
+
const usage = "usage: coffre export <project>/<environment> [--format dotenv|json|shell|github]";
|
|
2196
2228
|
if (!positionals[0]) fail(usage);
|
|
2197
2229
|
const format = values.format;
|
|
2198
|
-
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)");
|
|
2199
2233
|
const { project, environment } = parsePath(positionals[0]);
|
|
2200
2234
|
const revealed = await client().secrets.reveal(`${project}/${environment}`);
|
|
2201
2235
|
const entries = Object.entries(revealed.values).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
2202
|
-
|
|
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)));
|
|
2203
2243
|
if (process.stderr.isTTY) {
|
|
2204
2244
|
const count = entries.length;
|
|
2205
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`);
|
|
@@ -2527,7 +2567,7 @@ const USAGE = `coffre - secrets, with an audit log
|
|
|
2527
2567
|
coffre get <project>/<environment>/<KEY>
|
|
2528
2568
|
coffre set <project>/<environment>/<KEY> [value] (reads stdin if omitted)
|
|
2529
2569
|
coffre run <project>/<environment> -- <command>
|
|
2530
|
-
coffre export <project>/<environment> [--format dotenv|json|shell]
|
|
2570
|
+
coffre export <project>/<environment> [--format dotenv|json|shell|github]
|
|
2531
2571
|
coffre history <project>/<environment>/<KEY>
|
|
2532
2572
|
coffre rollback <project>/<environment>/<KEY> <version>
|
|
2533
2573
|
coffre import <project>/<environment> [--file .env] [--apply]
|
|
@@ -2566,10 +2606,10 @@ switch (command) {
|
|
|
2566
2606
|
await keys(rest);
|
|
2567
2607
|
break;
|
|
2568
2608
|
case "setup":
|
|
2569
|
-
await (await import("./setup-
|
|
2609
|
+
await (await import("./setup-Bs8q8Ily.js")).setup(rest);
|
|
2570
2610
|
break;
|
|
2571
2611
|
case "update":
|
|
2572
|
-
await (await import("./update-
|
|
2612
|
+
await (await import("./update-eYzLTNoL.js")).update(rest);
|
|
2573
2613
|
break;
|
|
2574
2614
|
case "migrate":
|
|
2575
2615
|
await (await import("./migrate-ERNNgYO0.js")).migrate(rest, migrateTarget);
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { d as esm_default } from "./esm-Bts-esAl.js";
|
|
2
|
-
import { n as init, t as KINDS } from "./init-
|
|
2
|
+
import { n as init, t as KINDS } from "./init-2xGelVG1.js";
|
|
3
3
|
import { i as keyValues, n as jsonWarning, o as needsTerminal, r as keyGuide, s as showSecrets, t as generateKeys } from "./keys-DDdMwm0N.js";
|
|
4
4
|
import { a as openTerminal, c as release, d as style, f as textLine, i as listed, l as row, o as paragraph, t as Cancelled, u as select } from "./tty-BDa14JX2.js";
|
|
5
5
|
import { r as postgresConnection } from "./connect-C4wevMJ5.js";
|
|
6
6
|
import { n as migrateDatabase, t as readDatabaseUrl } from "./database-url-CJ9yqoi_.js";
|
|
7
7
|
import { n as StepFailed, r as Steps, t as cliVersion } from "./version-CPk6zTTK.js";
|
|
8
|
-
import { a as
|
|
8
|
+
import { a as editWorker, d as readWorker, i as deploymentKind, s as install, u as placeholder } from "./deployment-Di8BG9qY.js";
|
|
9
9
|
import { parseArgs } from "node:util";
|
|
10
10
|
import { spawn } from "node:child_process";
|
|
11
11
|
import { existsSync } from "node:fs";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "coffre-node",
|
|
3
3
|
"private": true,
|
|
4
|
+
"packageManager": "pnpm@11.8.0",
|
|
4
5
|
"type": "module",
|
|
5
6
|
"engines": {
|
|
6
7
|
"node": ">=24"
|
|
@@ -13,11 +14,11 @@
|
|
|
13
14
|
"conformance": "coffre-conformance node"
|
|
14
15
|
},
|
|
15
16
|
"dependencies": {
|
|
16
|
-
"@coffre/server": "0.1.
|
|
17
|
-
"@coffre/vault": "0.1.
|
|
17
|
+
"@coffre/server": "0.1.15",
|
|
18
|
+
"@coffre/vault": "0.1.15"
|
|
18
19
|
},
|
|
19
20
|
"devDependencies": {
|
|
20
|
-
"@coffre/conformance": "0.1.
|
|
21
|
+
"@coffre/conformance": "0.1.15",
|
|
21
22
|
"@types/node": "26.1.1",
|
|
22
23
|
"typescript": "5.9.3"
|
|
23
24
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "coffre-workers",
|
|
3
3
|
"private": true,
|
|
4
|
+
"packageManager": "pnpm@11.8.0",
|
|
4
5
|
"type": "module",
|
|
5
6
|
"engines": {
|
|
6
7
|
"node": ">=24"
|
|
@@ -14,13 +15,13 @@
|
|
|
14
15
|
"conformance": "coffre-conformance workers"
|
|
15
16
|
},
|
|
16
17
|
"dependencies": {
|
|
17
|
-
"@coffre/server": "0.1.
|
|
18
|
-
"@coffre/ui": "0.1.
|
|
19
|
-
"@coffre/vault": "0.1.
|
|
18
|
+
"@coffre/server": "0.1.15",
|
|
19
|
+
"@coffre/ui": "0.1.15",
|
|
20
|
+
"@coffre/vault": "0.1.15"
|
|
20
21
|
},
|
|
21
22
|
"devDependencies": {
|
|
22
23
|
"@cloudflare/workers-types": "5.20260920.1",
|
|
23
|
-
"@coffre/conformance": "0.1.
|
|
24
|
+
"@coffre/conformance": "0.1.15",
|
|
24
25
|
"typescript": "5.9.3",
|
|
25
26
|
"wrangler": "4.118.0"
|
|
26
27
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { i as templateDir } from "./init-2xGelVG1.js";
|
|
1
2
|
import { a as openTerminal, c as release, d as style, i as listed, t as Cancelled } from "./tty-BDa14JX2.js";
|
|
2
3
|
import { n as StepFailed, r as Steps, t as cliVersion } from "./version-CPk6zTTK.js";
|
|
3
|
-
import {
|
|
4
|
+
import { c as minimumReleaseAge, f as removeCleared, i as deploymentKind, l as pinPackageManager, n as bumpPins, o as excludeUntil, r as coffrePins, s as install, t as HeldBack } from "./deployment-Di8BG9qY.js";
|
|
4
5
|
import { createRequire } from "node:module";
|
|
5
6
|
import { parseArgs } from "node:util";
|
|
6
7
|
import { spawn, spawnSync } from "node:child_process";
|
|
7
|
-
import { readFileSync, realpathSync } from "node:fs";
|
|
8
|
+
import { readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
8
9
|
import { dirname, join, sep } from "node:path";
|
|
9
10
|
import { fileURLToPath } from "node:url";
|
|
10
11
|
/**
|
|
@@ -221,20 +222,71 @@ async function update(args) {
|
|
|
221
222
|
}
|
|
222
223
|
});
|
|
223
224
|
if (deployment !== null) await steps.run(2, async (step) => {
|
|
224
|
-
|
|
225
|
+
const details = [];
|
|
226
|
+
const cleared = removeCleared(deployment, /* @__PURE__ */ new Date());
|
|
227
|
+
if (cleared.length > 0) details.push(`No longer excluded from minimumReleaseAge, now old enough: ${listed(cleared, "and")}`);
|
|
228
|
+
const pnpm = templatePackageManager(kind);
|
|
229
|
+
const repin = pnpm !== null && coffrePackageManager(deployment) !== pnpm;
|
|
230
|
+
const current = pinned.length === 1 && pinned[0] === latest;
|
|
231
|
+
if (current && !repin) return {
|
|
232
|
+
text: `This deployment's coffre packages are at ${latest} already`,
|
|
233
|
+
details
|
|
234
|
+
};
|
|
225
235
|
const was = pinned.length === 1 ? pinned[0] : listed(pinned, "and");
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
236
|
+
const on = pnpm?.replace("@", " ");
|
|
237
|
+
const question = current ? `Pin this deployment to ${on}, as coffre installs with, and install it?` : `Move this deployment from ${was} to ${latest}${repin ? `, on ${on},` : ""} and install it?`;
|
|
238
|
+
if (!await ask(question, step)) return {
|
|
239
|
+
text: `This deployment stays as it is, at ${was}`,
|
|
240
|
+
details
|
|
229
241
|
};
|
|
230
242
|
before = deploymentMigrations(deployment) ?? before;
|
|
243
|
+
const manifest = join(deployment, "package.json");
|
|
244
|
+
const saved = readFileSync(manifest, "utf8");
|
|
231
245
|
bumpPins(deployment, latest);
|
|
246
|
+
if (repin) {
|
|
247
|
+
const replaced = pinPackageManager(deployment, pnpm);
|
|
248
|
+
details.push(`Pinned ${pnpm}${replaced ? `, not ${replaced}` : ""}: every install, here, in CI and on Workers Builds, holds minimumReleaseAge alike`);
|
|
249
|
+
}
|
|
232
250
|
step.note(`Installing coffre ${latest}'s packages, with pnpm`);
|
|
233
|
-
|
|
251
|
+
try {
|
|
252
|
+
await install(deployment);
|
|
253
|
+
} catch (error) {
|
|
254
|
+
if (!(error instanceof HeldBack)) throw error;
|
|
255
|
+
const age = minimumReleaseAge(deployment);
|
|
256
|
+
const clears = error.held.map(({ spec, publishedAt }) => ({
|
|
257
|
+
spec,
|
|
258
|
+
clears: new Date(publishedAt.getTime() + age * 6e4)
|
|
259
|
+
})).sort((x, y) => x.clears.getTime() - y.clears.getTime());
|
|
260
|
+
const last = clears.at(-1).clears;
|
|
261
|
+
const lines = clears.map(({ spec, clears: at }) => `${spec}, old enough from ${when(at)}`);
|
|
262
|
+
const held = `pnpm holds back ${clears.length === 1 ? "1 package" : `${clears.length} packages`} younger than this deployment's minimumReleaseAge, ${days(age)}`;
|
|
263
|
+
if (yes || keys() === null) {
|
|
264
|
+
writeFileSync(manifest, saved);
|
|
265
|
+
throw new Error(`${held}: ${lines.join("; ")}. Run coffre update after ${when(last)}, or on a terminal to exclude them until then`);
|
|
266
|
+
}
|
|
267
|
+
step.under(lines);
|
|
268
|
+
const choice = await step.choose(held, [`Wait: stay at ${was}, and run coffre update again after ${when(last)}`, `Proceed: let ${clears.length === 1 ? "it" : "these"} through until each is old enough, named in pnpm-workspace.yaml`]);
|
|
269
|
+
step.under([]);
|
|
270
|
+
if (choice === 0) {
|
|
271
|
+
writeFileSync(manifest, saved);
|
|
272
|
+
return {
|
|
273
|
+
text: `This deployment stays at ${was}, until ${when(last)}`,
|
|
274
|
+
details: [
|
|
275
|
+
...details,
|
|
276
|
+
...lines,
|
|
277
|
+
`Run coffre update again after ${when(last)}`
|
|
278
|
+
]
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
excludeUntil(deployment, clears, `for coffre ${latest}`);
|
|
282
|
+
step.note(`Installing coffre ${latest}'s packages, with pnpm`);
|
|
283
|
+
await install(deployment);
|
|
284
|
+
details.push("Let through until each is old enough, named in pnpm-workspace.yaml:", ...clears.map(({ spec, clears: at }) => ` ${spec}, until ${when(at)}`), `The first coffre update after ${when(last)} removes them`);
|
|
285
|
+
}
|
|
234
286
|
after = deploymentMigrations(deployment) ?? after;
|
|
235
287
|
return {
|
|
236
|
-
text: `Moved this deployment from ${was} to ${latest}, and installed it`,
|
|
237
|
-
details
|
|
288
|
+
text: current ? `Pinned ${pnpm}, and installed it` : `Moved this deployment from ${was} to ${latest}, and installed it`,
|
|
289
|
+
details
|
|
238
290
|
};
|
|
239
291
|
});
|
|
240
292
|
} catch (error) {
|
|
@@ -255,6 +307,25 @@ async function update(args) {
|
|
|
255
307
|
out.write("\n");
|
|
256
308
|
}
|
|
257
309
|
}
|
|
310
|
+
/** The pnpm coffre installs with, as the template of a deployment of `kind` pins it. */
|
|
311
|
+
function templatePackageManager(kind) {
|
|
312
|
+
try {
|
|
313
|
+
return JSON.parse(readFileSync(join(templateDir(kind), "package.json"), "utf8")).packageManager ?? null;
|
|
314
|
+
} catch {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
function coffrePackageManager(dir) {
|
|
319
|
+
return JSON.parse(readFileSync(join(dir, "package.json"), "utf8")).packageManager;
|
|
320
|
+
}
|
|
321
|
+
/** A moment, to the minute, in UTC: when a package is old enough. */
|
|
322
|
+
function when(date) {
|
|
323
|
+
return `${date.toISOString().slice(0, 16).replace("T", " ")} UTC`;
|
|
324
|
+
}
|
|
325
|
+
function days(minutes) {
|
|
326
|
+
const d = minutes / 1440;
|
|
327
|
+
return Number.isInteger(d) ? `${d} day${d === 1 ? "" : "s"}` : `${minutes} minutes`;
|
|
328
|
+
}
|
|
258
329
|
function finish(out, message, code) {
|
|
259
330
|
if (message !== "") out.write(`${message}\n`);
|
|
260
331
|
process.exit(code);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coffre/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
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": {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
},
|
|
11
11
|
"type": "module",
|
|
12
12
|
"engines": {
|
|
13
|
-
"node": ">=
|
|
13
|
+
"node": ">=20"
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
16
16
|
"dist"
|
|
@@ -28,9 +28,9 @@
|
|
|
28
28
|
"pg": "8.16.3",
|
|
29
29
|
"tsdown": "0.23.0",
|
|
30
30
|
"typescript": "5.9.3",
|
|
31
|
-
"@coffre/
|
|
32
|
-
"@coffre/db": "0.1.
|
|
33
|
-
"@coffre/
|
|
31
|
+
"@coffre/core": "0.1.15",
|
|
32
|
+
"@coffre/db": "0.1.15",
|
|
33
|
+
"@coffre/client": "0.1.15"
|
|
34
34
|
},
|
|
35
35
|
"scripts": {
|
|
36
36
|
"build": "tsdown && node scripts/copy-templates.ts && node scripts/copy-migrations.ts",
|