create-davepi-app 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/README.md +58 -0
- package/bin/index.js +458 -0
- package/bin/sync-templates.js +42 -0
- package/package.json +36 -0
- package/templates/_shared/.github/workflows/client-gen.yml +65 -0
- package/templates/_shared/.github/workflows/deploy.yml +70 -0
- package/templates/_shared/.github/workflows/migrate.yml +66 -0
- package/templates/_shared/.github/workflows/test.yml +49 -0
- package/templates/_shared/agent.md +480 -0
- package/templates/_shared/tests/smoke.test.js +67 -0
- package/templates/b2b-saas/README.md +57 -0
- package/templates/b2b-saas/schema/versions/v1/billingEvent.js +46 -0
- package/templates/b2b-saas/schema/versions/v1/invite.js +30 -0
- package/templates/b2b-saas/schema/versions/v1/org.js +23 -0
- package/templates/b2b-saas/schema/versions/v1/workspace.js +13 -0
- package/templates/b2b-saas/seed.js +98 -0
- package/templates/blank/README.md +42 -0
- package/templates/blank/schema/versions/v1/note.js +10 -0
- package/templates/blank/seed.js +86 -0
- package/templates/content/README.md +58 -0
- package/templates/content/schema/versions/v1/article.js +77 -0
- package/templates/content/schema/versions/v1/category.js +30 -0
- package/templates/content/seed.js +97 -0
- package/templates/crm/README.md +59 -0
- package/templates/crm/schema/versions/v1/account.js +22 -0
- package/templates/crm/schema/versions/v1/activity.js +20 -0
- package/templates/crm/schema/versions/v1/contact.js +28 -0
- package/templates/crm/schema/versions/v1/deal.js +72 -0
- package/templates/crm/seed.js +124 -0
- package/templates/ticketing/README.md +46 -0
- package/templates/ticketing/schema/versions/v1/comment.js +26 -0
- package/templates/ticketing/schema/versions/v1/ticket.js +65 -0
- package/templates/ticketing/seed.js +97 -0
package/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# create-davepi-app
|
|
2
|
+
|
|
3
|
+
Scaffold a new [dAvePi](https://github.com/projik/davepi) project in one command.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx create-davepi-app my-app
|
|
7
|
+
npx create-davepi-app my-crm --template crm
|
|
8
|
+
npx create-davepi-app my-tickets --template ticketing
|
|
9
|
+
npx create-davepi-app my-blog --template content
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Templates
|
|
13
|
+
|
|
14
|
+
| Template | What you get |
|
|
15
|
+
|----------|--------------|
|
|
16
|
+
| `blank` | Minimal — one resource (`note`) with full-text search. |
|
|
17
|
+
| `crm` | Accounts / contacts / deals (state machine) / activities. Showcases relations, computed fields, file uploads, aggregations. |
|
|
18
|
+
| `ticketing` | Tickets with two state machines (status + priority) plus comments. Showcases ACL on a comment field. |
|
|
19
|
+
| `content` | Blog / CMS skeleton: articles (editorial workflow), categories, hero image uploads, computed slugs. |
|
|
20
|
+
| `b2b-saas` | Multi-tenant SaaS skeleton: orgs / workspaces / invites (state machine) / billing-event ledger with monthly aggregations. |
|
|
21
|
+
|
|
22
|
+
## What's scaffolded
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
my-app/
|
|
26
|
+
├── .env # random TOKEN_KEY, local Mongo URI
|
|
27
|
+
├── .gitignore
|
|
28
|
+
├── .mcp.json # Claude Code wiring, ready to go
|
|
29
|
+
├── .cursorrules # mirrors agent.md for Cursor
|
|
30
|
+
├── README.md
|
|
31
|
+
├── TEMPLATE.md # walkthrough of the chosen template's schemas
|
|
32
|
+
├── agent.md # drop-in agent guide
|
|
33
|
+
├── docker-compose.yml # local Mongo
|
|
34
|
+
├── index.js # `require('davepi')`
|
|
35
|
+
├── package.json
|
|
36
|
+
└── schema/
|
|
37
|
+
└── versions/
|
|
38
|
+
└── v1/
|
|
39
|
+
└── *.js # the template's resource definitions
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Flags
|
|
43
|
+
|
|
44
|
+
| Flag | Default | Effect |
|
|
45
|
+
|------|---------|--------|
|
|
46
|
+
| `--template <name>` | `blank` | Pick the starter from the table above. |
|
|
47
|
+
| `--no-install` | (run install) | Skip `npm install`. |
|
|
48
|
+
| `--davepi-version <range>` | `latest` | Pin a specific dAvePi version in the generated `package.json`. |
|
|
49
|
+
|
|
50
|
+
## Next steps after scaffolding
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
cd my-app
|
|
54
|
+
docker compose up -d # start Mongo
|
|
55
|
+
npm start # http://localhost:5050
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Then open the project in Claude Code or Cursor — the MCP server is already wired and the agent guide lives at `agent.md`. Ask the agent to add a resource and watch it land via hot-reload.
|
package/bin/index.js
ADDED
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `npx create-davepi-app <name> [--template blank|crm|ticketing|content]`
|
|
4
|
+
*
|
|
5
|
+
* Scaffolds a new dAvePi project: copies the chosen template's
|
|
6
|
+
* schema files into the target directory, generates a package.json
|
|
7
|
+
* pinned to the latest dAvePi, writes a `.env` with secure defaults,
|
|
8
|
+
* pre-configures `.mcp.json` for Claude Code, and prints the next
|
|
9
|
+
* three commands the user should run.
|
|
10
|
+
*
|
|
11
|
+
* Stays small on purpose: no `inquirer`, no fancy progress bars —
|
|
12
|
+
* the goal is "from cold install to running with auth + sample
|
|
13
|
+
* data" in under a minute, and the fewer failure modes the better.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const crypto = require('crypto');
|
|
21
|
+
const net = require('net');
|
|
22
|
+
|
|
23
|
+
const TEMPLATES = ['blank', 'crm', 'ticketing', 'content', 'b2b-saas'];
|
|
24
|
+
|
|
25
|
+
function out(line) {
|
|
26
|
+
process.stdout.write(line + '\n');
|
|
27
|
+
}
|
|
28
|
+
function err(line) {
|
|
29
|
+
process.stderr.write(line + '\n');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Read a `--name <value>` flag from argv. Returns:
|
|
34
|
+
* - null when the flag isn't present
|
|
35
|
+
* - true when the flag is present with no following token
|
|
36
|
+
* (treated as a boolean flag — e.g., --no-install)
|
|
37
|
+
* - string when the flag has a non-flag value following it
|
|
38
|
+
*
|
|
39
|
+
* Throws when the next token starts with `--` (e.g.,
|
|
40
|
+
* `--davepi-version --no-install` would otherwise pin davepi to
|
|
41
|
+
* "--no-install" and silently produce an un-installable project).
|
|
42
|
+
*/
|
|
43
|
+
function flag(args, name) {
|
|
44
|
+
const i = args.indexOf(name);
|
|
45
|
+
if (i === -1) return null;
|
|
46
|
+
const next = args[i + 1];
|
|
47
|
+
if (next === undefined) return true;
|
|
48
|
+
if (typeof next === 'string' && next.startsWith('--')) {
|
|
49
|
+
const errFn = require('util').inspect;
|
|
50
|
+
const e = new Error(
|
|
51
|
+
`Flag ${name} requires a value (got ${errFn(next)}, which looks like another flag).`
|
|
52
|
+
);
|
|
53
|
+
e.usage = true;
|
|
54
|
+
throw e;
|
|
55
|
+
}
|
|
56
|
+
return next;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Find an unused TCP port near the requested starting port. Tries
|
|
61
|
+
* ports sequentially up to `maxAttempts` apart so the scaffolded
|
|
62
|
+
* .env carries a value the user can probably bind to. The check is
|
|
63
|
+
* advisory — by the time `npm start` runs, another process could
|
|
64
|
+
* have grabbed the port — but it dramatically reduces "tutorial
|
|
65
|
+
* fails because port 5050 is in use" failures.
|
|
66
|
+
*/
|
|
67
|
+
async function pickPort(start = 5050, maxAttempts = 20) {
|
|
68
|
+
for (let p = start; p < start + maxAttempts; p++) {
|
|
69
|
+
if (await isPortFree(p)) return p;
|
|
70
|
+
}
|
|
71
|
+
// Fall back to OS-assigned (port 0) to guarantee something works.
|
|
72
|
+
return await new Promise((resolve, reject) => {
|
|
73
|
+
const srv = net.createServer();
|
|
74
|
+
srv.unref();
|
|
75
|
+
srv.on('error', reject);
|
|
76
|
+
srv.listen(0, () => {
|
|
77
|
+
const port = srv.address().port;
|
|
78
|
+
srv.close(() => resolve(port));
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function isPortFree(port) {
|
|
84
|
+
return new Promise((resolve) => {
|
|
85
|
+
const srv = net.createServer();
|
|
86
|
+
srv.unref();
|
|
87
|
+
srv.on('error', () => resolve(false));
|
|
88
|
+
srv.listen(port, '127.0.0.1', () => {
|
|
89
|
+
srv.close(() => resolve(true));
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function usage() {
|
|
95
|
+
out(`Usage: npx create-davepi-app <name> [--template <name>] [--no-install]
|
|
96
|
+
|
|
97
|
+
Templates:
|
|
98
|
+
blank Minimal — one resource, full-text search.
|
|
99
|
+
crm Accounts / contacts / deals (state machine) / activities.
|
|
100
|
+
ticketing Tickets (status + priority state machines) / comments.
|
|
101
|
+
content Articles (editorial workflow) / categories / file uploads.
|
|
102
|
+
b2b-saas Orgs / workspaces / invites (state machine) / billingEvent (aggregations).
|
|
103
|
+
|
|
104
|
+
Examples:
|
|
105
|
+
npx create-davepi-app my-app
|
|
106
|
+
npx create-davepi-app my-crm --template crm
|
|
107
|
+
npx create-davepi-app my-app --template blank --no-install`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Resolve the templates directory. When the package is installed
|
|
112
|
+
* from npm, templates live alongside `bin/` in the package itself.
|
|
113
|
+
* In the dAvePi monorepo (during dev / tests), templates live at
|
|
114
|
+
* the repo root one level up. Try both.
|
|
115
|
+
*/
|
|
116
|
+
function templatesDir() {
|
|
117
|
+
const local = path.resolve(__dirname, '..', 'templates');
|
|
118
|
+
if (fs.existsSync(local)) return local;
|
|
119
|
+
const monorepo = path.resolve(__dirname, '..', '..', 'templates');
|
|
120
|
+
if (fs.existsSync(monorepo)) return monorepo;
|
|
121
|
+
throw new Error(
|
|
122
|
+
'Cannot find templates directory. Reinstall create-davepi-app.'
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function copyTree(srcDir, destDir) {
|
|
127
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
128
|
+
for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
|
|
129
|
+
const src = path.join(srcDir, entry.name);
|
|
130
|
+
const dest = path.join(destDir, entry.name);
|
|
131
|
+
if (entry.isDirectory()) {
|
|
132
|
+
copyTree(src, dest);
|
|
133
|
+
} else {
|
|
134
|
+
fs.copyFileSync(src, dest);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function writeJson(filePath, obj) {
|
|
140
|
+
fs.writeFileSync(filePath, JSON.stringify(obj, null, 2) + '\n');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function randomSecret() {
|
|
144
|
+
return crypto.randomBytes(32).toString('hex');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function scaffold({ name, template, install, davepiVersion, port }) {
|
|
148
|
+
const target = path.resolve(name);
|
|
149
|
+
if (fs.existsSync(target)) {
|
|
150
|
+
const empty = fs.readdirSync(target).length === 0;
|
|
151
|
+
if (!empty) {
|
|
152
|
+
throw new Error(
|
|
153
|
+
`Target directory ${target} already exists and is not empty.`
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
} else {
|
|
157
|
+
fs.mkdirSync(target, { recursive: true });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!TEMPLATES.includes(template)) {
|
|
161
|
+
throw new Error(
|
|
162
|
+
`Unknown template '${template}'. Allowed: ${TEMPLATES.join(', ')}`
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 1. Schemas + template README
|
|
167
|
+
copyTree(path.join(templatesDir(), template), target);
|
|
168
|
+
|
|
169
|
+
// 1a. Shared files dropped on top of every scaffolded project:
|
|
170
|
+
// .github/workflows/* — starter CI (test, client-gen drift,
|
|
171
|
+
// migrate-with-approval-gate, deploy).
|
|
172
|
+
// tests/smoke.test.js — schema-shape smoke test, sufficient
|
|
173
|
+
// for `npm test` to be a useful default
|
|
174
|
+
// before the user adds their own.
|
|
175
|
+
// Anything under _shared/ that isn't a directory the templates
|
|
176
|
+
// depend on goes in here. agent.md is loaded separately because
|
|
177
|
+
// it has placeholder substitution.
|
|
178
|
+
const sharedRoot = path.join(templatesDir(), '_shared');
|
|
179
|
+
for (const dir of ['.github', 'tests']) {
|
|
180
|
+
const src = path.join(sharedRoot, dir);
|
|
181
|
+
if (fs.existsSync(src)) {
|
|
182
|
+
copyTree(src, path.join(target, dir));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 2. package.json — pin dAvePi as a runtime dep.
|
|
187
|
+
writeJson(path.join(target, 'package.json'), {
|
|
188
|
+
name,
|
|
189
|
+
version: '0.1.0',
|
|
190
|
+
private: true,
|
|
191
|
+
description: `${name} — built on dAvePi (${template} template).`,
|
|
192
|
+
scripts: {
|
|
193
|
+
// The consumer's index.js is just `require('davepi')` — that
|
|
194
|
+
// boots the server with the consumer's schemas/versions/.
|
|
195
|
+
start: 'node index.js',
|
|
196
|
+
dev: 'node index.js',
|
|
197
|
+
// Each template ships a `seed.js` that registers a demo user
|
|
198
|
+
// and POSTs sample records. Run AFTER `npm start` is up in
|
|
199
|
+
// another terminal.
|
|
200
|
+
seed: 'node seed.js',
|
|
201
|
+
// Schema-shape smoke test. node --test is built into Node 18+
|
|
202
|
+
// so this needs no extra dependencies. The shipped CI workflow
|
|
203
|
+
// runs this script.
|
|
204
|
+
test: 'node --test tests/*.test.js',
|
|
205
|
+
'gen-client': 'davepi gen-client --out client/davepi.ts',
|
|
206
|
+
'mcp:stdio': 'davepi mcp',
|
|
207
|
+
},
|
|
208
|
+
dependencies: {
|
|
209
|
+
davepi: davepiVersion || 'latest',
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// 3. index.js — the consumer's entry point.
|
|
214
|
+
fs.writeFileSync(
|
|
215
|
+
path.join(target, 'index.js'),
|
|
216
|
+
"// Boots the dAvePi server using this project's schema/versions/* files.\n" +
|
|
217
|
+
"// Add custom routes here AFTER the require, using `app.locals.schemaLoader`.\n" +
|
|
218
|
+
"require('davepi');\n"
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
// 4. .env — random TOKEN_KEY (NEVER use the default in prod), local Mongo defaults.
|
|
222
|
+
// Port was probed for availability before scaffolding so the
|
|
223
|
+
// user lands on something they can actually bind to.
|
|
224
|
+
const apiPort = port || (await pickPort());
|
|
225
|
+
fs.writeFileSync(
|
|
226
|
+
path.join(target, '.env'),
|
|
227
|
+
[
|
|
228
|
+
`# Generated by create-davepi-app. Don't commit this file.`,
|
|
229
|
+
`MONGO_URI=mongodb://127.0.0.1:27017/${name.replace(/[^A-Za-z0-9_-]/g, '_')}`,
|
|
230
|
+
`TOKEN_KEY=${randomSecret()}`,
|
|
231
|
+
`API_PORT=${apiPort}`,
|
|
232
|
+
`PAGE_SIZE=20`,
|
|
233
|
+
`CORS_ORIGINS=http://localhost:3000,http://localhost:5173`,
|
|
234
|
+
`# Set HOT_RELOAD_SCHEMAS=true in dev to pick up schema/versions/* changes live.`,
|
|
235
|
+
`HOT_RELOAD_SCHEMAS=true`,
|
|
236
|
+
`NODE_ENV=development`,
|
|
237
|
+
``,
|
|
238
|
+
].join('\n')
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
// 5. .gitignore — note `client/davepi.ts` is NOT listed here. The
|
|
242
|
+
// generated TS client is a committed artifact so the
|
|
243
|
+
// `client-gen.yml` drift workflow has something to diff against,
|
|
244
|
+
// and downstream consumers can vendor it as part of the project.
|
|
245
|
+
// The file doesn't exist until the user runs `npm run gen-client`,
|
|
246
|
+
// which is fine — `git diff --exit-code` against a non-existent
|
|
247
|
+
// file fails cleanly.
|
|
248
|
+
fs.writeFileSync(
|
|
249
|
+
path.join(target, '.gitignore'),
|
|
250
|
+
['node_modules', '.env', 'uploads', ''].join('\n')
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
// 6. .mcp.json — Claude Code wiring out of the box.
|
|
254
|
+
// Uses @davepi/mcp in HTTP-proxy mode pointed at the local server,
|
|
255
|
+
// so the agent talks to the SAME server the developer is already
|
|
256
|
+
// running (`npm start`). No duplicate process, and the wrapper
|
|
257
|
+
// installs on demand via `npx -y` — the user doesn't need to
|
|
258
|
+
// manage the binary path manually.
|
|
259
|
+
writeJson(path.join(target, '.mcp.json'), {
|
|
260
|
+
mcpServers: {
|
|
261
|
+
davepi: {
|
|
262
|
+
command: 'npx',
|
|
263
|
+
args: ['-y', '@davepi/mcp'],
|
|
264
|
+
env: {
|
|
265
|
+
DAVEPI_URL: `http://127.0.0.1:${apiPort}`,
|
|
266
|
+
DAVEPI_TOKEN:
|
|
267
|
+
'<run `npm start`, register a user, then paste the accessToken here>',
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// 7. Agent guide. Canonical content lives in `templates/_shared/agent.md`
|
|
274
|
+
// and is mirrored to four locations so each agent runtime picks it up
|
|
275
|
+
// from its conventional path:
|
|
276
|
+
// agent.md ← canonical, human-readable
|
|
277
|
+
// .cursorrules ← Cursor
|
|
278
|
+
// AGENTS.md ← OpenAI / Codex / agentic IDEs
|
|
279
|
+
// .claude/skills/davepi/SKILL.md ← Claude Code skill (with frontmatter)
|
|
280
|
+
// The shared file carries `{{PORT}}` placeholders that we substitute
|
|
281
|
+
// with the actually-bound API port so commands in the guide work
|
|
282
|
+
// out of the box.
|
|
283
|
+
const guideTemplate = fs.readFileSync(
|
|
284
|
+
path.join(templatesDir(), '_shared', 'agent.md'),
|
|
285
|
+
'utf8'
|
|
286
|
+
);
|
|
287
|
+
const agentGuide = guideTemplate.replace(/\{\{PORT\}\}/g, String(apiPort));
|
|
288
|
+
fs.writeFileSync(path.join(target, 'agent.md'), agentGuide);
|
|
289
|
+
fs.writeFileSync(path.join(target, '.cursorrules'), agentGuide);
|
|
290
|
+
fs.writeFileSync(path.join(target, 'AGENTS.md'), agentGuide);
|
|
291
|
+
|
|
292
|
+
// Claude Code skills require YAML frontmatter naming the skill and
|
|
293
|
+
// describing when to invoke it. Strip the leading "# Title" line
|
|
294
|
+
// from the canonical content (frontmatter replaces the H1) and
|
|
295
|
+
// prepend the metadata block.
|
|
296
|
+
const skillBody = agentGuide.replace(/^#\s+.+\n+/, '');
|
|
297
|
+
const skillContent =
|
|
298
|
+
[
|
|
299
|
+
'---',
|
|
300
|
+
'name: davepi',
|
|
301
|
+
'description: Conventions for adding resources / fields / relations / state machines / aggregations to this dAvePi project. Read before adding code.',
|
|
302
|
+
'---',
|
|
303
|
+
'',
|
|
304
|
+
].join('\n') + skillBody;
|
|
305
|
+
const skillDir = path.join(target, '.claude', 'skills', 'davepi');
|
|
306
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
307
|
+
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), skillContent);
|
|
308
|
+
|
|
309
|
+
// 8. docker-compose.yml — local Mongo for dev.
|
|
310
|
+
fs.writeFileSync(
|
|
311
|
+
path.join(target, 'docker-compose.yml'),
|
|
312
|
+
[
|
|
313
|
+
'# Local Mongo for development. Run: docker compose up -d',
|
|
314
|
+
'services:',
|
|
315
|
+
' mongo:',
|
|
316
|
+
' image: mongo:7',
|
|
317
|
+
' restart: unless-stopped',
|
|
318
|
+
' ports:',
|
|
319
|
+
' - "27017:27017"',
|
|
320
|
+
' volumes:',
|
|
321
|
+
' - mongo_data:/data/db',
|
|
322
|
+
'volumes:',
|
|
323
|
+
' mongo_data: {}',
|
|
324
|
+
'',
|
|
325
|
+
].join('\n')
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
// 9. README — project-level, not template-level. Renames the
|
|
329
|
+
// template README to TEMPLATE.md so both survive.
|
|
330
|
+
if (fs.existsSync(path.join(target, 'README.md'))) {
|
|
331
|
+
fs.renameSync(
|
|
332
|
+
path.join(target, 'README.md'),
|
|
333
|
+
path.join(target, 'TEMPLATE.md')
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
fs.writeFileSync(
|
|
337
|
+
path.join(target, 'README.md'),
|
|
338
|
+
[
|
|
339
|
+
`# ${name}`,
|
|
340
|
+
'',
|
|
341
|
+
`Built on dAvePi (\`${template}\` template).`,
|
|
342
|
+
'',
|
|
343
|
+
'## Get running',
|
|
344
|
+
'',
|
|
345
|
+
'```bash',
|
|
346
|
+
'docker compose up -d # local Mongo',
|
|
347
|
+
'npm install # install dAvePi',
|
|
348
|
+
'npm start # boot the server',
|
|
349
|
+
'```',
|
|
350
|
+
'',
|
|
351
|
+
'Then:',
|
|
352
|
+
`- REST: http://localhost:${apiPort}/api/v1/...`,
|
|
353
|
+
`- GraphQL: http://localhost:${apiPort}/graphql/`,
|
|
354
|
+
`- Swagger: http://localhost:${apiPort}/api-docs`,
|
|
355
|
+
`- Admin SPA: http://localhost:${apiPort}/admin (after \`npm run build:admin\` in node_modules/davepi)`,
|
|
356
|
+
`- Capability manifest: http://localhost:${apiPort}/_describe`,
|
|
357
|
+
'',
|
|
358
|
+
'## What\'s in this template',
|
|
359
|
+
'',
|
|
360
|
+
`See [TEMPLATE.md](./TEMPLATE.md) for the schema walkthrough.`,
|
|
361
|
+
'',
|
|
362
|
+
'## With Claude Code / Cursor',
|
|
363
|
+
'',
|
|
364
|
+
'The MCP server is pre-configured in `.mcp.json`. The agent guide is at',
|
|
365
|
+
'`agent.md` and mirrored to `.cursorrules`, `AGENTS.md`, and',
|
|
366
|
+
'`.claude/skills/davepi/SKILL.md` so each runtime picks it up from its',
|
|
367
|
+
'conventional path. Open the project in your editor and ask the agent',
|
|
368
|
+
"to add a resource — schema files in `schema/versions/v1/` hot-reload",
|
|
369
|
+
'as you save.',
|
|
370
|
+
'',
|
|
371
|
+
'## Regenerate the typed client',
|
|
372
|
+
'',
|
|
373
|
+
'```bash',
|
|
374
|
+
'npm run gen-client',
|
|
375
|
+
'```',
|
|
376
|
+
'',
|
|
377
|
+
'Output lands at `client/davepi.ts`. Pair with `client/davepi-runtime.ts`',
|
|
378
|
+
"from dAvePi's source tree (or copy from `node_modules/davepi/client/`).",
|
|
379
|
+
'',
|
|
380
|
+
].join('\n')
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
// 10. Done. Run npm install if requested.
|
|
384
|
+
out(`\nScaffolded ${name} (template: ${template})`);
|
|
385
|
+
if (install) {
|
|
386
|
+
out('\nInstalling dependencies...');
|
|
387
|
+
const { spawnSync } = require('child_process');
|
|
388
|
+
// npm ships as a .cmd shim on Windows, so the bare `npm`
|
|
389
|
+
// binary spawn fails with ENOENT there. Use the platform-
|
|
390
|
+
// specific shim name to keep the auto-install step working
|
|
391
|
+
// everywhere.
|
|
392
|
+
const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
393
|
+
const r = spawnSync(npmCmd, ['install'], { cwd: target, stdio: 'inherit' });
|
|
394
|
+
if (r.status !== 0) {
|
|
395
|
+
err(
|
|
396
|
+
`\nnpm install failed. Re-run manually: cd ${name} && npm install`
|
|
397
|
+
);
|
|
398
|
+
return target;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
out('');
|
|
403
|
+
out(`Next steps:`);
|
|
404
|
+
out(` cd ${name}`);
|
|
405
|
+
if (!install) out(` npm install`);
|
|
406
|
+
out(` docker compose up -d # start Mongo`);
|
|
407
|
+
out(` npm start # http://localhost:${apiPort}`);
|
|
408
|
+
out('');
|
|
409
|
+
out(`Try Claude Code: open the project, the MCP server is wired in .mcp.json.`);
|
|
410
|
+
out(`Agent guide: agent.md (also mirrored to .cursorrules, AGENTS.md, .claude/skills/davepi/SKILL.md)`);
|
|
411
|
+
return target;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async function main(argv) {
|
|
415
|
+
const args = argv.slice(2);
|
|
416
|
+
if (!args.length || args.includes('--help') || args.includes('-h')) {
|
|
417
|
+
usage();
|
|
418
|
+
process.exit(0);
|
|
419
|
+
}
|
|
420
|
+
const name = args.find((a) => !a.startsWith('--'));
|
|
421
|
+
if (!name) {
|
|
422
|
+
err('Project name required.');
|
|
423
|
+
usage();
|
|
424
|
+
process.exit(1);
|
|
425
|
+
}
|
|
426
|
+
let template = 'blank';
|
|
427
|
+
let davepiVersion = null;
|
|
428
|
+
let port = null;
|
|
429
|
+
try {
|
|
430
|
+
const tplArg = flag(args, '--template');
|
|
431
|
+
if (typeof tplArg === 'string') template = tplArg;
|
|
432
|
+
const v = flag(args, '--davepi-version');
|
|
433
|
+
if (typeof v === 'string') davepiVersion = v;
|
|
434
|
+
const p = flag(args, '--port');
|
|
435
|
+
if (typeof p === 'string' && /^\d+$/.test(p)) port = parseInt(p, 10);
|
|
436
|
+
} catch (parseErr) {
|
|
437
|
+
err(`\n${parseErr.message}`);
|
|
438
|
+
usage();
|
|
439
|
+
process.exit(1);
|
|
440
|
+
}
|
|
441
|
+
const noInstall = args.includes('--no-install');
|
|
442
|
+
|
|
443
|
+
try {
|
|
444
|
+
await scaffold({ name, template, install: !noInstall, davepiVersion, port });
|
|
445
|
+
} catch (e) {
|
|
446
|
+
err(`\nError: ${e.message}`);
|
|
447
|
+
process.exit(1);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (require.main === module) {
|
|
452
|
+
main(process.argv).catch((e) => {
|
|
453
|
+
err(`\nError: ${e && e.message ? e.message : String(e)}`);
|
|
454
|
+
process.exit(1);
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
module.exports = { scaffold, TEMPLATES, flag, pickPort, isPortFree };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Pre-publish hook: copy `templates/` from the dAvePi monorepo
|
|
4
|
+
* root into this package's directory so the published tarball
|
|
5
|
+
* carries the templates and consumers don't need a working
|
|
6
|
+
* monorepo layout to scaffold.
|
|
7
|
+
*
|
|
8
|
+
* The runtime CLI (`bin/index.js`) prefers a sibling `templates/`
|
|
9
|
+
* inside the package; this script populates that sibling. In dev
|
|
10
|
+
* (running tests, working in the monorepo) the CLI falls back to
|
|
11
|
+
* `../../templates` instead.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
|
|
19
|
+
const src = path.resolve(__dirname, '..', '..', 'templates');
|
|
20
|
+
const dest = path.resolve(__dirname, '..', 'templates');
|
|
21
|
+
|
|
22
|
+
if (!fs.existsSync(src)) {
|
|
23
|
+
process.stderr.write(
|
|
24
|
+
`sync-templates: ${src} not found. Run from inside the dAvePi monorepo.\n`
|
|
25
|
+
);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
30
|
+
|
|
31
|
+
function copyTree(s, d) {
|
|
32
|
+
fs.mkdirSync(d, { recursive: true });
|
|
33
|
+
for (const entry of fs.readdirSync(s, { withFileTypes: true })) {
|
|
34
|
+
const a = path.join(s, entry.name);
|
|
35
|
+
const b = path.join(d, entry.name);
|
|
36
|
+
if (entry.isDirectory()) copyTree(a, b);
|
|
37
|
+
else fs.copyFileSync(a, b);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
copyTree(src, dest);
|
|
42
|
+
process.stdout.write(`sync-templates: copied ${src} → ${dest}\n`);
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-davepi-app",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Scaffolder for new dAvePi projects. Run: npx create-davepi-app <name> [--template <name>]",
|
|
5
|
+
"license": "ISC",
|
|
6
|
+
"author": "David Baxter",
|
|
7
|
+
"bin": {
|
|
8
|
+
"create-davepi-app": "bin/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin/",
|
|
12
|
+
"templates/",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"prepublishOnly": "node bin/sync-templates.js"
|
|
17
|
+
},
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=18"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"davepi",
|
|
23
|
+
"scaffolder",
|
|
24
|
+
"create-app",
|
|
25
|
+
"template",
|
|
26
|
+
"rest",
|
|
27
|
+
"graphql",
|
|
28
|
+
"mcp"
|
|
29
|
+
],
|
|
30
|
+
"homepage": "https://github.com/projik/davepi#readme",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/projik/davepi.git",
|
|
34
|
+
"directory": "create-davepi-app"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
name: Typed client drift
|
|
2
|
+
|
|
3
|
+
# Regenerates client/davepi.ts from the live schema registry and
|
|
4
|
+
# fails the PR if the committed copy doesn't match. This catches
|
|
5
|
+
# the common bug where a schema field changes but the typed client
|
|
6
|
+
# wasn't regenerated — without this guard, frontend code keeps
|
|
7
|
+
# compiling against a stale type until someone hits a runtime
|
|
8
|
+
# 500.
|
|
9
|
+
#
|
|
10
|
+
# The job uses a Mongo service because `davepi gen-client` boots
|
|
11
|
+
# the schema loader (which connects to Mongo). It doesn't read or
|
|
12
|
+
# write any data — Mongo just needs to accept the connection.
|
|
13
|
+
|
|
14
|
+
on:
|
|
15
|
+
pull_request:
|
|
16
|
+
branches: [main]
|
|
17
|
+
paths:
|
|
18
|
+
# Only run when something that could affect the generated
|
|
19
|
+
# client has changed. Saves CI minutes on docs-only PRs.
|
|
20
|
+
- 'schema/**'
|
|
21
|
+
- 'package.json'
|
|
22
|
+
- 'package-lock.json'
|
|
23
|
+
- '.github/workflows/client-gen.yml'
|
|
24
|
+
|
|
25
|
+
permissions:
|
|
26
|
+
contents: read
|
|
27
|
+
|
|
28
|
+
jobs:
|
|
29
|
+
drift:
|
|
30
|
+
runs-on: ubuntu-latest
|
|
31
|
+
services:
|
|
32
|
+
mongo:
|
|
33
|
+
image: mongo:7
|
|
34
|
+
ports:
|
|
35
|
+
- 27017:27017
|
|
36
|
+
options: >-
|
|
37
|
+
--health-cmd "mongosh --quiet --eval 'db.adminCommand({ ping: 1 }).ok'"
|
|
38
|
+
--health-interval 5s
|
|
39
|
+
--health-timeout 5s
|
|
40
|
+
--health-retries 10
|
|
41
|
+
env:
|
|
42
|
+
MONGO_URI: mongodb://127.0.0.1:27017/client-gen
|
|
43
|
+
TOKEN_KEY: ci-only-not-a-real-secret
|
|
44
|
+
NODE_ENV: development
|
|
45
|
+
steps:
|
|
46
|
+
- uses: actions/checkout@v4
|
|
47
|
+
- uses: actions/setup-node@v4
|
|
48
|
+
with:
|
|
49
|
+
node-version: '20.x'
|
|
50
|
+
cache: npm
|
|
51
|
+
- run: npm ci
|
|
52
|
+
- name: Regenerate the typed client
|
|
53
|
+
run: npx davepi gen-client --out client/davepi.ts
|
|
54
|
+
- name: Fail if the committed client is stale or missing
|
|
55
|
+
run: |
|
|
56
|
+
# Mark untracked files as intent-to-add so they show up in
|
|
57
|
+
# `git diff`. Without this, a never-committed (or
|
|
58
|
+
# accidentally gitignored) client/davepi.ts would silently
|
|
59
|
+
# pass the drift check — defeating the whole point of the
|
|
60
|
+
# workflow.
|
|
61
|
+
git add --intent-to-add client/davepi.ts 2>/dev/null || true
|
|
62
|
+
if ! git diff --exit-code -- client/davepi.ts; then
|
|
63
|
+
echo "::error::client/davepi.ts is out of date or not committed. Run \`npm run gen-client\` locally and commit the result."
|
|
64
|
+
exit 1
|
|
65
|
+
fi
|