@runeditrun/rer 0.2.2 → 0.2.5
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 +9 -2
- package/cli/README.md +30 -0
- package/cli/container.mjs +11 -10
- package/cli/contract.mjs +5 -2
- package/cli/demo-host.mjs +47 -0
- package/cli/demo.mjs +47 -0
- package/cli/dependencies.mjs +26 -4
- package/cli/deploy-command.mjs +108 -0
- package/cli/deploy-hook.mjs +31 -0
- package/cli/deploy-inputs.mjs +22 -0
- package/cli/deploy-observation.mjs +29 -0
- package/cli/deploy-owner.mjs +59 -39
- package/cli/deploy-readiness.mjs +16 -0
- package/cli/deploy-source.mjs +24 -0
- package/cli/deploy.mjs +22 -10
- package/cli/detached-server.mjs +1 -1
- package/cli/export.mjs +1 -1
- package/cli/hosted-output.mjs +2 -1
- package/cli/inspect.mjs +2 -2
- package/cli/lifecycle.mjs +20 -7
- package/cli/local.mjs +7 -0
- package/cli/main.mjs +1 -1
- package/cli/managed-server.mjs +27 -18
- package/cli/parse.mjs +7 -2
- package/cli/pnpm-probe.mjs +4 -1
- package/cli/portability.mjs +4 -1
- package/cli/scheduled.mjs +2 -2
- package/cli/setup-input.mjs +1 -1
- package/global/CHANGELOG.md +63 -0
- package/global/README.md +9 -2
- package/global/launch/capacity.mjs +1 -1
- package/global/launch/facts.mjs +12 -6
- package/global/launch/install-pinned.mjs +26 -2
- package/global/launch/prepare.mjs +20 -8
- package/global/launch/state.mjs +64 -11
- package/global/main.mjs +18 -20
- package/global/update/command.mjs +2 -1
- package/modules/container/README.md +3 -1
- package/modules/container/boundary.mjs +5 -2
- package/modules/container/build.mjs +4 -2
- package/modules/container/host.mjs +1 -1
- package/modules/container/ingress.mjs +4 -1
- package/modules/container/migrations.mjs +2 -1
- package/modules/container/worker-engine.mjs +3 -1
- package/modules/demo-host/README.md +30 -0
- package/modules/demo-host/capacity.mjs +19 -0
- package/modules/demo-host/claim.mjs +21 -0
- package/modules/demo-host/container.mjs +74 -0
- package/modules/demo-host/pages.mjs +56 -0
- package/modules/demo-host/plan.mjs +52 -0
- package/modules/demo-host/worker.mjs +72 -0
- package/modules/deploy-kit/README.md +36 -7
- package/modules/deploy-kit/deploy.mjs +9 -2
- package/modules/deploy-kit/env-file.mjs +2 -1
- package/modules/deploy-kit/health-request.mjs +14 -7
- package/modules/deploy-kit/migration-ledger.mjs +38 -1
- package/modules/deploy-kit/migrations.mjs +38 -2
- package/modules/deploy-kit/owner-cli.mjs +30 -9
- package/modules/deploy-kit/owner-remove.mjs +6 -2
- package/modules/deploy-kit/owner-secret-names.mjs +31 -0
- package/modules/deploy-kit/owner-secret.mjs +71 -23
- package/modules/deploy-kit/owner.mjs +78 -25
- package/modules/deploy-kit/prepare.mjs +3 -1
- package/modules/deploy-kit/provider.mjs +3 -0
- package/modules/erasure/README.md +2 -3
- package/modules/operator-auth/accounts/settings-page.mjs +8 -3
- package/modules/operator-auth/csp.mjs +26 -0
- package/modules/operator-auth/dev/badge.mjs +1 -22
- package/modules/operator-auth/page.mjs +2 -2
- package/modules/operator-auth/routes.mjs +1 -1
- package/modules/portability/README.md +9 -0
- package/modules/portability/bridge.mjs +4 -1
- package/modules/portability/catalogue.mjs +9 -2
- package/modules/portability/index.d.mts +4 -0
- package/modules/portability/index.mjs +1 -0
- package/modules/portability/lines.mjs +3 -1
- package/modules/release/produce.mjs +1 -1
- package/modules/release/source.mjs +5 -2
- package/onboarding/adapter-labels.mjs +1 -0
- package/onboarding/adapter.mjs +9 -4
- package/onboarding/dependency-environment.mjs +33 -0
- package/onboarding/local/owner.mjs +1 -1
- package/onboarding/operator-installer.mjs +10 -4
- package/onboarding/redaction.mjs +22 -3
- package/package.json +6 -1
- package/scripts/publish-check/README.md +22 -1
- package/scripts/publish-check/c2pa.py +121 -0
- package/scripts/publish-check/content_identities.py +3 -1
- package/scripts/publish-check/controller_metadata.py +31 -0
- package/scripts/publish-check/history_scan.py +13 -3
- package/scripts/publish-check/media.py +283 -0
- package/scripts/publish-check/png_text.py +34 -0
- package/scripts/publish-check/public_hygiene.py +39 -4
- package/scripts/publish-check/publication-config.json +11 -1
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Mutation readiness errors carry confirmed state separately from the hosted
|
|
2
|
+
// response. Only show that response through the command's existing redactor.
|
|
3
|
+
export function mutationReadinessFailure(error, { again, redact }) {
|
|
4
|
+
if (!error.secretChange) return undefined;
|
|
5
|
+
const { binding, present } = error.secretChange;
|
|
6
|
+
const last = error.attempts?.at(-1);
|
|
7
|
+
const status = last?.status == null ? 'no HTTP response' : `HTTP ${last.status}`;
|
|
8
|
+
const first = redact(String(last?.body ?? '').split(/[\r\n]/, 1)[0]).slice(0, 256);
|
|
9
|
+
const state = error.ownerFinalization
|
|
10
|
+
? `Owner ${error.owner === 'kept' ? 'already claimed and kept' : 'claimed'}; setup token retirement was applied and its new Worker version recorded.`
|
|
11
|
+
: `The secret change for ${binding} was applied and its new Worker version recorded.`;
|
|
12
|
+
const step = error.ownerFinalization ? 'owner claim/token retirement' : `secret change for ${binding}`;
|
|
13
|
+
const reason = /^[a-zA-Z0-9_-]{1,120}$/.test(error.message) ? error.message : 'health-check-failed';
|
|
14
|
+
const retirement = error.ownerFinalization || binding === 'SETUP_TOKEN' && !present;
|
|
15
|
+
return redact(`${state} Hosted readiness failed after ${step}. ${status} (${reason}).${first ? ` First response line: ${first}` : ''} Update the seed/app configuration to match the changed secrets and redeploy with ${again}. ${retirement ? 'If the app requires the spent setup token, remove that requirement from its configuration before redeploying; keep the recorded owner and token retirement.' : 'If the secret input was incorrect, correct it and apply the intended secret change again.'}`);
|
|
16
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { refuse } from './errors.mjs';
|
|
3
|
+
import { deployAgain } from './deploy-inputs.mjs';
|
|
4
|
+
|
|
5
|
+
// rer deploy publishes the checkout's last commit: the deploy identity names it.
|
|
6
|
+
// Say so before any build or upload, naming the tracked files that differ.
|
|
7
|
+
export function committedSource(root, options) {
|
|
8
|
+
const git = (...args) => execFileSync('git', args, { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
9
|
+
const again = deployAgain(options);
|
|
10
|
+
let status, sha;
|
|
11
|
+
try { status = git('status', '--porcelain', '-z', '--untracked-files=no'); sha = git('rev-parse', 'HEAD').trim(); }
|
|
12
|
+
catch { throw refuse(`rer deploy publishes the checkout's last Git commit, and this checkout has none. Commit the source with Git, then run ${again} again. Nothing was uploaded.`); }
|
|
13
|
+
const entries = status.split('\0'), paths = [];
|
|
14
|
+
for (let index = 0; index < entries.length; index++) {
|
|
15
|
+
if (!entries[index]) continue;
|
|
16
|
+
paths.push(entries[index].slice(3));
|
|
17
|
+
// A rename or copy is followed by its original path.
|
|
18
|
+
if (/^([RC].|.[RC])/.test(entries[index])) index++;
|
|
19
|
+
}
|
|
20
|
+
if (!paths.length) return sha;
|
|
21
|
+
const shown = paths.slice(0, 20).map(path => ` ${path}`);
|
|
22
|
+
if (paths.length > shown.length) shown.push(` and ${paths.length - shown.length} more`);
|
|
23
|
+
throw refuse(`Deploy stopped before building: these tracked files have changes that are not committed:\n${shown.join('\n')}\nrer deploy publishes the last commit. Commit the changes (for example git commit -am "Describe the change") or discard them, then run ${again} again. Nothing was uploaded.`);
|
|
24
|
+
}
|
package/cli/deploy.mjs
CHANGED
|
@@ -6,8 +6,10 @@ import { missing, invalid, refuse, RerError } from './errors.mjs';
|
|
|
6
6
|
import { ownerPlan, ownerWorkerName, localOnlyBinding } from '../modules/deploy-kit/owner.mjs';
|
|
7
7
|
import { packageBuild } from '../modules/deploy-kit/prepare.mjs';
|
|
8
8
|
import { refuseLocalModels } from '../modules/deploy-kit/local-only.mjs';
|
|
9
|
-
import { deployEnvironment } from './deploy-inputs.mjs';
|
|
9
|
+
import { deployEnvironment, deployAgain } from './deploy-inputs.mjs';
|
|
10
10
|
import { claimHostedOwner } from './deploy-owner.mjs';
|
|
11
|
+
import { mutationReadinessFailure } from './deploy-readiness.mjs';
|
|
12
|
+
import { committedSource } from './deploy-source.mjs';
|
|
11
13
|
|
|
12
14
|
export function ownerDeploy(declarationPath, lifecycle) {
|
|
13
15
|
// rer verify serves the build with this declaration's vars and secrets.
|
|
@@ -18,13 +20,16 @@ export function ownerDeploy(declarationPath, lifecycle) {
|
|
|
18
20
|
const local = localOnlyBinding(declaration, localOnlyKeys);
|
|
19
21
|
if (local) throw refuse(`${declarationPath} names the local-only key ${local} as a deploy binding or secret. Remove it; local dev keys never reach a deployment.`);
|
|
20
22
|
const environment = options.environment ?? 'production';
|
|
21
|
-
const again =
|
|
23
|
+
const again = deployAgain(options);
|
|
22
24
|
const change = [options.remove && '--remove', options.setSecret && '--set-secret', options.unsetSecret && '--unset-secret'].filter(Boolean);
|
|
23
25
|
// The command that finishes what this run started: the same verb, never a plain deploy in its place.
|
|
24
26
|
const retry = options.remove ? `${again} --remove --yes` : options.setSecret ? `${again} --set-secret ${options.setSecret}` : options.unsetSecret ? `${again} --unset-secret ${options.unsetSecret}` : again;
|
|
25
27
|
if (change.length > 1 || change.length && options.dryRun) throw invalid(`Use ${change.join(', ')}${options.dryRun ? ' and --dry-run' : ''} on separate runs.`);
|
|
26
28
|
if (options.remove && !options.yes) throw invalid(`${again} --remove deletes this environment's Worker and every D1, R2, KV and queue rer created for it, with their data. Add --yes to confirm.`);
|
|
29
|
+
// A dry run reads the settings file too, so a refused key fails here and not only on the real deploy.
|
|
30
|
+
if (options.dryRun && options.envFile) await deployEnvironment({ root, options, declaration, localOnlyKeys });
|
|
27
31
|
if (!change.length) {
|
|
32
|
+
if (!options.dryRun) committedSource(root, options);
|
|
28
33
|
await lifecycle.run(root, {}, store, () => {}, signal, { verb: 'build', operationToken });
|
|
29
34
|
await checkBuild();
|
|
30
35
|
}
|
|
@@ -37,9 +42,9 @@ export function ownerDeploy(declarationPath, lifecycle) {
|
|
|
37
42
|
store.report('deploy: dry run complete; no deployment occurred and no provider request was made.');
|
|
38
43
|
return { dryRun: true, deployed: false };
|
|
39
44
|
}
|
|
40
|
-
const names = ['CLOUDFLARE_ACCOUNT_ID', 'CLOUDFLARE_API_TOKEN'];
|
|
41
|
-
if (names.some(name => !process.env[name]?.trim())) throw missing('CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN are required. See the deploy-kit owner-account instructions.');
|
|
42
45
|
const { env, supplied } = await deployEnvironment({ root, options, declaration, setupManifest, settings: lifecycle.settings, localOnlyKeys });
|
|
46
|
+
const names = ['CLOUDFLARE_ACCOUNT_ID', 'CLOUDFLARE_API_TOKEN'];
|
|
47
|
+
if (names.some(name => !env[name]?.trim())) throw missing('CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN are required. See the deploy-kit owner-account instructions.');
|
|
43
48
|
const { main } = await import('../modules/deploy-kit/owner-cli.mjs');
|
|
44
49
|
const { createRedaction, credentialValues } = await shared('redaction.mjs');
|
|
45
50
|
const redaction = createRedaction();
|
|
@@ -61,35 +66,40 @@ export function ownerDeploy(declarationPath, lifecycle) {
|
|
|
61
66
|
step = 'secrets';
|
|
62
67
|
const binding = options.setSecret ?? options.unsetSecret;
|
|
63
68
|
await owner('secret', [options.setSecret ? '--set' : '--unset', binding]);
|
|
64
|
-
store.report(`${options.setSecret ? `Set ${binding}` : `Removed ${binding}`} on this environment's Worker
|
|
69
|
+
store.report(`${options.setSecret ? `Set ${binding}` : `Removed ${binding}`} on this environment's Worker, recorded its new version and verified hosted readiness.`);
|
|
65
70
|
return { binding, present: Boolean(options.setSecret), deployed: false };
|
|
66
71
|
}
|
|
72
|
+
store.report(`Deploying the ${environment} environment to your Cloudflare account.`);
|
|
67
73
|
deployed = await owner('deploy', [], env, { notice: line => store.report(line), onCreated: resource => created.push(redaction.redact(resource)) });
|
|
68
74
|
const sent = supplied.filter(name => deployed.sentSecrets.includes(name));
|
|
69
75
|
if (sent.length) store.report(`rer supplied ${sent.join(', ')} from the local setup owner and fresh generated values.`);
|
|
70
76
|
step = 'health';
|
|
71
77
|
await owner('health');
|
|
78
|
+
store.report(`Your application is live on your Cloudflare account at ${deployed.origin}.`);
|
|
72
79
|
step = 'claim owner';
|
|
80
|
+
const { liveSecrets, retiredSecrets, ownerClaim } = await owner('inspect');
|
|
73
81
|
const linked = token => linkDependencies?.(deployed.origin, token);
|
|
74
|
-
const claimed = await claimHostedOwner({ hook: lifecycle.hooks?.owner, setupManifest, declaration, deployed, env, supplied, addSecret: value => redaction.addSecrets([value]),
|
|
75
|
-
secret: (extra, values) => owner('secret', extra, values), beforeClaim: linked });
|
|
82
|
+
const claimed = await claimHostedOwner({ hook: lifecycle.hooks?.owner, setupManifest, declaration, deployed: { ...deployed, liveSecrets, retiredSecrets, ownerClaim }, env, supplied, addSecret: value => { redaction.addSecrets([value]); store.addSecrets?.([value]); },
|
|
83
|
+
secret: (extra, values) => owner('secret', extra, values), beforeClaim: linked, recordClaim: outcome => owner('record-owner', ['--outcome', outcome]), inspect: () => owner('inspect'), store, signal });
|
|
76
84
|
// A seed that claims its owner its own way links with the token it was given.
|
|
77
85
|
if (claimed === undefined) await linked(env[declared(claim?.tokenField ?? 'SETUP_TOKEN')?.environment]);
|
|
78
86
|
// The app claims the OWNER_EMAIL the Worker holds: the local one only when this run uploaded it.
|
|
79
87
|
const email = declared(claim?.emailField);
|
|
80
88
|
const named = !email ? '' : deployed.sentSecrets.includes(email.binding) && env[email.environment] ? ` (${env[email.environment]})` : ` (the ${email.binding} already set on the Worker)`;
|
|
81
|
-
if (claimed) store.report(`${claimed === 'claimed' ? 'Claimed the hosted owner' : 'The hosted owner was already claimed and was kept'}${named}; the one-time setup token was
|
|
89
|
+
if (claimed) store.report(`${claimed === 'claimed' ? 'Claimed the hosted owner' : 'The hosted owner was already claimed and was kept'}${named}; the one-time setup token was removed. Sign in by email at ${deployed.origin}.`);
|
|
82
90
|
return { deployed: true, origin: deployed.origin, ...(claimed ? { owner: claimed } : {}), dependenciesLinked: true };
|
|
83
91
|
} catch (error) {
|
|
84
92
|
if (error instanceof RerError) throw error;
|
|
85
93
|
const { commandFailure } = await shared('failures.mjs');
|
|
86
94
|
throw commandFailure(failureText({ error, step, created, again, retry, secretChange: Boolean(change.length && !options.remove), deployed, redact: redaction.redact }));
|
|
87
95
|
}
|
|
88
|
-
}, { declaration: declarationPath });
|
|
96
|
+
}, { declaration: declarationPath, managesHostedOwner: true });
|
|
89
97
|
}
|
|
90
98
|
|
|
91
99
|
// The failing step, its redacted cause, what this run created and the command that continues.
|
|
92
100
|
function failureText({ error, step, created, again, retry, secretChange, deployed, redact }) {
|
|
101
|
+
const readiness = mutationReadinessFailure(error, { again, redact });
|
|
102
|
+
if (readiness) return readiness;
|
|
93
103
|
const diagnostic = deployDiagnostic(error, redact);
|
|
94
104
|
const code = diagnostic?.code ?? (/^[a-zA-Z0-9_-]{1,120}$/.test(error.message) ? redact(error.message) : 'deploy-failed');
|
|
95
105
|
const at = step === 'deploy' ? diagnostic?.step ?? 'prepare deploy' : step;
|
|
@@ -97,8 +107,10 @@ function failureText({ error, step, created, again, retry, secretChange, deploye
|
|
|
97
107
|
if (input) return `Deploy needs ${input}, which the deploy declaration lists as a secret and the Worker does not hold it. Set ${input} in the environment or in an --env-file file (an owned 0600 file of KEY=VALUE lines), then run the same command again. Nothing was uploaded.${created.length ? ` Resources created this attempt: ${created.join(', ')}.` : ''}`;
|
|
98
108
|
if (code === 'remove-incomplete') return `Removal stopped with ${error.remaining.join(', ')} still present${error.failed.length ? ` (${error.failed.map(item => `${item.resource}: ${item.code}`).join('; ')})` : ''}. Removed: ${error.removed.join(', ') || 'nothing'}. An R2 bucket must be empty before it can be deleted. Run ${again} --remove --yes again to finish.`;
|
|
99
109
|
if (code === 'nothing-to-remove') return `This checkout has no rer deployment record for this environment, so --remove deletes nothing. Run it from the checkout that deployed it.`;
|
|
100
|
-
if (code === 'owner-
|
|
110
|
+
if (code === 'owner-token-retirement-failed') return `The application is deployed at ${deployed?.origin}; owner ${error.owner}; setup token retirement failed (${redact(String(error.cause))}). Run ${again} --unset-secret ${error.tokenKey ?? 'SETUP_TOKEN'} using the same environment credentials, or run ${again} again to finish.`;
|
|
111
|
+
if (code === 'owner-claim-failed') return `The application is deployed at ${deployed?.origin}, but claiming its owner failed: ${redact(String(error.cause))}. Owner claim and setup token retirement are not confirmed; run ${again} again using the same environment credentials to finish.`;
|
|
101
112
|
const resource = diagnostic?.resource ?? (error.resource ? redact(String(error.resource)) : undefined);
|
|
113
|
+
if (code === 'resource-not-listed') return `Deploy stopped at ${at}: ${resource} was created, but Cloudflare's account listing still does not show it after two minutes. Nothing was uploaded.\nResources created this attempt: ${created.join(', ') || 'none'}.\nRun ${again} again once the listing catches up: it reuses ${resource}. To delete what this environment created instead: ${again} --remove --yes.`;
|
|
102
114
|
const lines = [`Deploy failed at ${at}: ${code}${resource ? ` (${resource})` : ''}${diagnostic?.providerCodes?.length ? ` (provider ${diagnostic.providerCodes.join(', ')})` : ''}.`];
|
|
103
115
|
if (diagnostic?.output && diagnostic.output !== '(no provider output)') lines.push(`First redacted output lines:\n${diagnostic.output}`);
|
|
104
116
|
lines.push(`Resources created this attempt: ${created.join(', ') || 'none'}.`);
|
package/cli/detached-server.mjs
CHANGED
|
@@ -35,7 +35,7 @@ export async function detachedServer(options, argv, out) {
|
|
|
35
35
|
if (event.event==='ready') {
|
|
36
36
|
keep=true;child.unref();
|
|
37
37
|
const dev = event.openUrl ? {openUrl:event.openUrl,identity:options.verb === 'dev' && !options.login,signedInAs:event.signedInAs,ownerLeft:options.verb === 'dev' && !options.login && event.signedInAs === null} : undefined;
|
|
38
|
-
ready(out,event.url,dev,options.json,Boolean(event.signInUrl),event.siteUrl);
|
|
38
|
+
ready(out,event.url,dev,options.json,Boolean(event.signInUrl),event.siteUrl,event.demo);
|
|
39
39
|
return 0;
|
|
40
40
|
}
|
|
41
41
|
if (event.event==='waiting' || event.event==='warning') out.event(event.event,event);
|
package/cli/export.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { cliVersion } from './version.mjs';
|
|
|
6
6
|
|
|
7
7
|
// Copy whole maintained source/tooling directories. Python publication scans
|
|
8
8
|
// load sibling modules and data dynamically; they belong in the inventory too.
|
|
9
|
-
export const sourceRoots = ['bin/rer.mjs', 'cli', 'modules/job-queue', 'modules/release', 'modules/deploy-kit', 'modules/container', 'modules/integrations/core', 'modules/mail', 'modules/ai', 'modules/portability', 'modules/erasure', 'modules/dependencies', 'modules/operator-auth', 'modules/owner-claim', 'operations', 'scripts/publish-check', 'scripts/publish-check.mjs', 'composition/git-source.mjs', 'edition-authoring/manifest.mjs', 'edition-authoring/files.mjs'];
|
|
9
|
+
export const sourceRoots = ['bin/rer.mjs', 'cli', 'modules/job-queue', 'modules/release', 'modules/deploy-kit', 'modules/container', 'modules/demo-host', 'modules/integrations/core', 'modules/mail', 'modules/ai', 'modules/portability', 'modules/erasure', 'modules/dependencies', 'modules/operator-auth', 'modules/owner-claim', 'operations', 'scripts/publish-check', 'scripts/publish-check.mjs', 'composition/git-source.mjs', 'edition-authoring/manifest.mjs', 'edition-authoring/files.mjs'];
|
|
10
10
|
export async function inventory(base, roots = sourceRoots) {
|
|
11
11
|
const files = [];
|
|
12
12
|
async function walk(path) {
|
package/cli/hosted-output.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import { pathToFileURL } from 'node:url';
|
|
|
6
6
|
import { RUN_WORKER_FIRST } from '../modules/deploy-kit/render.mjs';
|
|
7
7
|
import { publicAssetPaths } from '../modules/deploy-kit/artifact.mjs';
|
|
8
8
|
import { migrate } from '../modules/container/migrations.mjs';
|
|
9
|
+
import { readMigrationRewrites, withRewrites } from '../modules/deploy-kit/migrations.mjs';
|
|
9
10
|
import { RerError } from './errors.mjs';
|
|
10
11
|
|
|
11
12
|
// Serve the production build the way rer deploy hosts it (assets behind the
|
|
@@ -76,7 +77,7 @@ export async function serveHosted(root, { declaration: declarationPath = 'deploy
|
|
|
76
77
|
const sql = await readFile(join(directory, name), 'utf8');
|
|
77
78
|
plan.push({ name, sql, sha256: createHash('sha256').update(sql).digest('hex'), statements: split(sql) });
|
|
78
79
|
}
|
|
79
|
-
await migrate(await mf.getD1Database(item.binding, 'app'), plan);
|
|
80
|
+
await migrate(await mf.getD1Database(item.binding, 'app'), withRewrites(plan, await readMigrationRewrites(root), { partial: true }));
|
|
80
81
|
}
|
|
81
82
|
} catch (error) { await mf.dispose(); throw error; }
|
|
82
83
|
return { client, identity, origin: ORIGIN, close: () => mf.dispose(),
|
package/cli/inspect.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { inventory } from './export.mjs';
|
|
|
3
3
|
import { lstat, readFile } from 'node:fs/promises';
|
|
4
4
|
import { createHash } from 'node:crypto';
|
|
5
5
|
import { delimiter, dirname, resolve } from 'node:path';
|
|
6
|
-
import { aliases, pins, requiredHooks, serverDefaults, verbs, releaseFlags } from './contract.mjs';
|
|
6
|
+
import { aliases, demoVerb, pins, requiredHooks, serverDefaults, verbs, releaseFlags } from './contract.mjs';
|
|
7
7
|
import { runningVersion } from './version.mjs';
|
|
8
8
|
import { contained, readPackage } from './project.mjs';
|
|
9
9
|
import { shared } from './shared.mjs';
|
|
@@ -41,7 +41,7 @@ export async function describe(project) {
|
|
|
41
41
|
? operationsEffects
|
|
42
42
|
: verbs[verb]?.effects ?? 'Produce releases; keygen writes a public anchor and, on success, its private key to non-terminal stdout; sign writes a detached signature and prints (but does not execute) the GitHub upload command; receipt verifies against local anchors offline.';
|
|
43
43
|
return [verb, { flags: (verbs[verb]?.flags ?? releaseFlags).filter(flag => capability.flags?.includes(flag)), effects }];
|
|
44
|
-
})),
|
|
44
|
+
}).concat(capabilities.dev?.flags?.includes('--demo') ? [['demo', { flags: demoVerb.flags.filter(flag => capabilities.dev.flags.includes(flag)), effects: demoVerb.effects }]] : [])),
|
|
45
45
|
aliases: Object.fromEntries(Object.entries(aliases).filter(([alias, verb]) => pkg.scripts?.[alias] === commandLine(verb, declaredPackage(pkg) !== undefined))),
|
|
46
46
|
local: { state: project.adapter?.state, settings: project.adapter?.settings ?? [], installerState: '.seed/installer', outbox: '/__dev/outbox', ...await localState(project), origin: await savedOrigin(project), ...(project.adapter?.devIdentity ? { devIdentity: true, devKey: '.rer/local/dev-key.env' } : {}) },
|
|
47
47
|
// This pin serves a checkout through the machine's front door (rer door).
|
package/cli/lifecycle.mjs
CHANGED
|
@@ -17,6 +17,16 @@ import { ensureDevKey, readDevKey, refuseDevLayerOutput, openUrl, DEV_KEY_HEADER
|
|
|
17
17
|
import { announceDoorRoute, routeRunning, verifyDoorFor } from './door-mode.mjs';
|
|
18
18
|
|
|
19
19
|
export async function runCommand(project, options, out) {
|
|
20
|
+
if (options.verb === 'deploy' && !options.site && !options.demo) {
|
|
21
|
+
const { validateDeployOptions } = await import('./deploy-command.mjs');
|
|
22
|
+
validateDeployOptions(options);
|
|
23
|
+
const { withDeployEnvironment } = await import('./deploy-inputs.mjs');
|
|
24
|
+
return withDeployEnvironment(project.root, options, redaction => runLifecycle(project, options, out, redaction));
|
|
25
|
+
}
|
|
26
|
+
return runLifecycle(project, options, out);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function runLifecycle(project, options, out, deployRedaction) {
|
|
20
30
|
if(project.profile==='first-party'&&['dev','preview'].includes(options.verb))return (await import('./managed-server.mjs')).managedServer(project,options,out);
|
|
21
31
|
const cancellation = new AbortController(), signal = cancellation.signal;
|
|
22
32
|
let termination, operation, store, temporary, proxy, failed = false;
|
|
@@ -59,6 +69,7 @@ export async function runCommand(project, options, out) {
|
|
|
59
69
|
const state = { logs: [] };
|
|
60
70
|
store = await createCommands(directory, state, { terminal: text => out.progress(text), onWaiting: options.json ? details => out.event('waiting', details) : undefined, queue: ['install','setup','build','verify','deploy','export','import'].includes(options.verb)?{verb:options.verb,originalCI:process.env.CI==='true'}:undefined });
|
|
61
71
|
store.state = state;
|
|
72
|
+
if (deployRedaction) store.addSecrets?.(Object.values(process.env).filter(value => value && deployRedaction.redact(value) !== value));
|
|
62
73
|
store.setupManifest = project.setupManifest;
|
|
63
74
|
store.step = async label => { state.step = store.redact(label); store.report(label); };
|
|
64
75
|
store.phase = async phase => { state.phase = phase; };
|
|
@@ -69,8 +80,11 @@ export async function runCommand(project, options, out) {
|
|
|
69
80
|
const adapter = project.adapter;
|
|
70
81
|
// The run site: static pages built from edition identity, never the app build.
|
|
71
82
|
if (options.site) return await (await import('./site.mjs')).siteCommand(project, options, store, signal, out);
|
|
83
|
+
// The hosted demo: a front Worker and the seed's container image on demo.<domain>.
|
|
84
|
+
if (options.verb === 'deploy' && options.demo) return await (await import('./demo-host.mjs')).demoHostCommand(project, options, store, signal, out, operation);
|
|
72
85
|
if (options.verb === 'install') {
|
|
73
|
-
|
|
86
|
+
const { dependencyEnvironment } = await shared('dependency-environment.mjs');
|
|
87
|
+
await store.run('Installing frozen dependencies', 'pnpm', ['install', '--frozen-lockfile'], { cwd: project.root, signal, env: { ...dependencyEnvironment(), CI: 'true' } }); return {};
|
|
74
88
|
}
|
|
75
89
|
if (options.verb === 'verify') {
|
|
76
90
|
const target = adapter.verify;
|
|
@@ -86,10 +100,7 @@ export async function runCommand(project, options, out) {
|
|
|
86
100
|
if (options.verb === 'deploy') {
|
|
87
101
|
if (typeof adapter.deploy !== 'function') throw refuse('Owner-account deploy declaration is missing.');
|
|
88
102
|
const link = async (origin, appSetupToken) => linkDependencies({project,input:project.setupSeed?await savedSetupAnswers(project,project.setupSeed.model):undefined,origin,store,signal,appSetupToken});
|
|
89
|
-
|
|
90
|
-
// Base's owner-account deploy links while its setup token is live; another deploy hook is linked here.
|
|
91
|
-
if(deployed.deployed && !options.dryRun && !deployed.dependenciesLinked) await link(deployed.origin,process.env[project.setupManifest?.steps.find(step=>step.type==='owner-claim')?.tokenField??'SETUP_TOKEN']);
|
|
92
|
-
return deployed;
|
|
103
|
+
return await (await import('./deploy-command.mjs')).deployCommand({ adapter, root: project.root, options, store, signal, operationToken: operation.token, localOnlyKeys: adapter.localOnlyKeys ?? [], checkBuild: () => refuseDevLayerOutput(project.root, adapter.devIdentity), setupManifest: project.setupManifest, linkDependencies: link });
|
|
93
104
|
}
|
|
94
105
|
// Setup writes the checkout's dev key; its temporary app and the CLI's own
|
|
95
106
|
// outbox reads present it. Captured logs redact it.
|
|
@@ -114,8 +125,10 @@ export async function runCommand(project, options, out) {
|
|
|
114
125
|
if (['setup', 'build'].includes(options.verb)) await refuseDevLayerOutput(project.root, adapter.devIdentity);
|
|
115
126
|
const url = selected?.url, signInUrl = url ? url + '/__dev/outbox' : undefined;
|
|
116
127
|
if (options.verb === 'setup') {
|
|
117
|
-
|
|
118
|
-
|
|
128
|
+
// Only a connected provider leaves first setup open; a native claim with
|
|
129
|
+
// every optional dependency unanswered has already closed it.
|
|
130
|
+
const {connected}=await linkDependencies({project,input,origin:url,store,signal});
|
|
131
|
+
if(connected.length) {
|
|
119
132
|
const proof=await localCliSessionProof(project);
|
|
120
133
|
store.addSecrets?.([proof]);
|
|
121
134
|
const completed=await appFetch(url+'/auth/setup-complete',{method:'POST',headers:{Origin:url,'Content-Type':'application/json','x-rer-local-cli':proof},body:'{}',signal});
|
package/cli/local.mjs
CHANGED
|
@@ -73,6 +73,13 @@ export async function reset(project, options, out) {
|
|
|
73
73
|
await refuseLegacyProcess(project,{reset:true});
|
|
74
74
|
const { strandedOriginals } = await shared('local/recovery.mjs');
|
|
75
75
|
if ((await strandedOriginals(project.root)).length) throw refuse('An interrupted build holds private settings. Run rer build to recover them before reviewing reset.');
|
|
76
|
+
// The seed's own safety check (legacy data it still has to recover, for
|
|
77
|
+
// example) runs before the inventory and confirmation; --yes cannot skip it.
|
|
78
|
+
const preflight = project.adapter.resetPreflight;
|
|
79
|
+
if (preflight !== undefined) {
|
|
80
|
+
if (typeof preflight !== 'function') throw refuse('The seed adapter declares a reset preflight that is not a function.');
|
|
81
|
+
try { await preflight({ root: project.root }); } catch (error) { throw refuse(`Reset refused by the seed's preflight: ${error?.message || 'no reason given'}`); }
|
|
82
|
+
}
|
|
76
83
|
const declared = project.adapter.settings ?? [];
|
|
77
84
|
for (const path of declared) if (!/^\.dev\.vars(?:\.previous)?$|^data\/[a-zA-Z0-9_-]+\.json$/.test(path)) throw refuse('Generated settings inventory is unsafe; declare specific private settings files.');
|
|
78
85
|
const paths = [...new Set(['.wrangler/state', '.rer/local', '.seed/installer', ...declared])];
|
package/cli/main.mjs
CHANGED
|
@@ -27,7 +27,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
27
27
|
if (project.profile === 'independent' && !['describe', 'check', 'doctor'].includes(options.verb)) options = parse(argv, process.cwd(), { independent: true });
|
|
28
28
|
else if (options.forwarded.length) throw invalid('First-party commands accept only the documented flags, without forwarded arguments.');
|
|
29
29
|
out = output(options);
|
|
30
|
-
if (project.profile === 'first-party' && options.detach && ['dev','preview'].includes(options.verb)) return (await import('./detached-server.mjs')).detachedServer(options, argv, out);
|
|
30
|
+
if (project.profile === 'first-party' && options.detach && ['dev','preview'].includes(options.verb)) return await (await import('./detached-server.mjs')).detachedServer(options, argv, out);
|
|
31
31
|
if (options.verb === 'describe') { const result = await describe(project); out.human(options.json ? JSON.stringify(result) : JSON.stringify(result, null, 2)); return 0; }
|
|
32
32
|
if (options.verb === 'check') {
|
|
33
33
|
const result = await check(project, options.profile);
|
package/cli/managed-server.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { prepareLiveModels } from './ai-local.mjs';
|
|
2
2
|
import {fileURLToPath} from 'node:url';
|
|
3
3
|
import {join} from 'node:path';
|
|
4
|
+
import {rm} from 'node:fs/promises';
|
|
4
5
|
import { serverRegistry } from './server-registry.mjs';
|
|
5
6
|
import { activityClock } from './server-activity.mjs';
|
|
6
7
|
import { runtimeFrontDoor } from './runtime-front-door.mjs';
|
|
@@ -13,6 +14,7 @@ import { tools } from './inspect.mjs';
|
|
|
13
14
|
import { invalid,missing,refuse,RerError,lifecycleError } from './errors.mjs';
|
|
14
15
|
import { processInfo } from '../modules/job-queue/processes.mjs';
|
|
15
16
|
import { declaredCrons,runScheduled } from './scheduled.mjs';
|
|
17
|
+
import { demoAdopted,demoGuarded,demoState } from './demo.mjs';
|
|
16
18
|
import { readDevKey,requireDevKey,refuseManagedInstallation,resolvePersona,openUrl,DEV_KEY_HEADER } from './dev-identity.mjs';
|
|
17
19
|
import { announceDoorRoute,claimSavedName,moveCheckout,routeRunning,verifyDoorFor } from './door-mode.mjs';
|
|
18
20
|
|
|
@@ -22,9 +24,10 @@ import { announceDoorRoute,claimSavedName,moveCheckout,routeRunning,verifyDoorFo
|
|
|
22
24
|
// serves no outbox (a seed's preview without one) is not pointed at it.
|
|
23
25
|
// A seed that declares a run site also gets RER_SITE: its static run site
|
|
24
26
|
// (homepage, /run, docs) served locally on its own port, after the ready lines.
|
|
25
|
-
|
|
27
|
+
// A demo adds RER_DEMO_ENDS_ON_STOP: its own data folder, deleted when it stops.
|
|
28
|
+
export function ready(out,url,dev,json=false,outbox=true,siteUrl,demo){
|
|
26
29
|
const signInUrl=url+'/__dev/outbox',open=dev&&(dev.identity||outbox);
|
|
27
|
-
if(json){out.event('ready',{url,...(outbox?{signInUrl}:{}),...(dev?{signedInAs:dev.signedInAs??null,...(open?{openUrl:dev.openUrl}:{})}:{}),...(siteUrl?{siteUrl}:{})});return;}
|
|
30
|
+
if(json){out.event('ready',{url,...(outbox?{signInUrl}:{}),...(dev?{signedInAs:dev.signedInAs??null,...(open?{openUrl:dev.openUrl}:{})}:{}),...(siteUrl?{siteUrl}:{}),...(demo?{demo}:{})});return;}
|
|
28
31
|
out.human(`RER_READY ${url}`);
|
|
29
32
|
if(dev?.identity){if(dev.signedInAs)out.human(`RER_SIGNED_IN ${dev.signedInAs}`);else if(dev.ownerLeft)out.human('No active owner remains in this app, so rer dev cannot sign in as one. Use rer dev --login with another operator, or rer reset --yes and rer setup for a new owner.');out.human(`RER_OPEN ${dev.openUrl}`);out.human('Open that link on this computer. Its fragment holds this checkout\'s dev key, which the page keeps only as a cookie; never share the link.');}
|
|
30
33
|
else if(outbox){
|
|
@@ -33,10 +36,13 @@ export function ready(out,url,dev,json=false,outbox=true,siteUrl){
|
|
|
33
36
|
out.human(`Open ${dev?'the last link on this computer (it carries the dev key; never share it)':'the outbox'}, choose the newest sign-in message and follow its link. No email was sent.`);
|
|
34
37
|
}
|
|
35
38
|
if(siteUrl)out.human(`RER_SITE ${siteUrl}`);
|
|
39
|
+
if(demo){out.human(`RER_DEMO_ENDS_ON_STOP ${demo.state}`);out.human(`This demo runs on its own data in that folder, which is deleted when it stops: rer demo --stop, Ctrl-C, or at its end (${demo.ends.slice(11,16)}). Nothing it does is sent; this checkout's own data is untouched.`);}
|
|
36
40
|
}
|
|
37
41
|
export async function managedServer(project,options,out,{clock={}}={}){
|
|
38
42
|
const registry=await serverRegistry(project.root),controller=new AbortController();
|
|
39
|
-
|
|
43
|
+
// A demo registers as its own mode: `rer dev --stop` never stops it by accident.
|
|
44
|
+
const mode=options.demo?'demo':options.verb;
|
|
45
|
+
let store,operation,proxy,activity,app,termination,reason,cleanupPromise,finished,selected,cleaned,site,demo,demoEnd;
|
|
40
46
|
const scheduling=new AbortController();
|
|
41
47
|
let activityWrites=Promise.resolve(),pendingActivity,activityWriting=false,infrastructureFailure;
|
|
42
48
|
function recordActivity(fields){
|
|
@@ -45,27 +51,27 @@ export async function managedServer(project,options,out,{clock={}}={}){
|
|
|
45
51
|
}
|
|
46
52
|
const stopped=new Promise(resolve=>{finished=resolve;});
|
|
47
53
|
const cleanupDone=new Promise(resolve=>{cleaned=resolve;});
|
|
48
|
-
const cleanup=()=>cleanupPromise??=(async()=>{scheduling.abort();activity?.close();await site?.close();await store?.closeCommands();await proxy?.close();await operation?.release();})();
|
|
54
|
+
const cleanup=()=>cleanupPromise??=(async()=>{scheduling.abort();clearTimeout(demoEnd);activity?.close();await site?.close();await store?.closeCommands();if(demo)await rm(demo.state,{recursive:true,force:true});await proxy?.close();await operation?.release();})();
|
|
49
55
|
const stop=async why=>{reason??=why;controller.abort();finished();await cleanupDone;return {};};
|
|
50
56
|
const handlers=new Map(['SIGINT','SIGTERM'].map(name=>[name,()=>{termination=name;void stop(name);} ]));
|
|
51
57
|
for(const [name,handler]of handlers)process.on(name,handler);
|
|
52
58
|
try{
|
|
53
|
-
if(options.stop){await registry.stop(
|
|
59
|
+
if(options.stop){await registry.stop(mode,options);return {};}
|
|
54
60
|
controller.signal.throwIfAborted();
|
|
55
61
|
if(options.door||options.direct)await moveCheckout(project,options,out,registry);
|
|
56
62
|
const devIdentity=project.adapter.devIdentity;
|
|
57
63
|
if(options.as!==undefined&&!(devIdentity&&options.verb==='dev'))throw invalid('--as needs a seed that declares devIdentity.');
|
|
58
64
|
let devKey=devIdentity?await readDevKey(project.root):null;
|
|
59
|
-
let current=await registry.existing(
|
|
65
|
+
let current=await registry.existing(mode,options);
|
|
60
66
|
if(!current){
|
|
61
|
-
const reservation=await registry.reserve(
|
|
67
|
+
const reservation=await registry.reserve(mode,options);
|
|
62
68
|
current=reservation.existing;
|
|
63
69
|
}
|
|
64
70
|
if(current){
|
|
65
71
|
// Resolve a competing reservation through the same explicit-option check.
|
|
66
|
-
current=await registry.existing(
|
|
72
|
+
current=await registry.existing(mode,options);
|
|
67
73
|
const running=await registry.waitReady(current,controller.signal);
|
|
68
|
-
await registry.existing(
|
|
74
|
+
await registry.existing(mode,options);
|
|
69
75
|
if(options.verb==='dev' && (running.models??'fake')!==(options.models??'fake'))throw refuse('Stop the running dev server before changing --models mode.');
|
|
70
76
|
if(options.modelsService)throw refuse('Stop the running dev server before saving live-dev credentials.');
|
|
71
77
|
if(options.verb==='dev'&&Boolean(running.login)!==Boolean(options.login))throw refuse(`The running dev server ${running.login?'uses':'does not use'} --login. Stop it with rer dev --stop to change sign-in mode.`);
|
|
@@ -79,10 +85,10 @@ export async function managedServer(project,options,out,{clock={}}={}){
|
|
|
79
85
|
// A key is sent (or printed) for a door origin only once the door proves it is ours.
|
|
80
86
|
if(devIdentity)await verifyDoorFor(running.url);
|
|
81
87
|
const dev=devIdentity&&await devReady(running.url,devKey,options,controller.signal);
|
|
82
|
-
await emitReady(out,options,project.root,running.url,dev,true,running.siteUrl);return {reused:true};
|
|
88
|
+
await emitReady(out,options,project.root,running.url,dev,true,running.siteUrl,running.demo);return {reused:true};
|
|
83
89
|
}
|
|
84
90
|
await registry.serve(async action=>{
|
|
85
|
-
if(action==='status')return {...activity?.snapshot(),url:selected?.url,probe:proxy?.probe,siteUrl:site?.url};
|
|
91
|
+
if(action==='status')return {...activity?.snapshot(),url:selected?.url,probe:proxy?.probe,siteUrl:site?.url,demo};
|
|
86
92
|
if(action==='stop')return stop('explicit stop');
|
|
87
93
|
throw Error('Unknown management operation');
|
|
88
94
|
});
|
|
@@ -99,10 +105,11 @@ export async function managedServer(project,options,out,{clock={}}={}){
|
|
|
99
105
|
if(!selected.vars[keys.email])throw missing('Private owner settings are missing. Run rer setup first.');
|
|
100
106
|
if(devIdentity&&!devKey)devKey=await requireDevKey(project.root);
|
|
101
107
|
if(devIdentity&&options.verb==='dev'&&!options.login)refuseManagedInstallation(selected.vars,process.env);
|
|
102
|
-
if(project.adapter.localPrerequisites&&!await project.adapter.localPrerequisites({root:project.root,vars:selected.vars,email:selected.vars[keys.email]}))throw missing('Local migrations, owner or local authentication/mail settings are missing. Run rer setup first.');
|
|
108
|
+
if(project.adapter.localPrerequisites&&!await project.adapter.localPrerequisites({root:project.root,vars:selected.vars,email:selected.vars[keys.email],demo:Boolean(options.demo)}))throw missing('Local migrations, owner or local authentication/mail settings are missing. Run rer setup first.');
|
|
103
109
|
await registry.update({url:selected.url,host:selected.host,port:selected.port,login:Boolean(options.login),models:options.models??'fake'});
|
|
104
110
|
const {privateDirectory}=await shared('private-files.mjs');
|
|
105
|
-
await
|
|
111
|
+
if(options.demo){await demoAdopted(project.root);demo=await demoState(project.root,options.idleTimeout);}
|
|
112
|
+
else await privateDirectory(project.root,['.wrangler','state']);
|
|
106
113
|
const directory=await privateDirectory(project.root,['.seed','installer',...(process.env.RER_ONBOARDING_PROCESS_OWNER?[`commands-${process.pid}`]:[])]);
|
|
107
114
|
const {createCommands}=await shared('commands.mjs');const state={logs:[]};
|
|
108
115
|
store=await createCommands(directory,state,{terminal:text=>out.progress(text),onWaiting:options.json?details=>out.event('waiting',details):undefined,custodyToken:registry.custodyToken(),queue:{verb:options.verb,originalCI:process.env.CI==='true'}});
|
|
@@ -110,11 +117,12 @@ export async function managedServer(project,options,out,{clock={}}={}){
|
|
|
110
117
|
store.setupManifest=seed?.manifest;if(devKey)store.addSecrets([devKey]);
|
|
111
118
|
await tools(project.root,store,controller.signal);
|
|
112
119
|
activity=activityClock({...clock,seconds:options.idleTimeout,onIdle:()=>void stop('idle timeout'),onChange:recordActivity});
|
|
120
|
+
if(demo)demoEnd=setTimeout(()=>void stop('demo ended'),Math.min(Date.parse(demo.ends)-Date.now(),2**31-1));
|
|
113
121
|
const crons=project.adapter.scheduled!==false?await declaredCrons(project.root,project.adapter).catch(error=>{throw refuse(`The declared scheduled triggers cannot run locally: ${error.message}`);}):[];
|
|
114
122
|
proxy=await runtimeFrontDoor({host:selected.host,port:selected.port,activity,door:selected.door,devKey:devKey??undefined,origin:selected.url});const internal=proxy.runtimePort;
|
|
115
123
|
if(selected.door)await announceDoorRoute(selected,proxy,registry);
|
|
116
124
|
const modelsLive = options.verb==='dev' ? await prepareLiveModels(project.root,options,store,controller.signal) : undefined;
|
|
117
|
-
const adapterOptions={modelsLive,selectedOrigin:selected.url,operationToken:operation.token,privateSettings:project.adapter.settings,runtimePort:internal,devKey:devKey??undefined,devLogin:Boolean(options.login),scheduled:crons.length>0};
|
|
125
|
+
const adapterOptions={modelsLive,demo,selectedOrigin:selected.url,operationToken:operation.token,privateSettings:project.adapter.settings,runtimePort:internal,devKey:devKey??undefined,devLogin:Boolean(options.login),scheduled:crons.length>0};
|
|
118
126
|
const input={[keys.email]:selected.vars[keys.email]};
|
|
119
127
|
if(options.verb==='preview')await productionBuild(project,()=>store.run('Building fixed production output',process.execPath,[fileURLToPath(new URL('../bin/rer.mjs',import.meta.url)),'build','--cwd',project.root,'--json'],{cwd:project.root,phase:'build',signal:controller.signal,env:{...process.env,RER_LOCAL_OPERATION:operation.token},afterCommand:()=>recoverInterrupted(project.root,operation)}),text=>store.report(text));
|
|
120
128
|
await serverResources(project.root);
|
|
@@ -127,6 +135,7 @@ export async function managedServer(project,options,out,{clock={}}={}){
|
|
|
127
135
|
},controller.signal,{...adapterOptions,verb:options.verb,dev:project.adapter.dev,preview:project.adapter.preview});
|
|
128
136
|
// A key is sent (or printed) for a door origin only once the door proves it is ours.
|
|
129
137
|
if(devKey)await verifyDoorFor(selected.url);
|
|
138
|
+
if(demo)await demoGuarded(selected.url,demo.state,controller.signal);
|
|
130
139
|
const dev=devIdentity&&await devReady(selected.url,devKey,options,controller.signal);
|
|
131
140
|
const outbox=await outboxServed(selected.url,devKey,controller.signal);
|
|
132
141
|
if(options.verb==='dev'&&project.adapter.site){
|
|
@@ -140,7 +149,7 @@ export async function managedServer(project,options,out,{clock={}}={}){
|
|
|
140
149
|
if(options.json)out.event('warning',{code:error.code,message});else out.progress(`Warning: ${message}\n`);
|
|
141
150
|
}
|
|
142
151
|
}
|
|
143
|
-
activity.ready();await emitReady(out,options,project.root,selected.url,dev,outbox,site?.url);
|
|
152
|
+
activity.ready();await emitReady(out,options,project.root,selected.url,dev,outbox,site?.url,demo);
|
|
144
153
|
if(crons.length){
|
|
145
154
|
out.progress(`Running ${crons.length===1?'the scheduled trigger':'the scheduled triggers'} locally: ${crons.join(' | ')}\n`);
|
|
146
155
|
void runScheduled({crons,url:`http://127.0.0.1:${internal}`,path:options.verb==='preview'?'/cdn-cgi/local/scheduled':undefined,signal:scheduling.signal,report:run=>reportScheduled(out,options,run)});
|
|
@@ -150,7 +159,7 @@ export async function managedServer(project,options,out,{clock={}}={}){
|
|
|
150
159
|
if(infrastructureFailure)throw new RerError('server_bookkeeping_failed','Server bookkeeping failed; owned serving was stopped.',1);
|
|
151
160
|
if(result?.code)throw new RerError('app_exited','The application stopped unexpectedly.',result.code);
|
|
152
161
|
out.progress(`Server stopped: ${reason}.\n`);return {};
|
|
153
|
-
}catch(error){if(termination)throw new RerError('cancelled',`Stopped by ${termination} after owned children were stopped.`,termination==='SIGINT'?130:143);if(['idle timeout','explicit stop'].includes(reason))return {};throw await lifecycleError(error,store);}finally{await cleanup();cleaned();while(activityWriting)await activityWrites;await registry.close();for(const [name,handler]of handlers)process.off(name,handler);}
|
|
162
|
+
}catch(error){if(termination)throw new RerError('cancelled',`Stopped by ${termination} after owned children were stopped.`,termination==='SIGINT'?130:143);if(['idle timeout','explicit stop','demo ended'].includes(reason))return {};throw await lifecycleError(error,store);}finally{await cleanup();cleaned();while(activityWriting)await activityWrites;await registry.close();for(const [name,handler]of handlers)process.off(name,handler);}
|
|
154
163
|
}
|
|
155
164
|
function reportScheduled(out,options,run){
|
|
156
165
|
const error=options.verb==='preview'&&run.status===404 ? 'The preview target does not expose /cdn-cgi/local/scheduled. Forward the adapter context scheduled flag to previewTarget, or set RER_RUNTIME_TRIGGERS=1 in a custom target using the shared production runtime. For another runtime, expose that internal trigger endpoint or declare scheduled:false.' : run.error;
|
|
@@ -167,12 +176,12 @@ async function outboxServed(url,devKey,signal){
|
|
|
167
176
|
// dev also keeps its RER_OPEN link in owner-only .rer/local/open-url (0600),
|
|
168
177
|
// which reset removes with the rest of .rer/local, for a person or agent that
|
|
169
178
|
// did not keep the terminal output.
|
|
170
|
-
async function emitReady(out,options,root,url,dev,outbox,siteUrl){
|
|
179
|
+
async function emitReady(out,options,root,url,dev,outbox,siteUrl,demo){
|
|
171
180
|
if(options.verb==='dev'&&dev?.openUrl){
|
|
172
181
|
const {privateDirectory,writePrivateFile}=await shared('private-files.mjs');
|
|
173
182
|
await writePrivateFile(join(await privateDirectory(root,['.rer','local']),'open-url'),dev.openUrl+'\n');
|
|
174
183
|
}
|
|
175
|
-
ready(out,url,dev||undefined,options.json,outbox,siteUrl);
|
|
184
|
+
ready(out,url,dev||undefined,options.json,outbox,siteUrl,demo);
|
|
176
185
|
}
|
|
177
186
|
async function devReady(url,key,options,signal){
|
|
178
187
|
if(options.verb==='preview')return {openUrl:openUrl(url,key,{path:'/__dev/key',next:'/__dev/outbox'})};
|
package/cli/parse.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { resolve } from 'node:path';
|
|
|
2
2
|
import { commonFlags, serverDefaults, verbs } from './contract.mjs';
|
|
3
3
|
import { invalid, refuse } from './errors.mjs';
|
|
4
4
|
import { isLoopbackHost } from '../modules/operator-auth/loopback.mjs';
|
|
5
|
-
const booleans = new Set(['--help', '--json', '--non-interactive', '--yes', '--dry-run', '--open', '--stop', '--detach', '--foreground', '--no-queue', '--login', '--legacy-v1', '--acknowledge-unverified-metadata', '--door', '--direct', '--site', '--remove']);
|
|
5
|
+
const booleans = new Set(['--help', '--json', '--non-interactive', '--yes', '--dry-run', '--open', '--stop', '--detach', '--foreground', '--no-queue', '--login', '--legacy-v1', '--acknowledge-unverified-metadata', '--door', '--direct', '--site', '--remove', '--demo']);
|
|
6
6
|
export const mailbox = value => typeof value === 'string' && value.length <= 254 && /^[a-zA-Z0-9.!+_%\-]+@[a-zA-Z0-9\-]+(?:\.[a-zA-Z0-9\-]+)+$/.test(value);
|
|
7
7
|
// A loopback host: the shared predicate (including `*.localhost` door names),
|
|
8
8
|
// plus `::1` as --host and a bracket-stripped hostname spell it.
|
|
@@ -17,6 +17,8 @@ export function parse(argv, cwd = process.cwd(), { independent = false, deferTai
|
|
|
17
17
|
if (!verb || verb === '--help') return { help: true };
|
|
18
18
|
if (verb === 'release') return { verb, releaseArgs: args };
|
|
19
19
|
if (verb === 'archive') return { verb, archiveArgs: args };
|
|
20
|
+
// rer demo is rer dev --demo.
|
|
21
|
+
if (verb === 'demo' && !independent) return parse(['dev', '--demo', ...args], cwd, { independent, deferTail });
|
|
20
22
|
// Independent projects own verb names; only the common wrapper flags apply
|
|
21
23
|
// to unknown names. Profile resolution rejects undeclared commands later.
|
|
22
24
|
if (!/^[a-z][a-z0-9-]*$/.test(verb)) throw invalid('Unknown rer command. Run rer --help.');
|
|
@@ -53,8 +55,10 @@ export function parse(argv, cwd = process.cwd(), { independent = false, deferTai
|
|
|
53
55
|
if(options.models!==undefined && !['fake','live'].includes(options.models))throw invalid('--models takes fake or live.');
|
|
54
56
|
if(options.modelsService!==undefined && options.models!=='live')throw invalid('--models-service requires --models live.');
|
|
55
57
|
if(options.as!==undefined && (options.login || !/^[a-z][a-z0-9-]{0,39}$/.test(options.as)))throw invalid(options.login?'--as and --login cannot be combined.':'--as takes a persona name such as owner, signed-out or admin.');
|
|
58
|
+
if(options.demo&&['--door','--direct','--as','--login','--models','--models-service'].some(flag=>options.supplied.has(flag)))throw invalid('A demo opens signed in as the owner on its own data: --demo takes only --port, --host, --idle-timeout, --stop, --detach and --foreground.');
|
|
56
59
|
if(['dev','preview'].includes(verb) && !options.stop){
|
|
57
|
-
|
|
60
|
+
// A demo ends one hour after it starts unless --idle-timeout says otherwise.
|
|
61
|
+
const value=options.idleTimeout??(options.demo?'3600':process.env.RER_SERVER_IDLE_SECONDS??String(serverDefaults.idleSeconds));
|
|
58
62
|
if(!/^[1-9][0-9]*$/.test(value)||!Number.isSafeInteger(Number(value)))throw invalid('Idle timeout must be positive integer seconds.');
|
|
59
63
|
options.idleTimeout=Number(value);
|
|
60
64
|
}
|
|
@@ -73,6 +77,7 @@ export function parse(argv, cwd = process.cwd(), { independent = false, deferTai
|
|
|
73
77
|
if (options.profile !== undefined && options.profile !== 'first-party') throw invalid('The supported explicit profile is first-party.');
|
|
74
78
|
if (options.environment !== undefined && !/^[a-z][a-z0-9-]{0,39}$/.test(options.environment)) throw invalid('Provide a valid deployment environment name.');
|
|
75
79
|
for (const key of ['setSecret', 'unsetSecret']) if (options[key] !== undefined && !/^[A-Z][A-Z0-9_]{0,63}$/.test(options[key])) throw invalid('Name a declared secret binding, such as SESSION_SECRET.');
|
|
80
|
+
if(verb==='deploy'&&options.demo&&(options.site||['platform','image','bindAddress','httpPort','httpsPort','envFile','host','remove','setSecret','unsetSecret'].some(key=>options[key]!==undefined)))throw invalid('--demo takes only --environment and --dry-run: the hosted demo is its own Worker on demo.<domain>.');
|
|
76
81
|
if ((options.remove || options.setSecret || options.unsetSecret) && options.site) throw invalid('--remove, --set-secret and --unset-secret act on the application Worker, not the run site.');
|
|
77
82
|
if (options.origin !== undefined) {
|
|
78
83
|
let url; try { url = new URL(options.origin); } catch { throw invalid('Provide a valid credential-free origin.'); }
|
package/cli/pnpm-probe.mjs
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { missing } from './errors.mjs';
|
|
2
|
+
import { shared } from './shared.mjs';
|
|
2
3
|
|
|
3
4
|
// A version check owns its descendants just like a lifecycle command. Cleanup
|
|
4
5
|
// starts at leader exit, even when quiet descendants still hold the pipes.
|
|
6
|
+
// The package manager never sees the caller's credentials.
|
|
5
7
|
export async function probePnpm(root, store, signal, env = process.env) {
|
|
8
|
+
const { dependencyEnvironment } = await shared('dependency-environment.mjs');
|
|
6
9
|
const probeSignal = AbortSignal.any([signal, AbortSignal.timeout(10000)].filter(Boolean));
|
|
7
|
-
const child = store.spawn('Checking pinned pnpm', 'pnpm', ['--version'], { cwd: root, env, signal: probeSignal });
|
|
10
|
+
const child = store.spawn('Checking pinned pnpm', 'pnpm', ['--version'], { cwd: root, env: dependencyEnvironment(env), signal: probeSignal });
|
|
8
11
|
let version = '';
|
|
9
12
|
child.stdout.on('data', chunk => { version = (version + chunk).slice(-1024); });
|
|
10
13
|
const code = await new Promise(resolve => { child.once('error', () => resolve(null)); child.once('exit', code => resolve(code)); });
|
package/cli/portability.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import { readLegacyArchive } from '../modules/portability/format.mjs';
|
|
|
6
6
|
import { fileRecords } from '../modules/portability/files.mjs';
|
|
7
7
|
import { RerError, refuse } from './errors.mjs';
|
|
8
8
|
import { policyModuleURL, serializablePolicy } from './portability-policy.mjs';
|
|
9
|
+
import { shared } from './shared.mjs';
|
|
9
10
|
|
|
10
11
|
// Seeds declare their real bindings and data policy. The shared module owns the
|
|
11
12
|
// archive format and atomic restore; the shared runner owns its native children.
|
|
@@ -52,8 +53,10 @@ export function localPortability({ database, binding = 'DB', buckets = {}, polic
|
|
|
52
53
|
// Do not apply pending product migrations to a populated recovery target.
|
|
53
54
|
// Check real existing bindings first; finalize repeats this under the lock.
|
|
54
55
|
if(legacyRecovery)await nativeWorker(store,'legacy-preflight',input,signal);
|
|
56
|
+
// Local migrations need no provider account: wrangler runs without credentials.
|
|
57
|
+
const { dependencyEnvironment } = await shared('dependency-environment.mjs');
|
|
55
58
|
await store.run('Applying local data migrations', 'pnpm', ['exec', 'wrangler', 'd1', 'migrations', 'apply', database, '--local', '--persist-to', '.wrangler/state', '--config', 'wrangler.jsonc'], {
|
|
56
|
-
cwd: root, signal, env: { ...
|
|
59
|
+
cwd: root, signal, env: { ...dependencyEnvironment(), RER_LOCAL_OPERATION: operationToken, WRANGLER_SEND_METRICS: 'false' },
|
|
57
60
|
});
|
|
58
61
|
const receipt=await nativeWorker(store,direction,input,signal);
|
|
59
62
|
if(legacyRecovery)receipt.output=relative(root,legacyRecovery.output);
|
package/cli/scheduled.mjs
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { declaredCronSources } from './deploy-config.mjs';
|
|
2
|
-
|
|
3
1
|
// `rer dev` runs the project's declared Cron Triggers (wrangler `triggers.crons`, from wrangler.jsonc, wrangler.toml or a deploy template)
|
|
4
2
|
// on their schedule by calling the local runtime's scheduled test endpoint (the Vite plugin's under dev, Miniflare's own under preview), so
|
|
5
3
|
// alerts, retries and checks behave on the owner's computer as they do deployed.
|
|
@@ -40,6 +38,8 @@ export function cronMatches(cron, date) {
|
|
|
40
38
|
// triggers.crons must declare the same ones: otherwise `rer dev` would run a
|
|
41
39
|
// schedule the deployed Worker does not have.
|
|
42
40
|
export async function declaredCrons(root, adapter) {
|
|
41
|
+
// Imported here: the container image runs this file's parseCron and cronMatches without the CLI's project modules.
|
|
42
|
+
const { declaredCronSources } = await import('./deploy-config.mjs');
|
|
43
43
|
const sources = await declaredCronSources(root, adapter);
|
|
44
44
|
for (const { path, crons } of sources) for (const cron of crons) parseCron(cron);
|
|
45
45
|
const [first, ...others] = sources;
|
package/cli/setup-input.mjs
CHANGED
|
@@ -87,7 +87,7 @@ export async function setupInput(project, options, vars, signal, { origin } = {}
|
|
|
87
87
|
if(savedEmail&&!mailbox(savedEmail))throw refuse('Saved owner is malformed. Restore private settings or run rer reset --yes.');
|
|
88
88
|
if(savedEmail&&savedEmail.toLowerCase()!==answers[names.email]){
|
|
89
89
|
if(owners.length)throw refuse(`Private setting ${names.email} differs from the retained database owner. Restore ${names.email} in ${project.adapter.hooks?.secrets?.file??'.dev.vars'} to ${answers[names.email]}, then run rer setup --owner-email ${answers[names.email]}; or back up data and run rer reset --yes. Private bytes were kept.`);
|
|
90
|
-
throw refuse(`Owner conflicts with the saved installation (${savedEmail}). Run rer setup --owner-email ${savedEmail}, or back up data
|
|
90
|
+
throw refuse(`Owner conflicts with the saved installation (${savedEmail}). Run rer setup --owner-email ${savedEmail}, or back up data, run rer dev --stop, then rer reset --yes followed by rer setup --owner-email ${answers[names.email]}. Reset deletes local data.`);
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
try{return seed.model.validateInput(answers,known);}catch{throw missing('Installer answers are incomplete. Supply the active manifest fields using rer setup --answers answers.json --non-interactive.');}
|