create-flowdular 0.1.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 (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +51 -0
  3. package/dist/bin.js +526 -0
  4. package/package.json +35 -0
  5. package/template/default/.prettierrc.json +6 -0
  6. package/template/default/README.md +56 -0
  7. package/template/default/_gitignore +8 -0
  8. package/template/default/flowdular.json +14 -0
  9. package/template/default/modules/example/migrations/0001_example_core.down.sql +1 -0
  10. package/template/default/modules/example/migrations/0001_example_core.up.sql +14 -0
  11. package/template/default/modules/example/module.json +22 -0
  12. package/template/default/modules/example/package.json +27 -0
  13. package/template/default/modules/example/spec/module.yaml +43 -0
  14. package/template/default/modules/example/src/acl/permissions.ts +6 -0
  15. package/template/default/modules/example/src/api/endpoints.ts +81 -0
  16. package/template/default/modules/example/src/client/NotesView.tsrx +236 -0
  17. package/template/default/modules/example/src/client/api.ts +38 -0
  18. package/template/default/modules/example/src/client/contribution.tsrx +52 -0
  19. package/template/default/modules/example/src/client/index.ts +18 -0
  20. package/template/default/modules/example/src/client/navigation-copy.ts +9 -0
  21. package/template/default/modules/example/src/client/state.ts +14 -0
  22. package/template/default/modules/example/src/domain/types.ts +12 -0
  23. package/template/default/modules/example/src/index.ts +24 -0
  24. package/template/default/modules/example/src/platform.ts +23 -0
  25. package/template/default/modules/example/src/server/index.ts +3 -0
  26. package/template/default/modules/example/src/server/runtime.ts +84 -0
  27. package/template/default/modules/example/src/services/database-repository.ts +82 -0
  28. package/template/default/modules/example/src/services/index.ts +6 -0
  29. package/template/default/modules/example/src/services/migration.ts +35 -0
  30. package/template/default/modules/example/src/services/note-service.ts +54 -0
  31. package/template/default/modules/example/src/services/repository.ts +7 -0
  32. package/template/default/modules/example/tests/module.test.ts +67 -0
  33. package/template/default/modules/example/tests/notes.test.ts +83 -0
  34. package/template/default/modules/example/tests/support/database.ts +69 -0
  35. package/template/default/modules/example/translations/en.json +34 -0
  36. package/template/default/modules/example/translations/pl.json +34 -0
  37. package/template/default/modules/example/tsconfig.json +14 -0
  38. package/template/default/modules/example/vitest.config.ts +10 -0
  39. package/template/default/package.json +26 -0
  40. package/template/default/platform/index.html +13 -0
  41. package/template/default/platform/octane.config.ts +117 -0
  42. package/template/default/platform/package.json +27 -0
  43. package/template/default/platform/public/favicon.svg +1 -0
  44. package/template/default/platform/scripts/dev.mjs +18 -0
  45. package/template/default/platform/src/App.tsrx +80 -0
  46. package/template/default/platform/src/generated/modules.client.ts +15 -0
  47. package/template/default/platform/src/generated/modules.server.ts +31 -0
  48. package/template/default/platform/src/server/database.ts +25 -0
  49. package/template/default/platform/src/styles.css +39 -0
  50. package/template/default/platform/tsconfig.json +15 -0
  51. package/template/default/platform/vite.config.ts +16 -0
  52. package/template/default/pnpm-workspace.yaml +6 -0
  53. package/template/default/tsconfig.base.json +18 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Flowdular contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # create-flowdular
2
+
3
+ Scaffold a Flowdular application.
4
+
5
+ ```bash
6
+ npm create flowdular@latest my-app
7
+ ```
8
+
9
+ `pnpm create flowdular my-app` and `yarn create flowdular my-app` resolve to the
10
+ same package. `npx create-flowdular@latest my-app` runs it directly.
11
+
12
+ It creates the directory, copies the template (the platform plus one example
13
+ module), rewrites the package name, generates this app's secret keys into
14
+ `.env`, installs the pnpm workspace dependencies, and
15
+ makes the first commit.
16
+
17
+ ## Options
18
+
19
+ | Option | Default | Effect |
20
+ | ----------------------- | --------- | ------------------------------------------- |
21
+ | `-t, --template <name>` | `default` | Template to copy |
22
+ | `--pm <manager>` | `pnpm` | Use `pnpm` for this template |
23
+ | `--no-install` | installs | Skip dependency installation |
24
+ | `--no-git` | commits | Skip repository initialization |
25
+ | `-f, --force` | refuses | Scaffold into a directory that is not empty |
26
+ | `-h, --help` | | Show usage |
27
+ | `-v, --version` | | Show the version |
28
+
29
+ ## What it generates
30
+
31
+ Local development runs on embedded PostgreSQL, so a new app needs no database
32
+ server and no connection string. The generated `.env` sets
33
+ `FD_DATABASE_ADAPTER=pglite` and one fresh base64 encoded 32 byte key per
34
+ secret: `FD_AGENT_CREDENTIAL_KEY`, `FD_AGENT_RUN_GRANT_KEY`,
35
+ `FD_WORKFLOWS_PAYLOAD_KEY`, `FD_WORKFLOWS_CURSOR_KEY` and `FD_AUTH_MFA_KEY`. The
36
+ keys differ on every run and never leave the machine.
37
+
38
+ The scaffolder makes no network call of its own; the only one is the package
39
+ manager install, which `--no-install` skips. It collects nothing.
40
+
41
+ ## After it finishes
42
+
43
+ ```bash
44
+ cd my-app
45
+ pnpm flowdular setup quick --apply --confirm reset-local-auth
46
+ pnpm dev
47
+ ```
48
+
49
+ Then open <http://localhost:4310>.
50
+
51
+ `npm create` launches the generator; the generated application uses pnpm workspaces. If pnpm is missing, the generator runs the pinned pnpm version through `npm exec`. The only Flowdular dependencies published to npm are `@flowdular/sdk`, `flowdular` and `create-flowdular`. UI is available through `@flowdular/sdk/ui` and `@flowdular/sdk/ui/styles`.
package/dist/bin.js ADDED
@@ -0,0 +1,526 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { readFile as readFile2 } from "node:fs/promises";
5
+ import { relative as relative2, resolve as resolve3 } from "node:path";
6
+
7
+ // src/name.ts
8
+ var MAX_LENGTH = 214;
9
+ var NAME_PATTERN = /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
10
+ var RESERVED = /* @__PURE__ */ new Set(["node_modules", "favicon.ico"]);
11
+ function checkProjectName(name) {
12
+ if (name.length === 0) return { valid: false, reason: "name is empty" };
13
+ if (name.trim() !== name) {
14
+ return { valid: false, reason: "name has leading or trailing whitespace" };
15
+ }
16
+ if (name.length > MAX_LENGTH) {
17
+ return {
18
+ valid: false,
19
+ reason: `name is longer than ${String(MAX_LENGTH)} characters`
20
+ };
21
+ }
22
+ if (name.startsWith(".") || name.startsWith("_")) {
23
+ return { valid: false, reason: "name starts with a dot or an underscore" };
24
+ }
25
+ if (name !== name.toLowerCase()) {
26
+ return { valid: false, reason: "name contains uppercase letters" };
27
+ }
28
+ if (RESERVED.has(name)) {
29
+ return { valid: false, reason: `"${name}" is a reserved npm name` };
30
+ }
31
+ if (!NAME_PATTERN.test(name)) {
32
+ return {
33
+ valid: false,
34
+ reason: "name may contain only lowercase letters, digits, and the characters - . _ ~"
35
+ };
36
+ }
37
+ return { valid: true };
38
+ }
39
+ function checkTargetPath(target) {
40
+ if (target.length === 0) return { valid: false, reason: "path is empty" };
41
+ if (target.trim() !== target) {
42
+ return { valid: false, reason: "path has leading or trailing whitespace" };
43
+ }
44
+ const segments = target.split(/[\\/]+/);
45
+ if (segments.includes("..")) {
46
+ return { valid: false, reason: "path escapes the current directory" };
47
+ }
48
+ if (target.includes("\0")) {
49
+ return { valid: false, reason: "path contains a null byte" };
50
+ }
51
+ return { valid: true };
52
+ }
53
+
54
+ // src/args.ts
55
+ var PACKAGE_MANAGERS = ["pnpm", "npm", "yarn"];
56
+ var ArgumentError = class extends Error {
57
+ constructor(message) {
58
+ super(message);
59
+ this.name = "ArgumentError";
60
+ }
61
+ };
62
+ function isPackageManager(value) {
63
+ return PACKAGE_MANAGERS.includes(value);
64
+ }
65
+ function valueOf(flag, inline, next) {
66
+ if (inline !== void 0) {
67
+ if (inline.length === 0) throw new ArgumentError(`${flag} needs a value.`);
68
+ return { value: inline, consumedNext: false };
69
+ }
70
+ if (next === void 0 || next.startsWith("-")) {
71
+ throw new ArgumentError(`${flag} needs a value.`);
72
+ }
73
+ return { value: next, consumedNext: true };
74
+ }
75
+ function parseArguments(argv) {
76
+ let target;
77
+ let template = "default";
78
+ let packageManager;
79
+ let install = true;
80
+ let git2 = true;
81
+ let force = false;
82
+ let help = false;
83
+ let version = false;
84
+ for (let index = 0; index < argv.length; index += 1) {
85
+ const argument = argv[index];
86
+ const separator = argument.indexOf("=");
87
+ const flag = separator === -1 ? argument : argument.slice(0, separator);
88
+ const inline = separator === -1 ? void 0 : argument.slice(separator + 1);
89
+ switch (flag) {
90
+ case "--help":
91
+ case "-h":
92
+ help = true;
93
+ continue;
94
+ case "--version":
95
+ case "-v":
96
+ version = true;
97
+ continue;
98
+ case "--no-install":
99
+ install = false;
100
+ continue;
101
+ case "--no-git":
102
+ git2 = false;
103
+ continue;
104
+ case "--force":
105
+ case "-f":
106
+ force = true;
107
+ continue;
108
+ case "--template":
109
+ case "-t": {
110
+ const parsed = valueOf(flag, inline, argv[index + 1]);
111
+ if (!/^[a-z][a-z0-9-]*$/.test(parsed.value)) {
112
+ throw new ArgumentError(
113
+ `"${parsed.value}" is not a template name; use lowercase letters, digits and hyphens.`
114
+ );
115
+ }
116
+ template = parsed.value;
117
+ if (parsed.consumedNext) index += 1;
118
+ continue;
119
+ }
120
+ case "--pm": {
121
+ const parsed = valueOf(flag, inline, argv[index + 1]);
122
+ if (!isPackageManager(parsed.value)) {
123
+ throw new ArgumentError(
124
+ `--pm must be one of ${PACKAGE_MANAGERS.join(", ")}.`
125
+ );
126
+ }
127
+ packageManager = parsed.value;
128
+ if (parsed.consumedNext) index += 1;
129
+ continue;
130
+ }
131
+ default:
132
+ break;
133
+ }
134
+ if (argument.startsWith("-")) {
135
+ throw new ArgumentError(`Unknown option "${argument}".`);
136
+ }
137
+ if (target !== void 0) {
138
+ throw new ArgumentError(
139
+ `Unexpected argument "${argument}"; the directory is given once.`
140
+ );
141
+ }
142
+ target = argument;
143
+ }
144
+ if (help || version) {
145
+ return {
146
+ target: target ?? "",
147
+ template,
148
+ ...packageManager ? { packageManager } : {},
149
+ install,
150
+ git: git2,
151
+ force,
152
+ help,
153
+ version
154
+ };
155
+ }
156
+ if (target === void 0) {
157
+ throw new ArgumentError(
158
+ "Name the directory to create, for example: create-flowdular my-app"
159
+ );
160
+ }
161
+ const path = checkTargetPath(target);
162
+ if (!path.valid) {
163
+ throw new ArgumentError(
164
+ `"${target}" is not a usable directory: ${path.reason}.`
165
+ );
166
+ }
167
+ return {
168
+ target,
169
+ template,
170
+ ...packageManager ? { packageManager } : {},
171
+ install,
172
+ git: git2,
173
+ force,
174
+ help,
175
+ version
176
+ };
177
+ }
178
+
179
+ // src/git.ts
180
+ import { spawnSync } from "node:child_process";
181
+ function git(directory, ...arguments_) {
182
+ const result = spawnSync("git", arguments_, {
183
+ cwd: directory,
184
+ stdio: "ignore"
185
+ });
186
+ if (result.error) return { ok: false, reason: "git is not available" };
187
+ if (result.status !== 0) {
188
+ return { ok: false, reason: `git ${arguments_[0]} failed` };
189
+ }
190
+ return { ok: true };
191
+ }
192
+ function initializeRepository(directory) {
193
+ const initialized = git(directory, "init", "-b", "main");
194
+ if (!initialized.ok) return initialized;
195
+ const staged = git(directory, "add", "-A");
196
+ if (!staged.ok) return staged;
197
+ const committed = git(
198
+ directory,
199
+ "commit",
200
+ "-m",
201
+ "chore: scaffold Flowdular app"
202
+ );
203
+ if (!committed.ok) {
204
+ return {
205
+ ok: false,
206
+ reason: "the repository was created but the first commit failed, most likely because git has no user.name and user.email"
207
+ };
208
+ }
209
+ return { ok: true };
210
+ }
211
+
212
+ // src/pm.ts
213
+ import { spawnSync as spawnSync2 } from "node:child_process";
214
+ function detectPackageManager(_userAgent) {
215
+ return "pnpm";
216
+ }
217
+ function installDependencies(packageManager, directory) {
218
+ let result = spawnSync2(packageManager, ["install"], {
219
+ cwd: directory,
220
+ stdio: "inherit",
221
+ shell: process.platform === "win32"
222
+ });
223
+ if (packageManager === "pnpm" && result.error?.code === "ENOENT") {
224
+ result = spawnSync2(
225
+ "npm",
226
+ ["exec", "--yes", "--package=pnpm@11.17.0", "--", "pnpm", "install"],
227
+ {
228
+ cwd: directory,
229
+ stdio: "inherit",
230
+ shell: process.platform === "win32"
231
+ }
232
+ );
233
+ }
234
+ if (result.error) {
235
+ return { ok: false, reason: `${packageManager} could not be started` };
236
+ }
237
+ if (result.status !== 0) {
238
+ return {
239
+ ok: false,
240
+ reason: `${packageManager} install exited with code ${String(result.status)}`
241
+ };
242
+ }
243
+ return { ok: true };
244
+ }
245
+
246
+ // src/report.ts
247
+ var DEV_URL = "http://localhost:4310";
248
+ function runScript(packageManager, script, ...arguments_) {
249
+ if (arguments_.length === 0) {
250
+ return packageManager === "npm" ? `npm run ${script}` : `${packageManager} ${script}`;
251
+ }
252
+ const tail = arguments_.join(" ");
253
+ return packageManager === "npm" ? `npm run ${script} -- ${tail}` : `${packageManager} ${script} ${tail}`;
254
+ }
255
+ function nextSteps(input) {
256
+ return [
257
+ `cd ${input.directory}`,
258
+ ...input.installed ? [] : [`${input.packageManager} install`],
259
+ runScript(
260
+ input.packageManager,
261
+ "flowdular",
262
+ "setup",
263
+ "quick",
264
+ "--apply",
265
+ "--confirm",
266
+ "reset-local-auth"
267
+ ),
268
+ runScript(input.packageManager, "dev")
269
+ ];
270
+ }
271
+ function renderNextSteps(input) {
272
+ return [
273
+ "",
274
+ "Next:",
275
+ "",
276
+ ...nextSteps(input).map((step) => ` ${step}`),
277
+ "",
278
+ `Then open ${DEV_URL} and sign in as admin@example.com.`,
279
+ ""
280
+ ].join("\n");
281
+ }
282
+ var HELP = `create-flowdular
283
+
284
+ Scaffold a Flowdular application: the platform, one example module and the
285
+ secrets a fresh install needs.
286
+
287
+ Usage
288
+ npm create flowdular@latest <directory> [options]
289
+
290
+ Options
291
+ -t, --template <name> Template to copy (default: default)
292
+ --pm <manager> Use pnpm for the generated workspace (default: pnpm)
293
+ --no-install Skip dependency installation
294
+ --no-git Skip repository initialization
295
+ -f, --force Scaffold into a directory that is not empty
296
+ -h, --help Show this help
297
+ -v, --version Show the version
298
+ `;
299
+
300
+ // src/scaffold.ts
301
+ import { readdir as readdir2, readFile, stat, writeFile } from "node:fs/promises";
302
+ import { basename, join as join2, resolve as resolve2 } from "node:path";
303
+
304
+ // src/secrets.ts
305
+ import { randomBytes } from "node:crypto";
306
+ var SECRET_KEYS = [
307
+ "FD_AGENT_CREDENTIAL_KEY",
308
+ "FD_AGENT_RUN_GRANT_KEY",
309
+ "FD_WORKFLOWS_PAYLOAD_KEY",
310
+ "FD_WORKFLOWS_CURSOR_KEY",
311
+ "FD_AUTH_MFA_KEY"
312
+ ];
313
+ function generateSecrets() {
314
+ const secrets = {};
315
+ for (const key of SECRET_KEYS)
316
+ secrets[key] = randomBytes(32).toString("base64");
317
+ return secrets;
318
+ }
319
+ function renderEnvironmentFile(secrets) {
320
+ return [
321
+ "# Local configuration for this Flowdular app. Never commit it.",
322
+ "# Regenerate any key with: openssl rand -base64 32",
323
+ "",
324
+ "# Local development runs on embedded PostgreSQL, so there is no server to",
325
+ "# install and no connection string to configure. A deployment sets",
326
+ "# FD_DATABASE_ADAPTER=postgresql and the FD_DATABASE_* URLs instead.",
327
+ "FD_DATABASE_ADAPTER=pglite",
328
+ "",
329
+ "# Base64 encoded 32 byte keys, generated once for this app.",
330
+ ...SECRET_KEYS.map((key) => `${key}=${secrets[key]}`),
331
+ ""
332
+ ].join("\n");
333
+ }
334
+
335
+ // src/template.ts
336
+ import { readdir, copyFile, mkdir } from "node:fs/promises";
337
+ import { join, relative, resolve, sep } from "node:path";
338
+ function templatesRoot() {
339
+ return resolve(import.meta.dirname, "..", "template");
340
+ }
341
+ function templateRoot(name) {
342
+ return join(templatesRoot(), name);
343
+ }
344
+ var RENAMED = { _gitignore: ".gitignore" };
345
+ var TemplateError = class extends Error {
346
+ constructor(message) {
347
+ super(message);
348
+ this.name = "TemplateError";
349
+ }
350
+ };
351
+ function assertInside(root, candidate) {
352
+ const inside = relative(root, candidate);
353
+ if (inside.length === 0 || inside === ".." || inside.startsWith(".." + sep)) {
354
+ throw new TemplateError(
355
+ `The template tried to write outside the target directory: ${candidate}`
356
+ );
357
+ }
358
+ }
359
+ async function copyTemplate(source, destination) {
360
+ const root = resolve(destination);
361
+ await mkdir(root, { recursive: true });
362
+ let files = 0;
363
+ const pending = [{ from: source, to: root }];
364
+ while (pending.length > 0) {
365
+ const directory = pending.pop();
366
+ const entries = await readdir(directory.from, { withFileTypes: true });
367
+ for (const entry of entries) {
368
+ const to = join(directory.to, RENAMED[entry.name] ?? entry.name);
369
+ assertInside(root, to);
370
+ const from = join(directory.from, entry.name);
371
+ if (entry.isDirectory()) {
372
+ await mkdir(to, { recursive: true });
373
+ pending.push({ from, to });
374
+ continue;
375
+ }
376
+ if (!entry.isFile()) {
377
+ throw new TemplateError(
378
+ `The template contains an entry that is neither a file nor a directory: ${from}`
379
+ );
380
+ }
381
+ await copyFile(from, to);
382
+ files += 1;
383
+ }
384
+ }
385
+ return files;
386
+ }
387
+
388
+ // src/scaffold.ts
389
+ var ScaffoldError = class extends Error {
390
+ constructor(message) {
391
+ super(message);
392
+ this.name = "ScaffoldError";
393
+ }
394
+ };
395
+ async function directoryEntries(path) {
396
+ try {
397
+ return await readdir2(path);
398
+ } catch (error) {
399
+ if (error.code === "ENOENT") return [];
400
+ throw error;
401
+ }
402
+ }
403
+ async function assertTemplateExists(path, name) {
404
+ try {
405
+ if ((await stat(path)).isDirectory()) return;
406
+ } catch {
407
+ }
408
+ throw new ScaffoldError(`"${name}" is not a template this version ships.`);
409
+ }
410
+ async function rewritePackageName(directory, name) {
411
+ const path = join2(directory, "package.json");
412
+ const manifest = JSON.parse(await readFile(path, "utf8"));
413
+ manifest.name = name;
414
+ await writeFile(path, JSON.stringify(manifest, void 0, " ") + "\n");
415
+ }
416
+ async function scaffold(request) {
417
+ const directory = resolve2(request.cwd, request.target);
418
+ const name = basename(directory);
419
+ const check = checkProjectName(name);
420
+ if (!check.valid) {
421
+ throw new ScaffoldError(
422
+ `"${name}" is not a valid npm package name: ${check.reason}.`
423
+ );
424
+ }
425
+ const template = templateRoot(request.template);
426
+ await assertTemplateExists(template, request.template);
427
+ const existing = await directoryEntries(directory);
428
+ if (existing.length > 0 && !request.force) {
429
+ throw new ScaffoldError(
430
+ `${directory} is not empty. Pass --force to scaffold into it anyway.`
431
+ );
432
+ }
433
+ const files = await copyTemplate(template, directory);
434
+ await rewritePackageName(directory, name);
435
+ await writeFile(
436
+ join2(directory, ".env"),
437
+ renderEnvironmentFile(request.secrets ?? generateSecrets())
438
+ );
439
+ return { directory, name, files: files + 1 };
440
+ }
441
+
442
+ // src/index.ts
443
+ var processIo = {
444
+ cwd: process.cwd(),
445
+ out: (text) => process.stdout.write(text),
446
+ err: (text) => process.stderr.write(text)
447
+ };
448
+ async function packageVersion() {
449
+ const manifest = JSON.parse(
450
+ await readFile2(resolve3(import.meta.dirname, "..", "package.json"), "utf8")
451
+ );
452
+ return manifest.version ?? "0.0.0";
453
+ }
454
+ async function run(argv, io = processIo) {
455
+ let options;
456
+ try {
457
+ options = parseArguments(argv);
458
+ } catch (error) {
459
+ if (!(error instanceof ArgumentError)) throw error;
460
+ io.err(`ERROR ${error.message}
461
+ `);
462
+ return 1;
463
+ }
464
+ if (options.help) {
465
+ io.out(HELP);
466
+ return 0;
467
+ }
468
+ if (options.version) {
469
+ io.out(`${await packageVersion()}
470
+ `);
471
+ return 0;
472
+ }
473
+ const packageManager = options.packageManager ?? detectPackageManager(process.env.npm_config_user_agent);
474
+ if (packageManager !== "pnpm") {
475
+ io.err(
476
+ "ERROR This template uses pnpm workspaces. Run npm create flowdular@latest my-app without --pm, or choose --pm pnpm.\n"
477
+ );
478
+ return 1;
479
+ }
480
+ let result;
481
+ try {
482
+ result = await scaffold({
483
+ cwd: io.cwd,
484
+ target: options.target,
485
+ template: options.template,
486
+ force: options.force
487
+ });
488
+ } catch (error) {
489
+ if (!(error instanceof ScaffoldError) && !(error instanceof TemplateError)) {
490
+ throw error;
491
+ }
492
+ io.err(`ERROR ${error.message}
493
+ `);
494
+ return 1;
495
+ }
496
+ io.out(
497
+ `Created ${result.name} in ${result.directory} (${String(result.files)} files).
498
+ `
499
+ );
500
+ let installed = false;
501
+ if (options.install) {
502
+ io.out(`Installing dependencies with ${packageManager}.
503
+ `);
504
+ const install = installDependencies(packageManager, result.directory);
505
+ installed = install.ok;
506
+ if (!install.ok) io.err(`WARNING ${install.reason}.
507
+ `);
508
+ }
509
+ if (options.git) {
510
+ const git2 = initializeRepository(result.directory);
511
+ if (!git2.ok) io.err(`WARNING ${git2.reason}.
512
+ `);
513
+ }
514
+ const here = relative2(io.cwd, result.directory);
515
+ io.out(
516
+ renderNextSteps({
517
+ directory: here.length > 0 ? here : ".",
518
+ packageManager,
519
+ installed
520
+ })
521
+ );
522
+ return 0;
523
+ }
524
+
525
+ // src/bin.ts
526
+ process.exitCode = await run(process.argv.slice(2));
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "create-flowdular",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Scaffold a Flowdular application: the platform, one example module and the secrets a fresh install needs.",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/flowdular/flowdular.git",
10
+ "directory": "packages/create-flowdular"
11
+ },
12
+ "homepage": "https://flowdular.com",
13
+ "bugs": "https://github.com/flowdular/flowdular/issues",
14
+ "keywords": [
15
+ "flowdular",
16
+ "octanejs",
17
+ "scaffold",
18
+ "create",
19
+ "starter"
20
+ ],
21
+ "bin": {
22
+ "create-flowdular": "./dist/bin.js"
23
+ },
24
+ "files": [
25
+ "dist",
26
+ "template",
27
+ "README.md"
28
+ ],
29
+ "engines": {
30
+ "node": ">=22.22.2"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public"
34
+ }
35
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "plugins": ["@tsrx/prettier-plugin"],
3
+ "singleQuote": true,
4
+ "trailingComma": "all",
5
+ "useTabs": true
6
+ }
@@ -0,0 +1,56 @@
1
+ # Flowdular app
2
+
3
+ Created with `npx create-flowdular@latest`. This is the Flowdular platform, one
4
+ example module, and nothing else you have to configure.
5
+
6
+ ## Run it
7
+
8
+ ```bash
9
+ pnpm install
10
+ pnpm flowdular setup quick --apply --confirm reset-local-auth
11
+ pnpm dev
12
+ ```
13
+
14
+ Open <http://localhost:4310> and sign in as `admin@example.com`. Quick setup is
15
+ a destructive local reset: stop `pnpm dev` first, and never point it at a
16
+ deployed database.
17
+
18
+ Local development runs on embedded PostgreSQL, so there is no database server to
19
+ install. Data lives under `.flowdular/data`.
20
+
21
+ ## What is here
22
+
23
+ | Path | Contents |
24
+ | ----------------- | ------------------------------------------------------------ |
25
+ | `platform` | The deployable composition root and the application shell |
26
+ | `modules/example` | A tenant-scoped record module: API, migration, screen, tests |
27
+ | `flowdular.json` | Enabled modules and locales, owned by the CLI |
28
+ | `.env` | The keys generated for this app. Never commit it |
29
+
30
+ ## Build your first module
31
+
32
+ ```bash
33
+ pnpm flowdular module new sales.orders --spec modules/sales-orders/spec/module.yaml
34
+ pnpm flowdular module new sales.orders --spec modules/sales-orders/spec/module.yaml --apply
35
+ pnpm flowdular module enable sales.orders --apply
36
+ ```
37
+
38
+ A module is created only from a spec with `status: approved`. Copy
39
+ `modules/example` for the shape: an approved spec, one permission per action,
40
+ tenant-scoped SQL with forced row-level security, translations per locale, and
41
+ tests.
42
+
43
+ `platform/src/generated/**`, `platform/package.json` dependencies and
44
+ `modules.enabled` in `flowdular.json` are written by the CLI. Never edit them by
45
+ hand.
46
+
47
+ ## Commands
48
+
49
+ ```bash
50
+ pnpm dev # platform with HMR on http://localhost:4310
51
+ pnpm verify # typecheck, tests, module validation, format
52
+ pnpm flowdular doctor # workspace health
53
+ ```
54
+
55
+ Every write command is a dry run without `--apply`; destructive ones also need
56
+ `--confirm`.
@@ -0,0 +1,8 @@
1
+ node_modules/
2
+ dist/
3
+ .flowdular/
4
+ .env
5
+ .env.*
6
+ !.env.example
7
+ *.log
8
+ .DS_Store