deepspace 0.4.4 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +90 -0
  2. package/dist/cli.js +2912 -1984
  3. package/dist/cli.js.map +1 -1
  4. package/dist/index.d.ts +31 -4
  5. package/dist/index.js +511 -366
  6. package/dist/index.js.map +1 -1
  7. package/dist/server.d.ts +29 -108
  8. package/dist/server.js +126 -589
  9. package/dist/server.js.map +1 -1
  10. package/dist/testing.js.map +1 -1
  11. package/dist/worker.d.ts +29 -108
  12. package/dist/worker.js +126 -589
  13. package/dist/worker.js.map +1 -1
  14. package/features/admin-page/src/AdminPage.tsx +3 -5
  15. package/features/canvas/src/CanvasEditorPage.tsx +0 -2
  16. package/features/canvas/src/components/CanvasView.tsx +1 -1
  17. package/features/docs/src/LibrarySidebar.tsx +7 -1
  18. package/features/docs/src/use-yjs-room-with-awareness.ts +4 -0
  19. package/features/file-attachments/src/AttachmentsPage.tsx +7 -1
  20. package/features/file-attachments/src/useFilePreview.ts +7 -5
  21. package/features/file-manager/src/FileManagerPage.tsx +2 -3
  22. package/features/integration-test/feature.json +10 -3
  23. package/features/integration-test/src/IntegrationTestPage.tsx +15 -12
  24. package/features/landing/src/LandingPage.tsx +1 -1
  25. package/features/landing/src/sections/FeaturesGridSection.tsx +0 -92
  26. package/features/messaging/src/components/chat/MessageItem.tsx +5 -1
  27. package/features/messaging/src/hooks/useChatChannel.ts +5 -1
  28. package/features/presence-test/feature.json +2 -1
  29. package/features/sidebar/src/AppSidebar.tsx +1 -1
  30. package/features/tasks/src/TasksPage.tsx +1 -1
  31. package/features/testing/feature.json +2 -1
  32. package/features/testing/src/TestPage.tsx +8 -8
  33. package/package.json +4 -4
  34. package/scripts/add-feature.cjs +277 -272
@@ -8,30 +8,32 @@
8
8
  * Invoked by the `deepspace add` CLI in src/cli/commands/add.ts.
9
9
  */
10
10
 
11
- const fs = require('fs');
12
- const path = require('path');
11
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- CommonJS (.cjs) module: require is required
12
+ const fs = require('fs')
13
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- CommonJS (.cjs) module: require is required
14
+ const path = require('path')
13
15
 
14
16
  // ---------------------------------------------------------------------------
15
17
  // Environment-aware features path resolution
16
18
  // ---------------------------------------------------------------------------
17
19
 
18
20
  function findFeaturesDir() {
19
- const candidate = path.resolve(__dirname, '..', 'features');
20
- if (fs.existsSync(candidate)) return candidate;
21
- console.error('Error: Could not find features directory at ' + candidate);
22
- process.exit(1);
21
+ const candidate = path.resolve(__dirname, '..', 'features')
22
+ if (fs.existsSync(candidate)) return candidate
23
+ console.error('Error: Could not find features directory at ' + candidate)
24
+ process.exit(1)
23
25
  }
24
26
 
25
- const FEATURES_DIR = findFeaturesDir();
27
+ const FEATURES_DIR = findFeaturesDir()
26
28
 
27
29
  // ---------------------------------------------------------------------------
28
30
  // Resolve target directory
29
31
  // ---------------------------------------------------------------------------
30
32
 
31
33
  function resolveTargetDir(raw) {
32
- if (!raw) return null;
33
- if (path.isAbsolute(raw)) return raw;
34
- return path.resolve(raw);
34
+ if (!raw) return null
35
+ if (path.isAbsolute(raw)) return raw
36
+ return path.resolve(raw)
35
37
  }
36
38
 
37
39
  // ---------------------------------------------------------------------------
@@ -45,51 +47,53 @@ const CATEGORY_LABELS = {
45
47
  layout: 'Layouts',
46
48
  display: 'Display',
47
49
  landing: 'Landing Page Sections',
48
- };
50
+ }
49
51
 
50
- const CATEGORY_ORDER = ['assistant', 'data', 'nav', 'layout', 'display', 'landing'];
52
+ const CATEGORY_ORDER = ['assistant', 'data', 'nav', 'layout', 'display', 'landing']
51
53
 
52
54
  // ---------------------------------------------------------------------------
53
55
  // Core helpers
54
56
  // ---------------------------------------------------------------------------
55
57
 
56
58
  function loadFeatureConfig(featureId) {
57
- const configPath = path.join(FEATURES_DIR, featureId, 'feature.json');
58
- if (!fs.existsSync(configPath)) return null;
59
+ const configPath = path.join(FEATURES_DIR, featureId, 'feature.json')
60
+ if (!fs.existsSync(configPath)) return null
59
61
  try {
60
- return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
62
+ return JSON.parse(fs.readFileSync(configPath, 'utf-8'))
61
63
  } catch (e) {
62
- console.error(`Error reading ${configPath}:`, e.message);
63
- return null;
64
+ console.error(`Error reading ${configPath}:`, e.message)
65
+ return null
64
66
  }
65
67
  }
66
68
 
