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.
- package/README.md +90 -0
- package/dist/cli.js +3590 -1485
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +876 -3664
- package/dist/index.js +511 -366
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +132 -61
- package/dist/server.js +149 -110
- package/dist/server.js.map +1 -1
- package/dist/testing.js.map +1 -1
- package/dist/worker.d.ts +128 -59
- package/dist/worker.js +144 -99
- package/dist/worker.js.map +1 -1
- package/features/admin-page/src/AdminPage.tsx +3 -5
- package/features/canvas/src/CanvasEditorPage.tsx +0 -2
- package/features/canvas/src/components/CanvasView.tsx +1 -1
- package/features/docs/src/LibrarySidebar.tsx +7 -1
- package/features/docs/src/editor/useDocEditor.ts +5 -1
- package/features/docs/src/use-yjs-room-with-awareness.ts +4 -0
- package/features/file-attachments/src/AttachmentsPage.tsx +7 -1
- package/features/file-attachments/src/useFilePreview.ts +7 -5
- package/features/file-manager/src/FileManagerPage.tsx +2 -3
- package/features/integration-test/feature.json +10 -3
- package/features/integration-test/src/IntegrationTestPage.tsx +15 -12
- package/features/landing/src/LandingPage.tsx +1 -1
- package/features/landing/src/sections/FeaturesGridSection.tsx +0 -92
- package/features/messaging/src/ChatPage.tsx +6 -1
- package/features/messaging/src/components/chat/MessageItem.tsx +5 -1
- package/features/messaging/src/hooks/useChatChannel.ts +23 -7
- package/features/presence-test/feature.json +2 -1
- package/features/sidebar/src/AppSidebar.tsx +1 -1
- package/features/tasks/src/TasksPage.tsx +1 -1
- package/features/testing/feature.json +2 -1
- package/features/testing/src/TestPage.tsx +8 -8
- package/package.json +8 -4
- package/scripts/add-feature.cjs +296 -282
- package/dist/testing.d.ts +0 -115
package/scripts/add-feature.cjs
CHANGED
|
@@ -8,30 +8,32 @@
|
|
|
8
8
|
* Invoked by the `deepspace add` CLI in src/cli/commands/add.ts.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
const
|
|
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
|
-
|
|
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/(
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
//
|
|
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
|
|
106
|
-
|
|
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
|
-
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
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
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
return
|
|
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 =
|
|
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
|
-
|
|
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 =>
|
|
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()
|