@freecodecamp/universe-cli 0.1.1 → 0.3.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 +43866 -38499
- package/dist/index.js +295 -326
- package/package.json +22 -6
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
|
+
}).prefault({});
|
|
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,14 +118,26 @@ 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
|
}
|
|
207
125
|
throw err;
|
|
208
126
|
}
|
|
209
127
|
const parsed = parseYaml(raw);
|
|
210
|
-
const
|
|
128
|
+
const parseResult = platformSchema.safeParse(parsed);
|
|
129
|
+
if (!parseResult.success) {
|
|
130
|
+
const issues = parseResult.error.issues.map((issue) => {
|
|
131
|
+
const path = issue.path.length > 0 ? issue.path.join(".") : "(root)";
|
|
132
|
+
return ` - ${path}: ${issue.message}`;
|
|
133
|
+
}).join("\n");
|
|
134
|
+
throw new ConfigError(
|
|
135
|
+
`platform.yaml is invalid:
|
|
136
|
+
${issues}
|
|
137
|
+
See STAFF-GUIDE.md for the required format.`
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
const yamlValidated = parseResult.data;
|
|
211
141
|
const envOverrides = readEnvOverrides();
|
|
212
142
|
const flagOverrides = readFlagOverrides(options.flags);
|
|
213
143
|
const merged = {
|
|
@@ -218,17 +148,82 @@ function loadConfig(options = {}) {
|
|
|
218
148
|
...flagOverrides
|
|
219
149
|
}
|
|
220
150
|
};
|
|
151
|
+
assertSafeOutputDir(merged.static.output_dir, cwd);
|
|
221
152
|
return merged;
|
|
222
153
|
}
|
|
223
|
-
var init_loader = __esm({
|
|
224
|
-
"src/config/loader.ts"() {
|
|
225
|
-
"use strict";
|
|
226
|
-
init_schema();
|
|
227
|
-
}
|
|
228
|
-
});
|
|
229
154
|
|
|
230
155
|
// src/credentials/resolver.ts
|
|
231
156
|
import { execSync } from "child_process";
|
|
157
|
+
|
|
158
|
+
// src/output/redact.ts
|
|
159
|
+
var AWS_KEY_PREFIX_PATTERN = /(?:AKIA|ASIA|AROA|AIDA|ACCA|ANPA|ABIA|AGPA)[A-Z0-9]{12,}/g;
|
|
160
|
+
var URL_CREDS_PATTERN = /https?:\/\/[^@\s]+@/g;
|
|
161
|
+
var CREDENTIAL_CONTEXT_PATTERN = /(?:access_key_id|secret_access_key|accessKeyId|secretAccessKey|secret|password|token|key|credential|auth)\s*[=:]\s*([A-Za-z0-9/+=]{21,})/gi;
|
|
162
|
+
var HEX_CREDENTIAL_CONTEXT_PATTERN = /(?:secret|password|token|key|credential|auth|access_key_id|secret_access_key)\s*[=:]\s*([a-f0-9]{32,})/gi;
|
|
163
|
+
var JSON_CREDENTIAL_PATTERN = /"(?:secret|password|token|key|credential|auth|access_key_id|secret_access_key|accessKeyId|secretAccessKey)"\s*:\s*"[^"]+"/gi;
|
|
164
|
+
var BEARER_PATTERN = /\bBearer\s+[A-Za-z0-9._~+/=-]+/gi;
|
|
165
|
+
function maskAwsKey(match) {
|
|
166
|
+
return match.slice(0, 4) + "****" + match.slice(-4);
|
|
167
|
+
}
|
|
168
|
+
function maskUrlCreds(match) {
|
|
169
|
+
const atIndex = match.lastIndexOf("@");
|
|
170
|
+
const protocolEnd = match.indexOf("://") + 3;
|
|
171
|
+
return match.slice(0, protocolEnd) + "****:****@" + match.slice(atIndex + 1);
|
|
172
|
+
}
|
|
173
|
+
function redact(value) {
|
|
174
|
+
let result = value;
|
|
175
|
+
result = result.replace(URL_CREDS_PATTERN, maskUrlCreds);
|
|
176
|
+
result = result.replace(AWS_KEY_PREFIX_PATTERN, maskAwsKey);
|
|
177
|
+
result = result.replace(BEARER_PATTERN, "Bearer ****");
|
|
178
|
+
result = result.replace(JSON_CREDENTIAL_PATTERN, (match) => {
|
|
179
|
+
const colonIndex = match.indexOf(":");
|
|
180
|
+
return match.slice(0, colonIndex + 1) + '"****"';
|
|
181
|
+
});
|
|
182
|
+
result = result.replace(
|
|
183
|
+
CREDENTIAL_CONTEXT_PATTERN,
|
|
184
|
+
(_match, _secret, _offset, _full) => {
|
|
185
|
+
const eqIndex = _match.indexOf("=");
|
|
186
|
+
const colonIndex = _match.indexOf(":");
|
|
187
|
+
const sepIndex = eqIndex >= 0 && colonIndex >= 0 ? Math.min(eqIndex, colonIndex) : eqIndex >= 0 ? eqIndex : colonIndex;
|
|
188
|
+
const prefix = _match.slice(0, sepIndex + 1);
|
|
189
|
+
return prefix + "****";
|
|
190
|
+
}
|
|
191
|
+
);
|
|
192
|
+
result = result.replace(HEX_CREDENTIAL_CONTEXT_PATTERN, (_match, _secret) => {
|
|
193
|
+
const eqIndex = _match.indexOf("=");
|
|
194
|
+
const colonIndex = _match.indexOf(":");
|
|
195
|
+
const sepIndex = eqIndex >= 0 && colonIndex >= 0 ? Math.min(eqIndex, colonIndex) : eqIndex >= 0 ? eqIndex : colonIndex;
|
|
196
|
+
const prefix = _match.slice(0, sepIndex + 1);
|
|
197
|
+
return prefix + "****";
|
|
198
|
+
});
|
|
199
|
+
return result;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// src/credentials/resolver.ts
|
|
203
|
+
var LOCAL_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]", "::1"]);
|
|
204
|
+
function validateEndpoint(endpoint) {
|
|
205
|
+
let url;
|
|
206
|
+
try {
|
|
207
|
+
url = new URL(endpoint);
|
|
208
|
+
} catch {
|
|
209
|
+
throw new CredentialError(`S3_ENDPOINT is not a valid URL: ${endpoint}`);
|
|
210
|
+
}
|
|
211
|
+
if (url.username !== "" || url.password !== "") {
|
|
212
|
+
throw new CredentialError(
|
|
213
|
+
"S3_ENDPOINT must not contain credentials in the URL (user:pass@host). Use S3_ACCESS_KEY_ID / S3_SECRET_ACCESS_KEY instead."
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
217
|
+
throw new CredentialError(
|
|
218
|
+
`S3_ENDPOINT must use http or https, got: ${url.protocol}`
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
if (url.protocol === "http:" && !LOCAL_HOSTS.has(url.hostname)) {
|
|
222
|
+
throw new CredentialError(
|
|
223
|
+
`S3_ENDPOINT must use https for non-localhost hosts. Plaintext http is only allowed for localhost/127.0.0.1. Got: ${endpoint}`
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
232
227
|
function tryEnvCredentials() {
|
|
233
228
|
const key = process.env.S3_ACCESS_KEY_ID;
|
|
234
229
|
const secret = process.env.S3_SECRET_ACCESS_KEY;
|
|
@@ -237,6 +232,7 @@ function tryEnvCredentials() {
|
|
|
237
232
|
const present = [key, secret, endpoint].filter(Boolean);
|
|
238
233
|
if (present.length === 0) return "none";
|
|
239
234
|
if (present.length < 3) return "partial";
|
|
235
|
+
validateEndpoint(endpoint);
|
|
240
236
|
const creds = {
|
|
241
237
|
accessKeyId: key,
|
|
242
238
|
secretAccessKey: secret,
|
|
@@ -251,11 +247,11 @@ function fromRclone(remoteName) {
|
|
|
251
247
|
output = execSync("rclone config dump", { stdio: "pipe" });
|
|
252
248
|
} catch (err) {
|
|
253
249
|
if (err instanceof Error && err.code === "ENOENT") {
|
|
254
|
-
throw new
|
|
250
|
+
throw new CredentialError(
|
|
255
251
|
"rclone not found \u2014 install rclone or provide S3 credentials via environment variables"
|
|
256
252
|
);
|
|
257
253
|
}
|
|
258
|
-
throw new
|
|
254
|
+
throw new CredentialError(
|
|
259
255
|
`Failed to run rclone config dump: ${redact(err instanceof Error ? err.message : "unknown error")}`
|
|
260
256
|
);
|
|
261
257
|
}
|
|
@@ -263,12 +259,14 @@ function fromRclone(remoteName) {
|
|
|
263
259
|
try {
|
|
264
260
|
parsed = JSON.parse(output.toString("utf-8"));
|
|
265
261
|
} catch {
|
|
266
|
-
throw new
|
|
262
|
+
throw new CredentialError(
|
|
263
|
+
`Failed to parse rclone config for remote ${remoteName}`
|
|
264
|
+
);
|
|
267
265
|
}
|
|
268
266
|
output = null;
|
|
269
267
|
const remote = parsed[remoteName];
|
|
270
268
|
if (!remote) {
|
|
271
|
-
throw new
|
|
269
|
+
throw new CredentialError(
|
|
272
270
|
`Remote "${remoteName}" not found in rclone config. Available remotes: ${Object.keys(parsed).join(", ") || "(none)"}`
|
|
273
271
|
);
|
|
274
272
|
}
|
|
@@ -277,7 +275,7 @@ function fromRclone(remoteName) {
|
|
|
277
275
|
if (!remote.secret_access_key) missing.push("secret_access_key");
|
|
278
276
|
if (!remote.endpoint) missing.push("endpoint");
|
|
279
277
|
if (missing.length > 0) {
|
|
280
|
-
throw new
|
|
278
|
+
throw new CredentialError(
|
|
281
279
|
`Rclone remote "${remoteName}" is missing required fields: ${missing.join(", ")}`
|
|
282
280
|
);
|
|
283
281
|
}
|
|
@@ -292,7 +290,7 @@ function fromRclone(remoteName) {
|
|
|
292
290
|
function resolveCredentials(options) {
|
|
293
291
|
const envResult = tryEnvCredentials();
|
|
294
292
|
if (envResult === "partial") {
|
|
295
|
-
throw new
|
|
293
|
+
throw new CredentialError(
|
|
296
294
|
"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
295
|
);
|
|
298
296
|
}
|
|
@@ -301,12 +299,6 @@ function resolveCredentials(options) {
|
|
|
301
299
|
}
|
|
302
300
|
return fromRclone(options.remoteName);
|
|
303
301
|
}
|
|
304
|
-
var init_resolver = __esm({
|
|
305
|
-
"src/credentials/resolver.ts"() {
|
|
306
|
-
"use strict";
|
|
307
|
-
init_redact();
|
|
308
|
-
}
|
|
309
|
-
});
|
|
310
302
|
|
|
311
303
|
// src/storage/client.ts
|
|
312
304
|
import { S3Client } from "@aws-sdk/client-s3";
|
|
@@ -321,11 +313,6 @@ function createS3Client(credentials) {
|
|
|
321
313
|
forcePathStyle: true
|
|
322
314
|
});
|
|
323
315
|
}
|
|
324
|
-
var init_client = __esm({
|
|
325
|
-
"src/storage/client.ts"() {
|
|
326
|
-
"use strict";
|
|
327
|
-
}
|
|
328
|
-
});
|
|
329
316
|
|
|
330
317
|
// src/storage/operations.ts
|
|
331
318
|
import {
|
|
@@ -336,6 +323,8 @@ import {
|
|
|
336
323
|
DeleteObjectCommand,
|
|
337
324
|
DeleteObjectsCommand
|
|
338
325
|
} from "@aws-sdk/client-s3";
|
|
326
|
+
var MAX_RETRIES = 3;
|
|
327
|
+
var BASE_DELAY_MS = 500;
|
|
339
328
|
function isTransientError(error) {
|
|
340
329
|
if (!(error instanceof Error)) return false;
|
|
341
330
|
const meta = error.$metadata;
|
|
@@ -421,14 +410,6 @@ async function listObjects(client, params) {
|
|
|
421
410
|
} while (continuationToken);
|
|
422
411
|
return allItems;
|
|
423
412
|
}
|
|
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
413
|
|
|
433
414
|
// src/storage/aliases.ts
|
|
434
415
|
async function readAlias(client, bucket, site, aliasName) {
|
|
@@ -446,18 +427,12 @@ async function writeAlias(client, bucket, site, aliasName, deployId) {
|
|
|
446
427
|
contentType: "text/plain"
|
|
447
428
|
});
|
|
448
429
|
}
|
|
449
|
-
var init_aliases = __esm({
|
|
450
|
-
"src/storage/aliases.ts"() {
|
|
451
|
-
"use strict";
|
|
452
|
-
init_operations();
|
|
453
|
-
}
|
|
454
|
-
});
|
|
455
430
|
|
|
456
431
|
// src/deploy/id.ts
|
|
457
432
|
function generateDeployId(gitHash, force = false) {
|
|
458
433
|
if (gitHash === void 0) {
|
|
459
434
|
if (!force) {
|
|
460
|
-
throw new
|
|
435
|
+
throw new GitError("git hash is required unless --force is set");
|
|
461
436
|
}
|
|
462
437
|
return formatId("nogit");
|
|
463
438
|
}
|
|
@@ -473,11 +448,6 @@ function formatId(suffix) {
|
|
|
473
448
|
const s = String(now.getUTCSeconds()).padStart(2, "0");
|
|
474
449
|
return `${y}${mo}${d}-${h}${mi}${s}-${suffix}`;
|
|
475
450
|
}
|
|
476
|
-
var init_id = __esm({
|
|
477
|
-
"src/deploy/id.ts"() {
|
|
478
|
-
"use strict";
|
|
479
|
-
}
|
|
480
|
-
});
|
|
481
451
|
|
|
482
452
|
// src/deploy/git.ts
|
|
483
453
|
import { execSync as execSync2 } from "child_process";
|
|
@@ -490,50 +460,77 @@ function getGitState() {
|
|
|
490
460
|
return { hash: null, dirty: false, error: "not a git repository" };
|
|
491
461
|
}
|
|
492
462
|
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
463
|
+
|
|
464
|
+
// src/deploy/preflight.ts
|
|
465
|
+
import { existsSync, statSync as statSync2 } from "fs";
|
|
466
|
+
|
|
467
|
+
// src/deploy/walk.ts
|
|
468
|
+
import { readdirSync, realpathSync, statSync } from "fs";
|
|
469
|
+
import { join, relative as relative2, sep as sep2 } from "path";
|
|
470
|
+
function walkFiles(base) {
|
|
471
|
+
const baseReal = realpathSync(base);
|
|
472
|
+
const entries = readdirSync(base, { recursive: true, withFileTypes: true });
|
|
473
|
+
const files = [];
|
|
474
|
+
for (const entry of entries) {
|
|
475
|
+
const full = join(entry.parentPath, entry.name);
|
|
476
|
+
const relPath = relative2(base, full);
|
|
477
|
+
let targetIsFile;
|
|
478
|
+
try {
|
|
479
|
+
targetIsFile = statSync(full).isFile();
|
|
480
|
+
} catch {
|
|
481
|
+
throw new StorageError(
|
|
482
|
+
`"${relPath}" could not be stat'd (dangling symlink?)`
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
if (!targetIsFile) continue;
|
|
486
|
+
let resolved;
|
|
487
|
+
try {
|
|
488
|
+
resolved = realpathSync(full);
|
|
489
|
+
} catch {
|
|
490
|
+
throw new StorageError(
|
|
491
|
+
`"${relPath}" could not be resolved (dangling symlink?)`
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
const rel = relative2(baseReal, resolved);
|
|
495
|
+
if (rel === ".." || rel.startsWith(`..${sep2}`)) {
|
|
496
|
+
throw new StorageError(
|
|
497
|
+
`"${relPath}" resolves outside the output directory (symlink escape). Resolved to: ${resolved}`
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
files.push({ relPath, absPath: full });
|
|
496
501
|
}
|
|
497
|
-
|
|
502
|
+
return files;
|
|
503
|
+
}
|
|
498
504
|
|
|
499
505
|
// src/deploy/preflight.ts
|
|
500
|
-
import { existsSync, statSync, readdirSync } from "fs";
|
|
501
|
-
import { join } from "path";
|
|
502
506
|
function validateOutputDir(dir) {
|
|
503
507
|
if (!existsSync(dir)) {
|
|
504
508
|
return { valid: false, fileCount: 0, error: "directory not found" };
|
|
505
509
|
}
|
|
506
|
-
if (!
|
|
510
|
+
if (!statSync2(dir).isDirectory()) {
|
|
507
511
|
return { valid: false, fileCount: 0, error: "not a directory" };
|
|
508
512
|
}
|
|
509
|
-
|
|
513
|
+
let fileCount;
|
|
514
|
+
try {
|
|
515
|
+
fileCount = walkFiles(dir).length;
|
|
516
|
+
} catch (err) {
|
|
517
|
+
if (err instanceof StorageError) {
|
|
518
|
+
return { valid: false, fileCount: 0, error: err.message };
|
|
519
|
+
}
|
|
520
|
+
throw err;
|
|
521
|
+
}
|
|
510
522
|
if (fileCount === 0) {
|
|
511
523
|
return { valid: false, fileCount: 0, error: "directory is empty" };
|
|
512
524
|
}
|
|
513
525
|
return { valid: true, fileCount };
|
|
514
526
|
}
|
|
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
527
|
|
|
532
528
|
// src/deploy/upload.ts
|
|
533
|
-
import {
|
|
534
|
-
import { extname, basename
|
|
529
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
530
|
+
import { extname, basename } from "path";
|
|
535
531
|
import { lookup } from "mrmime";
|
|
536
532
|
import pLimit from "p-limit";
|
|
533
|
+
var FINGERPRINT_RE = /[.-][a-zA-Z0-9_-]{8,}\./;
|
|
537
534
|
function getContentType(filename) {
|
|
538
535
|
const mime = lookup(filename);
|
|
539
536
|
if (mime === void 0) {
|
|
@@ -554,25 +551,17 @@ function getCacheControl(filename) {
|
|
|
554
551
|
async function uploadDirectory(client, bucket, site, deployId, outputDir, options) {
|
|
555
552
|
const concurrency = options?.concurrency ?? 10;
|
|
556
553
|
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
|
-
});
|
|
554
|
+
const files = walkFiles(outputDir);
|
|
565
555
|
const errors = [];
|
|
566
556
|
let totalSize = 0;
|
|
567
557
|
let fileCount = 0;
|
|
568
558
|
const tasks = files.map(
|
|
569
|
-
(
|
|
570
|
-
const
|
|
571
|
-
const
|
|
572
|
-
const
|
|
573
|
-
const cacheControl = getCacheControl(filePath);
|
|
559
|
+
(file) => limit(async () => {
|
|
560
|
+
const key = `${site}/deploys/${deployId}/${file.relPath}`;
|
|
561
|
+
const contentType = getContentType(file.relPath);
|
|
562
|
+
const cacheControl = getCacheControl(file.relPath);
|
|
574
563
|
try {
|
|
575
|
-
const body = readFileSync2(
|
|
564
|
+
const body = readFileSync2(file.absPath);
|
|
576
565
|
totalSize += body.byteLength;
|
|
577
566
|
fileCount++;
|
|
578
567
|
await putObject(client, {
|
|
@@ -584,21 +573,13 @@ async function uploadDirectory(client, bucket, site, deployId, outputDir, option
|
|
|
584
573
|
});
|
|
585
574
|
} catch (err) {
|
|
586
575
|
const message = err instanceof Error ? err.message : "unknown upload error";
|
|
587
|
-
errors.push(`${
|
|
576
|
+
errors.push(`${file.relPath}: ${message}`);
|
|
588
577
|
}
|
|
589
578
|
})
|
|
590
579
|
);
|
|
591
580
|
await Promise.all(tasks);
|
|
592
581
|
return { fileCount, totalSize, errors };
|
|
593
582
|
}
|
|
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
583
|
|
|
603
584
|
// src/deploy/metadata.ts
|
|
604
585
|
async function writeDeployMetadata(client, bucket, site, deployId, meta) {
|
|
@@ -611,21 +592,70 @@ async function writeDeployMetadata(client, bucket, site, deployId, meta) {
|
|
|
611
592
|
contentType: "application/json"
|
|
612
593
|
});
|
|
613
594
|
}
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
595
|
+
|
|
596
|
+
// src/output/format.ts
|
|
597
|
+
import { log } from "@clack/prompts";
|
|
598
|
+
|
|
599
|
+
// src/output/envelope.ts
|
|
600
|
+
function buildEnvelope(command, success, data) {
|
|
601
|
+
return {
|
|
602
|
+
schemaVersion: "1",
|
|
603
|
+
command,
|
|
604
|
+
success,
|
|
605
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
606
|
+
...data
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
function buildErrorEnvelope(command, code, message, issues) {
|
|
610
|
+
const error = {
|
|
611
|
+
code,
|
|
612
|
+
message
|
|
613
|
+
};
|
|
614
|
+
if (issues !== void 0) {
|
|
615
|
+
error.issues = issues;
|
|
618
616
|
}
|
|
619
|
-
|
|
617
|
+
return {
|
|
618
|
+
schemaVersion: "1",
|
|
619
|
+
command,
|
|
620
|
+
success: false,
|
|
621
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
622
|
+
error
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// src/output/format.ts
|
|
627
|
+
function outputSuccess(ctx, humanMessage, data) {
|
|
628
|
+
if (ctx.json) {
|
|
629
|
+
const envelope = buildEnvelope(ctx.command, true, data);
|
|
630
|
+
process.stdout.write(JSON.stringify(envelope) + "\n");
|
|
631
|
+
} else {
|
|
632
|
+
log.success(humanMessage);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
function outputError(ctx, code, message, issues) {
|
|
636
|
+
const redactedMessage = redact(message);
|
|
637
|
+
const redactedIssues = issues?.map(redact);
|
|
638
|
+
if (ctx.json) {
|
|
639
|
+
const envelope = buildErrorEnvelope(
|
|
640
|
+
ctx.command,
|
|
641
|
+
code,
|
|
642
|
+
redactedMessage,
|
|
643
|
+
redactedIssues
|
|
644
|
+
);
|
|
645
|
+
process.stdout.write(JSON.stringify(envelope) + "\n");
|
|
646
|
+
} else {
|
|
647
|
+
log.error(redactedMessage);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
620
650
|
|
|
621
651
|
// src/commands/deploy.ts
|
|
622
|
-
var
|
|
623
|
-
|
|
624
|
-
deploy: () => deploy
|
|
625
|
-
});
|
|
652
|
+
var MAX_COLLISION_RETRIES = 3;
|
|
653
|
+
var COLLISION_DELAY_MS = 1e3;
|
|
626
654
|
async function resolveDeployId(client, bucket, site, gitHash, force) {
|
|
655
|
+
let lastDeployId = "";
|
|
627
656
|
for (let attempt = 0; attempt < MAX_COLLISION_RETRIES; attempt++) {
|
|
628
657
|
const deployId = generateDeployId(gitHash, force);
|
|
658
|
+
lastDeployId = deployId;
|
|
629
659
|
const prefix = `${site}/deploys/${deployId}/`;
|
|
630
660
|
const existing = await listObjects(client, { bucket, prefix });
|
|
631
661
|
if (existing.length === 0) {
|
|
@@ -635,7 +665,9 @@ async function resolveDeployId(client, bucket, site, gitHash, force) {
|
|
|
635
665
|
await new Promise((resolve2) => setTimeout(resolve2, COLLISION_DELAY_MS));
|
|
636
666
|
}
|
|
637
667
|
}
|
|
638
|
-
|
|
668
|
+
throw new StorageError(
|
|
669
|
+
`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.`
|
|
670
|
+
);
|
|
639
671
|
}
|
|
640
672
|
async function deploy(options) {
|
|
641
673
|
const config = loadConfig({
|
|
@@ -738,26 +770,6 @@ async function deploy(options) {
|
|
|
738
770
|
previewDomain
|
|
739
771
|
});
|
|
740
772
|
}
|
|
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
773
|
|
|
762
774
|
// src/storage/deploys.ts
|
|
763
775
|
async function listDeploys(client, bucket, site) {
|
|
@@ -778,18 +790,8 @@ async function deployExists(client, bucket, site, deployId) {
|
|
|
778
790
|
const items = await listObjects(client, { bucket, prefix });
|
|
779
791
|
return items.length > 0;
|
|
780
792
|
}
|
|
781
|
-
var init_deploys = __esm({
|
|
782
|
-
"src/storage/deploys.ts"() {
|
|
783
|
-
"use strict";
|
|
784
|
-
init_operations();
|
|
785
|
-
}
|
|
786
|
-
});
|
|
787
793
|
|
|
788
794
|
// src/commands/promote.ts
|
|
789
|
-
var promote_exports = {};
|
|
790
|
-
__export(promote_exports, {
|
|
791
|
-
promote: () => promote
|
|
792
|
-
});
|
|
793
795
|
async function promote(options) {
|
|
794
796
|
const config = loadConfig();
|
|
795
797
|
const credentials = resolveCredentials({
|
|
@@ -847,24 +849,8 @@ async function promote(options) {
|
|
|
847
849
|
productionDomain
|
|
848
850
|
});
|
|
849
851
|
}
|
|
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
852
|
|
|
863
853
|
// src/commands/rollback.ts
|
|
864
|
-
var rollback_exports = {};
|
|
865
|
-
__export(rollback_exports, {
|
|
866
|
-
rollback: () => rollback
|
|
867
|
-
});
|
|
868
854
|
import { confirm } from "@clack/prompts";
|
|
869
855
|
async function rollback(options) {
|
|
870
856
|
const config = loadConfig();
|
|
@@ -928,29 +914,15 @@ async function rollback(options) {
|
|
|
928
914
|
productionDomain
|
|
929
915
|
});
|
|
930
916
|
}
|
|
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
917
|
|
|
944
918
|
// src/cli.ts
|
|
945
|
-
|
|
946
|
-
init_exit_codes();
|
|
947
|
-
import { cac } from "cac";
|
|
948
|
-
var version = true ? "0.1.0" : "0.0.0";
|
|
919
|
+
var version = true ? "0.3.0" : "0.0.0";
|
|
949
920
|
function handleActionError(command, json, err) {
|
|
950
921
|
const ctx = { json, command };
|
|
951
922
|
const message = err instanceof Error ? err.message : "unknown error";
|
|
952
|
-
|
|
953
|
-
|
|
923
|
+
const code = err instanceof CliError ? err.exitCode : EXIT_USAGE;
|
|
924
|
+
outputError(ctx, code, message);
|
|
925
|
+
exitWithCode(code, message);
|
|
954
926
|
}
|
|
955
927
|
function run(argv = process.argv) {
|
|
956
928
|
const args = argv.slice(2);
|
|
@@ -959,8 +931,7 @@ function run(argv = process.argv) {
|
|
|
959
931
|
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
932
|
async (flags) => {
|
|
961
933
|
try {
|
|
962
|
-
|
|
963
|
-
await deploy2({
|
|
934
|
+
await deploy({
|
|
964
935
|
json: flags.json ?? false,
|
|
965
936
|
force: flags.force ?? false,
|
|
966
937
|
outputDir: flags.outputDir
|
|
@@ -973,8 +944,7 @@ function run(argv = process.argv) {
|
|
|
973
944
|
staticCli.command("promote [deploy-id]", "Promote a deploy to production").option("--json", "Output as JSON").action(
|
|
974
945
|
async (deployId, flags) => {
|
|
975
946
|
try {
|
|
976
|
-
|
|
977
|
-
await promote2({ json: flags.json ?? false, deployId });
|
|
947
|
+
await promote({ json: flags.json ?? false, deployId });
|
|
978
948
|
} catch (err) {
|
|
979
949
|
handleActionError("promote", flags.json ?? false, err);
|
|
980
950
|
}
|
|
@@ -982,8 +952,7 @@ function run(argv = process.argv) {
|
|
|
982
952
|
);
|
|
983
953
|
staticCli.command("rollback", "Rollback production to previous deploy").option("--json", "Output as JSON").option("--confirm", "Confirm rollback").action(async (flags) => {
|
|
984
954
|
try {
|
|
985
|
-
|
|
986
|
-
await rollback2({
|
|
955
|
+
await rollback({
|
|
987
956
|
json: flags.json ?? false,
|
|
988
957
|
confirm: flags.confirm ?? false
|
|
989
958
|
});
|