67
69
  function listFeatures() {
68
- const entries = fs.readdirSync(FEATURES_DIR, { withFileTypes: true });
69
- const configs = [];
70
+ const entries = fs.readdirSync(FEATURES_DIR, { withFileTypes: true })
71
+ const configs = []
70
72
  for (const e of entries) {
71
73
  if (e.isDirectory() && e.name !== 'scripts') {
72
- const config = loadFeatureConfig(e.name);
73
- if (config) configs.push(config);
74
+ const config = loadFeatureConfig(e.name)
75
+ // Internal features (e2e/QA fixtures) are installable by explicit name
76
+ // but hidden from `--list` and the interactive picker.
77
+ if (config && !config.internal) configs.push(config)
74
78
  }
75
79
  }
76
- return configs;
80
+ return configs
77
81
  }
78
82
 
79
83
  function groupByCategory(features) {
80
- const groups = new Map();
84
+ const groups = new Map()
81
85
  for (const f of features) {
82
- const category = f.category || 'other';
83
- if (!groups.has(category)) groups.set(category, []);
84
- groups.get(category).push(f);
86
+ const category = f.category || 'other'
87
+ if (!groups.has(category)) groups.set(category, [])
88
+ groups.get(category).push(f)
85
89
  }
86
- return groups;
90
+ return groups
87
91
  }
88
92
 
89
93
  function copyFile(src, dest) {
90
- const destDir = path.dirname(dest);
91
- if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
92
- fs.copyFileSync(src, dest);
94
+ const destDir = path.dirname(dest)
95
+ if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true })
96
+ fs.copyFileSync(src, dest)
93
97
  }
94
98
 
95
99
  // Copy + rewrite leading `../` imports. Used when a page file is redirected
