nimblybase 0.1.9 → 0.1.11
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 +48 -1
- package/dist/index.js +178 -38
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -122,7 +122,10 @@ cannot be combined with `--authorize`.
|
|
|
122
122
|
If approval is denied or fails, the project and link are retained. Retry
|
|
123
123
|
`nimbly login` with the API printed by the CLI; do not recreate the project.
|
|
124
124
|
A returning repository grant uses read endpoints for `nimbly init`, so it does
|
|
125
|
-
not need workspace creation permission or another browser approval.
|
|
125
|
+
not need workspace creation permission or another browser approval. Its JSON
|
|
126
|
+
keeps the shared ready/provisioning/attention-required status and next actions;
|
|
127
|
+
paused projects direct the agent to diagnosis, and default provisioning waits
|
|
128
|
+
remain in effect.
|
|
126
129
|
|
|
127
130
|
## Scripting
|
|
128
131
|
|
|
@@ -173,6 +176,15 @@ fails closed rather than open.
|
|
|
173
176
|
|
|
174
177
|
## Edge functions
|
|
175
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
|
+
|
|
176
188
|
```bash
|
|
177
189
|
nimbly functions deploy ./webhook.js
|
|
178
190
|
nimbly functions
|
|
@@ -191,6 +203,41 @@ It receives **this project's own resources** and nothing else: `env.DB`,
|
|
|
191
203
|
them. It cannot reach another project's anything, and it does not receive
|
|
192
204
|
the platform-wide bindings our own Worker carries.
|
|
193
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
|
+
Both site and function preparation refuse recognised management/provider
|
|
230
|
+
credential patterns without printing their values. Public application keys
|
|
231
|
+
remain allowed. Pattern scanning cannot detect every secret, encoded value or
|
|
232
|
+
private business datum; review what belongs in a public artifact.
|
|
233
|
+
|
|
234
|
+
`--dry-run` is refused on all other commands, including delete and rollback.
|
|
235
|
+
The preview proves local preparation only: it does not verify host installation,
|
|
236
|
+
project authorization, provider runtime compatibility or live health. A
|
|
237
|
+
cloud-only agent still needs a proven repository/build/upload path; connecting
|
|
238
|
+
to MCP alone does not supply one. The checks here guard the CLI upload path;
|
|
239
|
+
they do not add a server-side policy to direct HTTP or MCP uploads.
|
|
240
|
+
|
|
194
241
|
## When something is broken
|
|
195
242
|
|
|
196
243
|
```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.11";
|
|
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();
|
|
@@ -7918,19 +7924,24 @@ async function init(flags, name) {
|
|
|
7918
7924
|
await assertMayCreateLink(root, { relink: flags.relink });
|
|
7919
7925
|
}
|
|
7920
7926
|
if (ctx.credentialScope === "repository" && !name) {
|
|
7921
|
-
const
|
|
7927
|
+
const initial = await resolveProject(ctx, explicitProject ?? linkedProject);
|
|
7928
|
+
const project = flags.wait === false ? initial : await waitForActive(ctx, initial);
|
|
7922
7929
|
const backend = await ctx.client.describeProject(project.id);
|
|
7930
|
+
const status4 = project.status === "active" ? "ready" : project.status === "provisioning" ? "provisioning" : "attention_required";
|
|
7931
|
+
const result2 = { status: status4, project, backend, nextActions: status4 === "ready" ? [
|
|
7932
|
+
{ code: "inspect_backend", command: "nimbly describe", description: "Inspect available services before designing the backend." },
|
|
7933
|
+
{ code: "plan_schema", command: "nimbly db status", description: "Inspect the schema before applying changes." }
|
|
7934
|
+
] : status4 === "provisioning" ? [
|
|
7935
|
+
{ code: "wait_for_project", command: `nimbly project ${project.slug}`, description: "Wait for the project to become active." }
|
|
7936
|
+
] : [
|
|
7937
|
+
{ code: "diagnose_project", command: `nimbly diagnose ${project.slug}`, description: `The project is ${project.status}; inspect before using it.` }
|
|
7938
|
+
] };
|
|
7923
7939
|
if (isJsonMode()) {
|
|
7924
|
-
json({
|
|
7925
|
-
status: project.status === "active" ? "ready" : project.status,
|
|
7926
|
-
project,
|
|
7927
|
-
backend,
|
|
7928
|
-
linkedTo: ctx.link?.path ?? null,
|
|
7929
|
-
authorization: { state: "authorized", projectId: project.id }
|
|
7930
|
-
});
|
|
7940
|
+
json({ ...result2, linkedTo: ctx.link?.path ?? null, authorization: { state: "authorized", projectId: project.id } });
|
|
7931
7941
|
} else {
|
|
7932
7942
|
out(`${paint("green", "\u2713")} ${project.slug} is ${statusPaint(project.status)}`);
|
|
7933
|
-
note("This repository is already authorized.
|
|
7943
|
+
note("This repository is already authorized.");
|
|
7944
|
+
for (const action of result2.nextActions) note(`Next: ${action.command} \u2014 ${action.description}`);
|
|
7934
7945
|
}
|
|
7935
7946
|
return;
|
|
7936
7947
|
}
|
|
@@ -8719,8 +8730,66 @@ function render4(f) {
|
|
|
8719
8730
|
|
|
8720
8731
|
// src/commands/functions.ts
|
|
8721
8732
|
import { readFile as readFile5 } from "node:fs/promises";
|
|
8733
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
8722
8734
|
import { basename as basename2, dirname as dirname7, extname as extname2, resolve as resolve7 } from "node:path";
|
|
8723
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
|
+
var CREDENTIALS = [
|
|
8741
|
+
{ kind: "NimblyBase management credential", pattern: /nb_(?:svc|pat|oat|ort)_[A-Za-z0-9_-]{20,}|nb_plat_\d{1,12}_[0-9a-f]{64}/ },
|
|
8742
|
+
{ kind: "provider secret key", pattern: /(?:sk|rk)_(?:test|live)_[A-Za-z0-9]{20,}/ },
|
|
8743
|
+
{ kind: "AWS access key", pattern: /(?:AKIA|ASIA)[A-Z0-9]{16}/ },
|
|
8744
|
+
{ kind: "GitHub credential", pattern: /gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,}/ },
|
|
8745
|
+
{ kind: "Slack credential", pattern: /xox[baprs]-[A-Za-z0-9-]{20,}/ },
|
|
8746
|
+
{ kind: "private key", pattern: /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY-----/ },
|
|
8747
|
+
{ kind: "signed JWT credential", pattern: /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{20,}/ }
|
|
8748
|
+
];
|
|
8749
|
+
function assertNoEmbeddedCredentials(bytes, label) {
|
|
8750
|
+
const content = Buffer.from(bytes).toString("latin1");
|
|
8751
|
+
const found = CREDENTIALS.find(({ pattern }) => pattern.test(content));
|
|
8752
|
+
if (found) {
|
|
8753
|
+
const safeLabel = CREDENTIALS.reduce((text, { pattern }) => text.replace(new RegExp(pattern.source, "g"), "[redacted]"), label);
|
|
8754
|
+
throw new CliError(
|
|
8755
|
+
`${safeLabel} contains a recognised ${found.kind}; no artifact was uploaded.`,
|
|
8756
|
+
EXIT.usage,
|
|
8757
|
+
"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."
|
|
8758
|
+
);
|
|
8759
|
+
}
|
|
8760
|
+
}
|
|
8761
|
+
async function readArtifactFile(file, maxBytes, label) {
|
|
8762
|
+
const before = await lstat(file);
|
|
8763
|
+
if (before.isSymbolicLink()) throw new CliError(`${label} is a symlink; nothing was uploaded.`, EXIT.usage);
|
|
8764
|
+
if (!before.isFile()) throw new CliError(`${label} is not a regular file.`, EXIT.usage);
|
|
8765
|
+
const handle = await open2(file, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0)).catch(() => {
|
|
8766
|
+
throw new CliError(`Could not safely read ${label}; nothing was uploaded.`, EXIT.usage);
|
|
8767
|
+
});
|
|
8768
|
+
try {
|
|
8769
|
+
const info = await handle.stat();
|
|
8770
|
+
if (!info.isFile() || info.dev !== before.dev || info.ino !== before.ino) {
|
|
8771
|
+
throw new CliError(`${label} changed while being read; retry after the build finishes.`, EXIT.usage);
|
|
8772
|
+
}
|
|
8773
|
+
if (info.size > maxBytes) throw new CliError(`${label} is over the ${maxBytes}-byte artifact limit.`, EXIT.usage);
|
|
8774
|
+
const chunks = [];
|
|
8775
|
+
let size = 0;
|
|
8776
|
+
while (true) {
|
|
8777
|
+
const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, maxBytes - size + 1));
|
|
8778
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, null);
|
|
8779
|
+
if (!bytesRead) break;
|
|
8780
|
+
size += bytesRead;
|
|
8781
|
+
if (size > maxBytes) throw new CliError(`${label} is over the ${maxBytes}-byte artifact limit.`, EXIT.usage);
|
|
8782
|
+
chunks.push(buffer.subarray(0, bytesRead));
|
|
8783
|
+
}
|
|
8784
|
+
const bytes = Buffer.concat(chunks, size);
|
|
8785
|
+
assertNoEmbeddedCredentials(bytes, label);
|
|
8786
|
+
return bytes;
|
|
8787
|
+
} finally {
|
|
8788
|
+
await handle.close();
|
|
8789
|
+
}
|
|
8790
|
+
}
|
|
8791
|
+
|
|
8792
|
+
// src/commands/functions.ts
|
|
8724
8793
|
var NAME = /^[a-z][a-z0-9-]{0,30}$/;
|
|
8725
8794
|
var MAX_FUNCTION_BYTES = 1e6;
|
|
8726
8795
|
async function list4(flags) {
|
|
@@ -8784,6 +8853,31 @@ async function deploy(flags, file) {
|
|
|
8784
8853
|
"Remove unused dependencies or split this function before deploying."
|
|
8785
8854
|
);
|
|
8786
8855
|
}
|
|
8856
|
+
const bytes = Buffer.from(built.source, "utf8");
|
|
8857
|
+
assertNoEmbeddedCredentials(bytes, "The bundled function");
|
|
8858
|
+
if (flags.dryRun) {
|
|
8859
|
+
const preview = {
|
|
8860
|
+
kind: "edge-function",
|
|
8861
|
+
dryRun: true,
|
|
8862
|
+
published: false,
|
|
8863
|
+
name,
|
|
8864
|
+
modules: built.modules,
|
|
8865
|
+
bytes: bytes.length,
|
|
8866
|
+
sha256: createHash4("sha256").update(bytes).digest("hex"),
|
|
8867
|
+
hasDefaultExport: true,
|
|
8868
|
+
limits: { bytes: MAX_FUNCTION_BYTES },
|
|
8869
|
+
credentialCheck: "No recognised credential patterns detected; not a complete secret audit.",
|
|
8870
|
+
scope: "Local bundle only; provider runtime, project authorization and live health are not verified."
|
|
8871
|
+
};
|
|
8872
|
+
if (isJsonMode()) json(preview);
|
|
8873
|
+
else {
|
|
8874
|
+
out(`Prepared ${name}: ${built.modules} modules, ${human(bytes.length)}. Nothing uploaded.`);
|
|
8875
|
+
out(` SHA256 ${preview.sha256}`);
|
|
8876
|
+
note(preview.credentialCheck);
|
|
8877
|
+
note(preview.scope);
|
|
8878
|
+
}
|
|
8879
|
+
return;
|
|
8880
|
+
}
|
|
8787
8881
|
const ctx = await context(flags.api);
|
|
8788
8882
|
const project = await resolveProject(ctx, flags.project);
|
|
8789
8883
|
note(`Deploying ${name} to ${project.slug} (${built.modules} modules, ${human(Buffer.byteLength(built.source, "utf8"))})\u2026`);
|
|
@@ -9246,11 +9340,11 @@ function propertyName(name) {
|
|
|
9246
9340
|
}
|
|
9247
9341
|
|
|
9248
9342
|
// src/commands/deploy.ts
|
|
9249
|
-
import { readdir as readdir2, readFile as readFile6,
|
|
9250
|
-
import { createHash as
|
|
9343
|
+
import { readdir as readdir2, readFile as readFile6, lstat as lstat2 } from "node:fs/promises";
|
|
9344
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
9251
9345
|
import { spawn as spawn2 } from "node:child_process";
|
|
9252
|
-
import { join as join5, relative as relative2, sep } from "node:path";
|
|
9253
|
-
var SKIP = /* @__PURE__ */ new Set([".git", "node_modules", ".
|
|
9346
|
+
import { join as join5, relative as relative2, resolve as resolve8, sep } from "node:path";
|
|
9347
|
+
var SKIP = /* @__PURE__ */ new Set([".git", "node_modules", ".ds_store", "thumbs.db", ".npmrc", ".netrc", ".envrc", ".aws", ".ssh", ".nimblybase", "credentials.json"]);
|
|
9254
9348
|
var MAX_FILES = 2e4;
|
|
9255
9349
|
var MAX_FILE_BYTES = 25 * 1024 * 1024;
|
|
9256
9350
|
var MAX_SITE_BYTES = 100 * 1024 * 1024;
|
|
@@ -9263,7 +9357,7 @@ async function deploy2(flags, dir) {
|
|
|
9263
9357
|
for (const warning of detection.warnings) note(`${paint("yellow", "!")} ${warning}`);
|
|
9264
9358
|
}
|
|
9265
9359
|
if (flags.build) await buildSite(root, detection.buildScript ?? null, detection.buildCommand);
|
|
9266
|
-
const files2 = await collect(source2);
|
|
9360
|
+
const { files: files2, omitted } = await collect(source2);
|
|
9267
9361
|
if (files2.length === 0) {
|
|
9268
9362
|
throw new CliError(
|
|
9269
9363
|
`${source2} has nothing to publish.`,
|
|
@@ -9272,20 +9366,46 @@ async function deploy2(flags, dir) {
|
|
|
9272
9366
|
);
|
|
9273
9367
|
}
|
|
9274
9368
|
const index = files2.find((f) => f.path === "/index.html");
|
|
9275
|
-
if (!index && !flags.yes && !isJsonMode()) {
|
|
9369
|
+
if (!index && !flags.dryRun && !flags.yes && !isJsonMode()) {
|
|
9276
9370
|
note(`${paint("yellow", "!")} No index.html in ${source2}; nothing will answer at /.`);
|
|
9277
9371
|
if (!await confirm("Publish anyway?")) {
|
|
9278
9372
|
note("Not published.");
|
|
9279
9373
|
return;
|
|
9280
9374
|
}
|
|
9281
9375
|
}
|
|
9282
|
-
const ctx = await context(flags.api);
|
|
9283
|
-
const project = await resolveProject(ctx, flags.project);
|
|
9284
9376
|
const singlePageApp = flags.spa ?? detection.singlePageApp;
|
|
9285
9377
|
const manifest = {
|
|
9286
9378
|
singlePageApp,
|
|
9287
9379
|
files: Object.fromEntries(files2.map((f) => [f.path, { hash: f.hash, size: f.size }]))
|
|
9288
9380
|
};
|
|
9381
|
+
if (flags.dryRun) {
|
|
9382
|
+
const preview = {
|
|
9383
|
+
kind: "static-site",
|
|
9384
|
+
dryRun: true,
|
|
9385
|
+
published: false,
|
|
9386
|
+
buildExecuted: flags.build === true,
|
|
9387
|
+
files: files2.length,
|
|
9388
|
+
bytes: files2.reduce((sum, file) => sum + file.size, 0),
|
|
9389
|
+
hasIndex: Boolean(index),
|
|
9390
|
+
manifest,
|
|
9391
|
+
omitted,
|
|
9392
|
+
limits: { files: MAX_FILES, fileBytes: MAX_FILE_BYTES, totalBytes: MAX_SITE_BYTES },
|
|
9393
|
+
credentialCheck: "No recognised credential patterns detected; not a complete secret audit.",
|
|
9394
|
+
scope: "Local artifact only; project authorization, hosting compatibility and live health are not verified."
|
|
9395
|
+
};
|
|
9396
|
+
if (isJsonMode()) json(preview);
|
|
9397
|
+
else {
|
|
9398
|
+
out(`Prepared ${preview.files} files (${human2(preview.bytes)}). Nothing uploaded.`);
|
|
9399
|
+
for (const file of files2) out(` ${file.path} ${file.size} bytes ${file.hash}`);
|
|
9400
|
+
for (const path of omitted) note(`Omitted ${path}`);
|
|
9401
|
+
if (!index) note("No index.html: this artifact will not answer at /.");
|
|
9402
|
+
note(preview.credentialCheck);
|
|
9403
|
+
note(preview.scope);
|
|
9404
|
+
}
|
|
9405
|
+
return;
|
|
9406
|
+
}
|
|
9407
|
+
const ctx = await context(flags.api);
|
|
9408
|
+
const project = await resolveProject(ctx, flags.project);
|
|
9289
9409
|
const { missing } = await ctx.client.planSite(project.id, manifest);
|
|
9290
9410
|
const bytes = files2.filter((f) => missing.includes(f.hash)).reduce((sum, f) => sum + f.size, 0);
|
|
9291
9411
|
if (!isJsonMode()) {
|
|
@@ -9294,7 +9414,7 @@ async function deploy2(flags, dir) {
|
|
|
9294
9414
|
let done = 0;
|
|
9295
9415
|
for (const hash of missing) {
|
|
9296
9416
|
const file = files2.find((f) => f.hash === hash);
|
|
9297
|
-
await ctx.client.putSiteBlob(project.id, hash,
|
|
9417
|
+
await ctx.client.putSiteBlob(project.id, hash, file.bytes);
|
|
9298
9418
|
done += 1;
|
|
9299
9419
|
if (!isJsonMode()) note(` ${done}/${missing.length} ${file.path}`);
|
|
9300
9420
|
}
|
|
@@ -9418,17 +9538,18 @@ async function buildSite(root, script, display) {
|
|
|
9418
9538
|
const entries = await readdir2(root);
|
|
9419
9539
|
const runner = packageRunner(entries);
|
|
9420
9540
|
if (!isJsonMode()) note(`Running ${runner.label} run ${script}\u2026`);
|
|
9421
|
-
const code = await new Promise((
|
|
9541
|
+
const code = await new Promise((resolve10, reject) => {
|
|
9422
9542
|
const child = spawn2(runner.command, [...runner.args, "run", script], {
|
|
9423
9543
|
cwd: root,
|
|
9424
|
-
|
|
9544
|
+
// A build's progress must not corrupt a JSON preview on stdout.
|
|
9545
|
+
stdio: ["inherit", isJsonMode() ? 2 : "inherit", "inherit"],
|
|
9425
9546
|
// Node cannot execute .cmd shims directly on Windows. The command and
|
|
9426
9547
|
// every argument here come from our fixed framework/runner tables, so
|
|
9427
9548
|
// the Windows command shell receives no repository-controlled text.
|
|
9428
9549
|
shell: process.platform === "win32"
|
|
9429
9550
|
});
|
|
9430
9551
|
child.once("error", reject);
|
|
9431
|
-
child.once("close", (status4) =>
|
|
9552
|
+
child.once("close", (status4) => resolve10(status4 ?? 1));
|
|
9432
9553
|
}).catch((err) => {
|
|
9433
9554
|
throw new CliError(
|
|
9434
9555
|
`Could not start ${display ?? `${runner.label} run ${script}`}.`,
|
|
@@ -9459,19 +9580,32 @@ function packageRunner(entries) {
|
|
|
9459
9580
|
return { command: win ? "npm.cmd" : "npm", args: [], label: "npm" };
|
|
9460
9581
|
}
|
|
9461
9582
|
async function collect(dir) {
|
|
9462
|
-
|
|
9463
|
-
|
|
9583
|
+
dir = resolve8(dir);
|
|
9584
|
+
const root = await lstat2(dir).catch(() => null);
|
|
9585
|
+
if (root?.isSymbolicLink()) throw new CliError("The output directory is a symlink; nothing was uploaded.", EXIT.usage);
|
|
9586
|
+
if (!root?.isDirectory()) return { files: [], omitted: [] };
|
|
9464
9587
|
const found = [];
|
|
9588
|
+
const omitted = [];
|
|
9589
|
+
let total = 0;
|
|
9465
9590
|
async function walk(current) {
|
|
9466
9591
|
for (const entry of await readdir2(current, { withFileTypes: true })) {
|
|
9467
|
-
|
|
9592
|
+
const name = entry.name.toLowerCase();
|
|
9593
|
+
const path = toUrlPath(relative2(dir, join5(current, entry.name)));
|
|
9594
|
+
if (SKIP.has(name) || name === ".env" || name.startsWith(".env.") || name.endsWith(".map") || /\.(?:pem|p12|pfx|key)$/.test(name) || /^id_(?:rsa|ed25519|ecdsa|dsa)$/.test(name)) {
|
|
9595
|
+
omitted.push(path);
|
|
9596
|
+
continue;
|
|
9597
|
+
}
|
|
9598
|
+
if ([...entry.name].some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127 || "?#\\".includes(char))) {
|
|
9599
|
+
throw new CliError("An artifact filename contains unsafe URL characters; nothing was uploaded.", EXIT.usage);
|
|
9600
|
+
}
|
|
9468
9601
|
const absolute = join5(current, entry.name);
|
|
9602
|
+
const info = await lstat2(absolute);
|
|
9603
|
+
if (info.isSymbolicLink()) throw new CliError(`${path} is a symlink; nothing was uploaded.`, EXIT.usage);
|
|
9469
9604
|
if (entry.isDirectory()) {
|
|
9470
9605
|
await walk(absolute);
|
|
9471
9606
|
continue;
|
|
9472
9607
|
}
|
|
9473
9608
|
if (!entry.isFile()) continue;
|
|
9474
|
-
const info = await stat4(absolute);
|
|
9475
9609
|
if (info.size > MAX_FILE_BYTES) {
|
|
9476
9610
|
throw new CliError(
|
|
9477
9611
|
`${relative2(dir, absolute)} is ${human2(info.size)}, over the ${human2(MAX_FILE_BYTES)} limit.`,
|
|
@@ -9485,17 +9619,20 @@ async function collect(dir) {
|
|
|
9485
9619
|
"Static hosting is for a built site, not a source tree."
|
|
9486
9620
|
);
|
|
9487
9621
|
}
|
|
9622
|
+
assertSiteSize([total, info.size]);
|
|
9623
|
+
const bytes = await readArtifactFile(absolute, Math.min(MAX_FILE_BYTES, MAX_SITE_BYTES - total), path);
|
|
9624
|
+
total += bytes.length;
|
|
9488
9625
|
found.push({
|
|
9489
|
-
path
|
|
9490
|
-
|
|
9491
|
-
hash:
|
|
9492
|
-
size:
|
|
9626
|
+
path,
|
|
9627
|
+
bytes,
|
|
9628
|
+
hash: createHash5("sha256").update(bytes).digest("hex"),
|
|
9629
|
+
size: bytes.length
|
|
9493
9630
|
});
|
|
9494
9631
|
}
|
|
9495
9632
|
}
|
|
9496
9633
|
await walk(dir);
|
|
9497
9634
|
assertSiteSize(found.map((file) => file.size));
|
|
9498
|
-
return found.sort((a, b) => a.path.localeCompare(b.path));
|
|
9635
|
+
return { files: found.sort((a, b) => a.path.localeCompare(b.path)), omitted: omitted.sort() };
|
|
9499
9636
|
}
|
|
9500
9637
|
function assertSiteSize(sizes) {
|
|
9501
9638
|
const total = sizes.reduce((sum, size) => sum + size, 0);
|
|
@@ -9957,8 +10094,8 @@ async function remove4(flags, name) {
|
|
|
9957
10094
|
}
|
|
9958
10095
|
|
|
9959
10096
|
// src/commands/architect.ts
|
|
9960
|
-
import { readdir as readdir3, readFile as readFile8, stat as
|
|
9961
|
-
import { join as join6, resolve as
|
|
10097
|
+
import { readdir as readdir3, readFile as readFile8, stat as stat4 } from "node:fs/promises";
|
|
10098
|
+
import { join as join6, resolve as resolve9 } from "node:path";
|
|
9962
10099
|
var FEATURE_PATTERNS = {
|
|
9963
10100
|
auth: /\b(auth(?:entication|enticated|orization|orized)?|log-?ins?|sign-?ins?|sign-?ups?|sessions?|user accounts?|passport|clerk|auth0)\b/i,
|
|
9964
10101
|
storage: /\b(uploads?|attachments?|buckets?|object storage|file (?:uploads?|attachments?|storage)|s3|r2|multer|media)\b/i,
|
|
@@ -9972,7 +10109,7 @@ var FEATURE_PATTERNS = {
|
|
|
9972
10109
|
crawler: /\b(crawls?|crawlers?|scrapes?|playwright|puppeteer|browser automation)\b/i
|
|
9973
10110
|
};
|
|
9974
10111
|
async function architect(flags, ...words) {
|
|
9975
|
-
const root =
|
|
10112
|
+
const root = resolve9(flags.dir ?? ".");
|
|
9976
10113
|
const requirements = words.filter((word) => Boolean(word)).join(" ").trim();
|
|
9977
10114
|
const ctx = await context(flags.api);
|
|
9978
10115
|
if (!flags.project && !ctx.link) {
|
|
@@ -9990,7 +10127,7 @@ async function architect(flags, ...words) {
|
|
|
9990
10127
|
render6(proposal);
|
|
9991
10128
|
}
|
|
9992
10129
|
async function inspectRepository(root, requirements) {
|
|
9993
|
-
const rootStat = await
|
|
10130
|
+
const rootStat = await stat4(root).catch(() => null);
|
|
9994
10131
|
if (!rootStat?.isDirectory()) {
|
|
9995
10132
|
throw new CliError(`Cannot inspect ${root}.`, EXIT.notFound, "Pass --dir with a readable repository directory.");
|
|
9996
10133
|
}
|
|
@@ -10272,6 +10409,9 @@ async function run3(argv) {
|
|
|
10272
10409
|
return name || flags.help ? EXIT.ok : EXIT.usage;
|
|
10273
10410
|
}
|
|
10274
10411
|
try {
|
|
10412
|
+
if (flags.dryRun && !(name === "deploy" || name === "functions" && command[1] === "deploy")) {
|
|
10413
|
+
throw new CliError("--dry-run is only supported for deploy and functions deploy.", EXIT.usage);
|
|
10414
|
+
}
|
|
10275
10415
|
const group = GROUPS[name];
|
|
10276
10416
|
if (group) {
|
|
10277
10417
|
const sub = command[1] ?? "";
|