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