@vevedh/create-feathers-app 0.1.0-rc.1 → 0.1.0-rc.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +55 -18
  2. package/bin/create-feathers-app.mjs +13 -2
  3. package/lib/scaffold.mjs +178 -14
  4. package/lib/services.mjs +110 -0
  5. package/package.json +1 -1
  6. package/templates/common/dev-all-quasar-ssr.mjs +37 -0
  7. package/templates/daisyui-ssg/gitignore.template +20 -0
  8. package/templates/daisyui-ssg/src/style.css +3 -1
  9. package/templates/daisyui-ssr/gitignore.template +20 -0
  10. package/templates/daisyui-ssr/src/style.css +3 -1
  11. package/templates/quasar-ssr/.env.example +11 -0
  12. package/templates/quasar-ssr/api/server.mjs +45 -0
  13. package/templates/quasar-ssr/api/services/health/health.class.mjs +3 -0
  14. package/templates/quasar-ssr/api/services/health/health.service.mjs +4 -0
  15. package/templates/quasar-ssr/api/services/notes/notes.class.mjs +25 -0
  16. package/templates/quasar-ssr/api/services/notes/notes.service.mjs +10 -0
  17. package/templates/quasar-ssr/gitignore.template +36 -0
  18. package/templates/quasar-ssr/index.html +12 -0
  19. package/templates/quasar-ssr/quasar.config.ts +27 -0
  20. package/templates/quasar-ssr/scripts/edge-proxy.mjs +30 -0
  21. package/templates/quasar-ssr/scripts/edge-routing.mjs +14 -0
  22. package/templates/quasar-ssr/src/App.vue +1 -0
  23. package/templates/quasar-ssr/src/css/app.css +20 -0
  24. package/templates/quasar-ssr/src/pages/IndexPage.vue +146 -0
  25. package/templates/quasar-ssr/src/pages/NotFoundPage.vue +11 -0
  26. package/templates/quasar-ssr/src/router/index.ts +8 -0
  27. package/templates/quasar-ssr/src/router/routes.ts +6 -0
  28. package/templates/quasar-ssr/src/stores/index.ts +4 -0
  29. package/templates/quasar-ssr/src/stores/notes.ts +73 -0
  30. package/templates/quasar-ssr/src-ssr/middlewares/render.ts +15 -0
  31. package/templates/quasar-ssr/src-ssr/server.ts +42 -0
  32. package/templates/quasar-ssr/tsconfig.json +4 -0
  33. package/templates/spa/common/.env.example +1 -0
  34. package/templates/spa/common/api/server.mjs +2 -1
  35. package/templates/spa/common/gitignore.template +20 -0
  36. package/templates/spa/common/src/stores/notes.ts +71 -12
  37. package/templates/spa/daisyui/src/components/NotesPanel.vue +24 -7
  38. package/templates/spa/daisyui/src/style.css +3 -1
  39. package/templates/spa/quasar2/src/components/NotesPanel.vue +24 -6
package/README.md CHANGED
@@ -1,43 +1,80 @@
1
1
  # create-feathers-app
2
2
 
3
- Create local FeathersJS v5 applications with Vue 3, Pinia and Vite. Choose Quasar 2 or Tailwind CSS + daisyUI.
3
+ Générez des applications FeathersJS v5 avec Vue 3, Pinia, Vite, Quasar 2 ou Tailwind CSS + daisyUI.
4
4
 
