@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.
Files changed (94) hide show
  1. package/README.md +9 -2
  2. package/cli/README.md +30 -0
  3. package/cli/container.mjs +11 -10
  4. package/cli/contract.mjs +5 -2
  5. package/cli/demo-host.mjs +47 -0
  6. package/cli/demo.mjs +47 -0
  7. package/cli/dependencies.mjs +26 -4
  8. package/cli/deploy-command.mjs +108 -0
  9. package/cli/deploy-hook.mjs +31 -0
  10. package/cli/deploy-inputs.mjs +22 -0
  11. package/cli/deploy-observation.mjs +29 -0
  12. package/cli/deploy-owner.mjs +59 -39
  13. package/cli/deploy-readiness.mjs +16 -0
  14. package/cli/deploy-source.mjs +24 -0
  15. package/cli/deploy.mjs +22 -10
  16. package/cli/detached-server.mjs +1 -1
  17. package/cli/export.mjs +1 -1
  18. package/cli/hosted-output.mjs +2 -1
  19. package/cli/inspect.mjs +2 -2
  20. package/cli/lifecycle.mjs +20 -7
  21. package/cli/local.mjs +7 -0
  22. package/cli/main.mjs +1 -1
  23. package/cli/managed-server.mjs +27 -18
  24. package/cli/parse.mjs +7 -2
  25. package/cli/pnpm-probe.mjs +4 -1
  26. package/cli/portability.mjs +4 -1
  27. package/cli/scheduled.mjs +2 -2
  28. package/cli/setup-input.mjs +1 -1
  29. package/global/CHANGELOG.md +63 -0
  30. package/global/README.md +9 -2
  31. package/global/launch/capacity.mjs +1 -1
  32. package/global/launch/facts.mjs +12 -6
  33. package/global/launch/install-pinned.mjs +26 -2
  34. package/global/launch/prepare.mjs +20 -8
  35. package/global/launch/state.mjs +64 -11
  36. package/global/main.mjs +18 -20
  37. package/global/update/command.mjs +2 -1
  38. package/modules/container/README.md +3 -1
  39. package/modules/container/boundary.mjs +5 -2
  40. package/modules/container/build.mjs +4 -2
  41. package/modules/container/host.mjs +1 -1
  42. package/modules/container/ingress.mjs +4 -1
  43. package/modules/container/migrations.mjs +2 -1
  44. package/modules/container/worker-engine.mjs +3 -1
  45. package/modules/demo-host/README.md +30 -0
  46. package/modules/demo-host/capacity.mjs +19 -0
  47. package/modules/demo-host/claim.mjs +21 -0
  48. package/modules/demo-host/container.mjs +74 -0
  49. package/modules/demo-host/pages.mjs +56 -0
  50. package/modules/demo-host/plan.mjs +52 -0
  51. package/modules/demo-host/worker.mjs +72 -0
  52. package/modules/deploy-kit/README.md +36 -7
  53. package/modules/deploy-kit/deploy.mjs +9 -2
  54. package/modules/deploy-kit/env-file.mjs +2 -1
  55. package/modules/deploy-kit/health-request.mjs +14 -7
  56. package/modules/deploy-kit/migration-ledger.mjs +38 -1
  57. package/modules/deploy-kit/migrations.mjs +38 -2
  58. package/modules/deploy-kit/owner-cli.mjs +30 -9
  59. package/modules/deploy-kit/owner-remove.mjs +6 -2
  60. package/modules/deploy-kit/owner-secret-names.mjs +31 -0
  61. package/modules/deploy-kit/owner-secret.mjs +71 -23
  62. package/modules/deploy-kit/owner.mjs +78 -25
  63. package/modules/deploy-kit/prepare.mjs +3 -1
  64. package/modules/deploy-kit/provider.mjs +3 -0
  65. package/modules/erasure/README.md +2 -3
  66. package/modules/operator-auth/accounts/settings-page.mjs +8 -3
  67. package/modules/operator-auth/csp.mjs +26 -0
  68. package/modules/operator-auth/dev/badge.mjs +1 -22
  69. package/modules/operator-auth/page.mjs +2 -2
  70. package/modules/operator-auth/routes.mjs +1 -1
  71. package/modules/portability/README.md +9 -0
  72. package/modules/portability/bridge.mjs +4 -1
  73. package/modules/portability/catalogue.mjs +9 -2
  74. package/modules/portability/index.d.mts +4 -0
  75. package/modules/portability/index.mjs +1 -0
  76. package/modules/portability/lines.mjs +3 -1
  77. package/modules/release/produce.mjs +1 -1
  78. package/modules/release/source.mjs +5 -2
  79. package/onboarding/adapter-labels.mjs +1 -0
  80. package/onboarding/adapter.mjs +9 -4
  81. package/onboarding/dependency-environment.mjs +33 -0
  82. package/onboarding/local/owner.mjs +1 -1
  83. package/onboarding/operator-installer.mjs +10 -4
  84. package/onboarding/redaction.mjs +22 -3
  85. package/package.json +6 -1
  86. package/scripts/publish-check/README.md +22 -1
  87. package/scripts/publish-check/c2pa.py +121 -0
  88. package/scripts/publish-check/content_identities.py +3 -1
  89. package/scripts/publish-check/controller_metadata.py +31 -0
  90. package/scripts/publish-check/history_scan.py +13 -3
  91. package/scripts/publish-check/media.py +283 -0
  92. package/scripts/publish-check/png_text.py +34 -0
  93. package/scripts/publish-check/public_hygiene.py +39 -4
  94. 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 = `rer deploy --environment ${environment}`;
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 and recorded its new version; the next ${again} updates it.`);
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 used and removed. Sign in by email at ${deployed.origin}.`);
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-claim-failed') return `The application is deployed at ${deployed?.origin}, but claiming its owner failed: ${redact(String(error.cause))}. The setup token is still on the Worker; run ${again} again to retry the claim.`;
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'}.`);
@@ -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) {
@@ -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
- await store.run('Installing frozen dependencies', 'pnpm', ['install', '--frozen-lockfile'], { cwd: project.root, signal, env: { ...process.env, CI: 'true' } }); return {};
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
- const deployed = await adapter.deploy({ root: project.root, options, store, signal, operationToken: operation.token, localOnlyKeys: adapter.localOnlyKeys ?? [], checkBuild: () => refuseDevLayerOutput(project.root, adapter.devIdentity), setupManifest: project.setupManifest, linkDependencies: link });
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
- await linkDependencies({project,input,origin:url,store,signal});
118
- if(project.setupManifest?.dependencies?.length) {
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);
@@ -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
- export function ready(out,url,dev,json=false,outbox=true,siteUrl){
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
- let store,operation,proxy,activity,app,termination,reason,cleanupPromise,finished,selected,cleaned,site;
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(options.verb,options);return {};}
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(options.verb,options);
65
+ let current=await registry.existing(mode,options);
60
66
  if(!current){
61
- const reservation=await registry.reserve(options.verb,options);
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(options.verb,options);
72
+ current=await registry.existing(mode,options);
67
73
  const running=await registry.waitReady(current,controller.signal);
68
- await registry.existing(options.verb,options);
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 privateDirectory(project.root,['.wrangler','state']);
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
- const value=options.idleTimeout??process.env.RER_SERVER_IDLE_SECONDS??String(serverDefaults.idleSeconds);
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.'); }
@@ -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)); });
@@ -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: { ...process.env, RER_LOCAL_OPERATION: operationToken, WRANGLER_SEND_METRICS: 'false' },
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;
@@ -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 and run rer reset --yes.`);
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.');}