create-flowdular 0.4.3 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/README.md +16 -10
  2. package/agent-template/.agents/skills/agent-tool-design/SKILL.md +1 -1
  3. package/agent-template/.agents/skills/auth-security-review/SKILL.md +2 -2
  4. package/agent-template/.agents/skills/bug-hunt/SKILL.md +1 -1
  5. package/agent-template/.agents/skills/database-adapter/SKILL.md +5 -5
  6. package/agent-template/.agents/skills/database-adapter/references/first-run-and-matrix.md +2 -2
  7. package/agent-template/.agents/skills/deploy-operate/SKILL.md +1 -1
  8. package/agent-template/.agents/skills/migration-authoring/SKILL.md +4 -4
  9. package/agent-template/.agents/skills/module-new/SKILL.md +1 -1
  10. package/agent-template/.agents/skills/spec-interview/SKILL.md +20 -20
  11. package/agent-template/.agents/skills/test-hardening/SKILL.md +2 -2
  12. package/agent-template/.agents/skills/ux-design/SKILL.md +1 -1
  13. package/agent-template/.agents/skills/workflow-development/SKILL.md +95 -9
  14. package/agent-template/.ai/README.md +5 -3
  15. package/agent-template/.ai/agents/README.md +1 -1
  16. package/agent-template/.ai/agents/sandbox/agentic-engineer.md +1 -0
  17. package/agent-template/.ai/agents/sandbox/backend-engineer.md +1 -0
  18. package/agent-template/.ai/agents/sandbox/frontend-engineer.md +1 -0
  19. package/agent-template/.ai/blueprints/add-migration/README.md +1 -1
  20. package/agent-template/.ai/blueprints/add-migration/required-files.yaml +1 -1
  21. package/agent-template/.ai/blueprints/new-module/required-files.yaml +1 -1
  22. package/agent-template/.ai/examples/bad/client-imports-server/README.md +1 -1
  23. package/agent-template/.ai/examples/bad/missing-acl/README.md +1 -1
  24. package/agent-template/.ai/examples/bad/tenant-from-body/README.md +1 -1
  25. package/agent-template/.ai/guides/application-development.md +7 -5
  26. package/agent-template/.ai/platform-capabilities.md +9 -5
  27. package/agent-template/.ai/policies/capabilities.yaml +28 -12
  28. package/agent-template/.ai/policies/task-budgets.yaml +1 -1
  29. package/agent-template/.ai/rules/flowdular.md +3 -2
  30. package/agent-template/.ai/skills/README.md +1 -1
  31. package/agent-template/.ai/skills/agent-tool-design/SKILL.md +1 -1
  32. package/agent-template/.ai/skills/auth-security-review/SKILL.md +2 -2
  33. package/agent-template/.ai/skills/bug-hunt/SKILL.md +1 -1
  34. package/agent-template/.ai/skills/database-adapter/SKILL.md +5 -5
  35. package/agent-template/.ai/skills/database-adapter/references/first-run-and-matrix.md +2 -2
  36. package/agent-template/.ai/skills/deploy-operate/SKILL.md +1 -1
  37. package/agent-template/.ai/skills/migration-authoring/SKILL.md +4 -4
  38. package/agent-template/.ai/skills/module-new/SKILL.md +1 -1
  39. package/agent-template/.ai/skills/spec-interview/SKILL.md +20 -20
  40. package/agent-template/.ai/skills/test-hardening/SKILL.md +2 -2
  41. package/agent-template/.ai/skills/ux-design/SKILL.md +1 -1
  42. package/agent-template/.ai/skills/workflow-development/SKILL.md +96 -10
  43. package/agent-template/.ai/subagents/module-executor.md +25 -0
  44. package/agent-template/.ai/subagents/reviewer.md +23 -0
  45. package/agent-template/.ai/subagents/spec-author.md +23 -0
  46. package/agent-template/.claude/agents/module-executor.md +22 -0
  47. package/agent-template/.claude/agents/reviewer.md +24 -0
  48. package/agent-template/.claude/agents/spec-author.md +20 -0
  49. package/agent-template/.claude/skills/agent-tool-design/SKILL.md +1 -1
  50. package/agent-template/.claude/skills/auth-security-review/SKILL.md +2 -2
  51. package/agent-template/.claude/skills/bug-hunt/SKILL.md +1 -1
  52. package/agent-template/.claude/skills/database-adapter/SKILL.md +5 -5
  53. package/agent-template/.claude/skills/database-adapter/references/first-run-and-matrix.md +2 -2
  54. package/agent-template/.claude/skills/deploy-operate/SKILL.md +1 -1
  55. package/agent-template/.claude/skills/migration-authoring/SKILL.md +4 -4
  56. package/agent-template/.claude/skills/module-new/SKILL.md +1 -1
  57. package/agent-template/.claude/skills/spec-interview/SKILL.md +20 -20
  58. package/agent-template/.claude/skills/test-hardening/SKILL.md +2 -2
  59. package/agent-template/.claude/skills/ux-design/SKILL.md +1 -1
  60. package/agent-template/.claude/skills/workflow-development/SKILL.md +95 -9
  61. package/agent-template/.codex/agents/module-executor.toml +17 -0
  62. package/agent-template/.codex/agents/reviewer.toml +14 -0
  63. package/agent-template/.codex/agents/spec-author.toml +15 -0
  64. package/agent-template/AGENTS.md +3 -2
  65. package/agent-template/CLAUDE.md +3 -2
  66. package/agent-template/docs/adr/0007-module-owned-agents.md +35 -1
  67. package/agent-template/docs/agent-contract.md +2 -2
  68. package/agent-template/docs/cli.md +24 -3
  69. package/agent-template/docs/configuration.md +59 -5
  70. package/agent-template/docs/database-adapters.md +20 -20
  71. package/agent-template/docs/design-system.md +3 -3
  72. package/agent-template/docs/getting-started.md +25 -32
  73. package/agent-template/docs/module-distribution.md +79 -86
  74. package/agent-template/docs/module-web-surfaces.md +9 -7
  75. package/agent-template/docs/modules.md +9 -1
  76. package/agent-template/docs/sandbox.md +117 -6
  77. package/agent-template/platform/scripts/build.mjs +7 -0
  78. package/agent-template/rulesync.jsonc +1 -1
  79. package/dist/bin.js +12 -6
  80. package/package.json +2 -2
  81. package/template/default/.env.example +10 -3
  82. package/template/default/.prettierignore +2 -0
  83. package/template/default/.vercelignore +8 -0
  84. package/template/default/README.md +26 -15
  85. package/template/default/_gitignore +3 -2
  86. package/template/default/infra/README.md +86 -65
  87. package/template/default/infra/docker/.env.example +66 -0
  88. package/template/default/infra/docker/Dockerfile +24 -10
  89. package/template/default/infra/docker/app-entrypoint.mjs +5 -0
  90. package/template/default/infra/docker/compose.yaml +105 -58
  91. package/template/default/infra/docker/database-urls.mjs +28 -0
  92. package/template/default/infra/docker/pitr.sh +177 -0
  93. package/template/default/infra/docker/postgres/10-roles.sh +16 -12
  94. package/template/default/infra/docker/start.mjs +402 -0
  95. package/template/default/infra/kubernetes/database-secret.example.yaml +3 -3
  96. package/template/default/infra/vercel/README.md +262 -0
  97. package/template/default/infra/vercel/build.mjs +214 -0
  98. package/template/default/infra/vercel/handler.mjs +100 -0
  99. package/template/default/modules/example/migrations/0001_example_core.up.sql +2 -2
  100. package/template/default/modules/example/module.json +1 -1
  101. package/template/default/modules/example/package.json +3 -3
  102. package/template/default/modules/example/spec/module.yaml +1 -1
  103. package/template/default/modules/example/src/services/migration.ts +2 -2
  104. package/template/default/modules/example/tests/module.test.ts +1 -1
  105. package/template/default/package.json +3 -2
  106. package/template/default/platform/index.html +7 -19
  107. package/template/default/platform/octane.config.ts +252 -156
  108. package/template/default/platform/package.json +5 -5
  109. package/template/default/platform/public/favicon.svg +1 -1
  110. package/template/default/platform/scripts/build.mjs +56 -0
  111. package/template/default/platform/scripts/dev.mjs +38 -0
  112. package/template/default/platform/src/App.tsrx +25 -1
  113. package/template/default/platform/src/generated/modules.server.ts +3 -0
  114. package/template/default/platform/src/server/database.ts +24 -0
  115. package/template/default/platform/src/server/runtime-role.ts +33 -0
  116. package/template/default/platform/src/server/setup/access.ts +160 -0
  117. package/template/default/platform/src/server/setup/adapters.ts +554 -0
  118. package/template/default/platform/src/server/setup/environment.ts +154 -0
  119. package/template/default/platform/src/server/setup/gate.ts +84 -0
  120. package/template/default/platform/src/server/setup/index.ts +181 -0
  121. package/template/default/platform/src/server/setup/modules.ts +123 -0
  122. package/template/default/platform/src/server/setup/page.ts +497 -0
  123. package/template/default/platform/src/server/setup/routes.ts +787 -0
  124. package/template/default/platform/src/server/setup/sanitize.ts +111 -0
  125. package/template/default/platform/src/server/setup/seed.ts +145 -0
  126. package/template/default/platform/src/server/setup/token.ts +79 -0
  127. package/template/default/platform/src/server/worker-tick.ts +193 -0
  128. package/template/default/platform/src/server/workspace-root.ts +16 -0
  129. package/template/default/render.yaml +70 -0
  130. package/template/default/vercel.json +5 -0