5
- This package is a separate command-line tool. The runtime integration is published as [`@vevedh/vite-plugin-feathersjs`](https://www.npmjs.com/package/@vevedh/vite-plugin-feathersjs).
5
+ Le CLI est indépendant du runtime [`@vevedh/vite-plugin-feathersjs`](https://www.npmjs.com/package/@vevedh/vite-plugin-feathersjs) (version 0.2.6). La version candidate `0.1.0-rc.4` **n'est pas encore publiée**. Elle ajoute la PWA optionnelle au rendu SPA.
6
6
 
7
7
  ## Usage
8
8
 
9
9
  ```powershell
10
- npm exec --package @vevedh/create-feathers-app -- create-feathers-app portail --ui quasar2 --render spa --adapter koa --transport rest
10
+ # Préversion disponible sur npm : 0.1.0-rc.1
11
+ npm exec --yes --package=@vevedh/create-feathers-app@0.1.0-rc.1 -- create-feathers-app portail --ui daisyui --render spa
12
+
13
+ # R17-r3 candidat, depuis les sources mises en stage :
14
+ node .release/create-feathers-app/bin/create-feathers-app.mjs portail-ssr --ui quasar2 --render ssr
11
15
  ```
12
16
 
13
- After npm publication, `npm create @vevedh/feathers-app@latest portail -- --ui daisyui --render ssr` can be used (depending on npm's argument forwarding).
17
+ | Interface | SPA | SSR | SSG |
18
+ | --- | --- | --- | --- |
19
+ | Quasar 2 | Générable | Générable, validation Windows en cours | Bloqué |
20
+ | daisyUI 5 | Générable | Générable | Générable |
14
21
 
15
- For a local candidate, run `node bin/create-feathers-app.mjs --help` from this package.
22
+ ## PWA optionnelle (SPA uniquement)
16
23
 
17
- | UI | SPA | SSR | SSG |
18
- | --- | --- | --- | --- |
19
- | Quasar 2 | Yes | R16 standalone example only | Unsupported on certified CLI |
20
- | daisyUI 5 | Yes | Yes | Yes |
24
+ Le paramètre `--pwa on` ajoute `vite-plugin-pwa` v2 en dépendance de développement, une icône SVG locale de démonstration et un manifeste Web App. Le service worker, désactivé en développement, est généré au build et conserve uniquement des ressources statiques, jamais les réponses `/api` ou les échanges Socket.IO. Par défaut, `--pwa off` ne change pas les templates existants.
25
+
26
+ ```powershell
27
+ node .release/create-feathers-app/bin/create-feathers-app.mjs portail-spa --ui quasar2 --render spa --pwa on --dry-run
28
+ ```
29
+
30
+ SSR/SSG avec PWA est refusé tant que leur cycle de vie n'est pas certifié. Pour une installation sur les navigateurs ciblés, remplacer l'icône SVG de démonstration par des icônes PNG 192 et 512 pixels, puis contrôler HTTPS, les mises à jour et le mode hors-ligne.
31
+
32
+ ## Ajouter un service Feathers
21
33
 
22
- Generated projects include an independent Feathers REST API (`health`, `notes`), Koa or Express, Pinia, a `.env.example` and a `dev:all` command. Socket.IO is optional for the SPA variants only.
34
+ ```powershell
35
+ cd portail
36
+ node ../chemin/vers/create-feathers-app/bin/create-feathers-app.mjs service add tickets --schema zod --db main
37
+ ```
23
38
 
24
- ## Options
39
+ Le générateur ajoute le module `api/services/tickets` et son inscription dans le registre Feathers. Les schémas disponibles sont `typebox`, `json` et `zod`. Le service mémoire est destiné au laboratoire local, avec CRUD REST et validation des données.
25
40
 
26
- `--name`, `--dir`, `--ui`, `--render`, `--adapter`, `--transport`, `--dry-run`, `--json`, `--help`, `--version`.
41
+ ## Profils d'identité et de bases
27
42
 
28
- ## Windows development
43
+ La création de projet accepte `--auth none|jwt|oauth2|oidc|keycloak`, `--schema typebox|json|zod` et `--db main:memory,audit:mongodb` avec alias nommés. **Les authentifications externes et les connexions persistantes sont des profils de configuration, pas des intégrations opérationnelles.** L'API échoue volontairement au démarrage jusqu'au raccordement et à la certification du fournisseur ou du SGBD.
44
+
45
+ Les projets sans authentification sont exclusivement des démonstrations locales. Ils ne doivent jamais être exposés tels quels sur Internet. Aucun secret, mot de passe ou jeton n'est inclus dans les templates.
46
+
47
+ ## Windows
29
48
 
30
49
  ```powershell
31
50
  corepack pnpm install
32
- corepack pnpm run dev:all
33
51
  corepack pnpm run typecheck
34
52
  corepack pnpm run build
53
+ corepack pnpm run dev:all
35
54
  ```
36
55
 
37
- Generated apps are local demonstrations with an in-memory notes service and no authentication. Do not expose them directly on the Internet.
56
+ SPA et daisyUI SSR/SSG utilisent `http://127.0.0.1:5173`. Quasar SSR utilise `http://127.0.0.1:8080` via un proxy local ; le SSR brut écoute sur 9100.
57
+
58
+ La documentation publique VitePress figure dans `docs/cli` du dépôt et sera déployée sur GitHub Pages après activation du workflow de publication.
38
59
 
39
- The CLI does not generate OAuth/JWT, persistent databases, PWA or Quasar SSR/SSG yet.
60
+ ## API Feathers intégrée à Vite en développement (RC4)
40
61
 
41
- ## Documentation
62
+ Pour les applications SPA, `--dev-api vite` est désormais la valeur par défaut.
63
+ Le plugin public `@vevedh/vite-plugin-feathersjs@0.2.6` est chargé depuis
64
+ `vite.config.ts` et monte les services sur `/api` pendant `vite dev`.
65
+ Il n'y a pas de second serveur REST ni de proxy REST à démarrer.
66
+
67
+ L'option `--dev-api separate` conserve l'ancien comportement à deux processus.
68
+ Pour SSR/SSG, le mode séparé reste obligatoire. Avec `--transport both`,
69
+ Socket.IO reste sur le port privé `127.0.0.1:3031`, accessible depuis la SPA
70
+ via le proxy WebSocket Vite. La configuration de Feathers échoue au chargement
71
+ si les profils d'identité ou bases externes ne sont pas opérationnels.
72
+
73
+ ```powershell
74
+ node .release/create-feathers-app/bin/create-feathers-app.mjs mon-spa --ui quasar2 --render spa --dev-api vite --transport both
75
+ ```
42
76
 
43
- A complete French VitePress guide is included in the companion repository under `docs/cli` (installation, rendering modes, Windows, Feathers services, security and publishing). The documentation site is built and hosted separately from this npm package.
77
+ **Production** : le plugin Vite s'exécute uniquement en développement.
78
+ Le serveur `api/server.mjs` autonome reste disponible et obligatoire pour
79
+ le déploiement ; `vite preview` n'héberge pas l'API.
80
+ Le runtime `0.2.6` n'a pas été modifié.
@@ -3,6 +3,7 @@ import { dirname,resolve } from 'node:path'
3
3
  import { fileURLToPath } from 'node:url'
4
4
  import { readFile } from 'node:fs/promises'
5
5
  import { createGenerator } from '../lib/scaffold.mjs'
6
+ import { runServiceCommand } from '../lib/services.mjs'
6
7
  const packageRoot=resolve(dirname(fileURLToPath(import.meta.url)),'..')
7
8
  const {version}=JSON.parse(await readFile(resolve(packageRoot,'package.json'),'utf8'))
8
9
  const args=process.argv.slice(2)
@@ -16,13 +17,23 @@ Usage: create-feathers-app <name> [options]
16
17
  --render <spa|ssr|ssg> Default spa
17
18
  --adapter <koa|express> Default koa
18
19
  --transport <rest|both> Default rest (both only for SPA)
20
+ --pwa <on|off> Optional offline SPA shell (default off, no API caching)
21
+ --dev-api <vite|separate> SPA default vite; SSR/SSG always separate
19
22
  --dry-run Preview files without writing
20
23
  --json Machine-readable output
24
+ --schema <typebox|json|zod> Service schema preset (default typebox)
25
+ --auth <none|jwt|oauth2|oidc|keycloak> Identity configuration blueprint
26
+ --db <alias:adapter[,alias:adapter]> Named connections (default main:memory)
21
27
  --version Print CLI version
22
28
 
23
- Available: Quasar 2 SPA; daisyUI SPA, SSR, SSG.
24
- Quasar SSR: certified example in the companion repository; Quasar SSG not supported.`)
29
+ Service: create-feathers-app service add <name> [--project <path>] [--schema typebox|json|zod] [--db <alias>] [--auth none|jwt] [--dry-run]
30
+
31
+ Available: Quasar 2 SPA/SSR; daisyUI SPA, SSR, SSG.
32
+ Quasar SSR uses the R16-certified architecture; R17-generated SSR requires its own Windows E2E certification. Quasar SSG not supported.`)
25
33
  }else if(args.includes('--version')||args.includes('-v'))console.log(version)
34
+ else if(args[0]==='service') {
35
+ try { await runServiceCommand(args.slice(1),process.cwd()) } catch(error){console.error(`FAIL: ${error.message}`);process.exitCode=1}
36
+ }
26
37
  else {
27
38
  const {parse,generate}=createGenerator({outputRoot:process.cwd(),templatesRoot:resolve(packageRoot,'templates'),reportRoot:process.cwd()})
28
39
  try {
package/lib/scaffold.mjs CHANGED
@@ -3,9 +3,10 @@ import { existsSync } from 'node:fs'
3
3
  import { dirname, join, relative, resolve, sep } from 'node:path'
4
4
  import { buildProjectPlan } from './planner/plan.mjs'
5
5
 
6
- const KEYS = new Set(['--name', '--dir', '--ui', '--render', '--adapter', '--transport', '--dry-run', '--json'])
6
+ const KEYS = new Set(['--name', '--dir', '--ui', '--render', '--adapter', '--transport', '--dry-run', '--json', '--schema', '--auth', '--db', '--pwa', '--dev-api'])
7
7
  const CASES = Object.freeze({
8
8
  'quasar2:spa': 'quasar-spa',
9
+ 'quasar2:ssr': 'quasar-ssr',
9
10
  'daisyui:spa': 'daisyui-spa',
10
11
  'daisyui:ssr': 'daisyui-ssr',
11
12
  'daisyui:ssg': 'daisyui-ssg',
@@ -50,6 +51,7 @@ function spaManifest(o) {
50
51
  vite:'^7.3.7', '@vitejs/plugin-vue':'^6.0.0', 'vue-tsc':'^3.0.7',
51
52
  typescript:'^5.9.3', '@types/node':'^22.0.0',
52
53
  }
54
+ if (o.pwa === 'on') devDependencies['vite-plugin-pwa'] = '^2.0.0'
53
55
  if (o.ui === 'quasar2') { dependencies.quasar = '^2.35.2'; devDependencies['@quasar/vite-plugin']='^1.12.0';devDependencies['sass-embedded']='^1.93.2' }
54
56
  else { devDependencies.tailwindcss='^4.1.0';devDependencies['@tailwindcss/vite']='^4.1.0';devDependencies.daisyui='^5.0.0' }
55
57
  return {name:o.name,version:'0.1.0',private:true,type:'module',packageManager:'pnpm@10.30.2',engines:{node:'>=22.12'},
@@ -60,19 +62,99 @@ function spaManifest(o) {
60
62
  function generatedReadme(o) {
61
63
  const renderInfo = o.render === 'ssg' ? 'Le build pré-rend les routes publiques / et /about. Les notes restent dynamiques via /api.'
62
64
  : o.render === 'ssr' ? 'Le serveur Node effectue le SSR avec un état Pinia distinct par requête.'
63
- : 'Le frontend est une SPA Vite. L’API Feathers s’exécute séparément.'
65
+ : o.devApi === 'vite'
66
+ ? 'En développement, le plugin Vite monte les services Feathers directement sur /api. En production, l’API doit être lancée et déployée séparément.'
67
+ : 'Le frontend est une SPA Vite. En développement, une API Feathers indépendante écoute sur 127.0.0.1:3030.'
64
68
  return `# ${o.name}\n\nProjet ${o.ui} ${o.render.toUpperCase()} avec Vue 3, Pinia et FeathersJS v5 (${o.adapter}, ${o.transport}).\n\n`+
65
69
  `${renderInfo}\n\n## Démarrage local (Windows / PowerShell)\n\n`+
66
70
  '```powershell\ncorepack pnpm install\ncorepack pnpm run dev:all\n```\n\n' +
67
- 'Ouvrir http://127.0.0.1:5173. L’API utilise http://127.0.0.1:3030.\n\n' +
71
+ (o.devApi === 'vite' ? 'Ouvrir http://127.0.0.1:5173. Pendant le développement, Feathers REST est servi sur le même port via /api.\n\n' : 'Ouvrir http://127.0.0.1:5173. L’API indépendante utilise http://127.0.0.1:3030.\n\n') +
68
72
  '```powershell\ncorepack pnpm run typecheck\ncorepack pnpm run build\n```\n\n' +
69
73
  (o.render !== 'spa' ? 'Pour vérifier le build, lancer `corepack pnpm run serve:production` avec l’API active.\n\n' : '') +
70
- 'Les données sont en mémoire, sans authentification. Cet exemple est réservé au laboratoire local et ne doit pas être exposé sur Internet.\n'
74
+ (o.devApi === 'vite' ? 'Le plugin Vite est actif en développement uniquement. `pnpm run preview` sert les fichiers statiques et ne démarre pas l’API. Pour tester la SPA construite avec CRUD, lancer `corepack pnpm run dev:api` dans un premier terminal puis `corepack pnpm run preview` dans un second. Le proxy `preview.proxy` relaie /api vers 127.0.0.1:3030 (et Socket.IO vers 3031 si activé). En l’absence de backend, l’interface affiche « API indisponible » ; hors ligne, seule la coque statique reste disponible. Ne jamais exposer le serveur de démonstration sans authentification.\n\n' : '') +
75
+ `Profil: schema ${o.schema}; auth ${o.auth}; databases ${o.db}. Les profils d'identité et les bases externes nécessitent un raccordement explicite.\n\n`+
76
+ (o.ui==='quasar2' && o.render==='ssr'?'Quasar SSR : ouvrir http://127.0.0.1:8080 (reverse proxy local). Ne pas exposer le serveur SSR directement.\n\n':'')+
77
+ (o.pwa === 'on' ? 'PWA SPA activée : le service worker n’est produit qu’au build. Il met en cache les ressources statiques uniquement, jamais les réponses API ou Socket.IO.\nL’icône SVG d’exemple doit être remplacée par des icônes PNG 192/512 adaptées avant une distribution publique. En HTTPS, tester les mises à jour et l’installation sur le navigateur cible.\n\n' : '') +
78
+ 'Les données de démonstration sont en mémoire ; sans authentification les services ne doivent pas être exposés publiquement.\n'
71
79
  }
80
+ /** Replace the SPA proxy with the public Vite plugin. The standalone API remains for production. */
81
+ function enableSpaViteRuntime(files, options) {
82
+ const source = files.get('vite.config.ts')
83
+ const marker = 'plugins: [vue(),'
84
+ const oldProxy = ",\n proxy: { '/api': 'http://127.0.0.1:3030', '/socket.io': { target: 'ws://127.0.0.1:3031', ws: true } }"
85
+ if (typeof source !== 'string' || !source.includes(marker) || !source.includes(oldProxy)) {
86
+ throw new Error('Unsupported SPA Vite template for embedded Feathers; refusing partial generation')
87
+ }
88
+ const realtime = options.transport === 'both'
89
+ const socket = realtime
90
+ ? "{ port: 3031, host: '127.0.0.1', origins: ['http://127.0.0.1:5173'] }"
91
+ : 'false'
92
+ const runtime = `feathers({
93
+ serviceFiles: 'api/services/*/*.service.mjs',
94
+ watchDirectories: ['api/services'],
95
+ adapter: ${JSON.stringify(options.adapter)},
96
+ prefix: '/api',
97
+ cors: false,
98
+ socketio: ${socket},
99
+ })`
100
+ // 'vite preview' serves static assets: the Feathers Vite plugin is dev-only.
101
+ // Proxy API traffic to the separately started, loopback-bound demo API.
102
+ const previewProxy = `preview: { host: '127.0.0.1', port: 4173, strictPort: true,
103
+ proxy: { '/api': 'http://127.0.0.1:3030',${realtime ? " '/socket.io': { target: 'ws://127.0.0.1:3031', ws: true }" : ''} } },`
104
+ const socketProxy = realtime
105
+ ? ",\n proxy: { '/socket.io': { target: 'ws://127.0.0.1:3031', ws: true } }"
106
+ : ''
107
+ files.set('vite.config.ts',
108
+ `import feathers from '@vevedh/vite-plugin-feathersjs'\n` +
109
+ `import { assertCapabilities } from './api/config/capabilities.mjs'\n` +
110
+ source.replace(marker, `${marker} ${runtime},`).replace(oldProxy, socketProxy)
111
+ .replace('export default defineConfig({',
112
+ '// Do not silently start an unprotected or unconfigured identity/database service.\nassertCapabilities()\nexport default defineConfig({\n' +
113
+ ` ${previewProxy}\n` +
114
+ ` define: { 'import.meta.env.VITE_FEATHERS_REALTIME': '${JSON.stringify(realtime ? '1' : '0')}' },`))
115
+ }
116
+
117
+ /** PWA is opt-in, SPA-only, and must never cache API or realtime responses. */
118
+ function enableSpaPwa(files, options) {
119
+ const config = files.get('vite.config.ts')
120
+ const marker = 'plugins: [vue(),'
121
+ if (typeof config !== 'string' || !config.includes(marker) || config.includes('vite-plugin-pwa')) {
122
+ throw new Error('Unsupported Vite SPA template for PWA; refusing partial generation')
123
+ }
124
+ const pwa = `VitePWA({
125
+ registerType: 'prompt',
126
+ injectRegister: 'auto',
127
+ devOptions: { enabled: false },
128
+ includeAssets: ['pwa-icon.svg'],
129
+ manifest: {
130
+ name: ${JSON.stringify(options.name)},
131
+ short_name: ${JSON.stringify(options.name)},
132
+ description: 'Application Feathers et Vue',
133
+ lang: 'fr',
134
+ start_url: '/',
135
+ scope: '/',
136
+ display: 'standalone',
137
+ background_color: '#ffffff',
138
+ theme_color: '#ffffff',
139
+ icons: [{ src: '/pwa-icon.svg', sizes: 'any', type: 'image/svg+xml', purpose: 'any' }],
140
+ },
141
+ workbox: {
142
+ globPatterns: ['**/*.{js,css,html,svg}'],
143
+ globIgnores: ['**/api/**', '**/socket.io/**'],
144
+ navigateFallback: '/index.html',
145
+ navigateFallbackDenylist: [/^\\/api(?:\\/|$)/, /^\\/socket\\.io(?:\\/|$)/],
146
+ runtimeCaching: [],
147
+ },
148
+ })`
149
+ files.set('vite.config.ts', `import { VitePWA } from 'vite-plugin-pwa'\n${config.replace(marker, `${marker} ${pwa},`)}`)
150
+ if (files.has('public/pwa-icon.svg')) throw new Error('PWA icon conflict in source template')
151
+ files.set('public/pwa-icon.svg', '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 192 192" role="img" aria-label="Feathers app"><rect width="192" height="192" rx="40" fill="#1e293b"/><path d="M44 96l40 40 64-80" fill="none" stroke="#fff" stroke-width="14" stroke-linecap="round" stroke-linejoin="round"/></svg>\n')
152
+ }
153
+
72
154
  export function createGenerator({outputRoot, templatesRoot, examplesRoot = templatesRoot, reportRoot = outputRoot}) {
73
155
  const output = resolve(outputRoot), templates = resolve(templatesRoot), report = resolve(reportRoot)
74
156
  function parse(args) {
75
- const o={ui:'daisyui',render:'spa',adapter:'koa',transport:'rest',dryRun:false,json:false}
157
+ const o={ui:'daisyui',render:'spa',adapter:'koa',transport:'rest',schema:'typebox',auth:'none',db:'main:memory',pwa:'off',devApi:undefined,dryRun:false,json:false}
76
158
  const seen=new Set()
77
159
  if (args.length && !args[0].startsWith('-')) {o.name=args[0];args=args.slice(1);if(args.includes('--name'))throw new Error('Use either positional name or --name, not both')}
78
160
  for(let i=0;i<args.length;i++) {
@@ -80,20 +162,30 @@ export function createGenerator({outputRoot, templatesRoot, examplesRoot = templ
80
162
  if(!KEYS.has(flag)||seen.has(flag))throw new Error(`Unknown or duplicate option: ${flag}`)
81
163
  seen.add(flag)
82
164
  if(flag==='--dry-run'||flag==='--json') o[flag==='--dry-run'?'dryRun':'json']=true
83
- else { const value=args[++i];if(!value||value.startsWith('--'))throw new Error(`Missing ${flag}`);o[flag.slice(2)]=value }
165
+ else { const value=args[++i];if(!value||value.startsWith('--'))throw new Error(`Missing ${flag}`);o[flag==='--dev-api'?'devApi':flag.slice(2)]=value }
84
166
  }
85
167
  if(!safeProjectName(o.name))throw new Error('Invalid or missing --name (lowercase kebab-case, max 48 characters)')
168
+ o.devApi ??= o.render === 'spa' ? 'vite' : 'separate'
169
+ if (!['vite','separate'].includes(o.devApi)) throw new Error('Invalid --dev-api: expected vite|separate')
170
+ if (o.devApi === 'vite' && o.render !== 'spa') throw new Error('--dev-api vite is supported for SPA only; SSR/SSG require separate API')
86
171
  if(!['koa','express'].includes(o.adapter))throw new Error('Only koa|express adapters are supported')
87
172
  if(!['rest','both'].includes(o.transport))throw new Error('Only rest|both are supported')
173
+ if (!['on','off'].includes(o.pwa)) throw new Error('Invalid --pwa: expected on|off')
174
+ if (o.pwa==='on' && o.render!=='spa') throw new Error('PWA is supported for SPA only; SSR/SSG require separate certification')
175
+ if (!['typebox','json','zod'].includes(o.schema)) throw new Error('Invalid --schema: expected typebox|json|zod')
176
+ if (!['none','jwt','oauth2','oidc','keycloak'].includes(o.auth)) throw new Error('Invalid --auth: expected none|jwt|oauth2|oidc|keycloak')
177
+ if (!/^[a-z][a-z0-9-]{0,31}:(?:memory|mongodb|postgresql|mariadb|mysql|sqlite)(?:,[a-z][a-z0-9-]{0,31}:(?:memory|mongodb|postgresql|mariadb|mysql|sqlite))*$/.test(o.db)) throw new Error('Invalid --db: use alias:adapter[,alias:adapter]')
178
+ const dbPairs=o.db.split(',').map(x=>{const [alias,adapter]=x.split(':');return {alias,adapter}})
179
+ if(new Set(dbPairs.map(x=>x.alias)).size!==dbPairs.length)throw new Error('Duplicate database alias')
88
180
  // Reuse the actual R15 planner as the capability validator, not a separate feature catalogue.
89
181
  const plan=buildProjectPlan({name:o.name,ui:o.ui,render:o.render,transport:o.transport,
90
- engine:'auto',auth:'none',db:'main:memory',pwa:'off',schema:'typebox'})
91
- if(!CASES[`${o.ui}:${o.render}`])throw new Error('Unsupported combination: Quasar2 supports SPA; daisyui supports SPA, SSR, SSG')
182
+ engine:'auto',auth:o.auth==='oauth2'?'jwt,oauth':o.auth==='oidc'||o.auth==='keycloak'?'jwt,oidc':o.auth,db:o.db,primary:dbPairs[0].alias,pwa:o.pwa,schema:o.schema})
183
+ if(!CASES[`${o.ui}:${o.render}`])throw new Error('Unsupported combination: Quasar2 supports SPA/SSR; daisyui supports SPA/SSR/SSG')
92
184
  if(o.render!=='spa' && o.transport!=='rest')throw new Error('SSR/SSG supports REST only; Socket.IO proxy not certified')
93
185
  if(o.dir!==undefined && !safeProjectName(o.dir))throw new Error('--dir must be a single safe directory name')
94
186
  o.destination=resolve(output,o.dir||o.name)
95
187
  if(dirname(o.destination)!==output)throw new Error('Destination escapes output root')
96
- o.case=CASES[`${o.ui}:${o.render}`];o.plan=plan
188
+ o.case=CASES[`${o.ui}:${o.render}`];o.plan=plan;o.databases=dbPairs
97
189
  return o
98
190
  }
99
191
  async function generate(o) {
@@ -103,22 +195,66 @@ export function createGenerator({outputRoot, templatesRoot, examplesRoot = templ
103
195
  const files=new Map(o.render==='spa' ? [
104
196
  ...await walk(join(templates,'spa','common')),
105
197
  ...await walk(join(templates,'spa',o.ui)),
106
- ] : await walk(join(resolve(examplesRoot),`daisyui-${o.render}`)))
198
+ ] : await walk(join(resolve(examplesRoot),`${o.ui==='quasar2'?'quasar':'daisyui'}-${o.render}`)))
107
199
  // npm pack excludes files literally named .gitignore, even in subdirectories.
108
200
  // The published templates use gitignore.template, rewritten here to real .gitignore.
109
201
  if(files.has('gitignore.template')){files.set('.gitignore',files.get('gitignore.template'));files.delete('gitignore.template')}
202
+ if(files.has('.gitignore')) {
203
+ const ignores=files.get('.gitignore')
204
+ const additions=['.agents/','AGENTS.md','SKILLS.md','*.prompt.md','.ai/','.codex/','.cursor/','.claude/','.gemini/','.windsurf/','.aider*','.mcp.json','.vscode/mcp.json','.github/copilot-instructions.md','docs-private/','.npmrc','.env*','!.env.example']
205
+ files.set('.gitignore',ignores.trimEnd()+'\n\n# Local editor tooling and private maintenance files\n'+additions.filter(x=>!ignores.split(/\r?\n/).includes(x)).join('\n')+'\n')
206
+ }
110
207
  if(!files.has('api/server.mjs')||!files.has('.env.example')||!files.has('index.html'))throw new Error('Incomplete source template')
111
208
  files.set('.env.example',envChange(files.get('.env.example'),o))
209
+ if (o.devApi === 'vite') {
210
+ enableSpaViteRuntime(files, o)
211
+ files.set('api/config/capabilities.d.mts', 'export declare function assertCapabilities(): void\n')
212
+ }
213
+ if (o.pwa === 'on') enableSpaPwa(files,o)
214
+ if(o.ui==='quasar2' && o.render==='ssr') {
215
+ // R16 HTML has no Vite SPA identity marker; add one without touching frozen R16 sources.
216
+ const html=files.get('index.html')
217
+ files.set('index.html',html.replace('<meta charset="utf-8">','<meta charset="utf-8">\n <meta name="r17-example" content="quasar-ssr">\n <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns=\'http://www.w3.org/2000/svg\'/%3E">'))
218
+ }
112
219
  files.set('index.html',files.get('index.html').replace('__R17_CASE__',o.case).replace(/(<meta name="r17-example" content=")[^"]+("\s*\/?>)/,`$1${o.case}$2`))
113
- const manifest=o.render==='spa'?spaManifest(o):JSON.parse(files.get('package.json'))
220
+ const manifest=o.render==='spa'?spaManifest(o):o.ui==='quasar2'?quasarSSRManifest(o):JSON.parse(files.get('package.json'))
114
221
  manifest.name=o.name;manifest.private=true
115
- manifest.scripts={...manifest.scripts,'dev:all':'node scripts/dev-all.mjs'}
222
+ manifest.scripts={...manifest.scripts,'dev:all':o.devApi==='vite'?'vite --host 127.0.0.1':'node scripts/dev-all.mjs'}
116
223
  files.set('package.json',JSON.stringify(manifest,null,2)+'\n')
117
224
  files.set('README.md',generatedReadme(o))
118
- files.set('scripts/dev-all.mjs',await readFile(join(templates,'common','dev-all.mjs'),'utf8'))
225
+ // Keep the visible application description consistent with the actual dev API mode.
226
+ // The standalone production API is intentionally unaffected.
227
+ if (o.render === 'spa' && o.devApi === 'vite') {
228
+ const app = files.get('src/App.vue')
229
+ const separateDescription = 'Le service Feathers est exécuté dans un processus séparé.'
230
+ if (typeof app !== 'string' || !app.includes(separateDescription)) {
231
+ throw new Error('SPA description contract changed: refusing inconsistent Vite-native scaffold')
232
+ }
233
+ files.set('src/App.vue', app.replace(separateDescription,
234
+ 'En développement, les services Feathers sont intégrés directement à Vite.'))
235
+ }
236
+ if (o.devApi !== 'vite') files.set('scripts/dev-all.mjs',await readFile(join(templates,'common',o.ui==='quasar2'&&o.render==='ssr'?'dev-all-quasar-ssr.mjs':'dev-all.mjs'),'utf8'))
237
+ // A configuration contract, not a simulated authentication or persistence layer.
238
+ // Non-memory databases and external identity providers fail closed at startup
239
+ // until explicitly integrated and tested with a real provider.
240
+ files.set('api/config/capabilities.mjs',capabilityGuard(o))
241
+ const server=files.get('api/server.mjs').replace("from 'vite-plugin-feathersjs'","from '@vevedh/vite-plugin-feathersjs'")
242
+ files.set('api/server.mjs',"import { assertCapabilities } from './config/capabilities.mjs'\nassertCapabilities()\n" + server)
243
+ files.set('api/config/README.md',`# Configuration des capacités\n\nProfil sélectionné : schéma ${o.schema}, identité ${o.auth}, connexions ${o.db}.\n\nLes services en mémoire et sans authentification sont destinés au laboratoire local. Les bases externes nécessitent une instance réelle, un client Feathers/Knex ou MongoDB, ses migrations et la vérification de son cycle de vie.\n\nL'authentification JWT exige un service utilisateurs, un secret hors Git, une stratégie JWT active, des contrôles d'accès et des tests. OAuth2, OIDC et Keycloak exigent en plus un fournisseur HTTPS, la vérification de l'issuer, audience, JWKS, PKCE et le mapping des identités. Ne pas exposer l'API avant validation.\n\nLa commande \`service add tickets --schema zod --db main\` crée un service et l'enregistre dans l'API.\n`)
244
+ files.set('api/config/profile.json',JSON.stringify({schema:o.schema,auth:o.auth,databases:o.databases,primary:o.databases[0].alias,operational:o.auth==='none' && o.databases.every(db=>db.adapter==='memory')},null,2)+'\n')
245
+ const depMap={typebox:['@feathersjs/typebox','ajv'],json:['ajv'],zod:['zod']}
246
+ // Dependencies for the service subcommand; project options alone create no arbitrary services.
247
+ for(const dep of depMap[o.schema])manifest.dependencies[dep] ||= dep==='ajv'?'^8.17.1':dep==='zod'?'^4.1.0':'^5.0.50'
248
+ if(o.auth==='jwt')manifest.dependencies['@feathersjs/authentication']='^5.0.50'
249
+ if(o.auth==='oauth2')manifest.dependencies['@feathersjs/authentication-oauth']='^5.0.50'
250
+ for(const db of o.databases) {
251
+ const drivers={mongodb:['@feathersjs/mongodb','mongodb'],postgresql:['@feathersjs/knex','knex','pg'],mariadb:['@feathersjs/knex','knex','mysql2'],mysql:['@feathersjs/knex','knex','mysql2'],sqlite:['@feathersjs/knex','knex','better-sqlite3']}
252
+ for(const dep of drivers[db.adapter]||[])manifest.dependencies[dep]||='*'
253
+ }
254
+ files.set('package.json',JSON.stringify(manifest,null,2)+'\n')
119
255
  const list=[...files.keys()].sort()
120
256
  const result={name:o.name,ui:o.ui,render:o.render,adapter:o.adapter,transport:o.transport,
121
- destination:relative(report,o.destination).split(sep).join('/'),files:list}
257
+ destination:relative(report,o.destination).split(sep).join('/'),schema:o.schema,auth:o.auth,pwa:o.pwa,devApi:o.devApi,databases:o.databases,files:list}
122
258
  if(o.dryRun)return result
123
259
  // Do not follow symlinks and refuse any existing destination, including empty ones.
124
260
  await mkdir(output,{recursive:true})
@@ -134,3 +270,31 @@ export function createGenerator({outputRoot, templatesRoot, examplesRoot = templ
134
270
  }
135
271
  return {parse,generate}
136
272
  }
273
+
274
+ function quasarSSRManifest(o){
275
+ return {name:o.name,version:'0.1.0',private:true,type:'module',packageManager:'pnpm@10.30.2',engines:{node:'>=22.12'},
276
+ scripts:{'dev:ssr':'quasar dev -m ssr','build:ssr':'quasar build -m ssr',build:'quasar build -m ssr',
277
+ typecheck:'vue-tsc --noEmit', 'dev:api':'node --env-file=.env.example api/server.mjs',
278
+ 'serve:production':'node dist/ssr/index.js',postinstall:'quasar prepare', 'dev:edge':'node scripts/edge-proxy.mjs'},
279
+ dependencies:{'@vevedh/vite-plugin-feathersjs':'0.2.6', '@feathersjs/feathers':'^5.0.50', '@feathersjs/koa':'^5.0.50',
280
+ '@feathersjs/express':'^5.0.50', '@feathersjs/errors':'^5.0.50', '@feathersjs/socketio':'^5.0.50',
281
+ '@feathersjs/rest-client':'^5.0.50', '@feathersjs/socketio-client':'^5.0.50',
282
+ '@quasar/extras':'^1.18.0',pinia:'^3.0.4',quasar:'^2.35.2','socket.io-client':'^4.8.4',
283
+ vue:'^3.5.44','vue-router':'^4.6.4',express:'^4.22.3','serialize-javascript':'^7.1.4'},
284
+ devDependencies:{'@quasar/app-vite':'2.6.2','@types/node':'^22.0.0',typescript:'^5.9.3','vue-tsc':'^3.3.14',
285
+ // Quasar app-vite 2.6.2 runs Express v4. Keep its ambient types in
286
+ // the same major as Express runtime, including nested pnpm dependencies.
287
+ '@types/express':'4.17.25','@types/express-serve-static-core':'4.19.9',vite:'^7.3.7'},
288
+ pnpm:{overrides:{'@types/express':'4.17.25','@types/express-serve-static-core':'4.19.9'}}}
289
+ }
290
+ function capabilityGuard(o) {
291
+ const external=o.databases.filter(x=>x.adapter!=='memory')
292
+ const desc=JSON.stringify({auth:o.auth,databases:o.databases})
293
+ return `// Generated capability contract: never silently downgrade identity or persistence.\n`+
294
+ `export const profile=Object.freeze(${desc})\n`+
295
+ `export function assertCapabilities(){\n`+
296
+ ` if (profile.auth !== 'none') throw new Error('Authentication '+profile.auth+' is a configuration blueprint; identity provider and JWT validation must be integrated and certified before serving requests')\n`+
297
+ ` const missing=profile.databases.filter(item=>item.adapter!=='memory')\n`+
298
+ ` if(missing.length)throw new Error('Database adapters not provisioned: '+missing.map(x=>x.alias+':'+x.adapter).join(', ')+'. Configure named adapters before serving requests')\n`+
299
+ `}\n`
300
+ }
@@ -0,0 +1,110 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { lstat, mkdir, readFile, writeFile } from 'node:fs/promises'
3
+ import { dirname, join, resolve, sep } from 'node:path'
4
+
5
+ const VALID_NAME = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/
6
+ const VALID_SCHEMA = new Set(['typebox','json','zod'])
7
+ const VALID_AUTH = new Set(['none','jwt'])
8
+ function safeName(s){return typeof s==='string' && s.length<=48 && VALID_NAME.test(s) && !/^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])$/i.test(s)}
9
+ async function rejectLinks(path){
10
+ let current=path
11
+ while(true){if(existsSync(current)&&(await lstat(current)).isSymbolicLink())throw new Error('Symlink path refused');const next=dirname(current);if(next===current)break;current=next}
12
+ }
13
+ export function parseServiceArguments(args){
14
+ if(args[0]!=='add')throw new Error('Use: service add <name> [--project <dir>] [--schema typebox|json|zod] [--db <alias>] [--auth none|jwt] [--dry-run]')
15
+ if(!safeName(args[1]))throw new Error('Invalid service name (lowercase kebab-case)')
16
+ const options={name:args[1],project:'.',schema:undefined,db:undefined,auth:undefined,dryRun:false}
17
+ const keys=new Map([['--project','project'],['--schema','schema'],['--db','db'],['--auth','auth']])
18
+ const seen=new Set()
19
+ for(let i=2;i<args.length;i++){
20
+ const flag=args[i]
21
+ if(seen.has(flag))throw new Error('Duplicate option: '+flag)
22
+ seen.add(flag)
23
+ if(flag==='--dry-run'){options.dryRun=true;continue}
24
+ if(!keys.has(flag))throw new Error('Unknown service option: '+flag)
25
+ const value=args[++i]
26
+ if(!value||value.startsWith('--'))throw new Error('Missing service option: '+flag)
27
+ options[keys.get(flag)]=value
28
+ }
29
+ if(!VALID_SCHEMA.has(options.schema??'typebox'))throw new Error('Invalid schema')
30
+ if(!VALID_AUTH.has(options.auth??'none'))throw new Error('Invalid service auth: none|jwt')
31
+ if(options.db!==undefined&&!safeName(options.db))throw new Error('Invalid database alias')
32
+ if(!/^\.?\/?[a-zA-Z0-9._/-]*$/.test(options.project)||options.project.includes('..')||options.project.includes('\\')||options.project.startsWith('/'))throw new Error('Unsafe project path; use a local relative directory')
33
+ return options
34
+ }
35
+ function schemaSource(schema){
36
+ const opening="import { BadRequest } from '@feathersjs/errors'\n"
37
+ if(schema==='zod')return opening+`import { z } from 'zod'
38
+ export const dataSchema=z.object({title:z.string().trim().min(1).max(120)}).strict()
39
+ export function validateData(value){const parsed=dataSchema.safeParse(value);if(!parsed.success)throw new BadRequest('Invalid service data');return parsed.data}
40
+ `
41
+ const def=schema==='typebox'?`import { Type } from '@feathersjs/typebox'
42
+ export const dataSchema=Type.Object({title:Type.String({minLength:1,maxLength:120})},{additionalProperties:false})
43
+ `:`export const dataSchema={type:'object',additionalProperties:false,required:['title'],properties:{title:{type:'string',minLength:1,maxLength:120}}}
44
+ `
45
+ return opening+`import Ajv from 'ajv'
46
+ `+def+`const valid=new Ajv({allErrors:true}).compile(dataSchema)
47
+ export function validateData(value){if(!valid(value))throw new BadRequest('Invalid service data');return {title:value.title.trim()}}
48
+ `
49
+ }
50
+ function classSource(){return `import { NotFound, BadRequest } from '@feathersjs/errors'
51
+ import { validateData } from './schema.mjs'
52
+ export class GeneratedService {
53
+ #items=new Map()
54
+ #counter=1
55
+ constructor(){for(const method of ['find','get','create','patch','remove'])this[method]=this[method].bind(this)}
56
+ #id(value){const n=Number(value);if(!Number.isSafeInteger(n)||n<1)throw new BadRequest('Invalid ID');return n}
57
+ async find(){return [...this.#items.values()].map(x=>({...x}))}
58
+ async get(id){const item=this.#items.get(this.#id(id));if(!item)throw new NotFound('Not found');return {...item}}
59
+ async create(data){const row={id:this.#counter++,...validateData(data)};this.#items.set(row.id,row);return {...row}}
60
+ async patch(id,data){const key=this.#id(id);if(!this.#items.has(key))throw new NotFound('Not found');const row={id:key,...validateData(data)};this.#items.set(key,row);return {...row}}
61
+ async remove(id){const key=this.#id(id);const row=this.#items.get(key);if(!row)throw new NotFound('Not found');this.#items.delete(key);return {...row}}
62
+ }
63
+ `}
64
+ function registrar(name,auth){
65
+ return `import { GeneratedService } from './service.class.mjs'
66
+ ${auth==='jwt'?"import { authenticate } from '@feathersjs/authentication'\n":''}export default function register(app){
67
+ app.use('${name}',new GeneratedService(),{methods:['find','get','create','patch','remove']})
68
+ ${auth==='jwt'?`app.service('${name}').hooks({before:{all:[authenticate('jwt')]}})`:'// This demonstration service has no authentication; bind HTTP only to loopback.'}
69
+ }
70
+ `
71
+ }
72
+ export async function generateService(opts, cwd=process.cwd()){
73
+ const project=resolve(cwd,opts.project)
74
+ if(opts.project!=='.' && !project.startsWith(resolve(cwd)+sep))throw new Error('Project escapes current directory')
75
+ await rejectLinks(project)
76
+ const manifestPath=join(project,'package.json'),serverPath=join(project,'api/server.mjs'),profilePath=join(project,'api/config/profile.json')
77
+ if(!existsSync(manifestPath)||!existsSync(serverPath)||!existsSync(profilePath))throw new Error('Not a CLI generated project')
78
+ const manifest=JSON.parse(await readFile(manifestPath,'utf8'))
79
+ const profile=JSON.parse(await readFile(profilePath,'utf8'))
80
+ const schema=opts.schema??profile.schema,auth=opts.auth??profile.auth,db=opts.db??profile.primary
81
+ if(!VALID_SCHEMA.has(schema))throw new Error('Invalid schema')
82
+ if(!VALID_AUTH.has(auth))throw new Error('Service supports none|jwt; OAuth2/OIDC/Keycloak require audited identity integration')
83
+ if(auth!=='none' && profile.auth!==auth)throw new Error('Service auth must match configured project profile')
84
+ const connection=profile.databases.find(x=>x.alias===db)
85
+ if(!connection)throw new Error('Unknown database alias: '+db)
86
+ const destination=join(project,'api/services',opts.name)
87
+ await rejectLinks(destination)
88
+ if(existsSync(destination))throw new Error('Service already exists; refusing overwrite')
89
+ const text=await readFile(serverPath,'utf8')
90
+ const marker='const modules = [\n'
91
+ if(!text.includes(marker))throw new Error('Missing Feathers modules registry; no changes made')
92
+ const moduleEntry=` new URL('./services/${opts.name}/${opts.name}.service.mjs', import.meta.url),\n`
93
+ if(text.includes(moduleEntry))throw new Error('Duplicate service registration')
94
+ // Never replace a user-controlled arbitrary string outside of the explicit registry.
95
+ const output={name:opts.name,project,database:{alias:db,adapter:connection.adapter},auth,schema,files:[`api/services/${opts.name}/schema.mjs`,`api/services/${opts.name}/service.class.mjs`,`api/services/${opts.name}/${opts.name}.service.mjs`],ready:connection.adapter==='memory'&&auth==='none'}
96
+ if(opts.dryRun)return output
97
+ const dependencies=manifest.dependencies||{}
98
+ const deps=schema==='zod'?{'zod':'^4.1.0'}:schema==='typebox'?{'@feathersjs/typebox':'^5.0.50','ajv':'^8.17.1'}:{'ajv':'^8.17.1'}
99
+ if(auth==='jwt')deps['@feathersjs/authentication']='^5.0.50'
100
+ for(const [name,version] of Object.entries(deps))dependencies[name]??=version
101
+ await mkdir(destination,{recursive:false})
102
+ for(const [name,contents] of Object.entries({'schema.mjs':schemaSource(schema),'service.class.mjs':classSource(),[`${opts.name}.service.mjs`]:registrar(opts.name,auth)}))await writeFile(join(destination,name),contents,{flag:'wx'})
103
+ await writeFile(serverPath,text.replace(marker,marker+moduleEntry))
104
+ await writeFile(manifestPath,JSON.stringify({...manifest,dependencies},null,2)+'\n')
105
+ return output
106
+ }
107
+ export async function runServiceCommand(args,cwd){
108
+ const result=await generateService(parseServiceArguments(args),cwd)
109
+ console.log(JSON.stringify(result,null,2))
110
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vevedh/create-feathers-app",
3
- "version": "0.1.0-rc.1",
3
+ "version": "0.1.0-rc.4",
4
4
  "type": "module",
5
5
  "description": "Scaffold FeathersJS v5 projects with Quasar 2 or daisyUI, Vite, Vue 3 and Pinia",
6
6
  "license": "MIT",
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from 'node:child_process'
3
+ import { existsSync } from 'node:fs'
4
+ import { loadEnvFile } from 'node:process'
5
+ import { setTimeout as sleep } from 'node:timers/promises'
6
+ const file=existsSync('.env')?'.env':'.env.example'
7
+ if (!existsSync(file))throw new Error('Missing .env.example')
8
+ loadEnvFile(file)
9
+ const env={...process.env}
10
+ const children=[]
11
+ let closed=false
12
+ function stop(code=0){if(closed)return;closed=true;for(const child of children.slice().reverse())if(child.exitCode===null)try{child.kill()}catch{};process.exitCode=code}
13
+ for(const signal of ['SIGINT','SIGTERM'])process.on(signal,()=>stop(signal==='SIGINT'?130:143))
14
+ function start(label,command,args,opts={}) {
15
+ const proc=spawn(command,args,{env,stdio:'inherit',windowsHide:true,...opts})
16
+ children.push(proc)
17
+ proc.on('error',error=>{console.error(label,error.message);stop(1)})
18
+ proc.on('exit',code=>{if(!closed){console.error(label,'stopped',code);stop(code||1)}})
19
+ return proc
20
+ }
21
+ start('Feathers',process.execPath,[`--env-file=${file}`,'api/server.mjs'])
22
+ let ready=false
23
+ for(let attempt=0;attempt<90 && !closed;attempt++){
24
+ try{const res=await fetch('http://127.0.0.1:3030/api/health',{signal:AbortSignal.timeout(650)});if(res.ok){ready=true;break}}catch{}
25
+ await sleep(200)
26
+ }
27
+ if(!ready){console.error('Feathers health unavailable on loopback');stop(1)}
28
+ if(!closed){
29
+ start('Quasar SSR',process.platform==='win32'?'corepack.cmd':'corepack',['pnpm','run','dev:ssr'],{shell:process.platform==='win32'})
30
+ let ssrReady=false
31
+ for(let attempt=0;attempt<150 && !closed;attempt++){
32
+ try{const res=await fetch('http://127.0.0.1:9100/',{signal:AbortSignal.timeout(800)});if(res.ok){ssrReady=true;break}}catch{}
33
+ await sleep(350)
34
+ }
35
+ if(!ssrReady){console.error('Quasar SSR unavailable on loopback :9100');stop(1)}
36
+ else start('Edge',process.execPath,['scripts/edge-proxy.mjs'])
37
+ }
@@ -14,3 +14,23 @@ playwright-report/
14
14
  .mcp.json
15
15
  .vscode/mcp.json
16
16
  docs-private/
17
+
18
+ # Development-only configuration and internal artifacts
19
+ .agents/
20
+ .agent/
21
+ .ai/
22
+ .codex/
23
+ .claude/
24
+ .cursor/
25
+ .gemini/
26
+ .windsurf/
27
+ .ecc/
28
+ .mcp.json
29
+ .vscode/mcp.json
30
+ AGENTS.md
31
+ SKILLS.md
32
+ CLAUDE.md
33
+ GEMINI.md
34
+ *.prompt.md
35
+ docs-private/
36
+ .npmrc
@@ -1,3 +1,5 @@
1
- @import "tailwindcss";
1
+ @import "tailwindcss" source(none);
2
+ @source "./";
3
+ @source "../index.html";
2
4
  @plugin "daisyui";
3
5
  :root { font-family: ui-sans-serif, system-ui, sans-serif }
@@ -14,3 +14,23 @@ playwright-report/
14
14
  .mcp.json
15
15
  .vscode/mcp.json
16
16
  docs-private/
17
+
18
+ # Development-only configuration and internal artifacts
19
+ .agents/
20
+ .agent/
21
+ .ai/
22
+ .codex/
23
+ .claude/
24
+ .cursor/
25
+ .gemini/
26
+ .windsurf/
27
+ .ecc/
28
+ .mcp.json
29
+ .vscode/mcp.json
30
+ AGENTS.md
31
+ SKILLS.md
32
+ CLAUDE.md
33
+ GEMINI.md
34
+ *.prompt.md
35
+ docs-private/
36
+ .npmrc