deepspace 0.4.5 → 0.5.1

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