nimblybase 0.1.10 → 0.1.12
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 +59 -0
- package/dist/index.js +219 -29
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -176,6 +176,15 @@ fails closed rather than open.
|
|
|
176
176
|
|
|
177
177
|
## Edge functions
|
|
178
178
|
|
|
179
|
+
CLI 0.1.11+ can prepare a bundle without signing in or uploading source:
|
|
180
|
+
|
|
181
|
+
```sh
|
|
182
|
+
nimbly functions deploy ./webhook.ts --dry-run --json
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
The preview reports the bundled module count, byte size, SHA256 and local checks.
|
|
186
|
+
It does not prove provider runtime compatibility, authorization or live health.
|
|
187
|
+
|
|
179
188
|
```bash
|
|
180
189
|
nimbly functions deploy ./webhook.js
|
|
181
190
|
nimbly functions
|
|
@@ -194,6 +203,56 @@ It receives **this project's own resources** and nothing else: `env.DB`,
|
|
|
194
203
|
them. It cannot reach another project's anything, and it does not receive
|
|
195
204
|
the platform-wide bindings our own Worker carries.
|
|
196
205
|
|
|
206
|
+
## Prepare a static site before publishing
|
|
207
|
+
|
|
208
|
+
```sh
|
|
209
|
+
nimbly deploy ./my-app --dry-run --json
|
|
210
|
+
# Explicitly execute the detected package build script, then prepare its output:
|
|
211
|
+
nimbly deploy ./my-app --build --dry-run --json
|
|
212
|
+
# Publish after reviewing the local preview and confirming the linked project:
|
|
213
|
+
nimbly whoami
|
|
214
|
+
nimbly deploy ./my-app
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
CLI 0.1.11+ previews the exact file hashes, sizes, SPA setting, index presence
|
|
218
|
+
and exclusions. It reads no NimblyBase credentials and makes no management or
|
|
219
|
+
provider requests. `--build` explicitly runs repository code; that script may
|
|
220
|
+
itself use the network. Without `--build`, a preview never executes scripts.
|
|
221
|
+
Build output goes to stderr so stdout remains one JSON document.
|
|
222
|
+
|
|
223
|
+
Sites are limited to 20,000 files, 25 MiB per file and 100 MiB total; functions
|
|
224
|
+
to 1,000,000 bundled bytes. Site previews and uploads omit environment files,
|
|
225
|
+
credential configuration, private-key files, source maps, `.git` and
|
|
226
|
+
`node_modules`. Symlinks and unsafe URL filenames are refused. An upload uses
|
|
227
|
+
the byte snapshot that was scanned and hashed, even if a local file changes.
|
|
228
|
+
|
|
229
|
+
CLI 0.1.12+ uses a linear JWT scan so repeated token-like text cannot trigger
|
|
230
|
+
regex backtracking during preparation.
|
|
231
|
+
|
|
232
|
+
Both site and function preparation refuse recognised management/provider
|
|
233
|
+
credential patterns without printing their values. Public application keys
|
|
234
|
+
remain allowed. Pattern scanning cannot detect every secret, encoded value or
|
|
235
|
+
private business datum; review what belongs in a public artifact.
|
|
236
|
+
|
|
237
|
+
`--dry-run` is refused on all other commands, including delete and rollback.
|
|
238
|
+
The preview proves local preparation only: it does not verify host installation,
|
|
239
|
+
project authorization, provider runtime compatibility or live health. A
|
|
240
|
+
cloud-only agent still needs a proven repository/build/upload path; connecting
|
|
241
|
+
to MCP alone does not supply one.
|
|
242
|
+
|
|
243
|
+
The server independently refuses recognised credentials in function source
|
|
244
|
+
before recording/uploading code. Site uploads reject those patterns too;
|
|
245
|
+
manifest planning/commit refuses private paths, and commit/rollback rechecks
|
|
246
|
+
the actual stored bytes, hashes and sizes before changing the serving site.
|
|
247
|
+
Each publication reads each distinct blob once, sequentially (up to 100 MiB).
|
|
248
|
+
The server allows at most 9,000 distinct blobs to leave room below the default
|
|
249
|
+
Worker subrequest ceiling; up to 20,000 paths may share those blobs. A local
|
|
250
|
+
preview is not server admission. Safe existing sites remain served; an unsafe
|
|
251
|
+
historical release must be cleaned before it can be republished or restored.
|
|
252
|
+
These are accidental-leak guards, not a complete audit, malware scanner,
|
|
253
|
+
usage budget or protection against a project administrator rewriting R2
|
|
254
|
+
through their own privileged code. Function environment secrets remain valid.
|
|
255
|
+
|
|
197
256
|
## When something is broken
|
|
198
257
|
|
|
199
258
|
```bash
|
package/dist/index.js
CHANGED
|
@@ -67,6 +67,7 @@ function parse(argv) {
|
|
|
67
67
|
plan: { type: "string" },
|
|
68
68
|
spa: { type: "boolean" },
|
|
69
69
|
build: { type: "boolean" },
|
|
70
|
+
"dry-run": { type: "boolean" },
|
|
70
71
|
"token-only": { type: "boolean" },
|
|
71
72
|
device: { type: "boolean" },
|
|
72
73
|
off: { type: "boolean" },
|
|
@@ -141,6 +142,7 @@ function parse(argv) {
|
|
|
141
142
|
plan: v.plan,
|
|
142
143
|
spa: v.spa === true ? true : void 0,
|
|
143
144
|
build: v.build === true ? true : void 0,
|
|
145
|
+
dryRun: v["dry-run"] === true ? true : void 0,
|
|
144
146
|
tokenOnly: v["token-only"] === true ? true : void 0,
|
|
145
147
|
device: v.device === true ? true : void 0,
|
|
146
148
|
off: v.off === true ? true : void 0,
|
|
@@ -246,6 +248,7 @@ YOUR APP'S USERS
|
|
|
246
248
|
EDGE FUNCTIONS
|
|
247
249
|
functions What is deployed, and whether it is live
|
|
248
250
|
functions deploy <file> Publish a module to a PUBLIC URL at /fn/<name>
|
|
251
|
+
--dry-run to bundle and inspect without uploading
|
|
249
252
|
functions schedule <name> "<cron>"
|
|
250
253
|
Run it on a schedule, UTC. --off to stop
|
|
251
254
|
functions delete <name> Remove one
|
|
@@ -258,6 +261,7 @@ STATIC HOSTING
|
|
|
258
261
|
deploy [dir] Publish a built site. --dir to choose the folder,
|
|
259
262
|
--build to run its package script first, --spa if
|
|
260
263
|
a client-side router owns the URLs
|
|
264
|
+
--dry-run to inspect hashes/limits without uploading
|
|
261
265
|
site What is deployed
|
|
262
266
|
site releases What has been published, and what is live
|
|
263
267
|
site rollback [id] Put an earlier release back; no id means the previous one
|
|
@@ -294,6 +298,8 @@ OPTIONS
|
|
|
294
298
|
--type <anon|service> Key type (default anon)
|
|
295
299
|
-f, --file <path> Schema file (default schema.json beside the link)
|
|
296
300
|
--dir <path> Repository to inspect, or built directory to publish
|
|
301
|
+
--dry-run Offline preview for deploy/functions deploy only
|
|
302
|
+
--build still explicitly runs the site's build script
|
|
297
303
|
--intent <full-app|backend-only> Hosting intent for assessment and creation
|
|
298
304
|
--database <d1|postgres|retain-external|none> Declared database requirement
|
|
299
305
|
--recipe <static-d1> Explicit new-app recipe; cannot override existing code
|
|
@@ -378,7 +384,7 @@ function when(iso) {
|
|
|
378
384
|
|
|
379
385
|
// src/api.ts
|
|
380
386
|
var DEFAULT_API_URL = "https://api-uat.nimblybase.com";
|
|
381
|
-
var VERSION = "0.1.
|
|
387
|
+
var VERSION = "0.1.12";
|
|
382
388
|
function dashboardUrl(api) {
|
|
383
389
|
const match = /^https:\/\/api(-[a-z0-9]+)?\.(.+)$/.exec(api);
|
|
384
390
|
return match ? `https://app${match[1] ?? ""}.${match[2]}` : null;
|
|
@@ -813,7 +819,7 @@ async function deviceLogin(input, fetchImpl = fetch) {
|
|
|
813
819
|
throw new Error("the device verification page is outside the authorization origin");
|
|
814
820
|
}
|
|
815
821
|
const now = input.now ?? (() => performance.now());
|
|
816
|
-
const sleep = input.sleep ?? ((ms) => new Promise((
|
|
822
|
+
const sleep = input.sleep ?? ((ms) => new Promise((resolve10) => setTimeout(resolve10, ms)));
|
|
817
823
|
const deadline = now() + device.expires_in * 1e3;
|
|
818
824
|
let interval = Math.max(5, device.interval ?? 5) * 1e3;
|
|
819
825
|
input.announce({ verificationUri: verification.toString(), userCode: device.user_code, expiresInSeconds: device.expires_in });
|
|
@@ -1221,7 +1227,7 @@ async function readPipedInput() {
|
|
|
1221
1227
|
return value.length > 0 ? value : null;
|
|
1222
1228
|
}
|
|
1223
1229
|
function promptSecret(question) {
|
|
1224
|
-
return new Promise((
|
|
1230
|
+
return new Promise((resolve10, reject) => {
|
|
1225
1231
|
const rl = createInterface({ input: process.stdin, output: process.stderr, terminal: true });
|
|
1226
1232
|
let promptShown = false;
|
|
1227
1233
|
const output = rl;
|
|
@@ -1238,7 +1244,7 @@ function promptSecret(question) {
|
|
|
1238
1244
|
output.output.write = write;
|
|
1239
1245
|
process.stderr.write("\n");
|
|
1240
1246
|
rl.close();
|
|
1241
|
-
|
|
1247
|
+
resolve10(answer.trim());
|
|
1242
1248
|
});
|
|
1243
1249
|
rl.on("SIGINT", () => {
|
|
1244
1250
|
rl.close();
|
|
@@ -8724,8 +8730,122 @@ function render4(f) {
|
|
|
8724
8730
|
|
|
8725
8731
|
// src/commands/functions.ts
|
|
8726
8732
|
import { readFile as readFile5 } from "node:fs/promises";
|
|
8733
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
8727
8734
|
import { basename as basename2, dirname as dirname7, extname as extname2, resolve as resolve7 } from "node:path";
|
|
8728
8735
|
import { build as bundle } from "esbuild";
|
|
8736
|
+
|
|
8737
|
+
// src/artifact-safety.ts
|
|
8738
|
+
import { constants } from "node:fs";
|
|
8739
|
+
import { lstat, open as open2 } from "node:fs/promises";
|
|
8740
|
+
|
|
8741
|
+
// ../../packages/contracts/dist/artifact-safety.js
|
|
8742
|
+
var CREDENTIALS = [
|
|
8743
|
+
{ kind: "NimblyBase management credential", pattern: /nb_(?:svc|pat|oat|ort)_[A-Za-z0-9_-]{20,}|nb_plat_\d{1,12}_[0-9a-f]{64}/ },
|
|
8744
|
+
{ kind: "provider secret key", pattern: /(?:sk|rk)_(?:test|live)_[A-Za-z0-9]{20,}/ },
|
|
8745
|
+
{ kind: "AWS access key", pattern: /(?:AKIA|ASIA)[A-Z0-9]{16}/ },
|
|
8746
|
+
{ kind: "GitHub credential", pattern: /gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,}/ },
|
|
8747
|
+
{ kind: "Slack credential", pattern: /xox[baprs]-[A-Za-z0-9-]{20,}/ },
|
|
8748
|
+
{ kind: "private key", pattern: /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY-----/ }
|
|
8749
|
+
];
|
|
8750
|
+
function embeddedCredentialKind(content) {
|
|
8751
|
+
return CREDENTIALS.find(({ pattern }) => pattern.test(content))?.kind ?? (findSignedJwt(content) ? "signed JWT credential" : null);
|
|
8752
|
+
}
|
|
8753
|
+
function redactArtifactLabel(label) {
|
|
8754
|
+
const text = CREDENTIALS.reduce((text2, { pattern }) => text2.replace(new RegExp(pattern.source, "g"), "[redacted]"), label);
|
|
8755
|
+
let offset = 0;
|
|
8756
|
+
let result = "";
|
|
8757
|
+
let found;
|
|
8758
|
+
while (found = findSignedJwt(text, offset)) {
|
|
8759
|
+
result += text.slice(offset, found.start) + "[redacted]";
|
|
8760
|
+
offset = found.end;
|
|
8761
|
+
}
|
|
8762
|
+
return result + text.slice(offset);
|
|
8763
|
+
}
|
|
8764
|
+
function findSignedJwt(content, from = 0) {
|
|
8765
|
+
const segments = /[A-Za-z0-9_-]{10,}/g;
|
|
8766
|
+
segments.lastIndex = from;
|
|
8767
|
+
let previous = null;
|
|
8768
|
+
let header = null;
|
|
8769
|
+
for (let match = segments.exec(content); match; match = segments.exec(content)) {
|
|
8770
|
+
const start = match.index;
|
|
8771
|
+
const value = match[0];
|
|
8772
|
+
const end = start + value.length;
|
|
8773
|
+
const adjacent = previous && start === previous.end + 1 && content[previous.end] === ".";
|
|
8774
|
+
if (!adjacent)
|
|
8775
|
+
header = null;
|
|
8776
|
+
if (adjacent && header?.headerStart !== null && header?.headerStart !== void 0 && value.length >= 20) {
|
|
8777
|
+
return { start: header.headerStart, end };
|
|
8778
|
+
}
|
|
8779
|
+
const index = value.indexOf("eyJ");
|
|
8780
|
+
const current = { end, headerStart: index >= 0 && value.length - index >= 13 ? start + index : null };
|
|
8781
|
+
header = adjacent ? previous : null;
|
|
8782
|
+
previous = current;
|
|
8783
|
+
}
|
|
8784
|
+
return null;
|
|
8785
|
+
}
|
|
8786
|
+
var PRIVATE_ARTIFACT_NAMES = /* @__PURE__ */ new Set([
|
|
8787
|
+
".git",
|
|
8788
|
+
"node_modules",
|
|
8789
|
+
".ds_store",
|
|
8790
|
+
"thumbs.db",
|
|
8791
|
+
".npmrc",
|
|
8792
|
+
".netrc",
|
|
8793
|
+
".envrc",
|
|
8794
|
+
".aws",
|
|
8795
|
+
".ssh",
|
|
8796
|
+
".nimblybase",
|
|
8797
|
+
"credentials.json"
|
|
8798
|
+
]);
|
|
8799
|
+
function excludedArtifactName(value) {
|
|
8800
|
+
const name = value.toLowerCase();
|
|
8801
|
+
return PRIVATE_ARTIFACT_NAMES.has(name) || name === ".env" || name.startsWith(".env.") || name.endsWith(".map") || /\.(?:pem|p12|pfx|key)$/.test(name) || /^id_(?:rsa|ed25519|ecdsa|dsa)$/.test(name);
|
|
8802
|
+
}
|
|
8803
|
+
|
|
8804
|
+
// src/artifact-safety.ts
|
|
8805
|
+
function assertNoEmbeddedCredentials(bytes, label) {
|
|
8806
|
+
const content = Buffer.from(bytes).toString("latin1");
|
|
8807
|
+
const found = embeddedCredentialKind(content);
|
|
8808
|
+
if (found) {
|
|
8809
|
+
const safeLabel = redactArtifactLabel(label);
|
|
8810
|
+
throw new CliError(
|
|
8811
|
+
`${safeLabel} contains a recognised ${found}; no artifact was uploaded.`,
|
|
8812
|
+
EXIT.usage,
|
|
8813
|
+
"Remove credentials from the build. Use function environment secrets for private values. Public app keys are allowed; this pattern check does not detect every possible secret."
|
|
8814
|
+
);
|
|
8815
|
+
}
|
|
8816
|
+
}
|
|
8817
|
+
async function readArtifactFile(file, maxBytes, label) {
|
|
8818
|
+
const before = await lstat(file);
|
|
8819
|
+
if (before.isSymbolicLink()) throw new CliError(`${label} is a symlink; nothing was uploaded.`, EXIT.usage);
|
|
8820
|
+
if (!before.isFile()) throw new CliError(`${label} is not a regular file.`, EXIT.usage);
|
|
8821
|
+
const handle = await open2(file, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0)).catch(() => {
|
|
8822
|
+
throw new CliError(`Could not safely read ${label}; nothing was uploaded.`, EXIT.usage);
|
|
8823
|
+
});
|
|
8824
|
+
try {
|
|
8825
|
+
const info = await handle.stat();
|
|
8826
|
+
if (!info.isFile() || info.dev !== before.dev || info.ino !== before.ino) {
|
|
8827
|
+
throw new CliError(`${label} changed while being read; retry after the build finishes.`, EXIT.usage);
|
|
8828
|
+
}
|
|
8829
|
+
if (info.size > maxBytes) throw new CliError(`${label} is over the ${maxBytes}-byte artifact limit.`, EXIT.usage);
|
|
8830
|
+
const chunks = [];
|
|
8831
|
+
let size = 0;
|
|
8832
|
+
while (true) {
|
|
8833
|
+
const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, maxBytes - size + 1));
|
|
8834
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, null);
|
|
8835
|
+
if (!bytesRead) break;
|
|
8836
|
+
size += bytesRead;
|
|
8837
|
+
if (size > maxBytes) throw new CliError(`${label} is over the ${maxBytes}-byte artifact limit.`, EXIT.usage);
|
|
8838
|
+
chunks.push(buffer.subarray(0, bytesRead));
|
|
8839
|
+
}
|
|
8840
|
+
const bytes = Buffer.concat(chunks, size);
|
|
8841
|
+
assertNoEmbeddedCredentials(bytes, label);
|
|
8842
|
+
return bytes;
|
|
8843
|
+
} finally {
|
|
8844
|
+
await handle.close();
|
|
8845
|
+
}
|
|
8846
|
+
}
|
|
8847
|
+
|
|
8848
|
+
// src/commands/functions.ts
|
|
8729
8849
|
var NAME = /^[a-z][a-z0-9-]{0,30}$/;
|
|
8730
8850
|
var MAX_FUNCTION_BYTES = 1e6;
|
|
8731
8851
|
async function list4(flags) {
|
|
@@ -8789,6 +8909,31 @@ async function deploy(flags, file) {
|
|
|
8789
8909
|
"Remove unused dependencies or split this function before deploying."
|
|
8790
8910
|
);
|
|
8791
8911
|
}
|
|
8912
|
+
const bytes = Buffer.from(built.source, "utf8");
|
|
8913
|
+
assertNoEmbeddedCredentials(bytes, "The bundled function");
|
|
8914
|
+
if (flags.dryRun) {
|
|
8915
|
+
const preview = {
|
|
8916
|
+
kind: "edge-function",
|
|
8917
|
+
dryRun: true,
|
|
8918
|
+
published: false,
|
|
8919
|
+
name,
|
|
8920
|
+
modules: built.modules,
|
|
8921
|
+
bytes: bytes.length,
|
|
8922
|
+
sha256: createHash4("sha256").update(bytes).digest("hex"),
|
|
8923
|
+
hasDefaultExport: true,
|
|
8924
|
+
limits: { bytes: MAX_FUNCTION_BYTES },
|
|
8925
|
+
credentialCheck: "No recognised credential patterns detected; not a complete secret audit.",
|
|
8926
|
+
scope: "Local bundle only; provider runtime, project authorization and live health are not verified."
|
|
8927
|
+
};
|
|
8928
|
+
if (isJsonMode()) json(preview);
|
|
8929
|
+
else {
|
|
8930
|
+
out(`Prepared ${name}: ${built.modules} modules, ${human(bytes.length)}. Nothing uploaded.`);
|
|
8931
|
+
out(` SHA256 ${preview.sha256}`);
|
|
8932
|
+
note(preview.credentialCheck);
|
|
8933
|
+
note(preview.scope);
|
|
8934
|
+
}
|
|
8935
|
+
return;
|
|
8936
|
+
}
|
|
8792
8937
|
const ctx = await context(flags.api);
|
|
8793
8938
|
const project = await resolveProject(ctx, flags.project);
|
|
8794
8939
|
note(`Deploying ${name} to ${project.slug} (${built.modules} modules, ${human(Buffer.byteLength(built.source, "utf8"))})\u2026`);
|
|
@@ -9251,11 +9396,10 @@ function propertyName(name) {
|
|
|
9251
9396
|
}
|
|
9252
9397
|
|
|
9253
9398
|
// src/commands/deploy.ts
|
|
9254
|
-
import { readdir as readdir2, readFile as readFile6,
|
|
9255
|
-
import { createHash as
|
|
9399
|
+
import { readdir as readdir2, readFile as readFile6, lstat as lstat2 } from "node:fs/promises";
|
|
9400
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
9256
9401
|
import { spawn as spawn2 } from "node:child_process";
|
|
9257
|
-
import { join as join5, relative as relative2, sep } from "node:path";
|
|
9258
|
-
var SKIP = /* @__PURE__ */ new Set([".git", "node_modules", ".DS_Store", ".env", ".env.local", "Thumbs.db"]);
|
|
9402
|
+
import { join as join5, relative as relative2, resolve as resolve8, sep } from "node:path";
|
|
9259
9403
|
var MAX_FILES = 2e4;
|
|
9260
9404
|
var MAX_FILE_BYTES = 25 * 1024 * 1024;
|
|
9261
9405
|
var MAX_SITE_BYTES = 100 * 1024 * 1024;
|
|
@@ -9268,7 +9412,7 @@ async function deploy2(flags, dir) {
|
|
|
9268
9412
|
for (const warning of detection.warnings) note(`${paint("yellow", "!")} ${warning}`);
|
|
9269
9413
|
}
|
|
9270
9414
|
if (flags.build) await buildSite(root, detection.buildScript ?? null, detection.buildCommand);
|
|
9271
|
-
const files2 = await collect(source2);
|
|
9415
|
+
const { files: files2, omitted } = await collect(source2);
|
|
9272
9416
|
if (files2.length === 0) {
|
|
9273
9417
|
throw new CliError(
|
|
9274
9418
|
`${source2} has nothing to publish.`,
|
|
@@ -9277,20 +9421,46 @@ async function deploy2(flags, dir) {
|
|
|
9277
9421
|
);
|
|
9278
9422
|
}
|
|
9279
9423
|
const index = files2.find((f) => f.path === "/index.html");
|
|
9280
|
-
if (!index && !flags.yes && !isJsonMode()) {
|
|
9424
|
+
if (!index && !flags.dryRun && !flags.yes && !isJsonMode()) {
|
|
9281
9425
|
note(`${paint("yellow", "!")} No index.html in ${source2}; nothing will answer at /.`);
|
|
9282
9426
|
if (!await confirm("Publish anyway?")) {
|
|
9283
9427
|
note("Not published.");
|
|
9284
9428
|
return;
|
|
9285
9429
|
}
|
|
9286
9430
|
}
|
|
9287
|
-
const ctx = await context(flags.api);
|
|
9288
|
-
const project = await resolveProject(ctx, flags.project);
|
|
9289
9431
|
const singlePageApp = flags.spa ?? detection.singlePageApp;
|
|
9290
9432
|
const manifest = {
|
|
9291
9433
|
singlePageApp,
|
|
9292
9434
|
files: Object.fromEntries(files2.map((f) => [f.path, { hash: f.hash, size: f.size }]))
|
|
9293
9435
|
};
|
|
9436
|
+
if (flags.dryRun) {
|
|
9437
|
+
const preview = {
|
|
9438
|
+
kind: "static-site",
|
|
9439
|
+
dryRun: true,
|
|
9440
|
+
published: false,
|
|
9441
|
+
buildExecuted: flags.build === true,
|
|
9442
|
+
files: files2.length,
|
|
9443
|
+
bytes: files2.reduce((sum, file) => sum + file.size, 0),
|
|
9444
|
+
hasIndex: Boolean(index),
|
|
9445
|
+
manifest,
|
|
9446
|
+
omitted,
|
|
9447
|
+
limits: { files: MAX_FILES, fileBytes: MAX_FILE_BYTES, totalBytes: MAX_SITE_BYTES },
|
|
9448
|
+
credentialCheck: "No recognised credential patterns detected; not a complete secret audit.",
|
|
9449
|
+
scope: "Local artifact only; project authorization, hosting compatibility and live health are not verified."
|
|
9450
|
+
};
|
|
9451
|
+
if (isJsonMode()) json(preview);
|
|
9452
|
+
else {
|
|
9453
|
+
out(`Prepared ${preview.files} files (${human2(preview.bytes)}). Nothing uploaded.`);
|
|
9454
|
+
for (const file of files2) out(` ${file.path} ${file.size} bytes ${file.hash}`);
|
|
9455
|
+
for (const path of omitted) note(`Omitted ${path}`);
|
|
9456
|
+
if (!index) note("No index.html: this artifact will not answer at /.");
|
|
9457
|
+
note(preview.credentialCheck);
|
|
9458
|
+
note(preview.scope);
|
|
9459
|
+
}
|
|
9460
|
+
return;
|
|
9461
|
+
}
|
|
9462
|
+
const ctx = await context(flags.api);
|
|
9463
|
+
const project = await resolveProject(ctx, flags.project);
|
|
9294
9464
|
const { missing } = await ctx.client.planSite(project.id, manifest);
|
|
9295
9465
|
const bytes = files2.filter((f) => missing.includes(f.hash)).reduce((sum, f) => sum + f.size, 0);
|
|
9296
9466
|
if (!isJsonMode()) {
|
|
@@ -9299,7 +9469,7 @@ async function deploy2(flags, dir) {
|
|
|
9299
9469
|
let done = 0;
|
|
9300
9470
|
for (const hash of missing) {
|
|
9301
9471
|
const file = files2.find((f) => f.hash === hash);
|
|
9302
|
-
await ctx.client.putSiteBlob(project.id, hash,
|
|
9472
|
+
await ctx.client.putSiteBlob(project.id, hash, file.bytes);
|
|
9303
9473
|
done += 1;
|
|
9304
9474
|
if (!isJsonMode()) note(` ${done}/${missing.length} ${file.path}`);
|
|
9305
9475
|
}
|
|
@@ -9423,17 +9593,18 @@ async function buildSite(root, script, display) {
|
|
|
9423
9593
|
const entries = await readdir2(root);
|
|
9424
9594
|
const runner = packageRunner(entries);
|
|
9425
9595
|
if (!isJsonMode()) note(`Running ${runner.label} run ${script}\u2026`);
|
|
9426
|
-
const code = await new Promise((
|
|
9596
|
+
const code = await new Promise((resolve10, reject) => {
|
|
9427
9597
|
const child = spawn2(runner.command, [...runner.args, "run", script], {
|
|
9428
9598
|
cwd: root,
|
|
9429
|
-
|
|
9599
|
+
// A build's progress must not corrupt a JSON preview on stdout.
|
|
9600
|
+
stdio: ["inherit", isJsonMode() ? 2 : "inherit", "inherit"],
|
|
9430
9601
|
// Node cannot execute .cmd shims directly on Windows. The command and
|
|
9431
9602
|
// every argument here come from our fixed framework/runner tables, so
|
|
9432
9603
|
// the Windows command shell receives no repository-controlled text.
|
|
9433
9604
|
shell: process.platform === "win32"
|
|
9434
9605
|
});
|
|
9435
9606
|
child.once("error", reject);
|
|
9436
|
-
child.once("close", (status4) =>
|
|
9607
|
+
child.once("close", (status4) => resolve10(status4 ?? 1));
|
|
9437
9608
|
}).catch((err) => {
|
|
9438
9609
|
throw new CliError(
|
|
9439
9610
|
`Could not start ${display ?? `${runner.label} run ${script}`}.`,
|
|
@@ -9464,19 +9635,32 @@ function packageRunner(entries) {
|
|
|
9464
9635
|
return { command: win ? "npm.cmd" : "npm", args: [], label: "npm" };
|
|
9465
9636
|
}
|
|
9466
9637
|
async function collect(dir) {
|
|
9467
|
-
|
|
9468
|
-
|
|
9638
|
+
dir = resolve8(dir);
|
|
9639
|
+
const root = await lstat2(dir).catch(() => null);
|
|
9640
|
+
if (root?.isSymbolicLink()) throw new CliError("The output directory is a symlink; nothing was uploaded.", EXIT.usage);
|
|
9641
|
+
if (!root?.isDirectory()) return { files: [], omitted: [] };
|
|
9469
9642
|
const found = [];
|
|
9643
|
+
const omitted = [];
|
|
9644
|
+
let total = 0;
|
|
9470
9645
|
async function walk(current) {
|
|
9471
9646
|
for (const entry of await readdir2(current, { withFileTypes: true })) {
|
|
9472
|
-
|
|
9647
|
+
const name = entry.name.toLowerCase();
|
|
9648
|
+
const path = toUrlPath(relative2(dir, join5(current, entry.name)));
|
|
9649
|
+
if (excludedArtifactName(name)) {
|
|
9650
|
+
omitted.push(path);
|
|
9651
|
+
continue;
|
|
9652
|
+
}
|
|
9653
|
+
if ([...entry.name].some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127 || "?#\\".includes(char))) {
|
|
9654
|
+
throw new CliError("An artifact filename contains unsafe URL characters; nothing was uploaded.", EXIT.usage);
|
|
9655
|
+
}
|
|
9473
9656
|
const absolute = join5(current, entry.name);
|
|
9657
|
+
const info = await lstat2(absolute);
|
|
9658
|
+
if (info.isSymbolicLink()) throw new CliError(`${path} is a symlink; nothing was uploaded.`, EXIT.usage);
|
|
9474
9659
|
if (entry.isDirectory()) {
|
|
9475
9660
|
await walk(absolute);
|
|
9476
9661
|
continue;
|
|
9477
9662
|
}
|
|
9478
9663
|
if (!entry.isFile()) continue;
|
|
9479
|
-
const info = await stat4(absolute);
|
|
9480
9664
|
if (info.size > MAX_FILE_BYTES) {
|
|
9481
9665
|
throw new CliError(
|
|
9482
9666
|
`${relative2(dir, absolute)} is ${human2(info.size)}, over the ${human2(MAX_FILE_BYTES)} limit.`,
|
|
@@ -9490,17 +9674,20 @@ async function collect(dir) {
|
|
|
9490
9674
|
"Static hosting is for a built site, not a source tree."
|
|
9491
9675
|
);
|
|
9492
9676
|
}
|
|
9677
|
+
assertSiteSize([total, info.size]);
|
|
9678
|
+
const bytes = await readArtifactFile(absolute, Math.min(MAX_FILE_BYTES, MAX_SITE_BYTES - total), path);
|
|
9679
|
+
total += bytes.length;
|
|
9493
9680
|
found.push({
|
|
9494
|
-
path
|
|
9495
|
-
|
|
9496
|
-
hash:
|
|
9497
|
-
size:
|
|
9681
|
+
path,
|
|
9682
|
+
bytes,
|
|
9683
|
+
hash: createHash5("sha256").update(bytes).digest("hex"),
|
|
9684
|
+
size: bytes.length
|
|
9498
9685
|
});
|
|
9499
9686
|
}
|
|
9500
9687
|
}
|
|
9501
9688
|
await walk(dir);
|
|
9502
9689
|
assertSiteSize(found.map((file) => file.size));
|
|
9503
|
-
return found.sort((a, b) => a.path.localeCompare(b.path));
|
|
9690
|
+
return { files: found.sort((a, b) => a.path.localeCompare(b.path)), omitted: omitted.sort() };
|
|
9504
9691
|
}
|
|
9505
9692
|
function assertSiteSize(sizes) {
|
|
9506
9693
|
const total = sizes.reduce((sum, size) => sum + size, 0);
|
|
@@ -9962,8 +10149,8 @@ async function remove4(flags, name) {
|
|
|
9962
10149
|
}
|
|
9963
10150
|
|
|
9964
10151
|
// src/commands/architect.ts
|
|
9965
|
-
import { readdir as readdir3, readFile as readFile8, stat as
|
|
9966
|
-
import { join as join6, resolve as
|
|
10152
|
+
import { readdir as readdir3, readFile as readFile8, stat as stat4 } from "node:fs/promises";
|
|
10153
|
+
import { join as join6, resolve as resolve9 } from "node:path";
|
|
9967
10154
|
var FEATURE_PATTERNS = {
|
|
9968
10155
|
auth: /\b(auth(?:entication|enticated|orization|orized)?|log-?ins?|sign-?ins?|sign-?ups?|sessions?|user accounts?|passport|clerk|auth0)\b/i,
|
|
9969
10156
|
storage: /\b(uploads?|attachments?|buckets?|object storage|file (?:uploads?|attachments?|storage)|s3|r2|multer|media)\b/i,
|
|
@@ -9977,7 +10164,7 @@ var FEATURE_PATTERNS = {
|
|
|
9977
10164
|
crawler: /\b(crawls?|crawlers?|scrapes?|playwright|puppeteer|browser automation)\b/i
|
|
9978
10165
|
};
|
|
9979
10166
|
async function architect(flags, ...words) {
|
|
9980
|
-
const root =
|
|
10167
|
+
const root = resolve9(flags.dir ?? ".");
|
|
9981
10168
|
const requirements = words.filter((word) => Boolean(word)).join(" ").trim();
|
|
9982
10169
|
const ctx = await context(flags.api);
|
|
9983
10170
|
if (!flags.project && !ctx.link) {
|
|
@@ -9995,7 +10182,7 @@ async function architect(flags, ...words) {
|
|
|
9995
10182
|
render6(proposal);
|
|
9996
10183
|
}
|
|
9997
10184
|
async function inspectRepository(root, requirements) {
|
|
9998
|
-
const rootStat = await
|
|
10185
|
+
const rootStat = await stat4(root).catch(() => null);
|
|
9999
10186
|
if (!rootStat?.isDirectory()) {
|
|
10000
10187
|
throw new CliError(`Cannot inspect ${root}.`, EXIT.notFound, "Pass --dir with a readable repository directory.");
|
|
10001
10188
|
}
|
|
@@ -10277,6 +10464,9 @@ async function run3(argv) {
|
|
|
10277
10464
|
return name || flags.help ? EXIT.ok : EXIT.usage;
|
|
10278
10465
|
}
|
|
10279
10466
|
try {
|
|
10467
|
+
if (flags.dryRun && !(name === "deploy" || name === "functions" && command[1] === "deploy")) {
|
|
10468
|
+
throw new CliError("--dry-run is only supported for deploy and functions deploy.", EXIT.usage);
|
|
10469
|
+
}
|
|
10280
10470
|
const group = GROUPS[name];
|
|
10281
10471
|
if (group) {
|
|
10282
10472
|
const sub = command[1] ?? "";
|