@freecodecamp/universe-cli 0.1.1 → 0.2.0
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/index.cjs +35783 -40235
- package/dist/index.js +249 -324
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,172 +1,90 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
var __defProp = Object.defineProperty;
|
|
3
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
-
var __esm = (fn, res) => function __init() {
|
|
5
|
-
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
-
};
|
|
7
|
-
var __export = (target, all) => {
|
|
8
|
-
for (var name in all)
|
|
9
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
// src/output/envelope.ts
|
|
13
|
-
function buildEnvelope(command, success, data) {
|
|
14
|
-
return {
|
|
15
|
-
schemaVersion: "1",
|
|
16
|
-
command,
|
|
17
|
-
success,
|
|
18
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19
|
-
...data
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
function buildErrorEnvelope(command, code, message, issues) {
|
|
23
|
-
const error = {
|
|
24
|
-
code,
|
|
25
|
-
message
|
|
26
|
-
};
|
|
27
|
-
if (issues !== void 0) {
|
|
28
|
-
error.issues = issues;
|
|
29
|
-
}
|
|
30
|
-
return {
|
|
31
|
-
schemaVersion: "1",
|
|
32
|
-
command,
|
|
33
|
-
success: false,
|
|
34
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
35
|
-
error
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
var init_envelope = __esm({
|
|
39
|
-
"src/output/envelope.ts"() {
|
|
40
|
-
"use strict";
|
|
41
|
-
}
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
// src/output/redact.ts
|
|
45
|
-
function maskAkia(match) {
|
|
46
|
-
return match.slice(0, 4) + "****" + match.slice(-4);
|
|
47
|
-
}
|
|
48
|
-
function maskUrlCreds(match) {
|
|
49
|
-
const atIndex = match.lastIndexOf("@");
|
|
50
|
-
const protocolEnd = match.indexOf("://") + 3;
|
|
51
|
-
return match.slice(0, protocolEnd) + "****:****@" + match.slice(atIndex + 1);
|
|
52
|
-
}
|
|
53
|
-
function redact(value) {
|
|
54
|
-
let result = value;
|
|
55
|
-
result = result.replace(URL_CREDS_PATTERN, maskUrlCreds);
|
|
56
|
-
result = result.replace(AKIA_PATTERN, maskAkia);
|
|
57
|
-
result = result.replace(
|
|
58
|
-
CREDENTIAL_CONTEXT_PATTERN,
|
|
59
|
-
(_match, _secret, _offset, _full) => {
|
|
60
|
-
const eqIndex = _match.indexOf("=");
|
|
61
|
-
const colonIndex = _match.indexOf(":");
|
|
62
|
-
const sepIndex = eqIndex >= 0 && colonIndex >= 0 ? Math.min(eqIndex, colonIndex) : eqIndex >= 0 ? eqIndex : colonIndex;
|
|
63
|
-
const prefix = _match.slice(0, sepIndex + 1);
|
|
64
|
-
return prefix + "****";
|
|
65
|
-
}
|
|
66
|
-
);
|
|
67
|
-
result = result.replace(HEX_CREDENTIAL_CONTEXT_PATTERN, (_match, _secret) => {
|
|
68
|
-
const eqIndex = _match.indexOf("=");
|
|
69
|
-
const colonIndex = _match.indexOf(":");
|
|
70
|
-
const sepIndex = eqIndex >= 0 && colonIndex >= 0 ? Math.min(eqIndex, colonIndex) : eqIndex >= 0 ? eqIndex : colonIndex;
|
|
71
|
-
const prefix = _match.slice(0, sepIndex + 1);
|
|
72
|
-
return prefix + "****";
|
|
73
|
-
});
|
|
74
|
-
return result;
|
|
75
|
-
}
|
|
76
|
-
var AKIA_PATTERN, URL_CREDS_PATTERN, CREDENTIAL_CONTEXT_PATTERN, HEX_CREDENTIAL_CONTEXT_PATTERN;
|
|
77
|
-
var init_redact = __esm({
|
|
78
|
-
"src/output/redact.ts"() {
|
|
79
|
-
"use strict";
|
|
80
|
-
AKIA_PATTERN = /AKIA[A-Z0-9]{12,}/g;
|
|
81
|
-
URL_CREDS_PATTERN = /https?:\/\/[^@\s]+@/g;
|
|
82
|
-
CREDENTIAL_CONTEXT_PATTERN = /(?:secret|password|token|key|credential|auth)[=:]\s*([A-Za-z0-9/+=]{21,})/gi;
|
|
83
|
-
HEX_CREDENTIAL_CONTEXT_PATTERN = /(?:secret|password|token|key|credential|auth|access_key_id|secret_access_key)[=:]\s*([a-f0-9]{32,})/gi;
|
|
84
|
-
}
|
|
85
|
-
});
|
|
86
2
|
|
|
87
|
-
// src/
|
|
88
|
-
import {
|
|
89
|
-
function outputSuccess(ctx, humanMessage, data) {
|
|
90
|
-
if (ctx.json) {
|
|
91
|
-
const envelope = buildEnvelope(ctx.command, true, data);
|
|
92
|
-
process.stdout.write(JSON.stringify(envelope) + "\n");
|
|
93
|
-
} else {
|
|
94
|
-
log.success(humanMessage);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
function outputError(ctx, code, message, issues) {
|
|
98
|
-
const redactedMessage = redact(message);
|
|
99
|
-
const redactedIssues = issues?.map(redact);
|
|
100
|
-
if (ctx.json) {
|
|
101
|
-
const envelope = buildErrorEnvelope(
|
|
102
|
-
ctx.command,
|
|
103
|
-
code,
|
|
104
|
-
redactedMessage,
|
|
105
|
-
redactedIssues
|
|
106
|
-
);
|
|
107
|
-
process.stdout.write(JSON.stringify(envelope) + "\n");
|
|
108
|
-
} else {
|
|
109
|
-
log.error(redactedMessage);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
var init_format = __esm({
|
|
113
|
-
"src/output/format.ts"() {
|
|
114
|
-
"use strict";
|
|
115
|
-
init_envelope();
|
|
116
|
-
init_redact();
|
|
117
|
-
}
|
|
118
|
-
});
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { cac } from "cac";
|
|
119
5
|
|
|
120
6
|
// src/output/exit-codes.ts
|
|
7
|
+
var EXIT_USAGE = 10;
|
|
8
|
+
var EXIT_CONFIG = 11;
|
|
9
|
+
var EXIT_CREDENTIAL = 12;
|
|
10
|
+
var EXIT_STORAGE = 13;
|
|
11
|
+
var EXIT_OUTPUT_DIR = 14;
|
|
12
|
+
var EXIT_GIT = 15;
|
|
13
|
+
var EXIT_ALIAS = 16;
|
|
14
|
+
var EXIT_DEPLOY_NOT_FOUND = 17;
|
|
15
|
+
var EXIT_CONFIRM = 18;
|
|
16
|
+
var EXIT_PARTIAL = 19;
|
|
121
17
|
function exitWithCode(code, message) {
|
|
122
18
|
if (message !== void 0) {
|
|
123
19
|
process.stderr.write(message + "\n");
|
|
124
20
|
}
|
|
125
21
|
process.exit(code);
|
|
126
22
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
EXIT_GIT = 15;
|
|
134
|
-
EXIT_ALIAS = 16;
|
|
135
|
-
EXIT_DEPLOY_NOT_FOUND = 17;
|
|
136
|
-
EXIT_CONFIRM = 18;
|
|
137
|
-
EXIT_PARTIAL = 19;
|
|
23
|
+
|
|
24
|
+
// src/errors.ts
|
|
25
|
+
var CliError = class extends Error {
|
|
26
|
+
constructor(message) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.name = new.target.name;
|
|
138
29
|
}
|
|
139
|
-
}
|
|
30
|
+
};
|
|
31
|
+
var ConfigError = class extends CliError {
|
|
32
|
+
exitCode = EXIT_CONFIG;
|
|
33
|
+
};
|
|
34
|
+
var CredentialError = class extends CliError {
|
|
35
|
+
exitCode = EXIT_CREDENTIAL;
|
|
36
|
+
};
|
|
37
|
+
var StorageError = class extends CliError {
|
|
38
|
+
exitCode = EXIT_STORAGE;
|
|
39
|
+
};
|
|
40
|
+
var GitError = class extends CliError {
|
|
41
|
+
exitCode = EXIT_GIT;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// src/config/loader.ts
|
|
45
|
+
import { readFileSync } from "fs";
|
|
46
|
+
import { isAbsolute, relative, resolve, sep } from "path";
|
|
47
|
+
import { parse as parseYaml } from "yaml";
|
|
140
48
|
|
|
141
49
|
// src/config/schema.ts
|
|
142
50
|
import { z } from "zod";
|
|
143
|
-
var staticSchema
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
name: z.string().min(1).regex(/^[a-z0-9]([a-z0-9._-]*[a-z0-9])?$/i),
|
|
159
|
-
stack: z.literal("static"),
|
|
160
|
-
domain: domainSchema,
|
|
161
|
-
static: staticSchema
|
|
162
|
-
});
|
|
163
|
-
}
|
|
51
|
+
var staticSchema = z.object({
|
|
52
|
+
output_dir: z.string().min(1).default("dist"),
|
|
53
|
+
bucket: z.string().min(1).default("gxy-static-1"),
|
|
54
|
+
rclone_remote: z.string().min(1).default("gxy-static"),
|
|
55
|
+
region: z.string().min(1).default("auto")
|
|
56
|
+
}).default({});
|
|
57
|
+
var domainSchema = z.object({
|
|
58
|
+
production: z.string().min(1),
|
|
59
|
+
preview: z.string().min(1)
|
|
60
|
+
});
|
|
61
|
+
var platformSchema = z.object({
|
|
62
|
+
name: z.string().min(1).regex(/^[a-z0-9]([a-z0-9._-]*[a-z0-9])?$/i),
|
|
63
|
+
stack: z.literal("static"),
|
|
64
|
+
domain: domainSchema,
|
|
65
|
+
static: staticSchema
|
|
164
66
|
});
|
|
165
67
|
|
|
166
68
|
// src/config/loader.ts
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
69
|
+
function assertSafeOutputDir(outputDir, cwd) {
|
|
70
|
+
if (isAbsolute(outputDir)) {
|
|
71
|
+
throw new ConfigError(
|
|
72
|
+
`output_dir must be relative to the project root; absolute paths are rejected: ${outputDir}`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
const resolved = resolve(cwd, outputDir);
|
|
76
|
+
const rel = relative(cwd, resolved);
|
|
77
|
+
if (rel === "..") {
|
|
78
|
+
throw new ConfigError(
|
|
79
|
+
`output_dir resolves outside the project root: ${outputDir}`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
if (rel.startsWith(`..${sep}`)) {
|
|
83
|
+
throw new ConfigError(
|
|
84
|
+
`output_dir resolves outside the project root: ${outputDir}`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
170
88
|
function readEnvOverrides() {
|
|
171
89
|
const overrides = {};
|
|
172
90
|
if (process.env.UNIVERSE_STATIC_OUTPUT_DIR) {
|
|
@@ -200,7 +118,7 @@ function loadConfig(options = {}) {
|
|
|
200
118
|
raw = readFileSync(configPath, "utf-8");
|
|
201
119
|
} catch (err) {
|
|
202
120
|
if (err instanceof Error && err.code === "ENOENT") {
|
|
203
|
-
throw new
|
|
121
|
+
throw new ConfigError(
|
|
204
122
|
`platform.yaml not found at ${configPath}. See STAFF-GUIDE.md for the required format.`
|
|
205
123
|
);
|
|
206
124
|
}
|
|
@@ -218,17 +136,51 @@ function loadConfig(options = {}) {
|
|
|
218
136
|
...flagOverrides
|
|
219
137
|
}
|
|
220
138
|
};
|
|
139
|
+
assertSafeOutputDir(merged.static.output_dir, cwd);
|
|
221
140
|
return merged;
|
|
222
141
|
}
|
|
223
|
-
var init_loader = __esm({
|
|
224
|
-
"src/config/loader.ts"() {
|
|
225
|
-
"use strict";
|
|
226
|
-
init_schema();
|
|
227
|
-
}
|
|
228
|
-
});
|
|
229
142
|
|
|
230
143
|
// src/credentials/resolver.ts
|
|
231
144
|
import { execSync } from "child_process";
|
|
145
|
+
|
|
146
|
+
// src/output/redact.ts
|
|
147
|
+
var AKIA_PATTERN = /AKIA[A-Z0-9]{12,}/g;
|
|
148
|
+
var URL_CREDS_PATTERN = /https?:\/\/[^@\s]+@/g;
|
|
149
|
+
var CREDENTIAL_CONTEXT_PATTERN = /(?:secret|password|token|key|credential|auth)[=:]\s*([A-Za-z0-9/+=]{21,})/gi;
|
|
150
|
+
var HEX_CREDENTIAL_CONTEXT_PATTERN = /(?:secret|password|token|key|credential|auth|access_key_id|secret_access_key)[=:]\s*([a-f0-9]{32,})/gi;
|
|
151
|
+
function maskAkia(match) {
|
|
152
|
+
return match.slice(0, 4) + "****" + match.slice(-4);
|
|
153
|
+
}
|
|
154
|
+
function maskUrlCreds(match) {
|
|
155
|
+
const atIndex = match.lastIndexOf("@");
|
|
156
|
+
const protocolEnd = match.indexOf("://") + 3;
|
|
157
|
+
return match.slice(0, protocolEnd) + "****:****@" + match.slice(atIndex + 1);
|
|
158
|
+
}
|
|
159
|
+
function redact(value) {
|
|
160
|
+
let result = value;
|
|
161
|
+
result = result.replace(URL_CREDS_PATTERN, maskUrlCreds);
|
|
162
|
+
result = result.replace(AKIA_PATTERN, maskAkia);
|
|
163
|
+
result = result.replace(
|
|
164
|
+
CREDENTIAL_CONTEXT_PATTERN,
|
|
165
|
+
(_match, _secret, _offset, _full) => {
|
|
166
|
+
const eqIndex = _match.indexOf("=");
|
|
167
|
+
const colonIndex = _match.indexOf(":");
|
|
168
|
+
const sepIndex = eqIndex >= 0 && colonIndex >= 0 ? Math.min(eqIndex, colonIndex) : eqIndex >= 0 ? eqIndex : colonIndex;
|
|
169
|
+
const prefix = _match.slice(0, sepIndex + 1);
|
|
170
|
+
return prefix + "****";
|
|
171
|
+
}
|
|
172
|
+
);
|
|
173
|
+
result = result.replace(HEX_CREDENTIAL_CONTEXT_PATTERN, (_match, _secret) => {
|
|
174
|
+
const eqIndex = _match.indexOf("=");
|
|
175
|
+
const colonIndex = _match.indexOf(":");
|
|
176
|
+
const sepIndex = eqIndex >= 0 && colonIndex >= 0 ? Math.min(eqIndex, colonIndex) : eqIndex >= 0 ? eqIndex : colonIndex;
|
|
177
|
+
const prefix = _match.slice(0, sepIndex + 1);
|
|
178
|
+
return prefix + "****";
|
|
179
|
+
});
|
|
180
|
+
return result;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// src/credentials/resolver.ts
|
|
232
184
|
function tryEnvCredentials() {
|
|
233
185
|
const key = process.env.S3_ACCESS_KEY_ID;
|
|
234
186
|
const secret = process.env.S3_SECRET_ACCESS_KEY;
|
|
@@ -251,11 +203,11 @@ function fromRclone(remoteName) {
|
|
|
251
203
|
output = execSync("rclone config dump", { stdio: "pipe" });
|
|
252
204
|
} catch (err) {
|
|
253
205
|
if (err instanceof Error && err.code === "ENOENT") {
|
|
254
|
-
throw new
|
|
206
|
+
throw new CredentialError(
|
|
255
207
|
"rclone not found \u2014 install rclone or provide S3 credentials via environment variables"
|
|
256
208
|
);
|
|
257
209
|
}
|
|
258
|
-
throw new
|
|
210
|
+
throw new CredentialError(
|
|
259
211
|
`Failed to run rclone config dump: ${redact(err instanceof Error ? err.message : "unknown error")}`
|
|
260
212
|
);
|
|
261
213
|
}
|
|
@@ -263,12 +215,14 @@ function fromRclone(remoteName) {
|
|
|
263
215
|
try {
|
|
264
216
|
parsed = JSON.parse(output.toString("utf-8"));
|
|
265
217
|
} catch {
|
|
266
|
-
throw new
|
|
218
|
+
throw new CredentialError(
|
|
219
|
+
`Failed to parse rclone config for remote ${remoteName}`
|
|
220
|
+
);
|
|
267
221
|
}
|
|
268
222
|
output = null;
|
|
269
223
|
const remote = parsed[remoteName];
|
|
270
224
|
if (!remote) {
|
|
271
|
-
throw new
|
|
225
|
+
throw new CredentialError(
|
|
272
226
|
`Remote "${remoteName}" not found in rclone config. Available remotes: ${Object.keys(parsed).join(", ") || "(none)"}`
|
|
273
227
|
);
|
|
274
228
|
}
|
|
@@ -277,7 +231,7 @@ function fromRclone(remoteName) {
|
|
|
277
231
|
if (!remote.secret_access_key) missing.push("secret_access_key");
|
|
278
232
|
if (!remote.endpoint) missing.push("endpoint");
|
|
279
233
|
if (missing.length > 0) {
|
|
280
|
-
throw new
|
|
234
|
+
throw new CredentialError(
|
|
281
235
|
`Rclone remote "${remoteName}" is missing required fields: ${missing.join(", ")}`
|
|
282
236
|
);
|
|
283
237
|
}
|
|
@@ -292,7 +246,7 @@ function fromRclone(remoteName) {
|
|
|
292
246
|
function resolveCredentials(options) {
|
|
293
247
|
const envResult = tryEnvCredentials();
|
|
294
248
|
if (envResult === "partial") {
|
|
295
|
-
throw new
|
|
249
|
+
throw new CredentialError(
|
|
296
250
|
"Partial S3 credentials in environment. Set all three: S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_ENDPOINT \u2014 or remove all to use rclone fallback."
|
|
297
251
|
);
|
|
298
252
|
}
|
|
@@ -301,12 +255,6 @@ function resolveCredentials(options) {
|
|
|
301
255
|
}
|
|
302
256
|
return fromRclone(options.remoteName);
|
|
303
257
|
}
|
|
304
|
-
var init_resolver = __esm({
|
|
305
|
-
"src/credentials/resolver.ts"() {
|
|
306
|
-
"use strict";
|
|
307
|
-
init_redact();
|
|
308
|
-
}
|
|
309
|
-
});
|
|
310
258
|
|
|
311
259
|
// src/storage/client.ts
|
|
312
260
|
import { S3Client } from "@aws-sdk/client-s3";
|
|
@@ -321,11 +269,6 @@ function createS3Client(credentials) {
|
|
|
321
269
|
forcePathStyle: true
|
|
322
270
|
});
|
|
323
271
|
}
|
|
324
|
-
var init_client = __esm({
|
|
325
|
-
"src/storage/client.ts"() {
|
|
326
|
-
"use strict";
|
|
327
|
-
}
|
|
328
|
-
});
|
|
329
272
|
|
|
330
273
|
// src/storage/operations.ts
|
|
331
274
|
import {
|
|
@@ -336,6 +279,8 @@ import {
|
|
|
336
279
|
DeleteObjectCommand,
|
|
337
280
|
DeleteObjectsCommand
|
|
338
281
|
} from "@aws-sdk/client-s3";
|
|
282
|
+
var MAX_RETRIES = 3;
|
|
283
|
+
var BASE_DELAY_MS = 500;
|
|
339
284
|
function isTransientError(error) {
|
|
340
285
|
if (!(error instanceof Error)) return false;
|
|
341
286
|
const meta = error.$metadata;
|
|
@@ -421,14 +366,6 @@ async function listObjects(client, params) {
|
|
|
421
366
|
} while (continuationToken);
|
|
422
367
|
return allItems;
|
|
423
368
|
}
|
|
424
|
-
var MAX_RETRIES, BASE_DELAY_MS;
|
|
425
|
-
var init_operations = __esm({
|
|
426
|
-
"src/storage/operations.ts"() {
|
|
427
|
-
"use strict";
|
|
428
|
-
MAX_RETRIES = 3;
|
|
429
|
-
BASE_DELAY_MS = 500;
|
|
430
|
-
}
|
|
431
|
-
});
|
|
432
369
|
|
|
433
370
|
// src/storage/aliases.ts
|
|
434
371
|
async function readAlias(client, bucket, site, aliasName) {
|
|
@@ -446,18 +383,12 @@ async function writeAlias(client, bucket, site, aliasName, deployId) {
|
|
|
446
383
|
contentType: "text/plain"
|
|
447
384
|
});
|
|
448
385
|
}
|
|
449
|
-
var init_aliases = __esm({
|
|
450
|
-
"src/storage/aliases.ts"() {
|
|
451
|
-
"use strict";
|
|
452
|
-
init_operations();
|
|
453
|
-
}
|
|
454
|
-
});
|
|
455
386
|
|
|
456
387
|
// src/deploy/id.ts
|
|
457
388
|
function generateDeployId(gitHash, force = false) {
|
|
458
389
|
if (gitHash === void 0) {
|
|
459
390
|
if (!force) {
|
|
460
|
-
throw new
|
|
391
|
+
throw new GitError("git hash is required unless --force is set");
|
|
461
392
|
}
|
|
462
393
|
return formatId("nogit");
|
|
463
394
|
}
|
|
@@ -473,11 +404,6 @@ function formatId(suffix) {
|
|
|
473
404
|
const s = String(now.getUTCSeconds()).padStart(2, "0");
|
|
474
405
|
return `${y}${mo}${d}-${h}${mi}${s}-${suffix}`;
|
|
475
406
|
}
|
|
476
|
-
var init_id = __esm({
|
|
477
|
-
"src/deploy/id.ts"() {
|
|
478
|
-
"use strict";
|
|
479
|
-
}
|
|
480
|
-
});
|
|
481
407
|
|
|
482
408
|
// src/deploy/git.ts
|
|
483
409
|
import { execSync as execSync2 } from "child_process";
|
|
@@ -490,50 +416,77 @@ function getGitState() {
|
|
|
490
416
|
return { hash: null, dirty: false, error: "not a git repository" };
|
|
491
417
|
}
|
|
492
418
|
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
419
|
+
|
|
420
|
+
// src/deploy/preflight.ts
|
|
421
|
+
import { existsSync, statSync as statSync2 } from "fs";
|
|
422
|
+
|
|
423
|
+
// src/deploy/walk.ts
|
|
424
|
+
import { readdirSync, realpathSync, statSync } from "fs";
|
|
425
|
+
import { join, relative as relative2, sep as sep2 } from "path";
|
|
426
|
+
function walkFiles(base) {
|
|
427
|
+
const baseReal = realpathSync(base);
|
|
428
|
+
const entries = readdirSync(base, { recursive: true, withFileTypes: true });
|
|
429
|
+
const files = [];
|
|
430
|
+
for (const entry of entries) {
|
|
431
|
+
const full = join(entry.parentPath, entry.name);
|
|
432
|
+
const relPath = relative2(base, full);
|
|
433
|
+
let targetIsFile;
|
|
434
|
+
try {
|
|
435
|
+
targetIsFile = statSync(full).isFile();
|
|
436
|
+
} catch {
|
|
437
|
+
throw new StorageError(
|
|
438
|
+
`"${relPath}" could not be stat'd (dangling symlink?)`
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
if (!targetIsFile) continue;
|
|
442
|
+
let resolved;
|
|
443
|
+
try {
|
|
444
|
+
resolved = realpathSync(full);
|
|
445
|
+
} catch {
|
|
446
|
+
throw new StorageError(
|
|
447
|
+
`"${relPath}" could not be resolved (dangling symlink?)`
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
const rel = relative2(baseReal, resolved);
|
|
451
|
+
if (rel === ".." || rel.startsWith(`..${sep2}`)) {
|
|
452
|
+
throw new StorageError(
|
|
453
|
+
`"${relPath}" resolves outside the output directory (symlink escape). Resolved to: ${resolved}`
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
files.push({ relPath, absPath: full });
|
|
496
457
|
}
|
|
497
|
-
|
|
458
|
+
return files;
|
|
459
|
+
}
|
|
498
460
|
|
|
499
461
|
// src/deploy/preflight.ts
|
|
500
|
-
import { existsSync, statSync, readdirSync } from "fs";
|
|
501
|
-
import { join } from "path";
|
|
502
462
|
function validateOutputDir(dir) {
|
|
503
463
|
if (!existsSync(dir)) {
|
|
504
464
|
return { valid: false, fileCount: 0, error: "directory not found" };
|
|
505
465
|
}
|
|
506
|
-
if (!
|
|
466
|
+
if (!statSync2(dir).isDirectory()) {
|
|
507
467
|
return { valid: false, fileCount: 0, error: "not a directory" };
|
|
508
468
|
}
|
|
509
|
-
|
|
469
|
+
let fileCount;
|
|
470
|
+
try {
|
|
471
|
+
fileCount = walkFiles(dir).length;
|
|
472
|
+
} catch (err) {
|
|
473
|
+
if (err instanceof StorageError) {
|
|
474
|
+
return { valid: false, fileCount: 0, error: err.message };
|
|
475
|
+
}
|
|
476
|
+
throw err;
|
|
477
|
+
}
|
|
510
478
|
if (fileCount === 0) {
|
|
511
479
|
return { valid: false, fileCount: 0, error: "directory is empty" };
|
|
512
480
|
}
|
|
513
481
|
return { valid: true, fileCount };
|
|
514
482
|
}
|
|
515
|
-
function countFiles(dir) {
|
|
516
|
-
let count = 0;
|
|
517
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
518
|
-
if (entry.isFile()) {
|
|
519
|
-
count++;
|
|
520
|
-
} else if (entry.isDirectory()) {
|
|
521
|
-
count += countFiles(join(dir, entry.name));
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
return count;
|
|
525
|
-
}
|
|
526
|
-
var init_preflight = __esm({
|
|
527
|
-
"src/deploy/preflight.ts"() {
|
|
528
|
-
"use strict";
|
|
529
|
-
}
|
|
530
|
-
});
|
|
531
483
|
|
|
532
484
|
// src/deploy/upload.ts
|
|
533
|
-
import {
|
|
534
|
-
import { extname, basename
|
|
485
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
486
|
+
import { extname, basename } from "path";
|
|
535
487
|
import { lookup } from "mrmime";
|
|
536
488
|
import pLimit from "p-limit";
|
|
489
|
+
var FINGERPRINT_RE = /[.-][a-zA-Z0-9_-]{8,}\./;
|
|
537
490
|
function getContentType(filename) {
|
|
538
491
|
const mime = lookup(filename);
|
|
539
492
|
if (mime === void 0) {
|
|
@@ -554,25 +507,17 @@ function getCacheControl(filename) {
|
|
|
554
507
|
async function uploadDirectory(client, bucket, site, deployId, outputDir, options) {
|
|
555
508
|
const concurrency = options?.concurrency ?? 10;
|
|
556
509
|
const limit = pLimit(concurrency);
|
|
557
|
-
const
|
|
558
|
-
recursive: true,
|
|
559
|
-
withFileTypes: true
|
|
560
|
-
});
|
|
561
|
-
const files = entries.filter((entry) => entry.isFile() && !entry.isSymbolicLink()).map((entry) => {
|
|
562
|
-
const full = join2(entry.parentPath ?? entry.path, entry.name);
|
|
563
|
-
return relative(outputDir, full);
|
|
564
|
-
});
|
|
510
|
+
const files = walkFiles(outputDir);
|
|
565
511
|
const errors = [];
|
|
566
512
|
let totalSize = 0;
|
|
567
513
|
let fileCount = 0;
|
|
568
514
|
const tasks = files.map(
|
|
569
|
-
(
|
|
570
|
-
const
|
|
571
|
-
const
|
|
572
|
-
const
|
|
573
|
-
const cacheControl = getCacheControl(filePath);
|
|
515
|
+
(file) => limit(async () => {
|
|
516
|
+
const key = `${site}/deploys/${deployId}/${file.relPath}`;
|
|
517
|
+
const contentType = getContentType(file.relPath);
|
|
518
|
+
const cacheControl = getCacheControl(file.relPath);
|
|
574
519
|
try {
|
|
575
|
-
const body = readFileSync2(
|
|
520
|
+
const body = readFileSync2(file.absPath);
|
|
576
521
|
totalSize += body.byteLength;
|
|
577
522
|
fileCount++;
|
|
578
523
|
await putObject(client, {
|
|
@@ -584,21 +529,13 @@ async function uploadDirectory(client, bucket, site, deployId, outputDir, option
|
|
|
584
529
|
});
|
|
585
530
|
} catch (err) {
|
|
586
531
|
const message = err instanceof Error ? err.message : "unknown upload error";
|
|
587
|
-
errors.push(`${
|
|
532
|
+
errors.push(`${file.relPath}: ${message}`);
|
|
588
533
|
}
|
|
589
534
|
})
|
|
590
535
|
);
|
|
591
536
|
await Promise.all(tasks);
|
|
592
537
|
return { fileCount, totalSize, errors };
|
|
593
538
|
}
|
|
594
|
-
var FINGERPRINT_RE;
|
|
595
|
-
var init_upload = __esm({
|
|
596
|
-
"src/deploy/upload.ts"() {
|
|
597
|
-
"use strict";
|
|
598
|
-
init_operations();
|
|
599
|
-
FINGERPRINT_RE = /[.-][a-zA-Z0-9_-]{8,}\./;
|
|
600
|
-
}
|
|
601
|
-
});
|
|
602
539
|
|
|
603
540
|
// src/deploy/metadata.ts
|
|
604
541
|
async function writeDeployMetadata(client, bucket, site, deployId, meta) {
|
|
@@ -611,21 +548,70 @@ async function writeDeployMetadata(client, bucket, site, deployId, meta) {
|
|
|
611
548
|
contentType: "application/json"
|
|
612
549
|
});
|
|
613
550
|
}
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
551
|
+
|
|
552
|
+
// src/output/format.ts
|
|
553
|
+
import { log } from "@clack/prompts";
|
|
554
|
+
|
|
555
|
+
// src/output/envelope.ts
|
|
556
|
+
function buildEnvelope(command, success, data) {
|
|
557
|
+
return {
|
|
558
|
+
schemaVersion: "1",
|
|
559
|
+
command,
|
|
560
|
+
success,
|
|
561
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
562
|
+
...data
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
function buildErrorEnvelope(command, code, message, issues) {
|
|
566
|
+
const error = {
|
|
567
|
+
code,
|
|
568
|
+
message
|
|
569
|
+
};
|
|
570
|
+
if (issues !== void 0) {
|
|
571
|
+
error.issues = issues;
|
|
618
572
|
}
|
|
619
|
-
|
|
573
|
+
return {
|
|
574
|
+
schemaVersion: "1",
|
|
575
|
+
command,
|
|
576
|
+
success: false,
|
|
577
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
578
|
+
error
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// src/output/format.ts
|
|
583
|
+
function outputSuccess(ctx, humanMessage, data) {
|
|
584
|
+
if (ctx.json) {
|
|
585
|
+
const envelope = buildEnvelope(ctx.command, true, data);
|
|
586
|
+
process.stdout.write(JSON.stringify(envelope) + "\n");
|
|
587
|
+
} else {
|
|
588
|
+
log.success(humanMessage);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
function outputError(ctx, code, message, issues) {
|
|
592
|
+
const redactedMessage = redact(message);
|
|
593
|
+
const redactedIssues = issues?.map(redact);
|
|
594
|
+
if (ctx.json) {
|
|
595
|
+
const envelope = buildErrorEnvelope(
|
|
596
|
+
ctx.command,
|
|
597
|
+
code,
|
|
598
|
+
redactedMessage,
|
|
599
|
+
redactedIssues
|
|
600
|
+
);
|
|
601
|
+
process.stdout.write(JSON.stringify(envelope) + "\n");
|
|
602
|
+
} else {
|
|
603
|
+
log.error(redactedMessage);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
620
606
|
|
|
621
607
|
// src/commands/deploy.ts
|
|
622
|
-
var
|
|
623
|
-
|
|
624
|
-
deploy: () => deploy
|
|
625
|
-
});
|
|
608
|
+
var MAX_COLLISION_RETRIES = 3;
|
|
609
|
+
var COLLISION_DELAY_MS = 1e3;
|
|
626
610
|
async function resolveDeployId(client, bucket, site, gitHash, force) {
|
|
611
|
+
let lastDeployId = "";
|
|
627
612
|
for (let attempt = 0; attempt < MAX_COLLISION_RETRIES; attempt++) {
|
|
628
613
|
const deployId = generateDeployId(gitHash, force);
|
|
614
|
+
lastDeployId = deployId;
|
|
629
615
|
const prefix = `${site}/deploys/${deployId}/`;
|
|
630
616
|
const existing = await listObjects(client, { bucket, prefix });
|
|
631
617
|
if (existing.length === 0) {
|
|
@@ -635,7 +621,9 @@ async function resolveDeployId(client, bucket, site, gitHash, force) {
|
|
|
635
621
|
await new Promise((resolve2) => setTimeout(resolve2, COLLISION_DELAY_MS));
|
|
636
622
|
}
|
|
637
623
|
}
|
|
638
|
-
|
|
624
|
+
throw new StorageError(
|
|
625
|
+
`Deploy ID collision could not be resolved after ${MAX_COLLISION_RETRIES} attempts (last attempted: ${lastDeployId}). Commit a new change and retry, or wait for the timestamp to advance.`
|
|
626
|
+
);
|
|
639
627
|
}
|
|
640
628
|
async function deploy(options) {
|
|
641
629
|
const config = loadConfig({
|
|
@@ -738,26 +726,6 @@ async function deploy(options) {
|
|
|
738
726
|
previewDomain
|
|
739
727
|
});
|
|
740
728
|
}
|
|
741
|
-
var MAX_COLLISION_RETRIES, COLLISION_DELAY_MS;
|
|
742
|
-
var init_deploy = __esm({
|
|
743
|
-
"src/commands/deploy.ts"() {
|
|
744
|
-
"use strict";
|
|
745
|
-
init_loader();
|
|
746
|
-
init_resolver();
|
|
747
|
-
init_client();
|
|
748
|
-
init_aliases();
|
|
749
|
-
init_operations();
|
|
750
|
-
init_id();
|
|
751
|
-
init_git();
|
|
752
|
-
init_preflight();
|
|
753
|
-
init_upload();
|
|
754
|
-
init_metadata();
|
|
755
|
-
init_format();
|
|
756
|
-
init_exit_codes();
|
|
757
|
-
MAX_COLLISION_RETRIES = 3;
|
|
758
|
-
COLLISION_DELAY_MS = 1e3;
|
|
759
|
-
}
|
|
760
|
-
});
|
|
761
729
|
|
|
762
730
|
// src/storage/deploys.ts
|
|
763
731
|
async function listDeploys(client, bucket, site) {
|
|
@@ -778,18 +746,8 @@ async function deployExists(client, bucket, site, deployId) {
|
|
|
778
746
|
const items = await listObjects(client, { bucket, prefix });
|
|
779
747
|
return items.length > 0;
|
|
780
748
|
}
|
|
781
|
-
var init_deploys = __esm({
|
|
782
|
-
"src/storage/deploys.ts"() {
|
|
783
|
-
"use strict";
|
|
784
|
-
init_operations();
|
|
785
|
-
}
|
|
786
|
-
});
|
|
787
749
|
|
|
788
750
|
// src/commands/promote.ts
|
|
789
|
-
var promote_exports = {};
|
|
790
|
-
__export(promote_exports, {
|
|
791
|
-
promote: () => promote
|
|
792
|
-
});
|
|
793
751
|
async function promote(options) {
|
|
794
752
|
const config = loadConfig();
|
|
795
753
|
const credentials = resolveCredentials({
|
|
@@ -847,24 +805,8 @@ async function promote(options) {
|
|
|
847
805
|
productionDomain
|
|
848
806
|
});
|
|
849
807
|
}
|
|
850
|
-
var init_promote = __esm({
|
|
851
|
-
"src/commands/promote.ts"() {
|
|
852
|
-
"use strict";
|
|
853
|
-
init_loader();
|
|
854
|
-
init_resolver();
|
|
855
|
-
init_client();
|
|
856
|
-
init_aliases();
|
|
857
|
-
init_deploys();
|
|
858
|
-
init_format();
|
|
859
|
-
init_exit_codes();
|
|
860
|
-
}
|
|
861
|
-
});
|
|
862
808
|
|
|
863
809
|
// src/commands/rollback.ts
|
|
864
|
-
var rollback_exports = {};
|
|
865
|
-
__export(rollback_exports, {
|
|
866
|
-
rollback: () => rollback
|
|
867
|
-
});
|
|
868
810
|
import { confirm } from "@clack/prompts";
|
|
869
811
|
async function rollback(options) {
|
|
870
812
|
const config = loadConfig();
|
|
@@ -928,29 +870,15 @@ async function rollback(options) {
|
|
|
928
870
|
productionDomain
|
|
929
871
|
});
|
|
930
872
|
}
|
|
931
|
-
var init_rollback = __esm({
|
|
932
|
-
"src/commands/rollback.ts"() {
|
|
933
|
-
"use strict";
|
|
934
|
-
init_loader();
|
|
935
|
-
init_resolver();
|
|
936
|
-
init_client();
|
|
937
|
-
init_aliases();
|
|
938
|
-
init_deploys();
|
|
939
|
-
init_format();
|
|
940
|
-
init_exit_codes();
|
|
941
|
-
}
|
|
942
|
-
});
|
|
943
873
|
|
|
944
874
|
// src/cli.ts
|
|
945
|
-
init_format();
|
|
946
|
-
init_exit_codes();
|
|
947
|
-
import { cac } from "cac";
|
|
948
875
|
var version = true ? "0.1.0" : "0.0.0";
|
|
949
876
|
function handleActionError(command, json, err) {
|
|
950
877
|
const ctx = { json, command };
|
|
951
878
|
const message = err instanceof Error ? err.message : "unknown error";
|
|
952
|
-
|
|
953
|
-
|
|
879
|
+
const code = err instanceof CliError ? err.exitCode : EXIT_USAGE;
|
|
880
|
+
outputError(ctx, code, message);
|
|
881
|
+
exitWithCode(code, message);
|
|
954
882
|
}
|
|
955
883
|
function run(argv = process.argv) {
|
|
956
884
|
const args = argv.slice(2);
|
|
@@ -959,8 +887,7 @@ function run(argv = process.argv) {
|
|
|
959
887
|
staticCli.command("deploy", "Deploy static site to S3").option("--json", "Output as JSON").option("--force", "Force deploy without git hash").option("--output-dir <dir>", "Build output directory").action(
|
|
960
888
|
async (flags) => {
|
|
961
889
|
try {
|
|
962
|
-
|
|
963
|
-
await deploy2({
|
|
890
|
+
await deploy({
|
|
964
891
|
json: flags.json ?? false,
|
|
965
892
|
force: flags.force ?? false,
|
|
966
893
|
outputDir: flags.outputDir
|
|
@@ -973,8 +900,7 @@ function run(argv = process.argv) {
|
|
|
973
900
|
staticCli.command("promote [deploy-id]", "Promote a deploy to production").option("--json", "Output as JSON").action(
|
|
974
901
|
async (deployId, flags) => {
|
|
975
902
|
try {
|
|
976
|
-
|
|
977
|
-
await promote2({ json: flags.json ?? false, deployId });
|
|
903
|
+
await promote({ json: flags.json ?? false, deployId });
|
|
978
904
|
} catch (err) {
|
|
979
905
|
handleActionError("promote", flags.json ?? false, err);
|
|
980
906
|
}
|
|
@@ -982,8 +908,7 @@ function run(argv = process.argv) {
|
|
|
982
908
|
);
|
|
983
909
|
staticCli.command("rollback", "Rollback production to previous deploy").option("--json", "Output as JSON").option("--confirm", "Confirm rollback").action(async (flags) => {
|
|
984
910
|
try {
|
|
985
|
-
|
|
986
|
-
await rollback2({
|
|
911
|
+
await rollback({
|
|
987
912
|
json: flags.json ?? false,
|
|
988
913
|
confirm: flags.confirm ?? false
|
|
989
914
|
});
|