create-appystack 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 +54 -0
- package/bin/index.js +243 -0
- package/package.json +39 -0
- package/template/.claude/skills/recipe/SKILL.md +71 -0
- package/template/.claude/skills/recipe/domains/care-provider-operations.md +185 -0
- package/template/.claude/skills/recipe/domains/youtube-launch-optimizer.md +154 -0
- package/template/.claude/skills/recipe/references/file-crud.md +295 -0
- package/template/.claude/skills/recipe/references/nav-shell.md +233 -0
- package/template/.dockerignore +39 -0
- package/template/.env.example +13 -0
- package/template/.github/workflows/ci.yml +43 -0
- package/template/.husky/pre-commit +1 -0
- package/template/.prettierignore +7 -0
- package/template/.prettierrc +8 -0
- package/template/.vscode/launch.json +59 -0
- package/template/CLAUDE.md +114 -0
- package/template/Dockerfile +56 -0
- package/template/README.md +219 -0
- package/template/client/index.html +13 -0
- package/template/client/package.json +43 -0
- package/template/client/src/App.test.tsx +67 -0
- package/template/client/src/App.tsx +11 -0
- package/template/client/src/components/ErrorFallback.test.tsx +64 -0
- package/template/client/src/components/ErrorFallback.tsx +18 -0
- package/template/client/src/config/env.test.ts +64 -0
- package/template/client/src/config/env.ts +34 -0
- package/template/client/src/contexts/AppContext.test.tsx +81 -0
- package/template/client/src/contexts/AppContext.tsx +52 -0
- package/template/client/src/demo/ContactForm.test.tsx +97 -0
- package/template/client/src/demo/ContactForm.tsx +100 -0
- package/template/client/src/demo/DemoPage.tsx +56 -0
- package/template/client/src/demo/SocketDemo.test.tsx +160 -0
- package/template/client/src/demo/SocketDemo.tsx +65 -0
- package/template/client/src/demo/StatusGrid.test.tsx +181 -0
- package/template/client/src/demo/StatusGrid.tsx +77 -0
- package/template/client/src/demo/TechStackDisplay.test.tsx +63 -0
- package/template/client/src/demo/TechStackDisplay.tsx +75 -0
- package/template/client/src/hooks/useServerStatus.test.ts +133 -0
- package/template/client/src/hooks/useServerStatus.ts +67 -0
- package/template/client/src/hooks/useSocket.test.ts +152 -0
- package/template/client/src/hooks/useSocket.ts +43 -0
- package/template/client/src/lib/utils.test.ts +33 -0
- package/template/client/src/lib/utils.ts +14 -0
- package/template/client/src/main.test.tsx +113 -0
- package/template/client/src/main.tsx +14 -0
- package/template/client/src/pages/LandingPage.test.tsx +30 -0
- package/template/client/src/pages/LandingPage.tsx +29 -0
- package/template/client/src/styles/index.css +50 -0
- package/template/client/src/test/msw/browser.ts +4 -0
- package/template/client/src/test/msw/handlers.ts +12 -0
- package/template/client/src/test/msw/msw-example.test.ts +69 -0
- package/template/client/src/test/msw/server.ts +14 -0
- package/template/client/src/test/setup.ts +10 -0
- package/template/client/src/utils/api.test.ts +79 -0
- package/template/client/src/utils/api.ts +42 -0
- package/template/client/src/vite-env.d.ts +13 -0
- package/template/client/tsconfig.json +17 -0
- package/template/client/vite.config.ts +38 -0
- package/template/client/vitest.config.ts +36 -0
- package/template/docker-compose.yml +19 -0
- package/template/e2e/smoke.test.ts +95 -0
- package/template/e2e/socket.test.ts +96 -0
- package/template/eslint.config.js +2 -0
- package/template/package.json +50 -0
- package/template/playwright.config.ts +14 -0
- package/template/scripts/customize.ts +175 -0
- package/template/server/nodemon.json +5 -0
- package/template/server/package.json +45 -0
- package/template/server/src/app.test.ts +103 -0
- package/template/server/src/config/env.test.ts +97 -0
- package/template/server/src/config/env.ts +29 -0
- package/template/server/src/config/logger.test.ts +58 -0
- package/template/server/src/config/logger.ts +17 -0
- package/template/server/src/helpers/response.test.ts +53 -0
- package/template/server/src/helpers/response.ts +17 -0
- package/template/server/src/index.ts +118 -0
- package/template/server/src/middleware/errorHandler.test.ts +84 -0
- package/template/server/src/middleware/errorHandler.ts +27 -0
- package/template/server/src/middleware/rateLimiter.test.ts +68 -0
- package/template/server/src/middleware/rateLimiter.ts +8 -0
- package/template/server/src/middleware/requestLogger.test.ts +111 -0
- package/template/server/src/middleware/requestLogger.ts +17 -0
- package/template/server/src/middleware/validate.test.ts +213 -0
- package/template/server/src/middleware/validate.ts +23 -0
- package/template/server/src/routes/health.test.ts +17 -0
- package/template/server/src/routes/health.ts +12 -0
- package/template/server/src/routes/info.test.ts +20 -0
- package/template/server/src/routes/info.ts +19 -0
- package/template/server/src/shared.test.ts +53 -0
- package/template/server/src/shutdown.test.ts +98 -0
- package/template/server/src/socket.test.ts +185 -0
- package/template/server/src/static.test.ts +166 -0
- package/template/server/tsconfig.json +16 -0
- package/template/server/vitest.config.ts +22 -0
- package/template/shared/package.json +19 -0
- package/template/shared/src/constants.ts +11 -0
- package/template/shared/src/index.ts +8 -0
- package/template/shared/src/types.ts +33 -0
- package/template/shared/tsconfig.json +10 -0
package/README.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# create-appystack
|
|
2
|
+
|
|
3
|
+
Scaffold a new [AppyStack](https://github.com/appydave/appystack) RVETS project in one command.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx create-appystack my-app
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or without a name (interactive):
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx create-appystack
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The CLI will prompt for:
|
|
18
|
+
|
|
19
|
+
1. **Project name** — directory name and package name suffix
|
|
20
|
+
2. **Package scope** — npm scope (e.g. `@myorg`)
|
|
21
|
+
3. **Server port** — default `5501`
|
|
22
|
+
4. **Client port** — default `5500`
|
|
23
|
+
5. **Description** — short project description
|
|
24
|
+
|
|
25
|
+
Then it copies the template, applies your settings, runs `npm install`, and prints next steps.
|
|
26
|
+
|
|
27
|
+
## What You Get
|
|
28
|
+
|
|
29
|
+
A full-stack TypeScript monorepo with:
|
|
30
|
+
|
|
31
|
+
- **React 19 + Vite 7 + TailwindCSS v4** — client (your chosen port)
|
|
32
|
+
- **Express 5 + Socket.io + Pino + Zod** — server (your chosen port)
|
|
33
|
+
- **Shared TypeScript types** — workspace package
|
|
34
|
+
- **Vitest** — server + client tests
|
|
35
|
+
- **ESLint 9 flat config + Prettier** — via `@appydave/appystack-config`
|
|
36
|
+
- **Husky + lint-staged** — pre-commit hooks
|
|
37
|
+
|
|
38
|
+
## After Creation
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
cd my-app
|
|
42
|
+
npm run dev # Start client + server concurrently
|
|
43
|
+
npm test # Run all tests
|
|
44
|
+
npm run build # Production build
|
|
45
|
+
npm run typecheck # TypeScript check across all workspaces
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Stack
|
|
49
|
+
|
|
50
|
+
**R**eact · **V**ite · **E**xpress · **T**ypeScript · **S**ocket.io
|
|
51
|
+
|
|
52
|
+
## License
|
|
53
|
+
|
|
54
|
+
MIT
|
package/bin/index.js
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* create-appystack CLI
|
|
4
|
+
* Usage: npx create-appystack [project-name]
|
|
5
|
+
*/
|
|
6
|
+
import { intro, outro, text, cancel, isCancel, spinner } from '@clack/prompts';
|
|
7
|
+
import { readFileSync, writeFileSync, cpSync, existsSync, rmSync } from 'node:fs';
|
|
8
|
+
import { resolve, dirname, join } from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { execSync } from 'node:child_process';
|
|
11
|
+
|
|
12
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const TEMPLATE_DIR = resolve(__dirname, '../template');
|
|
14
|
+
|
|
15
|
+
function readFile(root, relPath) {
|
|
16
|
+
return readFileSync(resolve(root, relPath), 'utf-8');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function writeFile(root, relPath, content) {
|
|
20
|
+
writeFileSync(resolve(root, relPath), content, 'utf-8');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function replaceAll(content, from, to) {
|
|
24
|
+
return content.split(from).join(to);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const EXCLUDE = new Set(['node_modules', 'dist', 'coverage', 'test-results', '.git']);
|
|
28
|
+
|
|
29
|
+
function templateFilter(src) {
|
|
30
|
+
const segment = src.split('/').pop();
|
|
31
|
+
return !EXCLUDE.has(segment);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function applyCustomizations(root, { name, scope, serverPort, clientPort, desc }) {
|
|
35
|
+
const oldScope = '@appystack-template';
|
|
36
|
+
const oldRootName = '@appydave/appystack-template';
|
|
37
|
+
const oldServerPort = '5501';
|
|
38
|
+
const oldClientPort = '5500';
|
|
39
|
+
const oldTitle = 'AppyStack Template';
|
|
40
|
+
const oldDescription = 'RVETS stack boilerplate (React, Vite, Express, TypeScript, Socket.io)';
|
|
41
|
+
|
|
42
|
+
// Root package.json
|
|
43
|
+
let rootPkg = readFile(root, 'package.json');
|
|
44
|
+
rootPkg = replaceAll(rootPkg, oldRootName, `${scope}/${name}`);
|
|
45
|
+
rootPkg = replaceAll(rootPkg, oldDescription, desc);
|
|
46
|
+
writeFile(root, 'package.json', rootPkg);
|
|
47
|
+
|
|
48
|
+
// shared/package.json
|
|
49
|
+
let sharedPkg = readFile(root, 'shared/package.json');
|
|
50
|
+
sharedPkg = replaceAll(sharedPkg, oldScope, scope);
|
|
51
|
+
writeFile(root, 'shared/package.json', sharedPkg);
|
|
52
|
+
|
|
53
|
+
// server/package.json
|
|
54
|
+
let serverPkg = readFile(root, 'server/package.json');
|
|
55
|
+
serverPkg = replaceAll(serverPkg, oldScope, scope);
|
|
56
|
+
writeFile(root, 'server/package.json', serverPkg);
|
|
57
|
+
|
|
58
|
+
// client/package.json
|
|
59
|
+
let clientPkg = readFile(root, 'client/package.json');
|
|
60
|
+
clientPkg = replaceAll(clientPkg, oldScope, scope);
|
|
61
|
+
writeFile(root, 'client/package.json', clientPkg);
|
|
62
|
+
|
|
63
|
+
// .env.example
|
|
64
|
+
let envExample = readFile(root, '.env.example');
|
|
65
|
+
envExample = replaceAll(envExample, `PORT=${oldServerPort}`, `PORT=${serverPort}`);
|
|
66
|
+
envExample = replaceAll(
|
|
67
|
+
envExample,
|
|
68
|
+
`CLIENT_URL=http://localhost:${oldClientPort}`,
|
|
69
|
+
`CLIENT_URL=http://localhost:${clientPort}`
|
|
70
|
+
);
|
|
71
|
+
writeFile(root, '.env.example', envExample);
|
|
72
|
+
|
|
73
|
+
// server/src/config/env.ts
|
|
74
|
+
let envTs = readFile(root, 'server/src/config/env.ts');
|
|
75
|
+
envTs = replaceAll(
|
|
76
|
+
envTs,
|
|
77
|
+
`PORT: z.coerce.number().default(${oldServerPort})`,
|
|
78
|
+
`PORT: z.coerce.number().default(${serverPort})`
|
|
79
|
+
);
|
|
80
|
+
envTs = replaceAll(
|
|
81
|
+
envTs,
|
|
82
|
+
`CLIENT_URL: z.string().default('http://localhost:${oldClientPort}')`,
|
|
83
|
+
`CLIENT_URL: z.string().default('http://localhost:${clientPort}')`
|
|
84
|
+
);
|
|
85
|
+
writeFile(root, 'server/src/config/env.ts', envTs);
|
|
86
|
+
|
|
87
|
+
// client/vite.config.ts
|
|
88
|
+
let viteConfig = readFile(root, 'client/vite.config.ts');
|
|
89
|
+
viteConfig = replaceAll(viteConfig, `port: ${oldClientPort}`, `port: ${clientPort}`);
|
|
90
|
+
viteConfig = replaceAll(
|
|
91
|
+
viteConfig,
|
|
92
|
+
`target: 'http://localhost:${oldServerPort}'`,
|
|
93
|
+
`target: 'http://localhost:${serverPort}'`
|
|
94
|
+
);
|
|
95
|
+
writeFile(root, 'client/vite.config.ts', viteConfig);
|
|
96
|
+
|
|
97
|
+
// client/index.html
|
|
98
|
+
let indexHtml = readFile(root, 'client/index.html');
|
|
99
|
+
indexHtml = replaceAll(indexHtml, `<title>${oldTitle}</title>`, `<title>${name}</title>`);
|
|
100
|
+
writeFile(root, 'client/index.html', indexHtml);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function main() {
|
|
104
|
+
const argName = process.argv[2];
|
|
105
|
+
|
|
106
|
+
intro('create-appystack');
|
|
107
|
+
|
|
108
|
+
// --- Project name ---
|
|
109
|
+
let projectName;
|
|
110
|
+
if (argName) {
|
|
111
|
+
if (!/^[a-z0-9-]+$/.test(argName.trim())) {
|
|
112
|
+
console.error('Error: project name must use lowercase letters, numbers, and hyphens only');
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
projectName = argName.trim();
|
|
116
|
+
} else {
|
|
117
|
+
const result = await text({
|
|
118
|
+
message: 'Project name (e.g. my-app)',
|
|
119
|
+
placeholder: 'my-app',
|
|
120
|
+
validate(value) {
|
|
121
|
+
if (!value || value.trim().length === 0) return 'Project name is required';
|
|
122
|
+
if (!/^[a-z0-9-]+$/.test(value.trim()))
|
|
123
|
+
return 'Use lowercase letters, numbers, and hyphens only';
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
if (isCancel(result)) { cancel('Cancelled.'); process.exit(0); }
|
|
127
|
+
projectName = result.trim();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// --- Check target doesn't exist ---
|
|
131
|
+
const targetDir = resolve(process.cwd(), projectName);
|
|
132
|
+
if (existsSync(targetDir)) {
|
|
133
|
+
console.error(`Error: directory "${projectName}" already exists`);
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// --- Package scope ---
|
|
138
|
+
const scopeResult = await text({
|
|
139
|
+
message: 'Package scope (e.g. @myorg)',
|
|
140
|
+
placeholder: '@myorg',
|
|
141
|
+
validate(value) {
|
|
142
|
+
if (!value || value.trim().length === 0) return 'Package scope is required';
|
|
143
|
+
if (!value.trim().startsWith('@')) return 'Scope must start with @';
|
|
144
|
+
if (!/^@[a-z0-9-]+$/.test(value.trim())) return 'Use @lowercase-letters-numbers-hyphens only';
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
if (isCancel(scopeResult)) { cancel('Cancelled.'); process.exit(0); }
|
|
148
|
+
const packageScope = scopeResult.trim();
|
|
149
|
+
|
|
150
|
+
// --- Server port ---
|
|
151
|
+
const serverPortResult = await text({
|
|
152
|
+
message: 'Server port',
|
|
153
|
+
placeholder: '5501',
|
|
154
|
+
initialValue: '5501',
|
|
155
|
+
validate(value) {
|
|
156
|
+
const port = Number(value);
|
|
157
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
158
|
+
return 'Enter a valid port number (1–65535)';
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
if (isCancel(serverPortResult)) { cancel('Cancelled.'); process.exit(0); }
|
|
162
|
+
const serverPort = serverPortResult.trim();
|
|
163
|
+
|
|
164
|
+
// --- Client port ---
|
|
165
|
+
const clientPortResult = await text({
|
|
166
|
+
message: 'Client port',
|
|
167
|
+
placeholder: '5500',
|
|
168
|
+
initialValue: '5500',
|
|
169
|
+
validate(value) {
|
|
170
|
+
const port = Number(value);
|
|
171
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
172
|
+
return 'Enter a valid port number (1–65535)';
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
if (isCancel(clientPortResult)) { cancel('Cancelled.'); process.exit(0); }
|
|
176
|
+
const clientPort = clientPortResult.trim();
|
|
177
|
+
|
|
178
|
+
// --- Description ---
|
|
179
|
+
const descResult = await text({
|
|
180
|
+
message: 'Project description',
|
|
181
|
+
placeholder: 'My awesome app',
|
|
182
|
+
validate(value) {
|
|
183
|
+
if (!value || value.trim().length === 0) return 'Description is required';
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
if (isCancel(descResult)) { cancel('Cancelled.'); process.exit(0); }
|
|
187
|
+
const description = descResult.trim();
|
|
188
|
+
|
|
189
|
+
// --- Copy template ---
|
|
190
|
+
const s = spinner();
|
|
191
|
+
s.start('Copying template...');
|
|
192
|
+
try {
|
|
193
|
+
cpSync(TEMPLATE_DIR, targetDir, { recursive: true, filter: templateFilter });
|
|
194
|
+
s.stop('Template copied');
|
|
195
|
+
} catch (err) {
|
|
196
|
+
s.stop('Failed to copy template');
|
|
197
|
+
console.error(err.message);
|
|
198
|
+
rmSync(targetDir, { recursive: true, force: true });
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// --- Apply customizations ---
|
|
203
|
+
s.start('Customizing project...');
|
|
204
|
+
try {
|
|
205
|
+
applyCustomizations(targetDir, {
|
|
206
|
+
name: projectName,
|
|
207
|
+
scope: packageScope,
|
|
208
|
+
serverPort,
|
|
209
|
+
clientPort,
|
|
210
|
+
desc: description,
|
|
211
|
+
});
|
|
212
|
+
s.stop('Project customized');
|
|
213
|
+
} catch (err) {
|
|
214
|
+
s.stop('Customization failed');
|
|
215
|
+
console.error(err.message);
|
|
216
|
+
rmSync(targetDir, { recursive: true, force: true });
|
|
217
|
+
process.exit(1);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// --- npm install ---
|
|
221
|
+
s.start('Installing dependencies (this may take a minute)...');
|
|
222
|
+
try {
|
|
223
|
+
execSync('npm install', { cwd: targetDir, stdio: 'inherit' });
|
|
224
|
+
s.stop('Dependencies installed');
|
|
225
|
+
} catch (err) {
|
|
226
|
+
s.stop('npm install failed');
|
|
227
|
+
console.error('Run "npm install" manually after cd into the project.');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
outro(`Created ${projectName}
|
|
231
|
+
|
|
232
|
+
Next steps:
|
|
233
|
+
cd ${projectName}
|
|
234
|
+
npm run dev
|
|
235
|
+
|
|
236
|
+
Client: http://localhost:${clientPort}
|
|
237
|
+
Server: http://localhost:${serverPort}`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
main().catch((err) => {
|
|
241
|
+
console.error(err);
|
|
242
|
+
process.exit(1);
|
|
243
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-appystack",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Scaffold a new AppyStack RVETS project — React, Vite, Express, TypeScript, Socket.io",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-appystack": "bin/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"template/"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"sync": "node scripts/sync-template.js",
|
|
15
|
+
"prepublishOnly": "node scripts/sync-template.js"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@clack/prompts": "^1.0.1"
|
|
19
|
+
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=18"
|
|
22
|
+
},
|
|
23
|
+
"author": "David Cruwys",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/appydave/appystack.git"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"appystack",
|
|
31
|
+
"create",
|
|
32
|
+
"scaffold",
|
|
33
|
+
"react",
|
|
34
|
+
"vite",
|
|
35
|
+
"express",
|
|
36
|
+
"typescript",
|
|
37
|
+
"socket.io"
|
|
38
|
+
]
|
|
39
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: recipe
|
|
3
|
+
description: "App architecture recipes for AppyStack projects. Use when a developer wants to build a specific type of application on top of the RVETS template — e.g. 'What recipes are available?', 'I want to build a CRUD app', 'I want a sidebar navigation layout', 'help me set up a nav-shell app', 'build me a file-based entity system', 'what can I build with AppyStack?', 'scaffold an app for me', 'I want a nav + data app'. Presents available recipes, generates a concrete build prompt for the chosen recipe, and asks for confirmation before building. Recipes can be used alone or combined."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Recipe
|
|
7
|
+
|
|
8
|
+
## What Are Recipes?
|
|
9
|
+
|
|
10
|
+
Recipes are app architecture patterns that sit on top of the AppyStack RVETS template. Each recipe defines a specific structural shape — layout, data strategy, Socket.IO usage — that Claude scaffolds into the project.
|
|
11
|
+
|
|
12
|
+
Recipes are:
|
|
13
|
+
- **Stack-aware**: They know AppyStack's folder structure, installed libraries, and conventions
|
|
14
|
+
- **Composable**: Multiple recipes can run together (e.g. nav-shell + file-crud = a complete CRUD app)
|
|
15
|
+
- **Idempotent**: Each recipe checks whether it's already been applied before making changes
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Available Recipes
|
|
20
|
+
|
|
21
|
+
| Recipe | What it builds |
|
|
22
|
+
|--------|----------------|
|
|
23
|
+
| `nav-shell` | Left-sidebar navigation shell with header, collapsible sidebar, main content area, and optional footer. Menus can switch dynamically when sub-tools are active. Domain-agnostic layout scaffold. |
|
|
24
|
+
| `file-crud` | JSON file-based persistence for one or more entities. Each record is a file named `{slug}-{id}.json`. Real-time Socket.io sync. No database required. Includes chokidar file watcher. |
|
|
25
|
+
|
|
26
|
+
**Combinations:**
|
|
27
|
+
- `nav-shell` + `file-crud` = complete CRUD app with sidebar nav and file persistence
|
|
28
|
+
- `nav-shell` alone = visual shell, fill in data later
|
|
29
|
+
- `file-crud` alone = data layer only, wire up your own UI
|
|
30
|
+
|
|
31
|
+
**Reference files:**
|
|
32
|
+
- `references/nav-shell.md` — full nav-shell recipe spec
|
|
33
|
+
- `references/file-crud.md` — full file-crud recipe spec
|
|
34
|
+
|
|
35
|
+
**Domain samples** (for file-crud — example domains to draw entity/field inspiration from):
|
|
36
|
+
- `domains/care-provider-operations.md` — residential care provider (6 entities: Company, Site, User, Participant, Incident, Moment)
|
|
37
|
+
- `domains/youtube-launch-optimizer.md` — YouTube content production (5 entities: Channel, Video, Script, ThumbnailVariant, LaunchTask)
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Flow
|
|
42
|
+
|
|
43
|
+
1. **Identify** which recipe(s) fit. If intent is unclear, ask: "What kind of app are you building?" and present the table above.
|
|
44
|
+
2. **For file-crud**: ask if a domain sample applies, or collect entity details directly.
|
|
45
|
+
3. **Load** the relevant reference file(s). Load both if combining.
|
|
46
|
+
4. **Generate** a concrete build prompt — specific file structure, component names, data shapes, event names — tailored to this project. Not generic, not boilerplate descriptions.
|
|
47
|
+
5. **Present** the prompt: "Here's what I'll build: ..." Show the specifics.
|
|
48
|
+
6. **Ask**: "Shall I go ahead?"
|
|
49
|
+
7. **Build** on confirmation, following the patterns in the reference file(s).
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Combining Recipes
|
|
54
|
+
|
|
55
|
+
When running `nav-shell` + `file-crud` together, collect domain context before generating either build prompt:
|
|
56
|
+
|
|
57
|
+
1. Ask: what entities does the app need? (names, namish fields, key fields, relationships)
|
|
58
|
+
2. Ask: what views/tools does the app need? (these become nav items)
|
|
59
|
+
3. Ask: which entity maps to which view?
|
|
60
|
+
4. Then generate: shell build prompt (with real view names from step 2) + persistence build prompt (with real entities from step 1)
|
|
61
|
+
|
|
62
|
+
The shell recipe generates view stubs. The persistence recipe generates server handlers and the `useEntity` hook. The developer (or a follow-up step) wires the `useEntity` hook into the view stubs.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Notes
|
|
67
|
+
|
|
68
|
+
- Keep the generated prompt grounded in what's already in the template. Don't introduce new dependencies unless the recipe explicitly calls for them.
|
|
69
|
+
- The generated prompt is a useful artifact — if the developer says "not quite", refine it before building rather than starting over.
|
|
70
|
+
- For file-crud, always clarify the "namish field" — what field is used to name the file? Usually `name`, but not always.
|
|
71
|
+
- For nav-shell, always clarify which items are primary vs secondary, and whether any view needs a context-aware menu.
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# Domain Sample: Care Provider Operations
|
|
2
|
+
|
|
3
|
+
A residential disability support provider app. Manages the org hierarchy (companies → sites → workers), the people being supported (participants), and the two types of care records workers create during shifts — incidents and moments.
|
|
4
|
+
|
|
5
|
+
Grounded in Australian NDIS (National Disability Insurance Scheme) context, but the structure applies to any regulated residential care setting.
|
|
6
|
+
|
|
7
|
+
**Use with**: `file-crud` recipe. Optionally combine with `nav-shell` recipe.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Entities
|
|
12
|
+
|
|
13
|
+
### Company
|
|
14
|
+
The registered care provider organisation.
|
|
15
|
+
|
|
16
|
+
| Field | Type | Notes |
|
|
17
|
+
|-------|------|-------|
|
|
18
|
+
| `name` | string | **namish field** — organisation display name |
|
|
19
|
+
| `slug` | string | URL-safe short name, e.g. `sunrise-care` |
|
|
20
|
+
| `abn` | string | Australian Business Number |
|
|
21
|
+
| `registeredNdisProvider` | boolean | regulatory compliance flag |
|
|
22
|
+
| `status` | string | `'active'` / `'suspended'` / `'onboarding'` |
|
|
23
|
+
| `createdAt` | string | ISO 8601 timestamp |
|
|
24
|
+
|
|
25
|
+
Namish field: `name`
|
|
26
|
+
Example filename: `sunrise-care-group-sc4f2a.json`
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
### Site
|
|
31
|
+
A physical location (group home, day program, etc.) operated by a Company.
|
|
32
|
+
|
|
33
|
+
| Field | Type | Notes |
|
|
34
|
+
|-------|------|-------|
|
|
35
|
+
| `name` | string | **namish field** — location display name |
|
|
36
|
+
| `companyId` | string | FK → Company (6-char id) |
|
|
37
|
+
| `address` | string | street address |
|
|
38
|
+
| `suburb` | string | |
|
|
39
|
+
| `state` | string | e.g. `VIC`, `NSW`, `QLD` |
|
|
40
|
+
| `postcode` | string | |
|
|
41
|
+
| `status` | string | `'active'` / `'inactive'` |
|
|
42
|
+
|
|
43
|
+
Namish field: `name`
|
|
44
|
+
Example filename: `thornbury-house-th7k3m.json`
|
|
45
|
+
Relationship: `companyId` → Company
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
### User
|
|
50
|
+
A staff member (support worker, team leader, or admin) employed by a Company.
|
|
51
|
+
|
|
52
|
+
| Field | Type | Notes |
|
|
53
|
+
|-------|------|-------|
|
|
54
|
+
| `name` | string | **namish field** — full name |
|
|
55
|
+
| `email` | string | |
|
|
56
|
+
| `companyId` | string | FK → Company |
|
|
57
|
+
| `roles` | string[] | array — e.g. `['support-worker', 'team-leader']` |
|
|
58
|
+
| `status` | string | `'active'` / `'inactive'` / `'invited'` |
|
|
59
|
+
| `createdAt` | string | ISO 8601 timestamp |
|
|
60
|
+
|
|
61
|
+
Namish field: `name`
|
|
62
|
+
Example filename: `angela-brown-ab9p2x.json`
|
|
63
|
+
Relationship: `companyId` → Company
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
### Participant
|
|
68
|
+
A person receiving support under an NDIS plan, supported by a Company at a primary Site.
|
|
69
|
+
|
|
70
|
+
| Field | Type | Notes |
|
|
71
|
+
|-------|------|-------|
|
|
72
|
+
| `firstName` | string | |
|
|
73
|
+
| `lastName` | string | **namish field** (composite `firstName-lastName`) |
|
|
74
|
+
| `preferredName` | string \| null | optional — if different from first name |
|
|
75
|
+
| `ndisNumber` | string | NDIS plan number, e.g. `512384901` |
|
|
76
|
+
| `dateOfBirth` | string | ISO 8601 date |
|
|
77
|
+
| `companyId` | string | FK → Company |
|
|
78
|
+
| `defaultSiteId` | string | FK → Site (primary home) |
|
|
79
|
+
| `baselineDataTier` | number | NDIS funding tier: `1` / `2` / `3` / `4` |
|
|
80
|
+
| `status` | string | `'active'` / `'inactive'` / `'transitioned'` |
|
|
81
|
+
|
|
82
|
+
Namish field: composite `${firstName}-${lastName}`
|
|
83
|
+
Example filename: `rosie-fairweather-rf3n8k.json`
|
|
84
|
+
Relationships: `companyId` → Company, `defaultSiteId` → Site
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
### Incident
|
|
89
|
+
A significant event at a site involving a participant. Requires formal recording and may require regulatory reporting.
|
|
90
|
+
|
|
91
|
+
| Field | Type | Notes |
|
|
92
|
+
|-------|------|-------|
|
|
93
|
+
| `summary` | string | **namish field** — brief description of what happened |
|
|
94
|
+
| `type` | string | `'behavioural'` / `'medical'` / `'environmental'` / `'other'` |
|
|
95
|
+
| `severity` | string | `'low'` / `'medium'` / `'high'` / `'critical'` |
|
|
96
|
+
| `status` | string | `'draft'` / `'submitted'` / `'under-review'` / `'closed'` |
|
|
97
|
+
| `occurredAt` | string | ISO 8601 timestamp of the event |
|
|
98
|
+
| `antecedents` | string[] | triggering factors, e.g. `['routine change', 'unfamiliar worker']` |
|
|
99
|
+
| `companyId` | string | FK → Company |
|
|
100
|
+
| `participantId` | string | FK → Participant (who was involved) |
|
|
101
|
+
| `siteId` | string | FK → Site (where it happened) |
|
|
102
|
+
| `reportedById` | string | FK → User (who filed it) |
|
|
103
|
+
| `createdAt` | string | ISO 8601 timestamp — when it was logged |
|
|
104
|
+
|
|
105
|
+
Namish field: `summary`
|
|
106
|
+
Example filename: `distressed-during-morning-routine-inc-k4p9m.json`
|
|
107
|
+
Relationships: `companyId` → Company, `participantId` → Participant, `siteId` → Site, `reportedById` → User
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
### Moment
|
|
112
|
+
A routine care observation recorded by a worker during a shift. Lower-stakes than an incident — used to build a picture of a participant's daily wellbeing over time.
|
|
113
|
+
|
|
114
|
+
| Field | Type | Notes |
|
|
115
|
+
|-------|------|-------|
|
|
116
|
+
| `note` | string | **namish field** — the observation, e.g. "Tommy helped set the table today" |
|
|
117
|
+
| `category` | string | `'positive'` / `'concerning'` / `'neutral'` |
|
|
118
|
+
| `occurredAt` | string | ISO 8601 timestamp |
|
|
119
|
+
| `companyId` | string | FK → Company |
|
|
120
|
+
| `participantId` | string | FK → Participant (who this is about) |
|
|
121
|
+
| `siteId` | string | FK → Site |
|
|
122
|
+
| `reportedById` | string | FK → User (observer) |
|
|
123
|
+
| `createdAt` | string | ISO 8601 timestamp — when it was logged |
|
|
124
|
+
|
|
125
|
+
Namish field: `note` (truncated to slug-safe length)
|
|
126
|
+
Example filename: `helped-set-the-table-mom-r7n2x.json`
|
|
127
|
+
Relationships: `companyId` → Company, `participantId` → Participant, `siteId` → Site, `reportedById` → User
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Entity Classification
|
|
132
|
+
|
|
133
|
+
| Entity | Type | Notes |
|
|
134
|
+
|--------|------|-------|
|
|
135
|
+
| Company | System / configuration | Set up once, rarely changes |
|
|
136
|
+
| Site | System / configuration | Set up once per location |
|
|
137
|
+
| User | System / configuration | Managed by admin |
|
|
138
|
+
| Participant | System / configuration | Registered on intake, updated periodically |
|
|
139
|
+
| Incident | Domain / operational | Created when significant events occur |
|
|
140
|
+
| Moment | Domain / operational | Created every shift — high volume |
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Suggested Nav Mapping (for nav-shell recipe)
|
|
145
|
+
|
|
146
|
+
| Nav Item | View Key | Entity | Tier |
|
|
147
|
+
|----------|----------|--------|------|
|
|
148
|
+
| Dashboard | `dashboard` | — (summary counts + recent activity) | primary |
|
|
149
|
+
| Participants | `participants` | Participant | primary |
|
|
150
|
+
| Incidents | `incidents` | Incident | primary |
|
|
151
|
+
| Moments | `moments` | Moment | primary |
|
|
152
|
+
| Sites | `sites` | Site | secondary |
|
|
153
|
+
| Users | `users` | User | secondary |
|
|
154
|
+
| Companies | `companies` | Company | secondary |
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Data Folder Structure
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
data/
|
|
162
|
+
├── companies/
|
|
163
|
+
│ └── sunrise-care-group-sc4f2a.json
|
|
164
|
+
├── sites/
|
|
165
|
+
│ └── thornbury-house-th7k3m.json
|
|
166
|
+
├── users/
|
|
167
|
+
│ └── angela-brown-ab9p2x.json
|
|
168
|
+
├── participants/
|
|
169
|
+
│ └── rosie-fairweather-rf3n8k.json
|
|
170
|
+
├── incidents/
|
|
171
|
+
│ └── distressed-during-morning-routine-inc-k4p9m.json
|
|
172
|
+
└── moments/
|
|
173
|
+
└── helped-set-the-table-mom-r7n2x.json
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## Notes
|
|
179
|
+
|
|
180
|
+
- **Incidents vs Moments**: Both attach to a participant + site + worker. Incidents are formal reportable events with severity and workflow status. Moments are routine shift observations — high-frequency, qualitative, no escalation path.
|
|
181
|
+
- **Participant is the central entity**: Sites, Incidents, and Moments all link to a Participant. When viewing a participant's record, show their site, recent moments, and incident history together.
|
|
182
|
+
- **Company scoping**: Every entity except Company has a `companyId`. In a multi-company setup, always filter by `companyId` to avoid data leakage between orgs.
|
|
183
|
+
- **User roles are an array**: A team leader may also be a support worker. Don't assume a single role per user.
|
|
184
|
+
- **Participant `preferredName`**: Always check and display preferred name if set — this matters for person-centred care.
|
|
185
|
+
- **`baselineDataTier`** drives how much observation data is expected per participant. Tier 3-4 participants require more frequent Moments and stricter Incident review.
|