deepspace 0.6.2 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +44 -0
- package/README.md +22 -8
- package/dist/cli.js +10348 -5025
- package/dist/cli.js.map +1 -1
- package/dist/feature-installer.js +638 -0
- package/dist/feature-installer.js.map +1 -0
- package/dist/index.d.ts +229 -116
- package/dist/index.js +767 -788
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +403 -167
- package/dist/server.js +498 -367
- package/dist/server.js.map +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +3 -3
- package/dist/testing.js.map +1 -1
- package/dist/worker.d.ts +541 -159
- package/dist/worker.js +611 -367
- package/dist/worker.js.map +1 -1
- package/features/ai-chat/feature.json +2 -0
- package/features/ai-chat/src/AiChatPage.tsx +2 -3
- package/features/ai-chat/src/ChatPanel.messages.tsx +344 -0
- package/features/ai-chat/src/ChatPanel.stream.ts +397 -0
- package/features/ai-chat/src/ChatPanel.tsx +159 -825
- package/features/ai-chat/src/__tests__/ChatPanel.stream.test.ts +163 -0
- package/features/canvas/src/CanvasListPage.tsx +1 -1
- package/features/docs/feature.json +4 -8
- package/features/docs/src/DocsEditorPage.tsx +33 -326
- package/features/docs/src/DocsListPage.tsx +33 -29
- package/features/docs/src/InviteDialog.tsx +11 -40
- package/features/docs/src/__tests__/docs-presence-access.test.ts +71 -0
- package/features/docs/src/docs-library-types.ts +28 -3
- package/features/docs/src/docs-ui.css +1 -4
- package/features/docs/src/use-docs-presence-access.ts +330 -0
- package/features/file-attachments/feature.json +2 -2
- package/features/file-attachments/src/AttachmentPreview.tsx +428 -0
- package/features/file-attachments/src/AttachmentsPage.tsx +100 -462
- package/features/file-attachments/src/useFilePreview.ts +54 -68
- package/features/file-manager/src/FileManagerPage.tsx +1 -1
- package/features/integration-test/src/integration.spec.ts +1 -1
- package/features/items/feature.json +1 -5
- package/features/items/src/ItemsPage.tsx +207 -135
- package/features/landing/feature.json +3 -0
- package/features/landing/src/LandingPage.tsx +4 -18
- package/features/leaderboard/feature.json +1 -5
- package/features/leaderboard/src/LeaderboardPage.tsx +333 -198
- package/features/messaging/feature.json +0 -4
- package/features/messaging/src/ChatMultiPage.tsx +1 -3
- package/features/messaging/src/ChatPage.tsx +1 -3
- package/features/messaging/src/components/chat/MessageActionSheet.tsx +40 -16
- package/features/search-bar/feature.json +8 -3
- package/features/search-bar/src/SearchOverlay.tsx +108 -252
- package/features/search-bar/src/__tests__/search-model.test.ts +40 -0
- package/features/search-bar/src/search-model.ts +223 -0
- package/features/tasks/src/TasksPage.tsx +3 -3
- package/features/topbar/src/Topbar.tsx +0 -17
- package/package.json +1 -2
- package/features/docs/src/docs-favorites.ts +0 -18
- package/features/docs/src/use-yjs-room-with-awareness.ts +0 -289
- package/features/file-attachments/src/attachments-constants.ts +0 -9
- package/features/items/src/items-constants.ts +0 -10
- package/features/leaderboard/src/leaderboard-constants.ts +0 -21
- package/features/messaging/src/components/chat/ReactionPicker.tsx +0 -2
- package/scripts/add-feature.cjs +0 -807
package/scripts/add-feature.cjs
DELETED
|
@@ -1,807 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Feature Installation Script (CommonJS)
|
|
4
|
-
*
|
|
5
|
-
* Installs DeepSpace features into an app directory. Ships inside the
|
|
6
|
-
* `deepspace` npm package at `node_modules/deepspace/scripts/add-feature.cjs`
|
|
7
|
-
* and reads features from a sibling `node_modules/deepspace/features/` dir.
|
|
8
|
-
* Invoked by the `deepspace add` CLI in src/cli/commands/add.ts.
|
|
9
|
-
*/
|
|
10
|
-
|
|
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')
|
|
15
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports -- CommonJS (.cjs) module: require is required
|
|
16
|
-
const { spawnSync } = require('child_process')
|
|
17
|
-
|
|
18
|
-
// ---------------------------------------------------------------------------
|
|
19
|
-
// Environment-aware features path resolution
|
|
20
|
-
// ---------------------------------------------------------------------------
|
|
21
|
-
|
|
22
|
-
function findFeaturesDir() {
|
|
23
|
-
const candidate = path.resolve(__dirname, '..', 'features')
|
|
24
|
-
if (fs.existsSync(candidate)) return candidate
|
|
25
|
-
console.error('Error: Could not find features directory at ' + candidate)
|
|
26
|
-
process.exit(1)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const FEATURES_DIR = findFeaturesDir()
|
|
30
|
-
|
|
31
|
-
// ---------------------------------------------------------------------------
|
|
32
|
-
// Resolve target directory
|
|
33
|
-
// ---------------------------------------------------------------------------
|
|
34
|
-
|
|
35
|
-
function resolveTargetDir(raw) {
|
|
36
|
-
if (!raw) return null
|
|
37
|
-
if (path.isAbsolute(raw)) return raw
|
|
38
|
-
return path.resolve(raw)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// ---------------------------------------------------------------------------
|
|
42
|
-
// Category labels & ordering
|
|
43
|
-
// ---------------------------------------------------------------------------
|
|
44
|
-
|
|
45
|
-
const CATEGORY_LABELS = {
|
|
46
|
-
assistant: 'AI Features',
|
|
47
|
-
data: 'Data Features',
|
|
48
|
-
nav: 'Navigation',
|
|
49
|
-
layout: 'Layouts',
|
|
50
|
-
display: 'Display',
|
|
51
|
-
landing: 'Landing Page Sections',
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const CATEGORY_ORDER = ['assistant', 'data', 'nav', 'layout', 'display', 'landing']
|
|
55
|
-
|
|
56
|
-
// ---------------------------------------------------------------------------
|
|
57
|
-
// Core helpers
|
|
58
|
-
// ---------------------------------------------------------------------------
|
|
59
|
-
|
|
60
|
-
function loadFeatureConfig(featureId) {
|
|
61
|
-
const configPath = path.join(FEATURES_DIR, featureId, 'feature.json')
|
|
62
|
-
if (!fs.existsSync(configPath)) return null
|
|
63
|
-
try {
|
|
64
|
-
return JSON.parse(fs.readFileSync(configPath, 'utf-8'))
|
|
65
|
-
} catch (e) {
|
|
66
|
-
console.error(`Error reading ${configPath}:`, e.message)
|
|
67
|
-
return null
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function listFeatures() {
|
|
72
|
-
const entries = fs.readdirSync(FEATURES_DIR, { withFileTypes: true })
|
|
73
|
-
const configs = []
|
|
74
|
-
for (const e of entries) {
|
|
75
|
-
if (e.isDirectory() && e.name !== 'scripts') {
|
|
76
|
-
const config = loadFeatureConfig(e.name)
|
|
77
|
-
// Internal features (e2e/QA fixtures) are installable by explicit name
|
|
78
|
-
// but hidden from `--list` and the interactive picker.
|
|
79
|
-
if (config && !config.internal) configs.push(config)
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
return configs
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function groupByCategory(features) {
|
|
86
|
-
const groups = new Map()
|
|
87
|
-
for (const f of features) {
|
|
88
|
-
const category = f.category || 'other'
|
|
89
|
-
if (!groups.has(category)) groups.set(category, [])
|
|
90
|
-
groups.get(category).push(f)
|
|
91
|
-
}
|
|
92
|
-
return groups
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// First feature id in display order — used for a never-stale help example.
|
|
96
|
-
function firstFeatureId(groups) {
|
|
97
|
-
for (const category of CATEGORY_ORDER) {
|
|
98
|
-
const list = groups.get(category)
|
|
99
|
-
if (list && list.length > 0) return list[0].id
|
|
100
|
-
}
|
|
101
|
-
return null
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Small edit distance for "did you mean" suggestions on a typo'd feature id.
|
|
105
|
-
function levenshtein(a, b) {
|
|
106
|
-
const m = a.length
|
|
107
|
-
const n = b.length
|
|
108
|
-
const row = Array.from({ length: n + 1 }, (_, i) => i)
|
|
109
|
-
for (let i = 1; i <= m; i++) {
|
|
110
|
-
let prev = row[0]
|
|
111
|
-
row[0] = i
|
|
112
|
-
for (let j = 1; j <= n; j++) {
|
|
113
|
-
const tmp = row[j]
|
|
114
|
-
row[j] = Math.min(
|
|
115
|
-
row[j] + 1,
|
|
116
|
-
row[j - 1] + 1,
|
|
117
|
-
prev + (a[i - 1] === b[j - 1] ? 0 : 1),
|
|
118
|
-
)
|
|
119
|
-
prev = tmp
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
return row[n]
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Closest visible feature id to a typo, or null when nothing is close enough.
|
|
126
|
-
function suggestFeature(featureId) {
|
|
127
|
-
let best = null
|
|
128
|
-
let bestD = Infinity
|
|
129
|
-
for (const f of listFeatures()) {
|
|
130
|
-
const d = levenshtein(featureId, f.id)
|
|
131
|
-
if (d < bestD) {
|
|
132
|
-
bestD = d
|
|
133
|
-
best = f.id
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
const threshold = Math.max(2, Math.floor(featureId.length / 3))
|
|
137
|
-
return best && bestD <= threshold ? best : null
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Print a friendly unknown-feature error (with a suggestion) and exit.
|
|
141
|
-
function unknownFeature(featureId) {
|
|
142
|
-
console.error(`\nError: Unknown feature: ${featureId}`)
|
|
143
|
-
const guess = suggestFeature(featureId)
|
|
144
|
-
if (guess) console.error(`Did you mean "${guess}"?`)
|
|
145
|
-
console.error('Run `deepspace add --list` to see available features')
|
|
146
|
-
process.exit(1)
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Detect the target app's package manager from its lockfile (defaults to npm).
|
|
150
|
-
function detectPackageManager(targetDir) {
|
|
151
|
-
const has = (f) => fs.existsSync(path.join(targetDir, f))
|
|
152
|
-
if (has('pnpm-lock.yaml')) return 'pnpm'
|
|
153
|
-
if (has('yarn.lock')) return 'yarn'
|
|
154
|
-
if (has('bun.lockb') || has('bun.lock')) return 'bun'
|
|
155
|
-
return 'npm'
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// The nav features are mutually exclusive (competing navigation). Derive which
|
|
159
|
-
// are installed from the catalog itself — each nav feature's first installed
|
|
160
|
-
// file is its signature — so this never drifts from what a feature ships.
|
|
161
|
-
function installedNavFeatures(targetDir) {
|
|
162
|
-
return listFeatures()
|
|
163
|
-
.filter((f) => f.category === 'nav' && Array.isArray(f.files) && f.files.length > 0)
|
|
164
|
-
.filter((f) =>
|
|
165
|
-
fs.existsSync(path.join(targetDir, resolveDestPath(f.files[0].dest, f.route))),
|
|
166
|
-
)
|
|
167
|
-
.map((f) => f.id)
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function warnNavConflicts(config, targetDir) {
|
|
171
|
-
if (config.category !== 'nav') return
|
|
172
|
-
const present = installedNavFeatures(targetDir)
|
|
173
|
-
if (present.length > 1) {
|
|
174
|
-
console.log(`\n⚠ Multiple navigation features present (${present.join(', ')}).`)
|
|
175
|
-
console.log(
|
|
176
|
-
' sidebar, topbar, and tree are mutually exclusive — keep one and remove the',
|
|
177
|
-
)
|
|
178
|
-
console.log(' others to avoid competing navigation in your layout.')
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// Always-printed footer so every install ends with a clear "what now".
|
|
183
|
-
function printNextSteps(config, deps) {
|
|
184
|
-
console.log('\nNext steps:')
|
|
185
|
-
let n = 1
|
|
186
|
-
if (deps.added.length > 0 && !deps.installRan) {
|
|
187
|
-
console.log(` ${n++}. Install new dependencies: ${deps.pm} install`)
|
|
188
|
-
console.log(' (or re-run this command with --install to do it for you)')
|
|
189
|
-
}
|
|
190
|
-
console.log(` ${n++}. Start the dev server: deepspace dev`)
|
|
191
|
-
if (config.route && config.route.path) {
|
|
192
|
-
console.log(` ${n++}. Open ${config.route.path} once the dev server is up`)
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function copyFile(src, dest) {
|
|
197
|
-
const destDir = path.dirname(dest)
|
|
198
|
-
if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true })
|
|
199
|
-
fs.copyFileSync(src, dest)
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Copy + rewrite leading `../` imports. Used when a page file is redirected
|
|
203
|
-
// into a deeper route group (e.g. src/pages/(app)/ adds one directory level,
|
|
204
|
-
// src/pages/(app)/(protected)/ adds two) — every relative import that
|
|
205
|
-
// traverses upward needs `levels` extra `../`. Matches static `from '...'`,
|
|
206
|
-
// side-effect `import '...'`, and dynamic/type `import('...')` (e.g.
|
|
207
|
-
// `import('../hooks/x').T` in a type position, or `await import('../x')`).
|
|
208
|
-
// Same-directory (`./...`) and bare-package imports are untouched.
|
|
209
|
-
function copyFileWithImportShift(src, dest, levels) {
|
|
210
|
-
const destDir = path.dirname(dest)
|
|
211
|
-
if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true })
|
|
212
|
-
const content = fs.readFileSync(src, 'utf-8')
|
|
213
|
-
const prefix = '../'.repeat(levels)
|
|
214
|
-
const shifted = content.replace(/(from\s+|import\s+|import\()(['"])(\.\.\/)/g, `$1$2${prefix}$3`)
|
|
215
|
-
fs.writeFileSync(dest, shifted)
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// ---------------------------------------------------------------------------
|
|
219
|
-
// Install files
|
|
220
|
-
// ---------------------------------------------------------------------------
|
|
221
|
-
|
|
222
|
-
// Page files must land under src/pages/(app)/ so the (app)/_layout.tsx route
|
|
223
|
-
// group wraps them in the DeepSpace providers (auth + realtime records) — the
|
|
224
|
-
// top level of src/pages/ is reserved for static pages that mount no
|
|
225
|
-
// providers. Protected pages go one level deeper, under (app)/(protected)/,
|
|
226
|
-
// so the nested layout also wraps them in <AuthGate>. Both are
|
|
227
|
-
// folder-name-only route groups — the URL path is unchanged. Only page files
|
|
228
|
-
// (src/pages/*) get redirected; schemas, components, etc. keep their declared
|
|
229
|
-
// destinations. Page files already placed in the (app) group are left as-is.
|
|
230
|
-
function resolveDestPath(fileDest, route) {
|
|
231
|
-
const PAGES = 'src/pages/'
|
|
232
|
-
const APP = 'src/pages/(app)/'
|
|
233
|
-
if (!fileDest.startsWith(PAGES) || fileDest.startsWith(APP)) return fileDest
|
|
234
|
-
const rest = fileDest.slice(PAGES.length)
|
|
235
|
-
const isProtected = route && route.protected !== false
|
|
236
|
-
return isProtected ? `${APP}(protected)/${rest}` : `${APP}${rest}`
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
function installFeature(config, targetDir) {
|
|
240
|
-
let copied = 0
|
|
241
|
-
let skipped = 0
|
|
242
|
-
|
|
243
|
-
for (const file of config.files) {
|
|
244
|
-
const srcPath = path.join(FEATURES_DIR, config.id, file.src)
|
|
245
|
-
const resolvedDest = resolveDestPath(file.dest, config.route)
|
|
246
|
-
const destPath = path.join(targetDir, resolvedDest)
|
|
247
|
-
const wasRedirected = resolvedDest !== file.dest
|
|
248
|
-
// Each extra path segment in the resolved dest is one directory level the
|
|
249
|
-
// page dropped into (e.g. (app)/ = 1, (app)/(protected)/ = 2), which is
|
|
250
|
-
// exactly how many `../` its upward relative imports must gain.
|
|
251
|
-
const shiftLevels = resolvedDest.split('/').length - file.dest.split('/').length
|
|
252
|
-
|
|
253
|
-
if (!fs.existsSync(srcPath)) {
|
|
254
|
-
console.error(` Warning: source not found: ${file.src}`)
|
|
255
|
-
continue
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
if (fs.existsSync(destPath) && !file.overwrite) {
|
|
259
|
-
console.log(` Exists (skipped): ${resolvedDest}`)
|
|
260
|
-
skipped++
|
|
261
|
-
} else {
|
|
262
|
-
const replacing = fs.existsSync(destPath)
|
|
263
|
-
if (wasRedirected) {
|
|
264
|
-
copyFileWithImportShift(srcPath, destPath, shiftLevels)
|
|
265
|
-
} else {
|
|
266
|
-
copyFile(srcPath, destPath)
|
|
267
|
-
}
|
|
268
|
-
console.log(` ${replacing ? 'Overwrote' : 'Copied'}: ${resolvedDest}`)
|
|
269
|
-
copied++
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
return { copied, skipped }
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// ---------------------------------------------------------------------------
|
|
277
|
-
// Schema auto-integration (structural parse — no comments needed)
|
|
278
|
-
// ---------------------------------------------------------------------------
|
|
279
|
-
|
|
280
|
-
function integrateSchema(config, targetDir) {
|
|
281
|
-
if (!config.schema) return false
|
|
282
|
-
|
|
283
|
-
const schemasPath = path.join(targetDir, 'src', 'schemas.ts')
|
|
284
|
-
if (!fs.existsSync(schemasPath)) {
|
|
285
|
-
printSchemaInstructions(config.schema)
|
|
286
|
-
return false
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
let content = fs.readFileSync(schemasPath, 'utf-8')
|
|
290
|
-
const { exportName, importPath, spreadOperator } = config.schema
|
|
291
|
-
|
|
292
|
-
// Already integrated?
|
|
293
|
-
const importPattern = new RegExp(`^import\\s+\\{[^}]*\\b${exportName}\\b`, 'm')
|
|
294
|
-
if (importPattern.test(content)) {
|
|
295
|
-
console.log(` Schema already present: ${exportName}`)
|
|
296
|
-
return false
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// Strict checks: verify the file has the expected structure
|
|
300
|
-
const hasExportConst = content.includes('export const schemas')
|
|
301
|
-
const hasArrayOpening = /export const schemas:\s*CollectionSchema\[\]\s*=\s*\[/.test(content)
|
|
302
|
-
|
|
303
|
-
if (!hasExportConst || !hasArrayOpening) {
|
|
304
|
-
console.log(' Could not auto-integrate schema (schemas.ts has unexpected structure)')
|
|
305
|
-
printSchemaInstructions(config.schema)
|
|
306
|
-
return false
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// 1. Add import before "export const schemas"
|
|
310
|
-
const importLine = `import { ${exportName} } from '${importPath}'`
|
|
311
|
-
const afterImport = content.replace(
|
|
312
|
-
/export const schemas/,
|
|
313
|
-
`${importLine}\n\nexport const schemas`,
|
|
314
|
-
)
|
|
315
|
-
|
|
316
|
-
if (afterImport === content) {
|
|
317
|
-
console.log(' Could not insert import line')
|
|
318
|
-
printSchemaInstructions(config.schema)
|
|
319
|
-
return false
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// 2. Insert entry into the schemas array
|
|
323
|
-
const schemaEntry = spreadOperator ? `...${exportName}` : exportName
|
|
324
|
-
const afterEntry = afterImport.replace(
|
|
325
|
-
/export const schemas:\s*CollectionSchema\[\]\s*=\s*\[/,
|
|
326
|
-
`export const schemas: CollectionSchema[] = [\n ${schemaEntry},`,
|
|
327
|
-
)
|
|
328
|
-
|
|
329
|
-
if (afterEntry === afterImport) {
|
|
330
|
-
console.log(' Could not insert schema entry')
|
|
331
|
-
printSchemaInstructions(config.schema)
|
|
332
|
-
return false
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
fs.writeFileSync(schemasPath, afterEntry)
|
|
336
|
-
console.log(` Schema integrated: ${exportName} -> schemas.ts`)
|
|
337
|
-
return true
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
function printSchemaInstructions(schema) {
|
|
341
|
-
const { exportName, importPath, spreadOperator } = schema
|
|
342
|
-
const entry = spreadOperator ? `...${exportName}` : exportName
|
|
343
|
-
console.log('')
|
|
344
|
-
console.log(' Add manually to src/schemas.ts:')
|
|
345
|
-
console.log(` import { ${exportName} } from '${importPath}'`)
|
|
346
|
-
console.log(` // then add ${entry} to the schemas array`)
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
// ---------------------------------------------------------------------------
|
|
350
|
-
// Actions auto-integration (append to src/actions/index.ts registry)
|
|
351
|
-
// ---------------------------------------------------------------------------
|
|
352
|
-
|
|
353
|
-
function integrateActions(config, targetDir) {
|
|
354
|
-
if (!config.actions || config.actions.length === 0) return false
|
|
355
|
-
|
|
356
|
-
const actionsPath = path.join(targetDir, 'src', 'actions', 'index.ts')
|
|
357
|
-
if (!fs.existsSync(actionsPath)) {
|
|
358
|
-
printActionsInstructions(config.actions)
|
|
359
|
-
return false
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
let content = fs.readFileSync(actionsPath, 'utf-8')
|
|
363
|
-
|
|
364
|
-
// Strict check: must match the starter's shape so our regex patch is safe.
|
|
365
|
-
// Accepts both `ActionHandler` and `ActionHandler<Env>` (typed-env templates).
|
|
366
|
-
const hasRegistry =
|
|
367
|
-
/export const actions:\s*Record<string,\s*ActionHandler(?:<[^>]*>)?>\s*=\s*\{/.test(content)
|
|
368
|
-
if (!hasRegistry) {
|
|
369
|
-
console.log(' Could not auto-integrate actions (actions/index.ts has unexpected structure)')
|
|
370
|
-
printActionsInstructions(config.actions)
|
|
371
|
-
return false
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
let integrated = 0
|
|
375
|
-
for (const action of config.actions) {
|
|
376
|
-
const { exportName, registerAs, importPath } = action
|
|
377
|
-
|
|
378
|
-
// Already integrated?
|
|
379
|
-
const importPattern = new RegExp(`^import\\s+\\{[^}]*\\b${exportName}\\b`, 'm')
|
|
380
|
-
if (importPattern.test(content)) {
|
|
381
|
-
console.log(` Action already present: ${registerAs}`)
|
|
382
|
-
continue
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
// 1. Add import before the registry export.
|
|
386
|
-
const importLine = `import { ${exportName} } from '${importPath}'`
|
|
387
|
-
const withImport = content.replace(
|
|
388
|
-
/export const actions:/,
|
|
389
|
-
`${importLine}\n\nexport const actions:`,
|
|
390
|
-
)
|
|
391
|
-
if (withImport === content) {
|
|
392
|
-
console.log(` Could not insert import for ${exportName}`)
|
|
393
|
-
continue
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
// 2. Insert entry into the registry object. Works for both empty and
|
|
397
|
-
// populated registries because we inject right after the opening brace.
|
|
398
|
-
const registryEntry = ` '${registerAs}': ${exportName},`
|
|
399
|
-
const withEntry = withImport.replace(
|
|
400
|
-
/export const actions:\s*Record<string,\s*ActionHandler(?:<[^>]*>)?>\s*=\s*\{/,
|
|
401
|
-
(match) => `${match}\n${registryEntry}`,
|
|
402
|
-
)
|
|
403
|
-
if (withEntry === withImport) {
|
|
404
|
-
console.log(` Could not insert registry entry for ${registerAs}`)
|
|
405
|
-
continue
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
content = withEntry
|
|
409
|
-
integrated++
|
|
410
|
-
console.log(` Action integrated: ${registerAs} (${exportName})`)
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
if (integrated > 0) {
|
|
414
|
-
fs.writeFileSync(actionsPath, content)
|
|
415
|
-
}
|
|
416
|
-
return integrated > 0
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
function printActionsInstructions(actions) {
|
|
420
|
-
console.log('')
|
|
421
|
-
console.log(' Add manually to src/actions/index.ts:')
|
|
422
|
-
for (const a of actions) {
|
|
423
|
-
console.log(` import { ${a.exportName} } from '${a.importPath}'`)
|
|
424
|
-
console.log(` // then add '${a.registerAs}': ${a.exportName} to the actions object`)
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
// ---------------------------------------------------------------------------
|
|
429
|
-
// CSS auto-integration (append feature CSS to styles.css)
|
|
430
|
-
// ---------------------------------------------------------------------------
|
|
431
|
-
|
|
432
|
-
function integrateCss(config, targetDir) {
|
|
433
|
-
if (!config.css || config.css.length === 0) return false
|
|
434
|
-
|
|
435
|
-
const stylesPath = path.join(targetDir, 'src', 'styles.css')
|
|
436
|
-
if (!fs.existsSync(stylesPath)) {
|
|
437
|
-
console.log(' Warning: Cannot integrate CSS — src/styles.css not found')
|
|
438
|
-
return false
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
let stylesContent = fs.readFileSync(stylesPath, 'utf-8')
|
|
442
|
-
let integrated = 0
|
|
443
|
-
|
|
444
|
-
for (const cssFile of config.css) {
|
|
445
|
-
const srcPath = path.join(FEATURES_DIR, config.id, cssFile)
|
|
446
|
-
if (!fs.existsSync(srcPath)) {
|
|
447
|
-
console.error(` Warning: CSS source not found: ${cssFile}`)
|
|
448
|
-
continue
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
const cssContent = fs.readFileSync(srcPath, 'utf-8')
|
|
452
|
-
|
|
453
|
-
// Check if already integrated by looking for a unique class/keyframe from the CSS
|
|
454
|
-
// Use the first non-comment, non-empty line with a class or keyframe as fingerprint
|
|
455
|
-
const fingerprint = cssContent.match(/\.([\w-]+)\s*\{|@keyframes\s+([\w-]+)/)
|
|
456
|
-
if (fingerprint) {
|
|
457
|
-
const marker = fingerprint[1] || fingerprint[2]
|
|
458
|
-
if (stylesContent.includes(marker)) {
|
|
459
|
-
console.log(` CSS already present: ${cssFile} (found .${marker})`)
|
|
460
|
-
continue
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
stylesContent += '\n' + cssContent
|
|
465
|
-
integrated++
|
|
466
|
-
console.log(` CSS integrated: ${cssFile} -> styles.css`)
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
if (integrated > 0) {
|
|
470
|
-
fs.writeFileSync(stylesPath, stylesContent)
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
return integrated > 0
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
// ---------------------------------------------------------------------------
|
|
477
|
-
// Nav auto-wiring into nav.ts
|
|
478
|
-
// ---------------------------------------------------------------------------
|
|
479
|
-
|
|
480
|
-
const NAV_MARKER = '// ── Features add nav items below this line ──'
|
|
481
|
-
|
|
482
|
-
function integrateRoute(config, targetDir) {
|
|
483
|
-
if (!config.route) return false
|
|
484
|
-
|
|
485
|
-
const navPath = path.join(targetDir, 'src', 'nav.ts')
|
|
486
|
-
if (!fs.existsSync(navPath)) {
|
|
487
|
-
printRouteInstructions(config.route, config)
|
|
488
|
-
return false
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
let content = fs.readFileSync(navPath, 'utf-8')
|
|
492
|
-
const { path: routePath } = config.route
|
|
493
|
-
const label = config.name
|
|
494
|
-
|
|
495
|
-
// Already wired?
|
|
496
|
-
if (content.includes(`'${routePath}'`)) {
|
|
497
|
-
console.log(` Nav already present: ${routePath}`)
|
|
498
|
-
return false
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
if (!content.includes(NAV_MARKER)) {
|
|
502
|
-
console.log(' Could not auto-wire nav (marker not found in nav.ts)')
|
|
503
|
-
printRouteInstructions(config.route, config)
|
|
504
|
-
return false
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
const rolesStr = config.route.protected === false ? '' : ", roles: ['member' as Role]"
|
|
508
|
-
const entry = ` { path: '${routePath}', label: '${label}'${rolesStr} },`
|
|
509
|
-
|
|
510
|
-
content = content.replace(NAV_MARKER, `${NAV_MARKER}\n${entry}`)
|
|
511
|
-
fs.writeFileSync(navPath, content)
|
|
512
|
-
console.log(` Nav wired: ${routePath} (${label})`)
|
|
513
|
-
|
|
514
|
-
// Route is automatic via generouted — page file in src/pages/ is enough
|
|
515
|
-
console.log(` Route: automatic (file-based routing via src/pages/)`)
|
|
516
|
-
return true
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
function printRouteInstructions(route, config) {
|
|
520
|
-
const { path: routePath } = route
|
|
521
|
-
const label = config?.name ?? routePath
|
|
522
|
-
console.log('')
|
|
523
|
-
console.log(' Add manually to src/nav.ts:')
|
|
524
|
-
console.log(` { path: '${routePath}', label: '${label}' },`)
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
// ---------------------------------------------------------------------------
|
|
528
|
-
// Dependency integration — patches package.json with declared deps
|
|
529
|
-
// ---------------------------------------------------------------------------
|
|
530
|
-
|
|
531
|
-
/**
|
|
532
|
-
* Merge `config.dependencies` and `config.devDependencies` into the
|
|
533
|
-
* target app's package.json. Existing entries win — never downgrade or
|
|
534
|
-
* overwrite a user's pin. Prints a single line telling them to run
|
|
535
|
-
* their package manager when at least one new entry was added.
|
|
536
|
-
*
|
|
537
|
-
* Why we don't shell out to `npm install` here: the scaffolded app
|
|
538
|
-
* may use any of npm / pnpm / bun / yarn. Detecting the right one
|
|
539
|
-
* heuristically (lockfile presence, packageManager field) is doable
|
|
540
|
-
* but easy to get wrong; deferring to the user keeps the contract
|
|
541
|
-
* narrow and respects whatever lockfile setup they have.
|
|
542
|
-
*/
|
|
543
|
-
function integrateDependencies(config, targetDir, options = {}) {
|
|
544
|
-
const result = { added: [], installRan: false, pm: detectPackageManager(targetDir) }
|
|
545
|
-
|
|
546
|
-
const sections = [
|
|
547
|
-
{ key: 'dependencies', label: 'dependencies' },
|
|
548
|
-
{ key: 'devDependencies', label: 'devDependencies' },
|
|
549
|
-
]
|
|
550
|
-
|
|
551
|
-
// Skip the work if nothing is declared.
|
|
552
|
-
const hasAny = sections.some((s) => config[s.key] && Object.keys(config[s.key]).length > 0)
|
|
553
|
-
if (!hasAny) return result
|
|
554
|
-
|
|
555
|
-
const pkgPath = path.join(targetDir, 'package.json')
|
|
556
|
-
if (!fs.existsSync(pkgPath)) {
|
|
557
|
-
console.log(' Could not patch package.json (file not found)')
|
|
558
|
-
return result
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
let pkg
|
|
562
|
-
try {
|
|
563
|
-
pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
|
|
564
|
-
} catch (err) {
|
|
565
|
-
console.log(` Could not patch package.json (parse error: ${err.message})`)
|
|
566
|
-
return result
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
const added = []
|
|
570
|
-
const skipped = []
|
|
571
|
-
|
|
572
|
-
for (const { key } of sections) {
|
|
573
|
-
const declared = config[key]
|
|
574
|
-
if (!declared) continue
|
|
575
|
-
pkg[key] = pkg[key] || {}
|
|
576
|
-
for (const [name, version] of Object.entries(declared)) {
|
|
577
|
-
// Skip if already declared in this OR the sibling section — never let a
|
|
578
|
-
// package land in both dependencies and devDependencies (e.g. when a
|
|
579
|
-
// feature moves a dep between sections across versions). Don't report a
|
|
580
|
-
// name we added moments ago (from the sibling section of this same
|
|
581
|
-
// config) as "already present" — that would list it as both added+skipped.
|
|
582
|
-
if (pkg.dependencies?.[name] || pkg.devDependencies?.[name]) {
|
|
583
|
-
if (!added.includes(name)) skipped.push(name)
|
|
584
|
-
continue
|
|
585
|
-
}
|
|
586
|
-
pkg[key][name] = version
|
|
587
|
-
added.push(name)
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
if (added.length === 0) {
|
|
592
|
-
console.log(' Dependencies already present')
|
|
593
|
-
return result
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
// Re-write with two-space indent (Node's de-facto default; matches
|
|
597
|
-
// what most generators / lint tools agree on).
|
|
598
|
-
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf-8')
|
|
599
|
-
console.log(` Added to package.json: ${added.join(', ')}`)
|
|
600
|
-
if (skipped.length > 0) {
|
|
601
|
-
console.log(` Already present (kept your version): ${skipped.join(', ')}`)
|
|
602
|
-
}
|
|
603
|
-
result.added = added
|
|
604
|
-
|
|
605
|
-
// Opt-in (--install): run the detected package manager for the user. By
|
|
606
|
-
// default we only patch package.json and let the "Next steps" footer tell
|
|
607
|
-
// them to install, so we never guess wrong about their PM/lockfile.
|
|
608
|
-
if (options.runInstall) {
|
|
609
|
-
console.log(`\n Installing dependencies with ${result.pm}...`)
|
|
610
|
-
// shell:true on Windows, where npm/pnpm/yarn/bun are .cmd shims that
|
|
611
|
-
// spawnSync can't exec directly.
|
|
612
|
-
const res = spawnSync(result.pm, ['install'], {
|
|
613
|
-
cwd: targetDir,
|
|
614
|
-
stdio: 'inherit',
|
|
615
|
-
shell: process.platform === 'win32',
|
|
616
|
-
})
|
|
617
|
-
if (res.error) {
|
|
618
|
-
const why = res.error.code === 'ENOENT' ? `${result.pm} not found on PATH` : res.error.message
|
|
619
|
-
console.log(` Could not launch ${result.pm} (${why}) — run "${result.pm} install" manually.`)
|
|
620
|
-
} else if (res.status === 0) {
|
|
621
|
-
result.installRan = true
|
|
622
|
-
} else {
|
|
623
|
-
console.log(` ${result.pm} install did not complete — run "${result.pm} install" manually.`)
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
return result
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
// ---------------------------------------------------------------------------
|
|
631
|
-
// Post-install instructions (for features that need manual wiring)
|
|
632
|
-
// ---------------------------------------------------------------------------
|
|
633
|
-
|
|
634
|
-
function printPostInstallInstructions(config) {
|
|
635
|
-
const instructions = config.instructions || []
|
|
636
|
-
|
|
637
|
-
if (instructions.length > 0) {
|
|
638
|
-
console.log('\n--- Manual wiring needed ---\n')
|
|
639
|
-
instructions.forEach((inst, i) => {
|
|
640
|
-
console.log(`${i + 1}. ${inst}\n`)
|
|
641
|
-
})
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
// ---------------------------------------------------------------------------
|
|
646
|
-
// Main
|
|
647
|
-
// ---------------------------------------------------------------------------
|
|
648
|
-
|
|
649
|
-
function main() {
|
|
650
|
-
// Pull the `--install` flag out first so it can appear anywhere; the rest of
|
|
651
|
-
// main() treats args[0]/args[1] as feature + dir positionally.
|
|
652
|
-
const runInstall = process.argv.slice(2).includes('--install')
|
|
653
|
-
const args = process.argv.slice(2).filter((a) => a !== '--install')
|
|
654
|
-
|
|
655
|
-
// --help
|
|
656
|
-
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
657
|
-
const features = listFeatures()
|
|
658
|
-
const groups = groupByCategory(features)
|
|
659
|
-
|
|
660
|
-
console.log('\nAdd a feature to your DeepSpace app\n')
|
|
661
|
-
console.log('Usage: deepspace add <feature> [dir]\n')
|
|
662
|
-
console.log('Commands:')
|
|
663
|
-
console.log(' <feature> [dir] Install a feature (dir defaults to .)')
|
|
664
|
-
console.log(' --list, -l List all available features')
|
|
665
|
-
console.log(' --info <feature> Show detailed info about a feature')
|
|
666
|
-
console.log(' --install Run your package manager after adding deps')
|
|
667
|
-
console.log(' --help, -h Show this help')
|
|
668
|
-
|
|
669
|
-
for (const category of CATEGORY_ORDER) {
|
|
670
|
-
const categoryFeatures = groups.get(category)
|
|
671
|
-
if (categoryFeatures && categoryFeatures.length > 0) {
|
|
672
|
-
console.log(`\n${CATEGORY_LABELS[category] || category}:`)
|
|
673
|
-
for (const f of categoryFeatures) {
|
|
674
|
-
console.log(` ${f.id.padEnd(22)} ${f.name}`)
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
const example = firstFeatureId(groups) || '<feature>'
|
|
680
|
-
console.log('\nExamples:')
|
|
681
|
-
console.log(' deepspace add --list')
|
|
682
|
-
console.log(` deepspace add ${example}`)
|
|
683
|
-
console.log(` deepspace add ${example} ./my-app`)
|
|
684
|
-
process.exit(0)
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
// --list
|
|
688
|
-
if (args[0] === '--list' || args[0] === '-l') {
|
|
689
|
-
console.log('\nAvailable Features\n')
|
|
690
|
-
|
|
691
|
-
const features = listFeatures()
|
|
692
|
-
const groups = groupByCategory(features)
|
|
693
|
-
|
|
694
|
-
for (const category of CATEGORY_ORDER) {
|
|
695
|
-
const categoryFeatures = groups.get(category)
|
|
696
|
-
if (categoryFeatures && categoryFeatures.length > 0) {
|
|
697
|
-
console.log(`${CATEGORY_LABELS[category] || category}:`)
|
|
698
|
-
for (const f of categoryFeatures) {
|
|
699
|
-
console.log(` ${f.id.padEnd(22)} ${f.name.padEnd(24)} ${f.description}`)
|
|
700
|
-
}
|
|
701
|
-
console.log('')
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
console.log('Use: deepspace add <feature>')
|
|
706
|
-
process.exit(0)
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
// --info
|
|
710
|
-
if (args[0] === '--info' || args[0] === '-i') {
|
|
711
|
-
const featureId = args[1]
|
|
712
|
-
if (!featureId) {
|
|
713
|
-
console.error('\nError: Please specify a feature ID')
|
|
714
|
-
process.exit(1)
|
|
715
|
-
}
|
|
716
|
-
const config = loadFeatureConfig(featureId)
|
|
717
|
-
if (!config) unknownFeature(featureId)
|
|
718
|
-
|
|
719
|
-
console.log(`\n${config.name} (${config.id})`)
|
|
720
|
-
if (config.category) {
|
|
721
|
-
console.log(` Category: ${CATEGORY_LABELS[config.category] || config.category}`)
|
|
722
|
-
}
|
|
723
|
-
console.log(` ${config.description}\n`)
|
|
724
|
-
console.log(` ${config.details}\n`)
|
|
725
|
-
|
|
726
|
-
console.log(' Files:')
|
|
727
|
-
config.files.forEach((f) =>
|
|
728
|
-
console.log(` - ${f.src} -> ${resolveDestPath(f.dest, config.route)}`),
|
|
729
|
-
)
|
|
730
|
-
|
|
731
|
-
if (config.instructions && config.instructions.length > 0) {
|
|
732
|
-
console.log('\n Integration steps:')
|
|
733
|
-
config.instructions.forEach((inst, i) => console.log(` ${i + 1}. ${inst}`))
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
if (config.patterns && config.patterns.length > 0) {
|
|
737
|
-
console.log('\n Patterns:')
|
|
738
|
-
config.patterns.forEach((p) => console.log(` - ${p}`))
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
if (config.example) {
|
|
742
|
-
console.log('\n Example:')
|
|
743
|
-
config.example.split('\n').forEach((line) => console.log(` ${line}`))
|
|
744
|
-
}
|
|
745
|
-
console.log('')
|
|
746
|
-
process.exit(0)
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
// Install feature
|
|
750
|
-
const featureId = args[0]
|
|
751
|
-
const rawDir = args[1]
|
|
752
|
-
const targetDir = resolveTargetDir(rawDir)
|
|
753
|
-
|
|
754
|
-
if (!targetDir) {
|
|
755
|
-
console.error('\nError: Please specify an app directory')
|
|
756
|
-
console.error('Usage: deepspace add <feature> [dir]')
|
|
757
|
-
process.exit(1)
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
const config = loadFeatureConfig(featureId)
|
|
761
|
-
if (!config) unknownFeature(featureId)
|
|
762
|
-
|
|
763
|
-
if (!fs.existsSync(targetDir)) {
|
|
764
|
-
console.error(`\nError: Target directory not found: ${targetDir}`)
|
|
765
|
-
process.exit(1)
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
console.log(`\nInstalling: ${config.name}`)
|
|
769
|
-
console.log(` ${config.description}`)
|
|
770
|
-
console.log(` Target: ${targetDir}`)
|
|
771
|
-
|
|
772
|
-
const { copied, skipped } = installFeature(config, targetDir)
|
|
773
|
-
|
|
774
|
-
console.log(`\nCopied ${copied} file(s)${skipped > 0 ? `, skipped ${skipped} existing` : ''}`)
|
|
775
|
-
|
|
776
|
-
// Auto-integrate schema, actions, CSS, and route
|
|
777
|
-
integrateSchema(config, targetDir)
|
|
778
|
-
integrateActions(config, targetDir)
|
|
779
|
-
integrateCss(config, targetDir)
|
|
780
|
-
integrateRoute(config, targetDir)
|
|
781
|
-
|
|
782
|
-
// Add any new npm dependencies the feature declares to the target's
|
|
783
|
-
// package.json. Without this, copied source files import packages
|
|
784
|
-
// that don't exist in node_modules and the next `vite dev` blows up
|
|
785
|
-
// with a hard-to-diagnose "Failed to resolve import" error. By default we
|
|
786
|
-
// only patch package.json (the "Next steps" footer tells the user to
|
|
787
|
-
// install) so we never guess wrong about their package manager + lockfile;
|
|
788
|
-
// `--install` opts into running it for them.
|
|
789
|
-
const deps = integrateDependencies(config, targetDir, { runInstall })
|
|
790
|
-
|
|
791
|
-
// Print any remaining manual wiring instructions
|
|
792
|
-
printPostInstallInstructions(config)
|
|
793
|
-
|
|
794
|
-
if (config.patterns && config.patterns.length > 0) {
|
|
795
|
-
console.log('--- Key patterns ---\n')
|
|
796
|
-
config.patterns.forEach((p) => console.log(`- ${p}`))
|
|
797
|
-
console.log('')
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
// Warn when installing a nav feature alongside another (they're exclusive).
|
|
801
|
-
warnNavConflicts(config, targetDir)
|
|
802
|
-
|
|
803
|
-
// Always close with concrete next steps.
|
|
804
|
-
printNextSteps(config, deps)
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
main()
|