create-flowdular 0.2.5 → 0.3.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 (101) hide show
  1. package/agent-template/.agents/skills/agent-tool-design/SKILL.md +1 -1
  2. package/agent-template/.agents/skills/auth-security-review/SKILL.md +1 -1
  3. package/agent-template/.agents/skills/deploy-operate/SKILL.md +109 -0
  4. package/agent-template/.agents/skills/module-new/SKILL.md +29 -0
  5. package/agent-template/.agents/skills/module-update/SKILL.md +9 -1
  6. package/agent-template/.agents/skills/spec-interview/SKILL.md +114 -0
  7. package/agent-template/.agents/skills/ux-design/SKILL.md +34 -3
  8. package/agent-template/.ai/README.md +2 -1
  9. package/agent-template/.ai/agents/sandbox/business-manager.md +5 -1
  10. package/agent-template/.ai/blueprints/author-spec/README.md +1 -1
  11. package/agent-template/.ai/blueprints/author-spec/spec-requirements.yaml +44 -0
  12. package/agent-template/.ai/blueprints/author-spec/steps.yaml +5 -5
  13. package/agent-template/.ai/blueprints/author-spec/templates/module.yaml +99 -3
  14. package/agent-template/.ai/blueprints/edit-module/gates.yaml +4 -0
  15. package/agent-template/.ai/blueprints/edit-module/required-files.yaml +9 -0
  16. package/agent-template/.ai/blueprints/new-module/gates.yaml +4 -0
  17. package/agent-template/.ai/blueprints/new-module/spec-requirements.yaml +2 -2
  18. package/agent-template/.ai/blueprints/release/gates.yaml +4 -0
  19. package/agent-template/.ai/platform-capabilities.md +128 -0
  20. package/agent-template/.ai/policies/capabilities.yaml +100 -0
  21. package/agent-template/.ai/policies/path-ownership.yaml +5 -2
  22. package/agent-template/.ai/policies/task-budgets.yaml +5 -3
  23. package/agent-template/.ai/references/catalog/module.json +4 -4
  24. package/agent-template/.ai/references/catalog/package.json +2 -2
  25. package/agent-template/.ai/references/catalog/spec/module.yaml +5 -3
  26. package/agent-template/.ai/references/catalog/src/platform.ts +2 -0
  27. package/agent-template/.ai/references/catalog/src/services/catalog-service.ts +89 -1
  28. package/agent-template/.ai/references/catalog/src/services/data-classes.ts +47 -0
  29. package/agent-template/.ai/references/catalog/src/services/database-repository.ts +98 -1
  30. package/agent-template/.ai/references/catalog/src/services/repository.ts +22 -1
  31. package/agent-template/.ai/references/catalog/tests/data-classes.test.ts +157 -0
  32. package/agent-template/.ai/references/catalog.provenance.json +12 -10
  33. package/agent-template/.ai/rules/flowdular.md +4 -0
  34. package/agent-template/.ai/skills/README.md +10 -0
  35. package/agent-template/.ai/skills/agent-tool-design/SKILL.md +1 -2
  36. package/agent-template/.ai/skills/auth-security-review/SKILL.md +1 -1
  37. package/agent-template/.ai/skills/business-agent-design/SKILL.md +0 -1
  38. package/agent-template/.ai/skills/deploy-operate/SKILL.md +114 -0
  39. package/agent-template/.ai/skills/module-new/SKILL.md +29 -3
  40. package/agent-template/.ai/skills/module-update/SKILL.md +9 -3
  41. package/agent-template/.ai/skills/perf-audit/SKILL.md +0 -1
  42. package/agent-template/.ai/skills/release-eject-pr/SKILL.md +0 -1
  43. package/agent-template/.ai/skills/spec-interview/SKILL.md +120 -0
  44. package/agent-template/.ai/skills/test-hardening/SKILL.md +1 -0
  45. package/agent-template/.ai/skills/ux-design/SKILL.md +34 -3
  46. package/agent-template/.ai/skills/variables/SKILL.md +0 -2
  47. package/agent-template/.ai/skills/workflow-development/SKILL.md +0 -1
  48. package/agent-template/.claude/skills/agent-tool-design/SKILL.md +1 -1
  49. package/agent-template/.claude/skills/auth-security-review/SKILL.md +1 -1
  50. package/agent-template/.claude/skills/deploy-operate/SKILL.md +109 -0
  51. package/agent-template/.claude/skills/module-new/SKILL.md +29 -0
  52. package/agent-template/.claude/skills/module-update/SKILL.md +9 -1
  53. package/agent-template/.claude/skills/spec-interview/SKILL.md +114 -0
  54. package/agent-template/.claude/skills/ux-design/SKILL.md +34 -3
  55. package/agent-template/AGENTS.md +4 -0
  56. package/agent-template/CLAUDE.md +4 -0
  57. package/agent-template/docs/adr/0003-module-settings.md +1 -1
  58. package/agent-template/docs/adr/0006-agentic-workflows.md +24 -21
  59. package/agent-template/docs/agent-contract.md +2 -2
  60. package/agent-template/docs/cli-extensions.md +82 -0
  61. package/agent-template/docs/cli.md +190 -0
  62. package/agent-template/docs/configuration.md +593 -36
  63. package/agent-template/docs/design-system.md +185 -31
  64. package/agent-template/docs/getting-started.md +118 -0
  65. package/agent-template/docs/module-distribution.md +96 -0
  66. package/agent-template/docs/module-web-surfaces.md +221 -0
  67. package/agent-template/docs/modules.md +216 -0
  68. package/agent-template/docs/operations.md +464 -0
  69. package/agent-template/docs/sandbox.md +212 -0
  70. package/agent-template/platform/scripts/build.mjs +10 -0
  71. package/dist/bin.js +65 -1
  72. package/package.json +1 -1
  73. package/template/default/.dockerignore +14 -0
  74. package/template/default/.env.example +94 -0
  75. package/template/default/README.md +37 -1
  76. package/template/default/flowdular.json +15 -4
  77. package/template/default/infra/README.md +116 -0
  78. package/template/default/infra/docker/Dockerfile +37 -0
  79. package/template/default/infra/docker/compose.yaml +158 -0
  80. package/template/default/infra/docker/postgres/10-roles.sh +31 -0
  81. package/template/default/infra/docker/postgres/tls-init.sh +28 -0
  82. package/template/default/infra/kubernetes/database-secret.example.yaml +15 -0
  83. package/template/default/infra/kubernetes/deployment.yaml +211 -0
  84. package/template/default/infra/kubernetes/kustomization.yaml +9 -0
  85. package/template/default/infra/kubernetes/secrets.example.yaml +52 -0
  86. package/template/default/infra/kubernetes/service.yaml +13 -0
  87. package/template/default/modules/example/module.json +2 -1
  88. package/template/default/modules/example/package.json +2 -2
  89. package/template/default/modules/example/spec/module.yaml +1 -1
  90. package/template/default/modules/example/src/services/database-repository.ts +2 -12
  91. package/template/default/package.json +3 -2
  92. package/template/default/platform/octane.config.ts +99 -9
  93. package/template/default/platform/package.json +1 -1
  94. package/template/default/platform/src/generated/modules.client.ts +26 -2
  95. package/template/default/platform/src/generated/modules.server.ts +241 -10
  96. package/template/default/platform/src/server/health.ts +47 -0
  97. package/template/default/platform/src/server/metrics.ts +100 -0
  98. package/template/default/platform/src/server/storage.ts +172 -0
  99. package/template/default/platform/src/server/tracing.ts +85 -0
  100. package/template/default/pnpm-workspace.yaml +1 -0
  101. package/template/default/specs/application.yaml +15 -0