@@ -99,11 +103,11 @@ function copyFile(src, dest) {
99
103
  // (e.g. `import('../hooks/x').T` in a type position, or `await import('../x')`).
100
104
  // Same-directory (`./...`) and bare-package imports are untouched.
101
105
  function copyFileWithImportShift(src, dest) {
102
- const destDir = path.dirname(dest);
103
- if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
104
- const content = fs.readFileSync(src, 'utf-8');
105
- const shifted = content.replace(/(from\s+|import\s+|import\()(['"])\.\.\//g, '$1$2../../');
106
- fs.writeFileSync(dest, shifted);
106
+ const destDir = path.dirname(dest)
107
+ if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true })
108
+ const content = fs.readFileSync(src, 'utf-8')
109
+ const shifted = content.replace(/(from\s+|import\s+|import\()(['"])\.\.\//g, '$1$2../../')
110
+ fs.writeFileSync(dest, shifted)
107
111
  }
108
112
 
109
113
  // ---------------------------------------------------------------------------
@@ -116,45 +120,45 @@ function copyFileWithImportShift(src, dest) {
116
120
  // Only page files (src/pages/*) get rewritten; schemas, components, etc.
117
121
  // keep their declared destinations.
118
122
  function resolveDestPath(fileDest, route) {
119
- const PAGES = 'src/pages/';
120
- const PROTECTED = 'src/pages/(protected)/';
121
- const isProtected = route && route.protected !== false;
122
- if (!isProtected) return fileDest;
123
- if (!fileDest.startsWith(PAGES) || fileDest.startsWith(PROTECTED)) return fileDest;
124
- return PROTECTED + fileDest.slice(PAGES.length);
123
+ const PAGES = 'src/pages/'
124
+ const PROTECTED = 'src/pages/(protected)/'
125
+ const isProtected = route && route.protected !== false
126
+ if (!isProtected) return fileDest
127
+ if (!fileDest.startsWith(PAGES) || fileDest.startsWith(PROTECTED)) return fileDest
128
+ return PROTECTED + fileDest.slice(PAGES.length)
125
129
  }
126
130
 
127
131
  function installFeature(config, targetDir) {
128
- let copied = 0;
129
- let skipped = 0;
132
+ let copied = 0
133
+ let skipped = 0
130
134
 
131
135
  for (const file of config.files) {
132
- const srcPath = path.join(FEATURES_DIR, config.id, file.src);
133
- const resolvedDest = resolveDestPath(file.dest, config.route);
134
- const destPath = path.join(targetDir, resolvedDest);
135
- const wasRedirected = resolvedDest !== file.dest;
136
+ const srcPath = path.join(FEATURES_DIR, config.id, file.src)
137
+ const resolvedDest = resolveDestPath(file.dest, config.route)
138
+ const destPath = path.join(targetDir, resolvedDest)
139
+ const wasRedirected = resolvedDest !== file.dest
136
140
 
137
141
  if (!fs.existsSync(srcPath)) {
138
- console.error(` Warning: source not found: ${file.src}`);
139
- continue;
142
+ console.error(` Warning: source not found: ${file.src}`)
143
+ continue
140
144
  }
141
145
 
142
146
  if (fs.existsSync(destPath) && !file.overwrite) {
143
- console.log(` Exists (skipped): ${resolvedDest}`);
144
- skipped++;
147
+ console.log(` Exists (skipped): ${resolvedDest}`)
148
+ skipped++
145
149
  } else {
146
- const replacing = fs.existsSync(destPath);
150
+ const replacing = fs.existsSync(destPath)
147
151
  if (wasRedirected) {
148
- copyFileWithImportShift(srcPath, destPath);
152
+ copyFileWithImportShift(srcPath, destPath)
149
153
  } else {
150
- copyFile(srcPath, destPath);
154
+ copyFile(srcPath, destPath)
151
155
  }
152
- console.log(` ${replacing ? 'Overwrote' : 'Copied'}: ${resolvedDest}`);
153
- copied++;
156
+ console.log(` ${replacing ? 'Overwrote' : 'Copied'}: ${resolvedDest}`)
157
+ copied++
154
158
  }
155
159
  }
156
160
 
157
- return { copied, skipped };
161
+ return { copied, skipped }
158
162
  }
159
163
 
160
164
  // ---------------------------------------------------------------------------
@@ -162,72 +166,72 @@ function installFeature(config, targetDir) {
162
166
  // ---------------------------------------------------------------------------
163
167
 
164
168
  function integrateSchema(config, targetDir) {
165
- if (!config.schema) return false;
169
+ if (!config.schema) return false
166
170
 
167
- const schemasPath = path.join(targetDir, 'src', 'schemas.ts');
171
+ const schemasPath = path.join(targetDir, 'src', 'schemas.ts')
168
172
  if (!fs.existsSync(schemasPath)) {
169
- printSchemaInstructions(config.schema);
170
- return false;
173
+ printSchemaInstructions(config.schema)
174
+ return false
171
175
  }
172
176
 
173
- let content = fs.readFileSync(schemasPath, 'utf-8');
174
- const { exportName, importPath, spreadOperator } = config.schema;
177
+ let content = fs.readFileSync(schemasPath, 'utf-8')
178
+ const { exportName, importPath, spreadOperator } = config.schema
175
179
 
176
180
  // Already integrated?
177
- const importPattern = new RegExp(`^import\\s+\\{[^}]*\\b${exportName}\\b`, 'm');
181
+ const importPattern = new RegExp(`^import\\s+\\{[^}]*\\b${exportName}\\b`, 'm')
178
182
  if (importPattern.test(content)) {
179
- console.log(` Schema already present: ${exportName}`);
180
- return false;
183
+ console.log(` Schema already present: ${exportName}`)
184
+ return false
181
185
  }
182
186
 
183
187
  // Strict checks: verify the file has the expected structure
184
- const hasExportConst = content.includes('export const schemas');
185
- const hasArrayOpening = /export const schemas:\s*CollectionSchema\[\]\s*=\s*\[/.test(content);
188
+ const hasExportConst = content.includes('export const schemas')
189
+ const hasArrayOpening = /export const schemas:\s*CollectionSchema\[\]\s*=\s*\[/.test(content)
186
190
 
187
191
  if (!hasExportConst || !hasArrayOpening) {
188
- console.log(' Could not auto-integrate schema (schemas.ts has unexpected structure)');
189
- printSchemaInstructions(config.schema);
190
- return false;
192
+ console.log(' Could not auto-integrate schema (schemas.ts has unexpected structure)')
193
+ printSchemaInstructions(config.schema)
194
+ return false
191
195
  }
192
196
 
193
197
  // 1. Add import before "export const schemas"
194
- const importLine = `import { ${exportName} } from '${importPath}'`;
198
+ const importLine = `import { ${exportName} } from '${importPath}'`
195
199
  const afterImport = content.replace(
196
200
  /export const schemas/,
197
- `${importLine}\n\nexport const schemas`
198
- );
201
+ `${importLine}\n\nexport const schemas`,
202
+ )
199
203
 
200
204
  if (afterImport === content) {
201
- console.log(' Could not insert import line');
202
- printSchemaInstructions(config.schema);
203
- return false;
205
+ console.log(' Could not insert import line')
206
+ printSchemaInstructions(config.schema)
207
+ return false
204
208
  }
205
209
 
206
210
  // 2. Insert entry into the schemas array
207
- const schemaEntry = spreadOperator ? `...${exportName}` : exportName;
211
+ const schemaEntry = spreadOperator ? `...${exportName}` : exportName
208
212
  const afterEntry = afterImport.replace(
209
213
  /export const schemas:\s*CollectionSchema\[\]\s*=\s*\[/,
210
- `export const schemas: CollectionSchema[] = [\n ${schemaEntry},`
211
- );
214
+ `export const schemas: CollectionSchema[] = [\n ${schemaEntry},`,
215
+ )
212
216
 
213
217
  if (afterEntry === afterImport) {
214
- console.log(' Could not insert schema entry');
215
- printSchemaInstructions(config.schema);
216
- return false;
218
+ console.log(' Could not insert schema entry')
219
+ printSchemaInstructions(config.schema)
220
+ return false
217
221
  }
218
222
 
219
- fs.writeFileSync(schemasPath, afterEntry);
220
- console.log(` Schema integrated: ${exportName} -> schemas.ts`);
221
- return true;
223
+ fs.writeFileSync(schemasPath, afterEntry)
224
+ console.log(` Schema integrated: ${exportName} -> schemas.ts`)
225
+ return true
222
226
  }
223
227
 
224
228
  function printSchemaInstructions(schema) {
225
- const { exportName, importPath, spreadOperator } = schema;
226
- const entry = spreadOperator ? `...${exportName}` : exportName;
227
- console.log('');
228
- console.log(' Add manually to src/schemas.ts:');
229
- console.log(` import { ${exportName} } from '${importPath}'`);
230
- console.log(` // then add ${entry} to the schemas array`);
229
+ const { exportName, importPath, spreadOperator } = schema
230
+ const entry = spreadOperator ? `...${exportName}` : exportName
231
+ console.log('')
232
+ console.log(' Add manually to src/schemas.ts:')
233
+ console.log(` import { ${exportName} } from '${importPath}'`)
234
+ console.log(` // then add ${entry} to the schemas array`)
231
235
  }
232
236
 
233
237
  // ---------------------------------------------------------------------------
@@ -235,76 +239,77 @@ function printSchemaInstructions(schema) {
235
239
  // ---------------------------------------------------------------------------
236
240
 
237
241
  function integrateActions(config, targetDir) {
238
- if (!config.actions || config.actions.length === 0) return false;
242
+ if (!config.actions || config.actions.length === 0) return false
239
243
 
240
- const actionsPath = path.join(targetDir, 'src', 'actions', 'index.ts');
244
+ const actionsPath = path.join(targetDir, 'src', 'actions', 'index.ts')
241
245
  if (!fs.existsSync(actionsPath)) {
242
- printActionsInstructions(config.actions);
243
- return false;
246
+ printActionsInstructions(config.actions)
247
+ return false
244
248
  }
245
249
 
246
- let content = fs.readFileSync(actionsPath, 'utf-8');
250
+ let content = fs.readFileSync(actionsPath, 'utf-8')
247
251
 
248
252
  // Strict check: must match the starter's shape so our regex patch is safe.
249
253
  // Accepts both `ActionHandler` and `ActionHandler<Env>` (typed-env templates).
250
- const hasRegistry = /export const actions:\s*Record<string,\s*ActionHandler(?:<[^>]*>)?>\s*=\s*\{/.test(content);
254
+ const hasRegistry =
255
+ /export const actions:\s*Record<string,\s*ActionHandler(?:<[^>]*>)?>\s*=\s*\{/.test(content)
251
256
  if (!hasRegistry) {
252
- console.log(' Could not auto-integrate actions (actions/index.ts has unexpected structure)');
253
- printActionsInstructions(config.actions);
254
- return false;
257
+ console.log(' Could not auto-integrate actions (actions/index.ts has unexpected structure)')
258
+ printActionsInstructions(config.actions)
259
+ return false
255
260
  }
256
261
 
257
- let integrated = 0;
262
+ let integrated = 0
258
263
  for (const action of config.actions) {
259
- const { exportName, registerAs, importPath } = action;
264
+ const { exportName, registerAs, importPath } = action
260
265
 
261
266
  // Already integrated?
262
- const importPattern = new RegExp(`^import\\s+\\{[^}]*\\b${exportName}\\b`, 'm');
267
+ const importPattern = new RegExp(`^import\\s+\\{[^}]*\\b${exportName}\\b`, 'm')
263
268
  if (importPattern.test(content)) {
264
- console.log(` Action already present: ${registerAs}`);
265
- continue;
269
+ console.log(` Action already present: ${registerAs}`)
270
+ continue
266
271
  }
267
272
 
268
273
  // 1. Add import before the registry export.
269
- const importLine = `import { ${exportName} } from '${importPath}'`;
274
+ const importLine = `import { ${exportName} } from '${importPath}'`
270
275
  const withImport = content.replace(
271
276
  /export const actions:/,
272
277
  `${importLine}\n\nexport const actions:`,
273
- );
278
+ )
274
279
  if (withImport === content) {
275
- console.log(` Could not insert import for ${exportName}`);
276
- continue;
280
+ console.log(` Could not insert import for ${exportName}`)
281
+ continue
277
282
  }
278
283
 
279
284
  // 2. Insert entry into the registry object. Works for both empty and
280
285
  // populated registries because we inject right after the opening brace.
281
- const registryEntry = ` '${registerAs}': ${exportName},`;
286
+ const registryEntry = ` '${registerAs}': ${exportName},`
282
287
  const withEntry = withImport.replace(
283
288
  /export const actions:\s*Record<string,\s*ActionHandler(?:<[^>]*>)?>\s*=\s*\{/,
284
289
  (match) => `${match}\n${registryEntry}`,
285
- );
290
+ )
286
291
  if (withEntry === withImport) {
287
- console.log(` Could not insert registry entry for ${registerAs}`);
288
- continue;
292
+ console.log(` Could not insert registry entry for ${registerAs}`)
293
+ continue
289
294
  }
290
295
 
291
- content = withEntry;
292
- integrated++;
293
- console.log(` Action integrated: ${registerAs} (${exportName})`);
296
+ content = withEntry
297
+ integrated++
298
+ console.log(` Action integrated: ${registerAs} (${exportName})`)
294
299
  }
295
300
 
296
301
  if (integrated > 0) {
297
- fs.writeFileSync(actionsPath, content);
302
+ fs.writeFileSync(actionsPath, content)
298
303
  }
299
- return integrated > 0;
304
+ return integrated > 0
300
305
  }
301
306
 
302
307
  function printActionsInstructions(actions) {
303
- console.log('');
304
- console.log(' Add manually to src/actions/index.ts:');
308
+ console.log('')
309
+ console.log(' Add manually to src/actions/index.ts:')
305
310
  for (const a of actions) {
306
- console.log(` import { ${a.exportName} } from '${a.importPath}'`);
307
- console.log(` // then add '${a.registerAs}': ${a.exportName} to the actions object`);
311
+ console.log(` import { ${a.exportName} } from '${a.importPath}'`)
312
+ console.log(` // then add '${a.registerAs}': ${a.exportName} to the actions object`)
308
313
  }
309
314
  }
310
315
 
@@ -313,98 +318,98 @@ function printActionsInstructions(actions) {
313
318
  // ---------------------------------------------------------------------------
314
319
 
315
320
  function integrateCss(config, targetDir) {
316
- if (!config.css || config.css.length === 0) return false;
321
+ if (!config.css || config.css.length === 0) return false
317
322
 
318
- const stylesPath = path.join(targetDir, 'src', 'styles.css');
323
+ const stylesPath = path.join(targetDir, 'src', 'styles.css')
319
324
  if (!fs.existsSync(stylesPath)) {
320
- console.log(' Warning: Cannot integrate CSS — src/styles.css not found');
321
- return false;
325
+ console.log(' Warning: Cannot integrate CSS — src/styles.css not found')
326
+ return false
322
327
  }
323
328
 
324
- let stylesContent = fs.readFileSync(stylesPath, 'utf-8');
325
- let integrated = 0;
329
+ let stylesContent = fs.readFileSync(stylesPath, 'utf-8')
330
+ let integrated = 0
326
331
 
327
332
  for (const cssFile of config.css) {
328
- const srcPath = path.join(FEATURES_DIR, config.id, cssFile);
333
+ const srcPath = path.join(FEATURES_DIR, config.id, cssFile)
329
334
  if (!fs.existsSync(srcPath)) {
330
- console.error(` Warning: CSS source not found: ${cssFile}`);
331
- continue;
335
+ console.error(` Warning: CSS source not found: ${cssFile}`)
336
+ continue
332
337
  }
333
338
 
334
- const cssContent = fs.readFileSync(srcPath, 'utf-8');
339
+ const cssContent = fs.readFileSync(srcPath, 'utf-8')
335
340
 
336
341
  // Check if already integrated by looking for a unique class/keyframe from the CSS
337
342
  // Use the first non-comment, non-empty line with a class or keyframe as fingerprint
338
- const fingerprint = cssContent.match(/\.([\w-]+)\s*\{|@keyframes\s+([\w-]+)/);
343
+ const fingerprint = cssContent.match(/\.([\w-]+)\s*\{|@keyframes\s+([\w-]+)/)
339
344
  if (fingerprint) {
340
- const marker = fingerprint[1] || fingerprint[2];
345
+ const marker = fingerprint[1] || fingerprint[2]
341
346
  if (stylesContent.includes(marker)) {
342
- console.log(` CSS already present: ${cssFile} (found .${marker})`);
343
- continue;
347
+ console.log(` CSS already present: ${cssFile} (found .${marker})`)
348
+ continue
344
349
  }
345
350
  }
346
351
 
347
- stylesContent += '\n' + cssContent;
348
- integrated++;
349
- console.log(` CSS integrated: ${cssFile} -> styles.css`);
352
+ stylesContent += '\n' + cssContent
353
+ integrated++
354
+ console.log(` CSS integrated: ${cssFile} -> styles.css`)
350
355
  }
351
356
 
352
357
  if (integrated > 0) {
353
- fs.writeFileSync(stylesPath, stylesContent);
358
+ fs.writeFileSync(stylesPath, stylesContent)
354
359
  }
355
360
 
356
- return integrated > 0;
361
+ return integrated > 0
357
362
  }
358
363
 
359
364
  // ---------------------------------------------------------------------------
360
365
  // Nav auto-wiring into nav.ts
361
366
  // ---------------------------------------------------------------------------
362
367
 
363
- const NAV_MARKER = '// ── Features add nav items below this line ──';
368
+ const NAV_MARKER = '// ── Features add nav items below this line ──'
364
369
 
365
370
  function integrateRoute(config, targetDir) {
366
- if (!config.route) return false;
371
+ if (!config.route) return false
367
372
 
368
- const navPath = path.join(targetDir, 'src', 'nav.ts');
373
+ const navPath = path.join(targetDir, 'src', 'nav.ts')
369
374
  if (!fs.existsSync(navPath)) {
370
- printRouteInstructions(config.route, config);
371
- return false;
375
+ printRouteInstructions(config.route, config)
376
+ return false
372
377
  }
373
378
 
374
- let content = fs.readFileSync(navPath, 'utf-8');
375
- const { path: routePath } = config.route;
376
- const label = config.name;
379
+ let content = fs.readFileSync(navPath, 'utf-8')
380
+ const { path: routePath } = config.route
381
+ const label = config.name
377
382
 
378
383
  // Already wired?
379
384
  if (content.includes(`'${routePath}'`)) {
380
- console.log(` Nav already present: ${routePath}`);
381
- return false;
385
+ console.log(` Nav already present: ${routePath}`)
386
+ return false
382
387
  }
383
388
 
384
389
  if (!content.includes(NAV_MARKER)) {
385
- console.log(' Could not auto-wire nav (marker not found in nav.ts)');
386
- printRouteInstructions(config.route, config);
387
- return false;
390
+ console.log(' Could not auto-wire nav (marker not found in nav.ts)')
391
+ printRouteInstructions(config.route, config)
392
+ return false
388
393
  }
389
394
 
390
- const rolesStr = config.route.protected === false ? '' : ", roles: ['member' as Role]";
391
- const entry = ` { path: '${routePath}', label: '${label}'${rolesStr} },`;
395
+ const rolesStr = config.route.protected === false ? '' : ", roles: ['member' as Role]"
396
+ const entry = ` { path: '${routePath}', label: '${label}'${rolesStr} },`
392
397
 
393
- content = content.replace(NAV_MARKER, `${NAV_MARKER}\n${entry}`);
394
- fs.writeFileSync(navPath, content);
395
- console.log(` Nav wired: ${routePath} (${label})`);
398
+ content = content.replace(NAV_MARKER, `${NAV_MARKER}\n${entry}`)
399
+ fs.writeFileSync(navPath, content)
400
+ console.log(` Nav wired: ${routePath} (${label})`)
396
401
 
397
402
  // Route is automatic via generouted — page file in src/pages/ is enough
398
- console.log(` Route: automatic (file-based routing via src/pages/)`);
399
- return true;
403
+ console.log(` Route: automatic (file-based routing via src/pages/)`)
404
+ return true
400
405
  }
401
406
 
402
407
  function printRouteInstructions(route, config) {
403
- const { path: routePath } = route;
404
- const label = config?.name ?? routePath;
405
- console.log('');
406
- console.log(' Add manually to src/nav.ts:');
407
- console.log(` { path: '${routePath}', label: '${label}' },`);
408
+ const { path: routePath } = route
409
+ const label = config?.name ?? routePath
410
+ console.log('')
411
+ console.log(' Add manually to src/nav.ts:')
412
+ console.log(` { path: '${routePath}', label: '${label}' },`)
408
413
  }
409
414
 
410
415
  // ---------------------------------------------------------------------------
@@ -427,58 +432,56 @@ function integrateDependencies(config, targetDir) {
427
432
  const sections = [
428
433
  { key: 'dependencies', label: 'dependencies' },
429
434
  { key: 'devDependencies', label: 'devDependencies' },
430
- ];
435
+ ]
431
436
 
432
437
  // Skip the work if nothing is declared.
433
- const hasAny = sections.some(
434
- (s) => config[s.key] && Object.keys(config[s.key]).length > 0,
435
- );
436
- if (!hasAny) return;
438
+ const hasAny = sections.some((s) => config[s.key] && Object.keys(config[s.key]).length > 0)
439
+ if (!hasAny) return
437
440
 
438
- const pkgPath = path.join(targetDir, 'package.json');
441
+ const pkgPath = path.join(targetDir, 'package.json')
439
442
  if (!fs.existsSync(pkgPath)) {
440
- console.log(' Could not patch package.json (file not found)');
441
- return;
443
+ console.log(' Could not patch package.json (file not found)')
444
+ return
442
445
  }
443
446
 
444
- let pkg;
447
+ let pkg
445
448
  try {
446
- pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
449
+ pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
447
450
  } catch (err) {
448
- console.log(` Could not patch package.json (parse error: ${err.message})`);
449
- return;
451
+ console.log(` Could not patch package.json (parse error: ${err.message})`)
452
+ return
450
453
  }
451
454
 
452
- const added = [];
453
- const skipped = [];
455
+ const added = []
456
+ const skipped = []
454
457
 
455
458
  for (const { key } of sections) {
456
- const declared = config[key];
457
- if (!declared) continue;
458
- pkg[key] = pkg[key] || {};
459
+ const declared = config[key]
460
+ if (!declared) continue
461
+ pkg[key] = pkg[key] || {}
459
462
  for (const [name, version] of Object.entries(declared)) {
460
463
  if (pkg[key][name]) {
461
- skipped.push(name);
462
- continue;
464
+ skipped.push(name)
465
+ continue
463
466
  }
464
- pkg[key][name] = version;
465
- added.push(name);
467
+ pkg[key][name] = version
468
+ added.push(name)
466
469
  }
467
470
  }
468
471
 
469
472
  if (added.length === 0) {
470
- console.log(' Dependencies already present');
471
- return;
473
+ console.log(' Dependencies already present')
474
+ return
472
475
  }
473
476
 
474
477
  // Re-write with two-space indent (Node's de-facto default; matches
475
478
  // what most generators / lint tools agree on).
476
- fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf-8');
477
- console.log(` Added to package.json: ${added.join(', ')}`);
479
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf-8')
480
+ console.log(` Added to package.json: ${added.join(', ')}`)
478
481
  if (skipped.length > 0) {
479
- console.log(` Already present (kept your version): ${skipped.join(', ')}`);
482
+ console.log(` Already present (kept your version): ${skipped.join(', ')}`)
480
483
  }
481
- console.log(' ➜ Run `npm install` (or your package manager) before starting the dev server');
484
+ console.log(' ➜ Run `npm install` (or your package manager) before starting the dev server')
482
485
  }
483
486
 
484
487
  // ---------------------------------------------------------------------------
@@ -486,13 +489,13 @@ function integrateDependencies(config, targetDir) {
486
489
  // ---------------------------------------------------------------------------
487
490
 
488
491
  function printPostInstallInstructions(config) {
489
- const instructions = config.instructions || [];
492
+ const instructions = config.instructions || []
490
493
 
491
494
  if (instructions.length > 0) {
492
- console.log('\n--- Manual wiring needed ---\n');
495
+ console.log('\n--- Manual wiring needed ---\n')
493
496
  instructions.forEach((inst, i) => {
494
- console.log(`${i + 1}. ${inst}\n`);
495
- });
497
+ console.log(`${i + 1}. ${inst}\n`)
498
+ })
496
499
  }
497
500
  }
498
501
 
@@ -501,137 +504,139 @@ function printPostInstallInstructions(config) {
501
504
  // ---------------------------------------------------------------------------
502
505
 
503
506
  function main() {
504
- const args = process.argv.slice(2);
507
+ const args = process.argv.slice(2)
505
508
 
506
509
  // --help
507
510
  if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
508
- console.log('\nFeature Installation Script\n');
509
- console.log('Usage: node add-feature.js <feature-id> <app-dir>\n');
510
- console.log('Commands:');
511
- console.log(' <feature-id> <dir> Install a feature into app dir');
512
- console.log(' --list, -l List all available features');
513
- console.log(' --info <id> Show detailed info about a feature');
514
- console.log(' --help, -h Show this help\n');
511
+ console.log('\nFeature Installation Script\n')
512
+ console.log('Usage: node add-feature.js <feature-id> <app-dir>\n')
513
+ console.log('Commands:')
514
+ console.log(' <feature-id> <dir> Install a feature into app dir')
515
+ console.log(' --list, -l List all available features')
516
+ console.log(' --info <id> Show detailed info about a feature')
517
+ console.log(' --help, -h Show this help\n')
515
518
 
516
- const features = listFeatures();
517
- const groups = groupByCategory(features);
519
+ const features = listFeatures()
520
+ const groups = groupByCategory(features)
518
521
 
519
522
  for (const category of CATEGORY_ORDER) {
520
- const categoryFeatures = groups.get(category);
523
+ const categoryFeatures = groups.get(category)
521
524
  if (categoryFeatures && categoryFeatures.length > 0) {
522
- console.log(`\n${CATEGORY_LABELS[category] || category}:`);
525
+ console.log(`\n${CATEGORY_LABELS[category] || category}:`)
523
526
  for (const f of categoryFeatures) {
524
- console.log(` ${f.id.padEnd(22)} ${f.name}`);
527
+ console.log(` ${f.id.padEnd(22)} ${f.name}`)
525
528
  }
526
529
  }
527
530
  }
528
531
 
529
- console.log('\nExamples:');
530
- console.log(' add feature items-crud .');
531
- console.log(' node add-feature.js items-crud ../my-app');
532
- process.exit(0);
532
+ console.log('\nExamples:')
533
+ console.log(' add feature items-crud .')
534
+ console.log(' node add-feature.js items-crud ../my-app')
535
+ process.exit(0)
533
536
  }
534
537
 
535
538
  // --list
536
539
  if (args[0] === '--list' || args[0] === '-l') {
537
- console.log('\nAvailable Features\n');
540
+ console.log('\nAvailable Features\n')
538
541
 
539
- const features = listFeatures();
540
- const groups = groupByCategory(features);
542
+ const features = listFeatures()
543
+ const groups = groupByCategory(features)
541
544
 
542
545
  for (const category of CATEGORY_ORDER) {
543
- const categoryFeatures = groups.get(category);
546
+ const categoryFeatures = groups.get(category)
544
547
  if (categoryFeatures && categoryFeatures.length > 0) {
545
- console.log(`${CATEGORY_LABELS[category] || category}:`);
548
+ console.log(`${CATEGORY_LABELS[category] || category}:`)
546
549
  for (const f of categoryFeatures) {
547
- console.log(` ${f.id.padEnd(22)} ${f.name.padEnd(24)} ${f.description}`);
550
+ console.log(` ${f.id.padEnd(22)} ${f.name.padEnd(24)} ${f.description}`)
548
551
  }
549
- console.log('');
552
+ console.log('')
550
553
  }
551
554
  }
552
555
 
553
- console.log('Use: npx deepspace add <feature-id>');
554
- process.exit(0);
556
+ console.log('Use: npx deepspace add <feature-id>')
557
+ process.exit(0)
555
558
  }
556
559
 
557
560
  // --info
558
561
  if (args[0] === '--info' || args[0] === '-i') {
559
- const featureId = args[1];
562
+ const featureId = args[1]
560
563
  if (!featureId) {
561
- console.error('\nError: Please specify a feature ID');
562
- process.exit(1);
564
+ console.error('\nError: Please specify a feature ID')
565
+ process.exit(1)
563
566
  }
564
- const config = loadFeatureConfig(featureId);
567
+ const config = loadFeatureConfig(featureId)
565
568
  if (!config) {
566
- console.error(`\nError: Unknown feature: ${featureId}`);
567
- console.error('Use --list to see available features');
568
- process.exit(1);
569
+ console.error(`\nError: Unknown feature: ${featureId}`)
570
+ console.error('Use --list to see available features')
571
+ process.exit(1)
569
572
  }
570
573
 
571
- console.log(`\n${config.name} (${config.id})`);
574
+ console.log(`\n${config.name} (${config.id})`)
572
575
  if (config.category) {
573
- console.log(` Category: ${CATEGORY_LABELS[config.category] || config.category}`);
576
+ console.log(` Category: ${CATEGORY_LABELS[config.category] || config.category}`)
574
577
  }
575
- console.log(` ${config.description}\n`);
576
- console.log(` ${config.details}\n`);
578
+ console.log(` ${config.description}\n`)
579
+ console.log(` ${config.details}\n`)
577
580
 
578
- console.log(' Files:');
579
- config.files.forEach(f => console.log(` - ${f.src} -> ${resolveDestPath(f.dest, config.route)}`));
581
+ console.log(' Files:')
582
+ config.files.forEach((f) =>
583
+ console.log(` - ${f.src} -> ${resolveDestPath(f.dest, config.route)}`),
584
+ )
580
585
 
581
586
  if (config.instructions && config.instructions.length > 0) {
582
- console.log('\n Integration steps:');
583
- config.instructions.forEach((inst, i) => console.log(` ${i + 1}. ${inst}`));
587
+ console.log('\n Integration steps:')
588
+ config.instructions.forEach((inst, i) => console.log(` ${i + 1}. ${inst}`))
584
589
  }
585
590
 
586
591
  if (config.patterns && config.patterns.length > 0) {
587
- console.log('\n Patterns:');
588
- config.patterns.forEach(p => console.log(` - ${p}`));
592
+ console.log('\n Patterns:')
593
+ config.patterns.forEach((p) => console.log(` - ${p}`))
589
594
  }
590
595
 
591
596
  if (config.example) {
592
- console.log('\n Example:');
593
- config.example.split('\n').forEach(line => console.log(` ${line}`));
597
+ console.log('\n Example:')
598
+ config.example.split('\n').forEach((line) => console.log(` ${line}`))
594
599
  }
595
- console.log('');
596
- process.exit(0);
600
+ console.log('')
601
+ process.exit(0)
597
602
  }
598
603
 
599
604
  // Install feature
600
- const featureId = args[0];
601
- const rawDir = args[1];
602
- const targetDir = resolveTargetDir(rawDir);
605
+ const featureId = args[0]
606
+ const rawDir = args[1]
607
+ const targetDir = resolveTargetDir(rawDir)
603
608
 
604
609
  if (!targetDir) {
605
- console.error('\nError: Please specify an app directory');
606
- console.error('Usage: node add-feature.js <feature-id> <app-dir>');
607
- process.exit(1);
610
+ console.error('\nError: Please specify an app directory')
611
+ console.error('Usage: node add-feature.js <feature-id> <app-dir>')
612
+ process.exit(1)
608
613
  }
609
614
 
610
- const config = loadFeatureConfig(featureId);
615
+ const config = loadFeatureConfig(featureId)
611
616
  if (!config) {
612
- console.error(`\nError: Unknown feature: ${featureId}`);
613
- console.error('Use --list to see available features');
614
- process.exit(1);
617
+ console.error(`\nError: Unknown feature: ${featureId}`)
618
+ console.error('Use --list to see available features')
619
+ process.exit(1)
615
620
  }
616
621
 
617
622
  if (!fs.existsSync(targetDir)) {
618
- console.error(`\nError: Target directory not found: ${targetDir}`);
619
- process.exit(1);
623
+ console.error(`\nError: Target directory not found: ${targetDir}`)
624
+ process.exit(1)
620
625
  }
621
626
 
622
- console.log(`\nInstalling: ${config.name}`);
623
- console.log(` ${config.description}`);
624
- console.log(` Target: ${targetDir}`);
627
+ console.log(`\nInstalling: ${config.name}`)
628
+ console.log(` ${config.description}`)
629
+ console.log(` Target: ${targetDir}`)
625
630
 
626
- const { copied, skipped } = installFeature(config, targetDir);
631
+ const { copied, skipped } = installFeature(config, targetDir)
627
632
 
628
- console.log(`\nCopied ${copied} file(s)${skipped > 0 ? `, skipped ${skipped} existing` : ''}`);
633
+ console.log(`\nCopied ${copied} file(s)${skipped > 0 ? `, skipped ${skipped} existing` : ''}`)
629
634
 
630
635
  // Auto-integrate schema, actions, CSS, and route
631
- integrateSchema(config, targetDir);
632
- integrateActions(config, targetDir);
633
- integrateCss(config, targetDir);
634
- integrateRoute(config, targetDir);
636
+ integrateSchema(config, targetDir)
637
+ integrateActions(config, targetDir)
638
+ integrateCss(config, targetDir)
639
+ integrateRoute(config, targetDir)
635
640
 
636
641
  // Add any new npm dependencies the feature declares to the target's
637
642
  // package.json. Without this, copied source files import packages
@@ -640,16 +645,16 @@ function main() {
640
645
  // package.json (rather than running `npm install` ourselves) so the
641
646
  // user keeps control over their package manager + lockfile, and we
642
647
  // print a single clear instruction when changes were made.
643
- integrateDependencies(config, targetDir);
648
+ integrateDependencies(config, targetDir)
644
649
 
645
650
  // Print any remaining manual wiring instructions
646
- printPostInstallInstructions(config);
651
+ printPostInstallInstructions(config)
647
652
 
648
653
  if (config.patterns && config.patterns.length > 0) {
649
- console.log('--- Key patterns ---\n');
650
- config.patterns.forEach(p => console.log(`- ${p}`));
651
- console.log('');
654
+ console.log('--- Key patterns ---\n')
655
+ config.patterns.forEach((p) => console.log(`- ${p}`))
656
+ console.log('')
652
657
  }
653
658
  }
654
659
 
655
- main();
660
+ main()