@@ -0,0 +1,497 @@
1
+ import type {
2
+ DatabaseAdapterProbeResult,
3
+ DatabaseAdapterPublicDescriptor,
4
+ DatabaseConfigurationField,
5
+ } from '@flowdular/sdk/database';
6
+ import { SETUP_CSRF_FIELD } from './access.ts';
7
+ import type { EnvironmentWriteResult } from './environment.ts';
8
+ import type { FirstRunSeed } from './seed.ts';
9
+
10
+ /* The installer runs before the application bundle can be served: there is no
11
+ database, so there is no shell, no session and no client build to depend on.
12
+ It therefore ships its own markup and its own copy of the design tokens it
13
+ uses, taken from packages/ui/src/styles/tokens.css, and namespaces every
14
+ class as setup-* so it can never restyle a ui-* primitive. */
15
+
16
+ const STEPS = ['Unlock', 'Database', 'Workspace', 'Review', 'Sign in'] as const;
17
+ export type SetupStepName = (typeof STEPS)[number];
18
+
19
+ export interface ModuleCompatibility {
20
+ readonly moduleId: string;
21
+ readonly tenantOwned: boolean;
22
+ readonly issues: readonly string[];
23
+ }
24
+
25
+ export interface SetupPageView {
26
+ readonly step: SetupStepName;
27
+ readonly csrfToken: string | null;
28
+ readonly databasePreconfigured: boolean;
29
+ /** The application itself serves setup and needs no restart afterwards. */
30
+ readonly inPlace?: boolean;
31
+ readonly autoRestart: boolean;
32
+ readonly error: string | null;
33
+ readonly notice: string | null;
34
+ readonly adapters: readonly DatabaseAdapterPublicDescriptor[];
35
+ readonly selectedAdapterId: string | null;
36
+ readonly fieldErrors: Readonly<Record<string, string>>;
37
+ readonly values: Readonly<Record<string, string>>;
38
+ readonly probe: DatabaseAdapterProbeResult | null;
39
+ readonly modules: readonly ModuleCompatibility[];
40
+ readonly environment: EnvironmentWriteResult | null;
41
+ readonly seed: FirstRunSeed | null;
42
+ readonly modulesApproximated: boolean;
43
+ readonly tokenFile: string | null;
44
+ readonly passwordMinLength: number;
45
+ }
46
+
47
+ export function escapeHtml(value: string): string {
48
+ return value
49
+ .replaceAll('&', '&amp;')
50
+ .replaceAll('<', '&lt;')
51
+ .replaceAll('>', '&gt;')
52
+ .replaceAll('"', '&quot;')
53
+ .replaceAll("'", '&#39;');
54
+ }
55
+
56
+ /* Only the tokens this page uses, with the values tokens.css defines. A first
57
+ run cannot import the stylesheet: the container image ships platform/dist
58
+ and nothing else. */
59
+ const STYLES = `
60
+ :root{
61
+ --ink-0:#ffffff;--ink-25:#f9fafc;--ink-50:#f4f6fa;--ink-100:#e7ebf2;
62
+ --ink-200:#d3dae5;--ink-300:#b2bccc;--ink-400:#8794ab;--ink-500:#64738f;
63
+ --ink-600:#475572;--ink-900:#141b2e;--blue-50:#f1f5fe;--blue-100:#e3ebfc;
64
+ --blue-600:#2557d6;--blue-700:#1c45b3;--copper-300:#e08a45;--copper-500:#c9722d;
65
+ --green-50:#e6f5ec;--green-200:#b7e3c8;--green-600:#1b7f4e;
66
+ --amber-50:#fff4d6;--amber-200:#f3dc9a;--amber-600:#9a6700;
67
+ --red-50:#fce9e7;--red-200:#f2bdb9;--red-600:#c4322b;
68
+ --bg:var(--ink-50);--surface:var(--ink-0);--surface-2:var(--ink-25);
69
+ --line:var(--ink-200);--line-2:var(--ink-100);--ink:var(--ink-900);
70
+ --ink-2:var(--ink-600);--ink-3:var(--ink-500);--ink-4:var(--ink-400);
71
+ --primary:var(--blue-600);--primary-hover:var(--blue-700);--focus:var(--blue-600);
72
+ --success:var(--green-600);--success-bg:var(--green-50);--success-line:var(--green-200);
73
+ --warning:var(--amber-600);--warning-bg:var(--amber-50);--warning-line:var(--amber-200);
74
+ --danger:var(--red-600);--danger-bg:var(--red-50);--danger-line:var(--red-200);
75
+ --font-sans:'IBM Plex Sans Variable','IBM Plex Sans','Segoe UI',system-ui,-apple-system,sans-serif;
76
+ --font-mono:'IBM Plex Mono',ui-monospace,SFMono-Regular,Menlo,monospace;
77
+ --text-xs:11px;--text-sm:12px;--text-md:13px;--text-base:14px;--text-lg:16px;
78
+ --control-h:36px;--r-sm:4px;--r:6px;--r-md:8px;--r-lg:12px;
79
+ --shadow-sm:0 1px 2px rgba(20,27,46,.06);
80
+ }
81
+ *{box-sizing:border-box}
82
+ html,body{margin:0;padding:0}
83
+ body{font-family:var(--font-sans);font-size:var(--text-base);color:var(--ink);background:var(--bg);-webkit-font-smoothing:antialiased}
84
+ .setup-page{display:grid;min-height:100vh;grid-template-columns:minmax(360px,.95fr) minmax(480px,1.05fr)}
85
+ .setup-story{position:relative;display:flex;min-height:100vh;flex-direction:column;padding:40px 48px;color:var(--ink-0);background:var(--ink-900);overflow:hidden}
86
+ .setup-story>*{position:relative}
87
+ .setup-backdrop{position:absolute;inset:0;width:100%;height:100%;pointer-events:none}
88
+ .setup-brand{display:inline-flex;width:fit-content;align-items:center;gap:9px;font-size:var(--text-lg);font-weight:600;letter-spacing:-.01em}
89
+ .setup-brand svg{display:block}
90
+ .setup-kicker{font-size:var(--text-xs);font-weight:600;letter-spacing:.09em;text-transform:uppercase;color:var(--ink-3)}
91
+ .setup-story .setup-kicker{color:var(--copper-300)}
92
+ .setup-story__copy{max-width:560px;margin-block:auto;padding:60px 0}
93
+ .setup-story h1{max-width:16ch;margin:14px 0 16px;font-size:clamp(28px,3vw,36px);font-weight:600;line-height:1.12;letter-spacing:-.02em}
94
+ .setup-story__copy p{max-width:44ch;margin:0;font-size:var(--text-base);line-height:1.6;color:var(--ink-300)}
95
+ .setup-story footer{display:flex;justify-content:space-between;gap:16px;font-family:var(--font-mono);font-size:var(--text-xs);letter-spacing:.04em;color:var(--ink-400)}
96
+ .setup-panel{display:grid;place-items:center;padding:48px 32px;background:var(--bg)}
97
+ .setup-card{display:grid;gap:14px;width:min(520px,100%);padding:28px;background:var(--surface);border:1px solid var(--line);border-radius:var(--r-lg);box-shadow:var(--shadow-sm)}
98
+ .setup-card h2{margin:2px 0 0;font-size:20px;font-weight:600;letter-spacing:-.015em}
99
+ .setup-card__sub{margin:0 0 6px;font-size:var(--text-md);line-height:1.55;color:var(--ink-3)}
100
+ .setup-steps{display:flex;flex-wrap:wrap;gap:14px;margin:0 0 4px;padding:0;list-style:none}
101
+ .setup-steps li{display:inline-flex;align-items:center;gap:7px;font-size:var(--text-sm);font-weight:500;color:var(--ink-4)}
102
+ .setup-steps li>i{display:grid;width:20px;height:20px;place-items:center;font-family:var(--font-mono);font-size:10px;font-style:normal;color:var(--ink-3);background:var(--surface-2);border:1px solid var(--line);border-radius:50%}
103
+ .setup-steps li.is-active{color:var(--ink)}
104
+ .setup-steps li.is-active>i{color:var(--ink-0);background:var(--primary);border-color:var(--primary)}
105
+ .setup-steps li.is-done{color:var(--ink-3)}
106
+ .setup-steps li.is-done>i{color:var(--success);background:var(--success-bg);border-color:var(--success-line)}
107
+ .setup-form{display:grid;gap:14px}
108
+ .setup-field{display:grid;gap:5px}
109
+ .setup-label{font-size:var(--text-sm);font-weight:500;color:var(--ink-2)}
110
+ .setup-input,.setup-select{width:100%;height:var(--control-h);padding:0 10px;font:inherit;font-size:var(--text-md);color:var(--ink);background:var(--surface);border:1px solid var(--line);border-radius:var(--r)}
111
+ .setup-input[readonly]{color:var(--ink-2);background:var(--surface-2)}
112
+ .setup-input:focus-visible,.setup-select:focus-visible,.setup-btn:focus-visible,.setup-choice:focus-within{outline:2px solid var(--focus);outline-offset:1px}
113
+ .setup-input--error{border-color:var(--danger)}
114
+ .setup-help{font-size:var(--text-sm);line-height:1.5;color:var(--ink-3)}
115
+ .setup-help--error{color:var(--danger)}
116
+ .setup-row{display:grid;gap:12px;grid-template-columns:repeat(2,minmax(0,1fr))}
117
+ .setup-btn{display:inline-flex;height:var(--control-h);align-items:center;justify-content:center;gap:6px;padding:0 16px;font:inherit;font-size:var(--text-md);font-weight:500;color:var(--ink);text-decoration:none;cursor:pointer;background:var(--surface);border:1px solid var(--line);border-radius:var(--r)}
118
+ .setup-btn:hover{background:var(--surface-2)}
119
+ .setup-btn--primary{color:var(--ink-0);background:var(--primary);border-color:var(--primary)}
120
+ .setup-btn--primary:hover{background:var(--primary-hover);border-color:var(--primary-hover)}
121
+ .setup-btn--block{width:100%}
122
+ .setup-foot{display:flex;align-items:center;justify-content:space-between;gap:10px}
123
+ .setup-foot>:only-child{margin-left:auto}
124
+ .setup-alert{padding:10px 12px;font-size:var(--text-md);line-height:1.5;border:1px solid var(--danger-line);border-radius:var(--r);color:var(--danger);background:var(--danger-bg)}
125
+ .setup-alert--info{color:var(--ink-2);background:var(--blue-50);border-color:var(--blue-100)}
126
+ .setup-alert--warning{color:var(--amber-600);background:var(--warning-bg);border-color:var(--warning-line)}
127
+ .setup-alert--success{color:var(--green-600);background:var(--success-bg);border-color:var(--success-line)}
128
+ .setup-choices{display:grid;gap:10px}
129
+ .setup-choice{display:grid;grid-template-columns:auto 1fr;gap:10px;padding:12px 14px;border:1px solid var(--line);border-radius:var(--r-md);background:var(--surface)}
130
+ .setup-choice b{font-size:var(--text-base);font-weight:600}
131
+ .setup-choice span{display:block;margin-top:3px;font-size:var(--text-sm);line-height:1.5;color:var(--ink-3)}
132
+ .setup-list{display:grid;gap:6px;margin:0;padding:0;list-style:none}
133
+ .setup-list li{display:flex;align-items:baseline;justify-content:space-between;gap:12px;padding:7px 10px;font-size:var(--text-md);background:var(--surface-2);border:1px solid var(--line-2);border-radius:var(--r-sm)}
134
+ .setup-list li>*{min-width:0;overflow-wrap:anywhere}
135
+ .setup-list code{font-family:var(--font-mono);font-size:var(--text-sm)}
136
+ .setup-state{font-size:var(--text-sm);font-weight:500}
137
+ .setup-state--ok{color:var(--success)}
138
+ .setup-state--bad{color:var(--danger)}
139
+ .setup-credentials{display:grid;gap:8px;margin:0;padding:0}
140
+ .setup-credentials div{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:4px 12px;align-items:baseline;padding:10px 12px;background:var(--surface-2);border:1px solid var(--line);border-radius:var(--r)}
141
+ .setup-credentials code{font-family:var(--font-mono);font-size:var(--text-md);overflow-wrap:anywhere;-webkit-user-select:all;user-select:all}
142
+ .setup-credentials b{grid-column:1/-1;font-size:var(--text-xs);font-weight:600;letter-spacing:.09em;text-transform:uppercase;color:var(--ink-3)}
143
+ .setup-fieldset{border:0;margin:0;padding:0}
144
+ .setup-legend{padding:0}
145
+ .setup-sublabel{margin:0 0 6px}
146
+ .setup-note--gap{margin-top:8px}
147
+ .setup-note{margin:0;font-size:var(--text-sm);line-height:1.55;color:var(--ink-3)}
148
+ @media (max-width:900px){
149
+ .setup-page{grid-template-columns:1fr}
150
+ .setup-story{min-height:280px;padding:28px}
151
+ .setup-story__copy{padding:36px 0 16px}
152
+ .setup-story footer{display:none}
153
+ .setup-panel{padding:28px 16px 48px}
154
+ .setup-row{grid-template-columns:1fr}
155
+ }
156
+ @media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
157
+ `;
158
+
159
+ /* Decoration only: aria-hidden, pointer-events none, and every step is
160
+ readable with the canvas missing entirely. Mirrors the sign-in backdrop so
161
+ the installer looks like the product it installs. */
162
+ const BACKDROP = `
163
+ (function(){
164
+ var c=document.getElementById('setup-backdrop');
165
+ var p=c&&c.parentElement,x=c&&c.getContext&&c.getContext('2d');
166
+ if(!c||!p||!x)return;
167
+ var S=30,R=130,dots=[],w=0,h=0,s=1,t=0,px=-1e4,py=-1e4,frame=0;
168
+ var reduced=window.matchMedia('(prefers-reduced-motion: reduce)');
169
+ function build(){var r=p.getBoundingClientRect();w=r.width;h=r.height;
170
+ s=Math.min(window.devicePixelRatio||1,2);c.width=Math.max(1,Math.round(w*s));c.height=Math.max(1,Math.round(h*s));
171
+ dots=[];var i=0;for(var y=S/2;y<h;y+=S)for(var q=S/2;q<w;q+=S)dots.push({x:q,y:y,ox:q,oy:y,vx:0,vy:0,copper:i++%23===0});}
172
+ function draw(animate){x.setTransform(s,0,0,s,0,0);x.clearRect(0,0,w,h);
173
+ for(var i=0;i<dots.length;i++){var d=dots[i];
174
+ if(animate){var dx=d.x-px,dy=d.y-py,q=dx*dx+dy*dy;
175
+ if(q<R*R){var l=Math.sqrt(q)||1,f=(1-l/R)*3.2;d.vx+=dx/l*f;d.vy+=dy/l*f;}
176
+ d.vx+=(d.ox+Math.sin(t*.7+d.oy*.045)*1.6-d.x)*.02;
177
+ d.vy+=(d.oy+Math.cos(t*.6+d.ox*.05)*1.6-d.y)*.02;
178
+ d.vx*=.9;d.vy*=.9;d.x+=d.vx;d.y+=d.vy;}
179
+ var drift=Math.hypot(d.x-d.ox,d.y-d.oy),a=Math.min(.5,.14+drift*.02);
180
+ x.beginPath();x.arc(d.x,d.y,d.copper?1.9:1.4,0,Math.PI*2);
181
+ x.fillStyle=d.copper?'rgba(224,138,69,'+(a+.14)+')':'rgba(135,148,171,'+a+')';x.fill();}}
182
+ function tick(){t+=1/60;draw(true);frame=requestAnimationFrame(tick);}
183
+ build();
184
+ if(reduced.matches){draw(false);}else{
185
+ p.addEventListener('pointermove',function(e){var r=p.getBoundingClientRect();px=e.clientX-r.left;py=e.clientY-r.top;});
186
+ p.addEventListener('pointerleave',function(){px=-1e4;py=-1e4;});
187
+ frame=requestAnimationFrame(tick);}
188
+ new ResizeObserver(function(){cancelAnimationFrame(frame);build();
189
+ if(reduced.matches){draw(false);}else{frame=requestAnimationFrame(tick);}}).observe(p);
190
+ })();
191
+ `;
192
+
193
+ const MARK = `<svg width="24" height="24" viewBox="0 0 24 24" aria-hidden="true"><g fill="#ffffff"><rect x="2" y="3" width="20" height="5" rx="2.5"/><rect x="8" y="10" width="14" height="5" rx="2.5"/></g><g fill="#e08a45"><rect x="14" y="17" width="8" height="5" rx="2.5"/></g></svg>`;
194
+
195
+ function steps(current: SetupStepName, databasePreconfigured: boolean): string {
196
+ const index = STEPS.indexOf(current);
197
+ const items = STEPS.map((name, position) => {
198
+ const done =
199
+ position < index || (databasePreconfigured && name === 'Database');
200
+ const state = position === index ? 'is-active' : done ? 'is-done' : '';
201
+ const marker =
202
+ done && position !== index ? '&#10003;' : `0${position + 1}`.slice(-2);
203
+ const label =
204
+ databasePreconfigured && name === 'Database'
205
+ ? 'Database, configured by deployment'
206
+ : name;
207
+ return `<li class="${state}"${position === index ? ' aria-current="step"' : ''} aria-label="${escapeHtml(label)}"><i aria-hidden="true">${marker}</i>${escapeHtml(name)}</li>`;
208
+ });
209
+ return `<ol class="setup-steps" aria-label="Setup progress">${items.join('')}</ol>`;
210
+ }
211
+
212
+ function field(
213
+ definition: DatabaseConfigurationField,
214
+ adapterId: string,
215
+ view: SetupPageView,
216
+ ): string {
217
+ const name = `field:${adapterId}:${definition.key}`;
218
+ const id = `f-${adapterId.replaceAll('.', '-')}-${definition.key}`;
219
+ const error = view.fieldErrors[definition.key];
220
+ const raw = view.values[name] ?? '';
221
+ /* A secret is never rendered back into the form: the operator retypes it
222
+ rather than having the browser and the response carry it again. */
223
+ const value = definition.secret ? '' : raw;
224
+ const describedBy = `${id}-help`;
225
+ const control = definition.secret
226
+ ? `<input class="setup-input${error ? ' setup-input--error' : ''}" id="${id}" name="${escapeHtml(name)}" type="password" autocomplete="new-password" spellcheck="false" maxlength="512"${definition.required ? ' required' : ''} aria-describedby="${describedBy}">`
227
+ : definition.kind === 'select'
228
+ ? `<select class="setup-select${error ? ' setup-input--error' : ''}" id="${id}" name="${escapeHtml(name)}" aria-describedby="${describedBy}">${(
229
+ definition.options ?? []
230
+ )
231
+ .map(
232
+ (option) =>
233
+ `<option value="${escapeHtml(option.value)}"${option.value === value ? ' selected' : ''}>${escapeHtml(option.label)}</option>`,
234
+ )
235
+ .join('')}</select>`
236
+ : `<input class="setup-input${error ? ' setup-input--error' : ''}" id="${id}" name="${escapeHtml(name)}" type="${definition.kind === 'integer' ? 'number' : 'text'}" value="${escapeHtml(value)}" spellcheck="false" maxlength="512"${definition.required ? ' required' : ''} aria-describedby="${describedBy}">`;
237
+ return `<div class="setup-field"><label class="setup-label" for="${id}">${escapeHtml(definition.label)}</label>${control}<p class="setup-help${error ? ' setup-help--error' : ''}" id="${describedBy}"${error ? ' role="alert"' : ''}>${escapeHtml(error ?? definition.description)}</p></div>`;
238
+ }
239
+
240
+ function csrf(view: SetupPageView): string {
241
+ return view.csrfToken
242
+ ? `<input type="hidden" name="${SETUP_CSRF_FIELD}" value="${escapeHtml(view.csrfToken)}">`
243
+ : '';
244
+ }
245
+
246
+ function alerts(view: SetupPageView): string {
247
+ return [
248
+ view.error
249
+ ? `<p class="setup-alert" role="alert">${escapeHtml(view.error)}</p>`
250
+ : '',
251
+ view.notice
252
+ ? `<p class="setup-alert setup-alert--info">${escapeHtml(view.notice)}</p>`
253
+ : '',
254
+ ].join('');
255
+ }
256
+
257
+ function unlockStep(view: SetupPageView): string {
258
+ return `<header><span class="setup-kicker">First run</span><h2>Unlock setup</h2>
259
+ <p class="setup-card__sub">${
260
+ view.inPlace
261
+ ? 'The setup token was printed by the command that deployed this app'
262
+ : `The setup token was printed in this deployment's output when it started${
263
+ view.tokenFile
264
+ ? ` and written to <code>${escapeHtml(view.tokenFile)}</code>`
265
+ : ''
266
+ }`
267
+ }. Paste it here to continue.</p></header>
268
+ ${steps(view.step, view.databasePreconfigured)}${alerts(view)}
269
+ <form class="setup-form" method="post" action="/setup"><input type="hidden" name="step" value="unlock">
270
+ <div class="setup-field"><label class="setup-label" for="setup-token">Setup token</label>
271
+ <input class="setup-input" id="setup-token" name="token" type="password" autocomplete="off" spellcheck="false" maxlength="256" required autofocus aria-describedby="setup-token-help">
272
+ <p class="setup-help" id="setup-token-help">${view.inPlace ? 'Running the deploy command again issues a new token.' : 'Restarting this deployment issues a new token.'}</p></div>
273
+ <button class="setup-btn setup-btn--primary setup-btn--block" type="submit">Continue</button></form>`;
274
+ }
275
+
276
+ function configureStep(view: SetupPageView): string {
277
+ const selected = view.selectedAdapterId ?? view.adapters[0]?.adapterId ?? '';
278
+ const choices = view.adapters
279
+ .map(
280
+ (adapter) =>
281
+ `<label class="setup-choice"><input type="radio" name="adapter" value="${escapeHtml(adapter.adapterId)}"${adapter.adapterId === selected ? ' checked' : ''}><span><b>${escapeHtml(adapter.label)}</b><span>${escapeHtml(adapter.description)}</span></span></label>`,
282
+ )
283
+ .join('');
284
+ const fieldsets = view.adapters
285
+ .map((adapter) => {
286
+ const inner = adapter.configurationSchema.fields
287
+ .map((definition) => field(definition, adapter.adapterId, view))
288
+ .join('');
289
+ /* Every fieldset is rendered open. The script below collapses the ones
290
+ the operator did not pick, so the form stays complete without it. */
291
+ return `<fieldset class="setup-form setup-fieldset" data-adapter="${escapeHtml(adapter.adapterId)}"><legend class="setup-label setup-legend">${escapeHtml(adapter.label)}</legend>${inner || '<p class="setup-help">Nothing to configure.</p>'}</fieldset>`;
292
+ })
293
+ .join('');
294
+ return `<header><span class="setup-kicker">Database</span><h2>Connect PostgreSQL</h2>
295
+ <p class="setup-card__sub">Choose a PostgreSQL connection. Flowdular tests it before running migrations or creating a workspace.</p></header>
296
+ ${steps(view.step, view.databasePreconfigured)}${alerts(view)}
297
+ <form class="setup-form" method="post" action="/setup">${csrf(view)}<input type="hidden" name="step" value="configure">
298
+ <div class="setup-choices">${choices}</div>${fieldsets}
299
+ <button class="setup-btn setup-btn--primary setup-btn--block" type="submit">Test connection</button></form>`;
300
+ }
301
+
302
+ /* Mirrors validateWorkspaceSlug (3 to 48 characters, no double hyphen) in the
303
+ syntax browsers compile pattern attributes with (the v flag). */
304
+ const WORKSPACE_SLUG_INPUT_PATTERN =
305
+ '(?!.*--)[a-z0-9][a-z0-9\\-]{1,46}[a-z0-9]';
306
+
307
+ function workspaceField(
308
+ view: SetupPageView,
309
+ name: string,
310
+ label: string,
311
+ options: {
312
+ readonly type?: 'text' | 'email' | 'password';
313
+ readonly autocomplete?: string;
314
+ readonly autocapitalize?: string;
315
+ readonly autocorrect?: string;
316
+ readonly hint: string;
317
+ readonly maxlength: number;
318
+ readonly spellcheck?: boolean;
319
+ readonly readonly?: boolean;
320
+ readonly pattern?: string;
321
+ },
322
+ ): string {
323
+ const id = `setup-${name}`;
324
+ const error = view.fieldErrors[name];
325
+ const type = options.type ?? 'text';
326
+ const value = type === 'password' ? '' : (view.values[name] ?? '');
327
+ return `<div class="setup-field"><label class="setup-label" for="${id}">${escapeHtml(label)}</label>
328
+ <input class="setup-input${error ? ' setup-input--error' : ''}" id="${id}" name="${name}" type="${type}"${type === 'password' ? '' : ` value="${escapeHtml(value)}"`}${options.autocomplete ? ` autocomplete="${escapeHtml(options.autocomplete)}"` : ''}${options.autocapitalize ? ` autocapitalize="${escapeHtml(options.autocapitalize)}"` : ''}${options.autocorrect ? ` autocorrect="${escapeHtml(options.autocorrect)}"` : ''}${options.spellcheck === false ? ' spellcheck="false"' : ''}${options.readonly ? ' readonly' : ''}${options.pattern ? ` pattern="${escapeHtml(options.pattern)}"` : ''} maxlength="${options.maxlength}" required aria-describedby="${id}-help"${error ? ' aria-invalid="true"' : ''}>
329
+ <p class="setup-help${error ? ' setup-help--error' : ''}" id="${id}-help"${error ? ' role="alert"' : ''}>${escapeHtml(error ?? options.hint)}</p></div>`;
330
+ }
331
+
332
+ function workspaceStep(view: SetupPageView): string {
333
+ const back = view.databasePreconfigured
334
+ ? ''
335
+ : '<button class="setup-btn" type="submit" name="step" value="back" formnovalidate>Back</button>';
336
+ const nextClass = view.databasePreconfigured
337
+ ? 'setup-btn setup-btn--primary setup-btn--block'
338
+ : 'setup-btn setup-btn--primary';
339
+ return `<header><span class="setup-kicker">Workspace</span><h2>Create your workspace</h2>
340
+ <p class="setup-card__sub">Set up the first workspace and the owner account you will use to sign in.</p></header>
341
+ ${steps(view.step, view.databasePreconfigured)}${alerts(view)}
342
+ ${view.databasePreconfigured ? '<p class="setup-alert setup-alert--success">PostgreSQL is already configured for this deployment.</p>' : ''}
343
+ <form class="setup-form" method="post" action="/setup">${csrf(view)}
344
+ ${workspaceField(view, 'workspaceName', 'Workspace name', { hint: 'The name shown to people in this workspace.', maxlength: 120 })}
345
+ ${workspaceField(view, 'workspaceSlug', 'Workspace address', { hint: '3 to 48 lowercase letters, numbers, or single hyphens.', maxlength: 48, autocapitalize: 'none', autocorrect: 'off', spellcheck: false, pattern: WORKSPACE_SLUG_INPUT_PATTERN })}
346
+ <div class="setup-row">
347
+ ${workspaceField(view, 'ownerName', 'Your name', { hint: 'Shown on your owner account.', maxlength: 80, autocomplete: 'name' })}
348
+ ${workspaceField(view, 'ownerEmail', 'Your email', { type: 'email', hint: 'Used to sign in.', maxlength: 254, autocomplete: 'email', spellcheck: false })}
349
+ </div>
350
+ ${workspaceField(view, 'ownerPassword', 'Your password', { type: 'password', hint: `Use at least ${view.passwordMinLength} characters. Your password will not be shown again.`, maxlength: 512, autocomplete: 'new-password' })}
351
+ ${workspaceField(view, 'ownerPasswordConfirm', 'Confirm your password', { type: 'password', hint: 'Re-enter the password for the owner account.', maxlength: 512, autocomplete: 'new-password' })}
352
+ ${workspaceField(view, 'applicationPath', 'Backoffice address', { hint: view.databasePreconfigured ? 'This address is set by the deployment. Change its configuration and restart to choose another.' : 'Use /app or another path, such as /backoffice. Takes effect after restart.', maxlength: 64, spellcheck: false, readonly: view.databasePreconfigured })}
353
+ <div class="setup-foot">${back}<button class="${nextClass}" type="submit" name="step" value="workspace">Review setup</button></div></form>`;
354
+ }
355
+
356
+ function reviewStep(view: SetupPageView): string {
357
+ const blocked = view.modules.filter((module) => module.issues.length > 0);
358
+ const probe = view.probe;
359
+ const databaseReady =
360
+ probe?.status === 'ready' || (view.databasePreconfigured && probe === null);
361
+ const modules = view.modules
362
+ .map(
363
+ (module) =>
364
+ `<li><code>${escapeHtml(module.moduleId)}</code><span class="setup-state ${module.issues.length > 0 ? 'setup-state--bad' : 'setup-state--ok'}">${
365
+ module.issues.length > 0
366
+ ? escapeHtml(module.issues.join(' '))
367
+ : module.tenantOwned
368
+ ? 'tenant isolated'
369
+ : 'compatible'
370
+ }</span></li>`,
371
+ )
372
+ .join('');
373
+ return `<header><span class="setup-kicker">Review</span><h2>Review setup</h2>
374
+ <p class="setup-card__sub">${
375
+ blocked.length > 0
376
+ ? 'This database cannot serve every enabled module, so it is not activated.'
377
+ : view.inPlace
378
+ ? 'Applying creates your workspace and owner account.'
379
+ : 'Applying creates your workspace and owner account. Enabled modules finish preparing when the app restarts.'
380
+ }</p></header>
381
+ ${steps(view.step, view.databasePreconfigured)}${alerts(view)}
382
+ ${
383
+ probe?.status === 'ready'
384
+ ? `<p class="setup-alert setup-alert--success">Connected in ${probe.latencyMs} ms as the runtime role, which holds neither SUPERUSER nor BYPASSRLS.</p>`
385
+ : view.databasePreconfigured && probe === null
386
+ ? '<p class="setup-alert setup-alert--success">PostgreSQL is configured by this deployment.</p>'
387
+ : `<p class="setup-alert" role="alert">${escapeHtml(probe?.message ?? 'The connection could not be tested.')}</p>`
388
+ }
389
+ <div><p class="setup-label setup-sublabel">Your workspace</p><ul class="setup-list">
390
+ <li><span>Name</span><strong>${escapeHtml(view.values.workspaceName ?? '')}</strong></li>
391
+ <li><span>Address</span><code>${escapeHtml(view.values.workspaceSlug ?? '')}</code></li>
392
+ <li><span>Owner</span><span>${escapeHtml(view.values.ownerName ?? '')} &lt;${escapeHtml(view.values.ownerEmail ?? '')}&gt;</span></li>
393
+ <li><span>Backoffice</span><code>${escapeHtml(view.values.applicationPath ?? '/app')}</code></li>
394
+ </ul></div>
395
+ <div><p class="setup-label setup-sublabel">Enabled modules that own tables</p><ul class="setup-list">${modules || '<li><span class="setup-help">No enabled module owns database tables.</span></li>'}</ul>${
396
+ view.modulesApproximated
397
+ ? '<p class="setup-note setup-note--gap">This build ships without the module manifests, so every enabled module is checked against the strictest requirements any of them can ask for.</p>'
398
+ : ''
399
+ }</div>
400
+ <form class="setup-form" method="post" action="/setup">${csrf(view)}
401
+ <div class="setup-foot"><button class="setup-btn" type="submit" name="step" value="back">Back</button>
402
+ ${
403
+ blocked.length === 0 && databaseReady
404
+ ? '<button class="setup-btn setup-btn--primary" type="submit" name="step" value="apply">Migrate and create workspace</button>'
405
+ : ''
406
+ }</div></form>`;
407
+ }
408
+
409
+ function environmentSection(result: EnvironmentWriteResult): string {
410
+ const keys = [...result.block.matchAll(/(?:^|\n)(FD_[A-Z0-9_]+)(?:=|$)/g)]
411
+ .map((match) => match[1])
412
+ .filter((key): key is string => Boolean(key));
413
+ const keyNames =
414
+ keys.length > 0
415
+ ? escapeHtml(keys.join(', '))
416
+ : 'the database connection keys';
417
+ if (result.status === 'read-only') {
418
+ return `<p class="setup-alert setup-alert--warning">This deployment's filesystem is read only. Set ${keyNames} in this service's environment using the connection values you entered, then restart. Without those settings, a restart returns to setup.</p>`;
419
+ }
420
+ if (result.status === 'failed') {
421
+ return `<p class="setup-alert" role="alert">The connection settings could not be stored in <code>${escapeHtml(result.path)}</code>. Set ${keyNames} in this service's environment using the connection values you entered, then restart.</p>`;
422
+ }
423
+ if (result.status === 'unchanged') {
424
+ return `<p class="setup-alert setup-alert--info">The required connection settings were already present in <code>${escapeHtml(result.path)}</code>. Nothing was replaced.</p>`;
425
+ }
426
+ return `<p class="setup-note">Stored in <code>${escapeHtml(result.path)}</code>, readable only by this user${
427
+ result.kept.length > 0
428
+ ? `. ${escapeHtml(result.kept.join(', '))} was already set there and was left as it is`
429
+ : ''
430
+ }.</p>`;
431
+ }
432
+
433
+ function doneStep(view: SetupPageView): string {
434
+ const seed = view.seed;
435
+ const owner = seed?.accounts[0];
436
+ const needsEnvironment =
437
+ view.environment?.status === 'read-only' ||
438
+ view.environment?.status === 'failed';
439
+ const nextStep = needsEnvironment
440
+ ? 'Save the connection settings in this deployment, then restart it to sign in.'
441
+ : view.inPlace
442
+ ? 'Sign in to start using it.'
443
+ : view.autoRestart
444
+ ? 'The app is restarting. Wait a moment, then sign in. If it does not restart, restart the deployment.'
445
+ : 'Restart this deployment to leave setup and open the sign-in screen.';
446
+ return `<header><span class="setup-kicker">Sign in</span><h2>Flowdular is ready</h2>
447
+ <p class="setup-card__sub">The workspace ${escapeHtml(seed?.workspace.name ?? '')} is ready. ${nextStep}</p></header>
448
+ ${steps(view.step, view.databasePreconfigured)}${alerts(view)}
449
+ <div><p class="setup-label setup-sublabel">Sign in as the workspace owner</p><div class="setup-credentials"><div><b>Owner account</b><code>${escapeHtml(owner?.email ?? view.values.ownerEmail ?? '')}</code></div></div>
450
+ <p class="setup-note setup-note--gap">After signing in, you can connect an AI model in Providers. This is optional and can be done later.</p></div>
451
+ ${view.environment ? environmentSection(view.environment) : ''}
452
+ <a class="setup-btn setup-btn--primary setup-btn--block" href="${escapeHtml(view.values.applicationPath ?? '/app')}">Go to sign in</a>`;
453
+ }
454
+
455
+ export function renderSetupPage(view: SetupPageView, nonce: string): string {
456
+ /* This script runs only after the entire completion page has loaded. The
457
+ restart request is separate from the response that created the owner. */
458
+ const restartScript =
459
+ view.step === 'Sign in' && view.autoRestart && view.csrfToken
460
+ ? `<script nonce="${nonce}">fetch('/setup',{method:'POST',credentials:'same-origin',headers:{'content-type':'application/x-www-form-urlencoded'},body:new URLSearchParams({step:'restart',setupCsrf:'${view.csrfToken}'}),keepalive:true}).catch(()=>{});</script>`
461
+ : '';
462
+ const body =
463
+ view.step === 'Unlock'
464
+ ? unlockStep(view)
465
+ : view.step === 'Database'
466
+ ? configureStep(view)
467
+ : view.step === 'Workspace'
468
+ ? workspaceStep(view)
469
+ : view.step === 'Review'
470
+ ? reviewStep(view)
471
+ : doneStep(view);
472
+ return `<!doctype html>
473
+ <html lang="en"><head><meta charset="utf-8">
474
+ <meta name="viewport" content="width=device-width,initial-scale=1">
475
+ <meta name="robots" content="noindex,nofollow,noarchive">
476
+ <title>Set up Flowdular</title>
477
+ <style nonce="${nonce}">${STYLES}</style></head>
478
+ <body><main class="setup-page">
479
+ <section class="setup-story" aria-label="Flowdular">
480
+ <canvas class="setup-backdrop" id="setup-backdrop" aria-hidden="true"></canvas>
481
+ <span class="setup-brand">${MARK}Flowdular</span>
482
+ <div class="setup-story__copy"><span class="setup-kicker">Installation</span>
483
+ <h1>Set up your workspace and make it yours.</h1>
484
+ <p>Flowdular will check the database, prepare your modules, and create your first workspace. If this deployment already provides PostgreSQL, you can start with your workspace details.</p></div>
485
+ <footer><span>Agentic foundation platform</span><span>Setup</span></footer></section>
486
+ <section class="setup-panel"><div class="setup-card">${body}</div></section>
487
+ </main>
488
+ <script nonce="${nonce}">${BACKDROP}</script>
489
+ <script nonce="${nonce}">
490
+ (function(){var f=document.querySelectorAll('input[name="adapter"]');if(!f.length)return;
491
+ function sync(){for(var i=0;i<f.length;i++){var s=document.querySelector('fieldset[data-adapter="'+f[i].value+'"]');
492
+ if(s)s.hidden=!f[i].checked;}}
493
+ for(var i=0;i<f.length;i++)f[i].addEventListener('change',sync);sync();})();
494
+ </script>
495
+ ${restartScript}
496
+ </body></html>`;
497
+ }