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.
- package/LICENSE +21 -0
- package/README.md +51 -0
- package/dist/bin.js +526 -0
- package/package.json +35 -0
- package/template/default/.prettierrc.json +6 -0
- package/template/default/README.md +56 -0
- package/template/default/_gitignore +8 -0
- package/template/default/flowdular.json +14 -0
- package/template/default/modules/example/migrations/0001_example_core.down.sql +1 -0
- package/template/default/modules/example/migrations/0001_example_core.up.sql +14 -0
- package/template/default/modules/example/module.json +22 -0
- package/template/default/modules/example/package.json +27 -0
- package/template/default/modules/example/spec/module.yaml +43 -0
- package/template/default/modules/example/src/acl/permissions.ts +6 -0
- package/template/default/modules/example/src/api/endpoints.ts +81 -0
- package/template/default/modules/example/src/client/NotesView.tsrx +236 -0
- package/template/default/modules/example/src/client/api.ts +38 -0
- package/template/default/modules/example/src/client/contribution.tsrx +52 -0
- package/template/default/modules/example/src/client/index.ts +18 -0
- package/template/default/modules/example/src/client/navigation-copy.ts +9 -0
- package/template/default/modules/example/src/client/state.ts +14 -0
- package/template/default/modules/example/src/domain/types.ts +12 -0
- package/template/default/modules/example/src/index.ts +24 -0
- package/template/default/modules/example/src/platform.ts +23 -0
- package/template/default/modules/example/src/server/index.ts +3 -0
- package/template/default/modules/example/src/server/runtime.ts +84 -0
- package/template/default/modules/example/src/services/database-repository.ts +82 -0
- package/template/default/modules/example/src/services/index.ts +6 -0
- package/template/default/modules/example/src/services/migration.ts +35 -0
- package/template/default/modules/example/src/services/note-service.ts +54 -0
- package/template/default/modules/example/src/services/repository.ts +7 -0
- package/template/default/modules/example/tests/module.test.ts +67 -0
- package/template/default/modules/example/tests/notes.test.ts +83 -0
- package/template/default/modules/example/tests/support/database.ts +69 -0
- package/template/default/modules/example/translations/en.json +34 -0
- package/template/default/modules/example/translations/pl.json +34 -0
- package/template/default/modules/example/tsconfig.json +14 -0
- package/template/default/modules/example/vitest.config.ts +10 -0
- package/template/default/package.json +26 -0
- package/template/default/platform/index.html +13 -0
- package/template/default/platform/octane.config.ts +117 -0
- package/template/default/platform/package.json +27 -0
- package/template/default/platform/public/favicon.svg +1 -0
- package/template/default/platform/scripts/dev.mjs +18 -0
- package/template/default/platform/src/App.tsrx +80 -0
- package/template/default/platform/src/generated/modules.client.ts +15 -0
- package/template/default/platform/src/generated/modules.server.ts +31 -0
- package/template/default/platform/src/server/database.ts +25 -0
- package/template/default/platform/src/styles.css +39 -0
- package/template/default/platform/tsconfig.json +15 -0
- package/template/default/platform/vite.config.ts +16 -0
- package/template/default/pnpm-workspace.yaml +6 -0
- 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,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`.
|