@@ -0,0 +1,212 @@
1
+ # Sandbox
2
+
3
+ The sandbox is a separate, chat-first application that builds a change in an
4
+ isolated workspace, drives the coding agent your team already uses, runs the
5
+ same gates the platform runs, and previews the result inside the real
6
+ application shell. A session carries as many modules as the work touches.
7
+
8
+ Full documentation lives with the package:
9
+ [packages/sandbox/README.md](https://github.com/flowdular/flowdular/blob/main/packages/sandbox/README.md).
10
+
11
+ ## Start it
12
+
13
+ ```bash
14
+ pnpm sandbox # from this repository
15
+ npx @flowdular/sandbox # from any Flowdular workspace
16
+ ```
17
+
18
+ The launcher walks up to `flowdular.json` to find the workspace and opens
19
+ `http://127.0.0.1:4320`. `--port`, `--workspace`, `--host` and `--mode` override
20
+ the defaults.
21
+
22
+ ## Connect it to a running application
23
+
24
+ The sandbox is a client of a running Flowdular application and never opens the
25
+ platform database. The preview gets its own embedded PostgreSQL under the
26
+ session data directory, with the same roles and the same forced row-level
27
+ security a deployment has, and it is thrown away with the session.
28
+
29
+ 1. In the application, open Administration, API tokens, and issue a token with
30
+ `sandbox.access.use` plus the read scopes the preview should see. Add
31
+ `sandbox.preview.data` for live data and `sandbox.modules.eject` for eject.
32
+ 2. Grant sandbox access to the account, in the app under Development, Sandbox,
33
+ or from the CLI:
34
+
35
+ ```bash
36
+ pnpm flowdular sandbox grant --email admin@example.com --tenant operations-demo --apply
37
+ pnpm flowdular sandbox access --tenant operations-demo
38
+ ```
39
+
40
+ 3. Paste the token and the application address into the sandbox connect screen.
41
+
42
+ The token is encrypted at rest under `.flowdular/sandbox/secret.key` and is never
43
+ returned to the browser. The application may run anywhere: locally on
44
+ `http://127.0.0.1:4310` or a deployment.
45
+
46
+ ## Modes
47
+
48
+ | Mode | Binding | Coding agents |
49
+ | ------------- | -------------------- | ----------------------------------- |
50
+ | `loopback` | loopback interface | local `claude` and `codex`, or BYOK |
51
+ | `self-hosted` | configured interface | bring your own key only |
52
+
53
+ A non-loopback `--host` forces `self-hosted`. A sandbox that cannot prove it is
54
+ loopback never offers a local binary, because a local binary carries the
55
+ operator's own login.
56
+
57
+ ## How a session works
58
+
59
+ A planner names the modules the brief touches and the first specialist role.
60
+ Business, UX, backend, frontend and agentic roles hand off inside one session;
61
+ each turn writes only in its own allowed paths and is followed by the gates that
62
+ role declares. The roles live in [`.ai/agents/sandbox`](../.ai/agents/sandbox),
63
+ so a workspace can change them.
64
+
65
+ When the work is done, eject it into `modules/` and enable it, or open a pull
66
+ request with the gate evidence attached.
67
+
68
+ ## Deliver as a pull request
69
+
70
+ The eject route (`POST /sandbox/api/sessions/:id/eject`) takes `target:
71
+ 'workspace' | 'git-pr'`. `workspace` copies the session modules into `modules/`
72
+ of this checkout. `git-pr` (`packages/sandbox/src/server/delivery/git-pr.ts`)
73
+ commits the same change on a branch and opens a pull request; the operator's
74
+ working tree and index stay untouched because the work happens in a detached
75
+ worktree under `.flowdular/sandbox/worktrees/<session id>`, removed afterwards.
76
+
77
+ ### Configuration
78
+
79
+ Project settings live in `flowdular.json` under `sandbox.delivery`, read at
80
+ request time (`delivery/configuration.ts`): `targets`, `default`,
81
+ `maxChangedFiles` and `git` with `remote`
82
+ (`origin`), `repository` (`owner/name`, derived from the remote when null),
83
+ `baseBranch` (`main`), `branchPrefix` (`sandbox`), `provider` (`github` or
84
+ `none`), `mode` (`auto`, `direct` or `fork`), `forkOwner` and `reviewers`.
85
+
86
+ Operator settings live in the sandbox configuration
87
+ (`.flowdular/sandbox/config.json`, `GitHubDeliveryConfiguration` in
88
+ `server/config.ts`) and are set from the sandbox settings screen, not from
89
+ environment variables: `enabled`, `overridesProject`, `remote`, `repository`,
90
+ `baseBranch`, `branchPrefix`, `mode`, `forkOwner`, `reviewers` (settings
91
+ request fields `githubEnabled`, `githubOverridesProject`, `githubRemote` and so
92
+ on). With `overridesProject` the operator values replace the project `git`
93
+ block except `provider`. `enabled: false` disables the target with
94
+ `EJECT_TARGET_DISABLED`. A provider token (`githubToken` in the settings
95
+ request, stored sealed as `gitProviderToken`) is handed to `gh` as `GH_TOKEN`
96
+ and to `git` as a redacted authorization header; it never appears in command
97
+ arguments, remote URLs or step output.
98
+
99
+ ### Branch and pull request
100
+
101
+ The branch is `<branchPrefix>/<module directory>-<first 8 characters of the
102
+ session id>`, created from `<remote>/<baseBranch>`. In the worktree the sandbox
103
+ stages the modules, runs `pnpm install`, `module enable` for each new module and
104
+ the platform typecheck, then checks the guardrails: changed paths limited to the
105
+ session modules, the lockfile and the CLI-owned composition files, the file
106
+ count within `maxChangedFiles` (else `.ai/policies/task-budgets.yaml`), new
107
+ packages within `maxNewDependencies` from `.ai/policies/task-budgets.yaml`
108
+ (default 0, per-kind overrides). Owners and the cross-owner reviewer
109
+ requirement (`crossOwnerChanges.requireReviewer`) come from
110
+ `.ai/policies/path-ownership.yaml`. The commit reads
111
+ `sandbox: add|update <module id>` with the session id and the gate summary; the
112
+ push uses `--force-with-lease`. A branch that exists but was not created for
113
+ this session is refused (`GIT_BRANCH_CONFLICT`).
114
+
115
+ With `provider: github` and a working `gh auth status`, `gh pr create` opens the
116
+ pull request against `baseBranch` with `--reviewer` from `reviewers`; when a
117
+ pull request for the branch is already open, the push updates it. `mode: auto`
118
+ uses a direct push only after GitHub confirms push access and otherwise asks the
119
+ operator to choose `direct` or `fork`; only an explicit `fork` creates or reuses
120
+ `<forkOwner>/<name>`. Without `gh`, without a login, or with `provider: none`,
121
+ the branch is still pushed; the plan shows a GitHub compare link when
122
+ `repository` is configured or derived from a GitHub remote URL, otherwise the
123
+ remote and branch name.
124
+ `.flowdular/sandbox/sessions/<id>/delivery.json` keeps the branch and the URL.
125
+
126
+ ### Gates as evidence
127
+
128
+ Delivery reruns every gate from `delivery/plan.ts` (`spec-schema`,
129
+ `module-schema`, `dependencies`, `typecheck`, `tests`, `format`, `auto-review`)
130
+ before anything is committed; a missing, failed or skipped result stops it
131
+ (`EJECT_GATES_MISSING`, `EJECT_GATES_FAILED`), and each module needs a current
132
+ review record (`EJECT_REVIEW_REQUIRED`) and an approved spec hash. The pull
133
+ request body carries the spec version and status per module, a table of gate,
134
+ module and result, the files added, modified and removed, the risks the
135
+ guardrails noticed, and the post-merge `auth sync-scopes` command. The table is
136
+ what the sandbox measured on the delivered bytes; `pnpm verify` on the branch
137
+ and a human review remain the repository's own gate.
138
+
139
+ ## Decisions the specialist needs
140
+
141
+ A specialist that cannot continue without a business decision ends its reply
142
+ with one fenced block tagged `questions` holding a single JSON object:
143
+
144
+ ````
145
+ ```questions
146
+ {
147
+ "questions": [
148
+ {
149
+ "id": "Q-1",
150
+ "question": "Who may cancel a booking?",
151
+ "options": ["Only the owner", "Any team member"],
152
+ "recommended": "Only the owner",
153
+ "allowFreeText": true
154
+ }
155
+ ]
156
+ }
157
+ ```
158
+ ````
159
+
160
+ The sandbox parses it server side and bounds it: at most 12 questions, unique
161
+ ids shaped `Q-1`, a question of 1 to 400 characters, at most 8 distinct options
162
+ of 1 to 120 characters, and a recommendation that must be one of those options.
163
+ `allowFreeText` defaults to false, and a question with neither an option nor
164
+ free text cannot be answered, so it is refused. The block has to be the last
165
+ thing in the reply apart from the mandatory handoff line, and a reply carries at
166
+ most one. A block the sandbox cannot read is a warning on that turn, never a
167
+ failed turn: the words of the reply still stand and the transcript says why the
168
+ block was ignored.
169
+
170
+ A readable block is stored on the session as `pendingQuestions`, with the
171
+ transcript sequence of the message that asked, the role that asked and the
172
+ module it asked about. Records written before the field existed read as `null`.
173
+ Every turn rewrites it, so the form only ever shows what the newest specialist
174
+ is waiting on.
175
+
176
+ The session view shows the questions as a list rather than as JSON, and offers a
177
+ form below the open handoff: one radio group per question with the
178
+ recommendation preselected, a free-text field where the specialist allowed one,
179
+ and an optional note. Submitting posts
180
+
181
+ ```
182
+ POST /sandbox/api/sessions/:id/answers
183
+ { "answers": [{ "id": "Q-1", "answer": "Only the owner" }], "message": "optional" }
184
+ ```
185
+
186
+ behind the same origin, header and ownership checks as every other mutation. It
187
+ clears `pendingQuestions` and starts the next turn in the role that asked, in
188
+ the module it asked about, with the decisions leading the request text:
189
+
190
+ ```
191
+ Decisions:
192
+ - Q-1: Who may cancel a booking? -> Only the owner
193
+
194
+ <the operator's optional message>
195
+ ```
196
+
197
+ The response is the turn stream, exactly as `POST /sandbox/api/sessions/:id/turn`
198
+ answers. Refusals, each a stable error code: `409 NO_PENDING_QUESTIONS` when
199
+ nothing is waiting, `409 SESSION_ARCHIVED`, `409 SESSION_DELIVERED`, and `400
200
+ INVALID_INPUT` for a body that leaves a question unanswered, names a question
201
+ the session did not ask, exceeds 400 characters, or gives an answer that is not
202
+ one of the offered options when the specialist allowed no free text.
203
+
204
+ ## Operator commands
205
+
206
+ ```bash
207
+ pnpm flowdular sandbox sessions --tenant <tenant>
208
+ pnpm flowdular sandbox session-archive --tenant <tenant> --id <session-id> --apply
209
+ pnpm flowdular sandbox session-delete --tenant <tenant> --id <session-id> --apply
210
+ pnpm flowdular sandbox revoke --email <email> --tenant <tenant> --apply
211
+ pnpm flowdular sandbox audit-verify --tenant <tenant>
212
+ ```
@@ -17,13 +17,23 @@ const environment = {
17
17
  throwaway state directory below; runtime still reads its real adapter. */
18
18
  FD_ENV: 'development',
19
19
  FD_INTERNAL_BUILD: 'true',
20
+ /* The bundler evaluates the server composition. It must not open a trace or
21
+ error egress to the deployment's collector while building. */
22
+ FD_TRACE_EXPORTER: 'none',
23
+ FD_ERROR_SINK: 'none',
20
24
  FD_DATABASE_ADAPTER: 'pglite',
21
25
  FD_DATABASE_PGLITE_DIRECTORY: join(stateDirectory, 'pglite'),
22
26
  FD_AGENT_CREDENTIAL_KEY: buildSecret(),
23
27
  FD_AGENT_RUN_GRANT_KEY: buildSecret(),
24
28
  FD_AUTOMATIONS_CREDENTIAL_KEY: buildSecret(),
29
+ FD_NOTIFICATIONS_SECRET_KEY: buildSecret(),
25
30
  FD_WORKFLOWS_PAYLOAD_KEY: buildSecret(),
26
31
  FD_WORKFLOWS_CURSOR_KEY: buildSecret(),
32
+ FD_STORAGE_ADAPTER: 'local',
33
+ FD_STORAGE_LOCAL_DIRECTORY: join(stateDirectory, 'storage'),
34
+ FD_STORAGE_ENCRYPTION_KEY: buildSecret(),
35
+ FD_CONNECTORS_SECRET_KEY: buildSecret(),
36
+ FD_AUDIT_ANCHOR_KEY: buildSecret(),
27
37
  };
28
38
 
29
39
  try {
package/dist/bin.js CHANGED
@@ -36,6 +36,11 @@ function checkProjectName(name) {
36
36
  }
37
37
  return { valid: true };
38
38
  }
39
+ function applicationSlug(name) {
40
+ const slug = name.replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
41
+ if (slug.length === 0) return "app";
42
+ return /^[a-z]/.test(slug) ? slug : `app-${slug}`;
43
+ }
39
44
  function checkTargetPath(target) {
40
45
  if (target.length === 0) return { valid: false, reason: "path is empty" };
41
46
  if (target.trim() !== target) {
@@ -243,6 +248,38 @@ function installDependencies(packageManager, directory) {
243
248
  return { ok: true };
244
249
  }
245
250
 
251
+ // ../dev-console/src/brand.mjs
252
+ var MARK = [
253
+ " XXX XXX",
254
+ " XXX XXX",
255
+ " XXXXXXXXXXXXXXXXX",
256
+ "XXXXXXXXXXXXXXXXXXX",
257
+ " XXX XXX",
258
+ "XXXXXXXXXXXXXXXXXXX",
259
+ " XXXXXXXXXXXXXXXXX",
260
+ " XXX XXX",
261
+ " XXX XXX"
262
+ ];
263
+ function renderBrandHeader({
264
+ title = "FLOWDULAR",
265
+ subtitle = "",
266
+ color = false,
267
+ columns = process.stdout.columns ?? 80,
268
+ terminal = Boolean(process.stdout.isTTY)
269
+ } = {}) {
270
+ const brand = (text) => color ? `\x1B[1;38;5;42m${text}\x1B[0m` : text;
271
+ const muted = (text) => color ? `\x1B[90m${text}\x1B[0m` : text;
272
+ if (!terminal) {
273
+ return ` ${brand(title)}${subtitle ? ` ${muted(subtitle)}` : ""}`;
274
+ }
275
+ const heading = [
276
+ ` ${brand(title)}`,
277
+ ...subtitle ? [` ${muted(subtitle)}`] : []
278
+ ];
279
+ if (columns < 21) return heading.join("\n");
280
+ return [...MARK.map((row) => ` ${brand(row)}`), "", ...heading].join("\n");
281
+ }
282
+
246
283
  // src/report.ts
247
284
  import { styleText } from "node:util";
248
285
  var DEV_URL = "http://localhost:4310";
@@ -271,13 +308,20 @@ function renderNextSteps(input, color = false) {
271
308
  ];
272
309
  return [
273
310
  "",
274
- ` ${paint("FLOWDULAR", "bold")} ${paint("Project created", "green")}`,
311
+ renderBrandHeader({
312
+ title: "FLOWDULAR",
313
+ subtitle: "Project created",
314
+ color
315
+ }),
275
316
  "",
276
317
  ...nextSteps(input).flatMap((step, index) => [
277
318
  ` ${paint(`${index + 1}.`, "cyan")} ${labels[index]}`,
278
319
  ` ${paint(step, "bold")}`,
279
320
  ""
280
321
  ]),
322
+ ` ${paint("Build a module by chat", "dim")} ${paint(runScript(input.packageManager, "sandbox"), "bold")}`,
323
+ ` ${paint("Agent guidance", "dim")} AGENTS.md, CLAUDE.md, .ai/skills`,
324
+ "",
281
325
  ` ${paint("Local URL", "dim")} ${paint(DEV_URL, "cyan")}`,
282
326
  ` ${paint("Demo login", "dim")} admin@example.com`,
283
327
  ` ${paint("Demo account is created only with local demo setup.", "dim")}`,
@@ -311,8 +355,13 @@ import { randomBytes } from "node:crypto";
311
355
  var SECRET_KEYS = [
312
356
  "FD_AGENT_CREDENTIAL_KEY",
313
357
  "FD_AGENT_RUN_GRANT_KEY",
358
+ "FD_AUTOMATIONS_CREDENTIAL_KEY",
359
+ "FD_NOTIFICATIONS_SECRET_KEY",
314
360
  "FD_WORKFLOWS_PAYLOAD_KEY",
315
361
  "FD_WORKFLOWS_CURSOR_KEY",
362
+ "FD_STORAGE_ENCRYPTION_KEY",
363
+ "FD_CONNECTORS_SECRET_KEY",
364
+ "FD_AUDIT_ANCHOR_KEY",
316
365
  "FD_AUTH_MFA_KEY"
317
366
  ];
318
367
  function generateSecrets() {
@@ -421,6 +470,20 @@ async function rewritePackageName(directory, name) {
421
470
  manifest.name = name;
422
471
  await writeFile(path, JSON.stringify(manifest, void 0, " ") + "\n");
423
472
  }
473
+ var APPLICATION_SPEC_ID = "application.app-name";
474
+ async function rewriteApplicationSpecId(directory, name) {
475
+ const path = join2(directory, "specs", "application.yaml");
476
+ const spec = await readFile(path, "utf8");
477
+ if (!spec.includes(APPLICATION_SPEC_ID)) {
478
+ throw new ScaffoldError(
479
+ `The template spec ${path} no longer carries the "${APPLICATION_SPEC_ID}" placeholder.`
480
+ );
481
+ }
482
+ await writeFile(
483
+ path,
484
+ spec.replace(APPLICATION_SPEC_ID, `application.${applicationSlug(name)}`)
485
+ );
486
+ }
424
487
  async function scaffold(request) {
425
488
  const directory = resolve2(request.cwd, request.target);
426
489
  const name = basename(directory);
@@ -442,6 +505,7 @@ async function scaffold(request) {
442
505
  await assertTemplateExists(agentTemplate, "agent guidance");
443
506
  const files = await copyTemplate(template, directory) + await copyTemplate(agentTemplate, directory);
444
507
  await rewritePackageName(directory, name);
508
+ await rewriteApplicationSpecId(directory, name);
445
509
  await writeFile(
446
510
  join2(directory, ".env"),
447
511
  renderEnvironmentFile(request.secrets ?? generateSecrets())
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-flowdular",
3
- "version": "0.2.5",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "description": "Scaffold a Flowdular application: the platform, one example module and the secrets a fresh install needs.",
6
6
  "license": "MIT",
@@ -0,0 +1,14 @@
1
+ .git
2
+ .github
3
+ .DS_Store
4
+ # Matched against the whole context-relative path, so a root-anchored .env*
5
+ # would still copy infra/docker/.env into the builder image.
6
+ **/.env
7
+ **/.env.*
8
+ !**/.env.example
9
+ **/node_modules
10
+ **/dist
11
+ **/coverage
12
+ **/.flowdular
13
+ .flowdular/
14
+ *.log
@@ -0,0 +1,94 @@
1
+ # Production configuration for this Flowdular app. Copy it, fill in every value
2
+ # and keep the copy out of git. The .env this scaffold generated is the local
3
+ # development file and already carries working keys for embedded PostgreSQL.
4
+ #
5
+ # Generate each 32 byte key with: openssl rand -base64 32
6
+ # For FD_AUTH_MFA_KEY use the URL-safe alphabet: openssl rand -base64 32 | tr '+/' '-_'
7
+
8
+ NODE_ENV=production
9
+ FD_PORT=3000
10
+
11
+ # One PostgreSQL database, three roles. The migrator owns the schema, the
12
+ # runtime role holds neither SUPERUSER nor BYPASSRLS so the row-level security
13
+ # every tenant table forces actually binds the app, and the background role
14
+ # serves the cross-tenant scheduler poll. pglite is refused in production.
15
+ FD_DATABASE_ADAPTER=postgresql
16
+ FD_DATABASE_URL=postgresql://coreloom_runtime:REPLACE_ME@postgres:5432/flowdular
17
+ FD_DATABASE_MIGRATOR_URL=postgresql://coreloom_migrator:REPLACE_ME@postgres:5432/flowdular
18
+ FD_DATABASE_BACKGROUND_URL=postgresql://coreloom_background:REPLACE_ME@postgres:5432/flowdular
19
+ FD_DATABASE_TLS=verify-full
20
+ FD_DATABASE_TLS_CA_FILE=/tls/server.crt
21
+
22
+ # TLS terminates in front of the app. Set this to false only for a plain-HTTP
23
+ # run on a workstation: the session cookie then loses the Secure flag and the
24
+ # __Host- prefix.
25
+ FD_AUTH_SECURE_COOKIE=true
26
+ FD_AUTH_ALLOW_SIGN_UP=false
27
+
28
+ # Trust x-forwarded-for and x-forwarded-proto. Keep false unless a proxy you
29
+ # control terminates TLS in front of the app and overwrites those headers; on a
30
+ # directly reachable port a client can forge the address the sign-in limiter
31
+ # and the audit trail record. The compose sample publishes the port directly.
32
+ FD_TRUST_PROXY=false
33
+
34
+ # agents.core, automations.core and workflows.core each refuse to boot in
35
+ # production without their key.
36
+ FD_AGENT_CREDENTIAL_KEY=
37
+ FD_AGENT_RUN_GRANT_KEY=
38
+ FD_AUTOMATIONS_CREDENTIAL_KEY=
39
+ FD_NOTIFICATIONS_SECRET_KEY=
40
+ FD_WORKFLOWS_PAYLOAD_KEY=
41
+ FD_WORKFLOWS_CURSOR_KEY=
42
+
43
+ # Object storage. The local adapter is refused in production, so a deployment
44
+ # points at an S3-compatible bucket (AWS, MinIO, R2). Objects are encrypted with
45
+ # AES-256-GCM under this key before they are written, and the key is required in
46
+ # production: without it stored objects cannot be read back.
47
+ FD_STORAGE_ADAPTER=s3
48
+ FD_STORAGE_S3_BUCKET=
49
+ FD_STORAGE_S3_REGION=
50
+ FD_STORAGE_S3_ENDPOINT=
51
+ FD_STORAGE_S3_ACCESS_KEY_ID=
52
+ FD_STORAGE_S3_SECRET_ACCESS_KEY=
53
+ FD_STORAGE_S3_FORCE_PATH_STYLE=false
54
+ FD_STORAGE_MAX_OBJECT_BYTES=26214400
55
+ FD_STORAGE_ENCRYPTION_KEY=
56
+ FD_CONNECTORS_SECRET_KEY=
57
+ FD_AUDIT_ANCHOR_KEY=
58
+
59
+ # Retired keys, comma separated, at most eight each. Rotation replaces a key
60
+ # without losing what it sealed: move the old key here, put the new one above,
61
+ # deploy, re-seal the stored rows with the matching "<module> secrets-rotate
62
+ # --apply", then empty the entry. Full procedure in docs/operations.md.
63
+ FD_AGENT_CREDENTIAL_KEY_PREVIOUS=
64
+ FD_AUTOMATIONS_CREDENTIAL_KEY_PREVIOUS=
65
+ FD_NOTIFICATIONS_SECRET_KEY_PREVIOUS=
66
+ FD_WORKFLOWS_PAYLOAD_KEY_PREVIOUS=
67
+ FD_WORKFLOWS_CURSOR_KEY_PREVIOUS=
68
+ FD_STORAGE_ENCRYPTION_KEY_PREVIOUS=
69
+ FD_CONNECTORS_SECRET_KEY_PREVIOUS=
70
+ FD_AUDIT_ANCHOR_KEY_PREVIOUS=
71
+
72
+ # Base64url 32 byte key encrypting MFA secrets. Without it the app runs and only
73
+ # MFA enrollment is unavailable. It rotates the same way, through
74
+ # "flowdular auth secrets-rotate --apply".
75
+ FD_AUTH_MFA_KEY=
76
+ FD_AUTH_MFA_KEY_PREVIOUS=
77
+
78
+ # Outgoing mail. With none, workspace invitations are refused and password reset
79
+ # messages are never delivered. Set smtp and both values below to send them; the
80
+ # URL carries the relay password, so treat the filled copy as a secret.
81
+ FD_AUTH_MAIL_TRANSPORT=none
82
+ FD_AUTH_SMTP_URL=
83
+ FD_AUTH_MAIL_FROM=
84
+
85
+ # Read by infra/docker/compose.yaml only. It builds the three URLs above from
86
+ # these passwords and publishes the container port on FD_PORT.
87
+ FD_POSTGRES_SUPERUSER_PASSWORD=
88
+ FD_DATABASE_MIGRATOR_PASSWORD=
89
+ FD_DATABASE_RUNTIME_PASSWORD=
90
+ FD_DATABASE_BACKGROUND_PASSWORD=
91
+
92
+ # Prometheus exposition on GET /api/metrics. Leave FD_METRICS unset to keep it off.
93
+ FD_METRICS=false
94
+ FD_METRICS_TOKEN=
@@ -24,8 +24,11 @@ install. Data lives under `.flowdular/data`.
24
24
  | ----------------- | ------------------------------------------------------------ |
25
25
  | `platform` | The deployable composition root and the application shell |
26
26
  | `modules/example` | A tenant-scoped record module: API, migration, screen, tests |
27
+ | `specs` | The platform spec of this application |
28
+ | `infra` | Dockerfile, compose stack and Kubernetes base |
27
29
  | `flowdular.json` | Enabled modules and locales, owned by the CLI |
28
30
  | `.env` | The keys generated for this app. Never commit it |
31
+ | `.env.example` | Every key a production deployment reads |
29
32
 
30
33
  ## Work with coding agents
31
34
 
@@ -56,11 +59,44 @@ tests.
56
59
  `modules.enabled` in `flowdular.json` are written by the CLI. Never edit them by
57
60
  hand.
58
61
 
62
+ ## Build a module by chat
63
+
64
+ ```bash
65
+ pnpm sandbox
66
+ ```
67
+
68
+ The sandbox is a chat-first builder on `http://127.0.0.1:4320`. You describe the
69
+ change, it writes the module in an isolated workspace, runs the same gates this
70
+ repository runs, and previews the result inside the real application shell.
71
+ `--port`, `--workspace`, `--host` and `--mode` override the defaults.
72
+
73
+ It is a client of a running application and never opens its database. Connect it
74
+ once: in the app open Administration, API tokens, issue a token with
75
+ `sandbox.access.use` plus the read scopes the preview should see, grant the
76
+ account access, then paste the token and the application address into the
77
+ connect screen.
78
+
79
+ ```bash
80
+ pnpm flowdular sandbox grant --email admin@example.com --tenant operations-demo --apply
81
+ ```
82
+
83
+ ## Deploy
84
+
85
+ ```bash
86
+ cp .env.example infra/docker/.env # then fill in every value
87
+ docker compose -f infra/docker/compose.yaml up --build
88
+ ```
89
+
90
+ `.env.example` lists every key the server reads in production. `infra/README.md`
91
+ covers the container image, the PostgreSQL roles and TLS, migrations on rollout,
92
+ and the Kubernetes base.
93
+
59
94
  ## Commands
60
95
 
61
96
  ```bash
62
97
  pnpm dev # platform with HMR on http://localhost:4310
63
- pnpm verify # typecheck, tests, module validation, format
98
+ pnpm sandbox # chat-first module builder on http://127.0.0.1:4320
99
+ pnpm verify # typecheck, tests, spec and module validation, format
64
100
  pnpm flowdular doctor # workspace health
65
101
  ```
66
102
 
@@ -11,14 +11,25 @@
11
11
  "enabled": [
12
12
  "system.core",
13
13
  "auth.core",
14
+ "access.core",
15
+ "reports.core",
16
+ "metering.core",
14
17
  "agents.core",
15
- "automations.core",
18
+ "approvals.core",
19
+ "audit.core",
16
20
  "workflows.core",
17
- "automations-workflows.integration",
18
- "example.core",
21
+ "automations.core",
22
+ "connectors.core",
23
+ "directory.core",
24
+ "documents.core",
25
+ "exports.core",
26
+ "import.core",
27
+ "notifications.core",
19
28
  "profile.core",
20
29
  "sandbox.core",
21
- "users.core"
30
+ "search.core",
31
+ "users.core",
32
+ "example.core"
22
33
  ]
23
34
  },
24
35
  "locales": ["en", "pl"],
@@ -0,0 +1,116 @@
1
+ # Deployment
2
+
3
+ The production artifact is the server built from `platform`. It runs as a
4
+ non-root user and serves two public probes from
5
+ `platform/src/server/health.ts`: `GET /api/health` answers as soon as the
6
+ process is up and touches nothing else, so it is the liveness probe, and
7
+ `GET /api/ready` calls the database provider, checks that the runtime and
8
+ background roles hold neither `SUPERUSER` nor `BYPASSRLS`, and answers 503 with
9
+ `retry-after: 1` while the database is unavailable.
10
+
11
+ ## Local container
12
+
13
+ ```bash
14
+ cp .env.example infra/docker/.env # then fill in every value
15
+ docker compose -f infra/docker/compose.yaml up --build
16
+ ```
17
+
18
+ Compose refuses to start while a key or a database password is empty, and the
19
+ owning module would refuse at boot anyway. The app is published on
20
+ `http://localhost:3000`; set `FD_PORT` to change the host port.
21
+
22
+ The build stage runs `pnpm install --frozen-lockfile`, so commit
23
+ `pnpm-lock.yaml` before building.
24
+
25
+ Compose also starts PostgreSQL. A one-shot `postgres-tls` service generates a
26
+ self-signed server certificate for `CN=postgres` on first run, Postgres serves
27
+ TLS with it, and the app verifies it through `FD_DATABASE_TLS_CA_FILE` under
28
+ `verify-full`. First cluster initialization creates three roles:
29
+ `coreloom_migrator` owns the schema, `coreloom_runtime` holds neither
30
+ `SUPERUSER` nor `BYPASSRLS`, so the row-level security tenant tables force
31
+ actually binds the application, and `coreloom_background` serves the
32
+ cross-tenant scheduler poll with no default table grant at all.
33
+
34
+ Public sign-up is disabled. The session cookie is Secure (`__Host-` prefix)
35
+ because the container expects TLS in front of it. For a plain-HTTP run on a
36
+ workstation set `FD_AUTH_SECURE_COOKIE=false` in `infra/docker/.env`; do not do
37
+ this for anything reachable from a network.
38
+
39
+ The runtime image contains only `platform/dist` and `platform/package.json`. The
40
+ server bundle imports node built-ins exclusively, so no `node_modules` directory
41
+ ships with it.
42
+
43
+ ## Migrations on rollout
44
+
45
+ Every module owns its migrations and applies them itself, inside a migration
46
+ lease that connects as `FD_DATABASE_MIGRATOR_URL`, the first time its runtime is
47
+ used. A rollout therefore needs no apply step: start the new image and the
48
+ schema catches up under the schema-owning role, while requests keep running as
49
+ the runtime role.
50
+
51
+ What a rollout should do is fail before the new image serves traffic when an
52
+ already applied migration no longer matches the code being deployed. That is the
53
+ one-shot `migration-check` service in `infra/docker/compose.yaml`: it runs
54
+ `flowdular migration status --json`, reports what is pending, and exits non-zero
55
+ on a checksum mismatch. `app` starts only after it completes successfully.
56
+
57
+ ```bash
58
+ docker compose -f infra/docker/compose.yaml run --rm migration-check
59
+ ```
60
+
61
+ Outside compose, run the same check from a checkout with the deployment's
62
+ `FD_DATABASE_*` values in the environment. On Kubernetes it belongs in a
63
+ pre-upgrade Job or in the pipeline step ahead of `kubectl apply`. After the
64
+ rollout, `flowdular migration verify` should come back clean.
65
+
66
+ `flowdular migration apply` exists, but it is not part of a rollout. It takes one
67
+ module at a time (`--module <id> --apply`; there is no `--all` flag) and the
68
+ capability is local-only: the runner refuses it unless `FD_ENV` or `NODE_ENV` is
69
+ `development` or `test`. Use it on a workstation against a local database:
70
+
71
+ ```bash
72
+ pnpm flowdular migration status # pending work, per module
73
+ pnpm flowdular migration apply --module example.core # dry run
74
+ pnpm flowdular migration apply --module example.core --apply
75
+ ```
76
+
77
+ Repeat the last two lines for each module `migration status` reports.
78
+
79
+ Applied migrations are immutable. To change schema, add a numbered migration;
80
+ never edit one the ledger already recorded.
81
+
82
+ ## Published image
83
+
84
+ Build and publish the image from `infra/docker/Dockerfile`, then set the name in
85
+ `infra/kubernetes/kustomization.yaml` and `deployment.yaml`, which ship with an
86
+ `ghcr.io/OWNER/REPOSITORY` placeholder.
87
+
88
+ ```bash
89
+ kubectl create secret generic flowdular-secrets \
90
+ --from-literal=agentCredentialKey="$(openssl rand -base64 32)" \
91
+ --from-literal=agentRunGrantKey="$(openssl rand -base64 32)" \
92
+ --from-literal=automationsCredentialKey="$(openssl rand -base64 32)" \
93
+ --from-literal=workflowsPayloadKey="$(openssl rand -base64 32)" \
94
+ --from-literal=workflowsCursorKey="$(openssl rand -base64 32)"
95
+ kubectl create secret generic flowdular-database \
96
+ --from-literal=migratorUrl='postgresql://coreloom_migrator:...@postgres:5432/flowdular' \
97
+ --from-literal=runtimeUrl='postgresql://coreloom_runtime:...@postgres:5432/flowdular' \
98
+ --from-literal=backgroundUrl='postgresql://coreloom_background:...@postgres:5432/flowdular'
99
+ kubectl apply -k infra/kubernetes
100
+ ```
101
+
102
+ `secrets.example.yaml` and `database-secret.example.yaml` show both Secret shapes
103
+ with placeholders and are deliberately not part of the kustomization. Rotate
104
+ `agentCredentialKey` and `workflowsPayloadKey` through the `*_KEY_PREVIOUS`
105
+ variables rather than by replacing the value: the retired key keeps opening the
106
+ stored rows until `flowdular agents secrets-rotate --apply` and
107
+ `flowdular workflows secrets-rotate --apply` have re-sealed them. Replacing a key
108
+ outright does lose what it sealed, so follow the key rotation section of
109
+ `docs/operations.md`.
110
+
111
+ The Kubernetes base carries no volume for application data: the deployment is
112
+ stateless and every module writes to PostgreSQL. Scaling writers across nodes
113
+ needs nothing beyond the database they already share.
114
+
115
+ `.env.example` at the repository root lists every key the server reads,
116
+ including the optional mail transport settings.