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.
Files changed (63) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +22 -8
  3. package/dist/cli.js +10348 -5025
  4. package/dist/cli.js.map +1 -1
  5. package/dist/feature-installer.js +638 -0
  6. package/dist/feature-installer.js.map +1 -0
  7. package/dist/index.d.ts +229 -116
  8. package/dist/index.js +767 -788
  9. package/dist/index.js.map +1 -1
  10. package/dist/server.d.ts +403 -167
  11. package/dist/server.js +498 -367
  12. package/dist/server.js.map +1 -1
  13. package/dist/testing.d.ts +1 -1
  14. package/dist/testing.js +3 -3
  15. package/dist/testing.js.map +1 -1
  16. package/dist/worker.d.ts +541 -159
  17. package/dist/worker.js +611 -367
  18. package/dist/worker.js.map +1 -1
  19. package/features/ai-chat/feature.json +2 -0
  20. package/features/ai-chat/src/AiChatPage.tsx +2 -3
  21. package/features/ai-chat/src/ChatPanel.messages.tsx +344 -0
  22. package/features/ai-chat/src/ChatPanel.stream.ts +397 -0
  23. package/features/ai-chat/src/ChatPanel.tsx +159 -825
  24. package/features/ai-chat/src/__tests__/ChatPanel.stream.test.ts +163 -0
  25. package/features/canvas/src/CanvasListPage.tsx +1 -1
  26. package/features/docs/feature.json +4 -8
  27. package/features/docs/src/DocsEditorPage.tsx +33 -326
  28. package/features/docs/src/DocsListPage.tsx +33 -29
  29. package/features/docs/src/InviteDialog.tsx +11 -40
  30. package/features/docs/src/__tests__/docs-presence-access.test.ts +71 -0
  31. package/features/docs/src/docs-library-types.ts +28 -3
  32. package/features/docs/src/docs-ui.css +1 -4
  33. package/features/docs/src/use-docs-presence-access.ts +330 -0
  34. package/features/file-attachments/feature.json +2 -2
  35. package/features/file-attachments/src/AttachmentPreview.tsx +428 -0
  36. package/features/file-attachments/src/AttachmentsPage.tsx +100 -462
  37. package/features/file-attachments/src/useFilePreview.ts +54 -68
  38. package/features/file-manager/src/FileManagerPage.tsx +1 -1
  39. package/features/integration-test/src/integration.spec.ts +1 -1
  40. package/features/items/feature.json +1 -5
  41. package/features/items/src/ItemsPage.tsx +207 -135
  42. package/features/landing/feature.json +3 -0
  43. package/features/landing/src/LandingPage.tsx +4 -18
  44. package/features/leaderboard/feature.json +1 -5
  45. package/features/leaderboard/src/LeaderboardPage.tsx +333 -198
  46. package/features/messaging/feature.json +0 -4
  47. package/features/messaging/src/ChatMultiPage.tsx +1 -3
  48. package/features/messaging/src/ChatPage.tsx +1 -3
  49. package/features/messaging/src/components/chat/MessageActionSheet.tsx +40 -16
  50. package/features/search-bar/feature.json +8 -3
  51. package/features/search-bar/src/SearchOverlay.tsx +108 -252
  52. package/features/search-bar/src/__tests__/search-model.test.ts +40 -0
  53. package/features/search-bar/src/search-model.ts +223 -0
  54. package/features/tasks/src/TasksPage.tsx +3 -3
  55. package/features/topbar/src/Topbar.tsx +0 -17
  56. package/package.json +1 -2
  57. package/features/docs/src/docs-favorites.ts +0 -18
  58. package/features/docs/src/use-yjs-room-with-awareness.ts +0 -289
  59. package/features/file-attachments/src/attachments-constants.ts +0 -9
  60. package/features/items/src/items-constants.ts +0 -10
  61. package/features/leaderboard/src/leaderboard-constants.ts +0 -21
  62. package/features/messaging/src/components/chat/ReactionPicker.tsx +0 -2
  63. package/scripts/add-feature.cjs +0 -807
@@ -0,0 +1,638 @@
1
+ // src/cli/commands/feature-installer.ts
2
+ import {
3
+ closeSync,
4
+ copyFileSync,
5
+ existsSync as existsSync2,
6
+ fstatSync,
7
+ mkdirSync,
8
+ mkdtempSync,
9
+ openSync,
10
+ readFileSync as readFileSync2,
11
+ readSync,
12
+ readdirSync,
13
+ rmSync,
14
+ writeFileSync
15
+ } from "fs";
16
+ import { spawnSync } from "child_process";
17
+ import { tmpdir } from "os";
18
+ import { dirname, isAbsolute, join as join2, resolve } from "path";
19
+ import { fileURLToPath } from "url";
20
+
21
+ // src/cli/lib/package-manager.ts
22
+ import { existsSync, readFileSync } from "fs";
23
+ import { join } from "path";
24
+ function detectPackageManager(appDir) {
25
+ try {
26
+ const pkg = JSON.parse(readFileSync(join(appDir, "package.json"), "utf-8"));
27
+ const name = typeof pkg.packageManager === "string" ? pkg.packageManager.split("@")[0] : "";
28
+ if (name === "npm" || name === "pnpm" || name === "yarn" || name === "bun") return name;
29
+ } catch {
30
+ }
31
+ if (existsSync(join(appDir, "pnpm-lock.yaml"))) return "pnpm";
32
+ if (existsSync(join(appDir, "yarn.lock"))) return "yarn";
33
+ if (existsSync(join(appDir, "bun.lock")) || existsSync(join(appDir, "bun.lockb"))) return "bun";
34
+ return "npm";
35
+ }
36
+
37
+ // src/cli/commands/feature-installer.ts
38
+ var NAV_MARKER = "// \u2500\u2500 Features add nav items below this line \u2500\u2500";
39
+ function featurePackageRoot(moduleUrl = import.meta.url) {
40
+ let dir = dirname(fileURLToPath(moduleUrl));
41
+ for (; ; ) {
42
+ const packagePath = join2(dir, "package.json");
43
+ if (existsSync2(packagePath)) {
44
+ try {
45
+ const pkg = JSON.parse(readFileSync2(packagePath, "utf-8"));
46
+ if (pkg.name === "deepspace") return dir;
47
+ } catch {
48
+ }
49
+ }
50
+ const parent = dirname(dir);
51
+ if (parent === dir) throw new Error("Could not locate the deepspace package root");
52
+ dir = parent;
53
+ }
54
+ }
55
+ function hasFeatureCatalog(packageRoot = featurePackageRoot()) {
56
+ return existsSync2(join2(packageRoot, "features"));
57
+ }
58
+ function readFeatureConfig(id, packageRoot = featurePackageRoot()) {
59
+ const configPath = join2(packageRoot, "features", id, "feature.json");
60
+ if (!existsSync2(configPath)) return null;
61
+ try {
62
+ const config = JSON.parse(readFileSync2(configPath, "utf-8"));
63
+ if (typeof config.id !== "string" || typeof config.name !== "string" || !Array.isArray(config.files)) {
64
+ return null;
65
+ }
66
+ return config;
67
+ } catch {
68
+ return null;
69
+ }
70
+ }
71
+ function listFeatureConfigs(packageRoot = featurePackageRoot()) {
72
+ const featuresDir = join2(packageRoot, "features");
73
+ if (!existsSync2(featuresDir)) return [];
74
+ return readdirSync(featuresDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => readFeatureConfig(entry.name, packageRoot)).filter((config) => config !== null && !config.internal).sort((a, b) => a.id.localeCompare(b.id));
75
+ }
76
+ function featureCatalog(packageRoot = featurePackageRoot()) {
77
+ return listFeatureConfigs(packageRoot).map((config) => ({
78
+ id: config.id,
79
+ name: config.name,
80
+ category: config.category ?? null,
81
+ description: config.description ?? null
82
+ }));
83
+ }
84
+ function editDistance(a, b) {
85
+ const row = Array.from({ length: b.length + 1 }, (_, index) => index);
86
+ for (let i = 1; i <= a.length; i++) {
87
+ let previous = row[0];
88
+ row[0] = i;
89
+ for (let j = 1; j <= b.length; j++) {
90
+ const current = row[j];
91
+ row[j] = Math.min(row[j] + 1, row[j - 1] + 1, previous + (a[i - 1] === b[j - 1] ? 0 : 1));
92
+ previous = current;
93
+ }
94
+ }
95
+ return row[b.length];
96
+ }
97
+ function suggestFeature(featureId, packageRoot = featurePackageRoot()) {
98
+ let closest = null;
99
+ let closestDistance = Number.POSITIVE_INFINITY;
100
+ for (const feature of listFeatureConfigs(packageRoot)) {
101
+ const distance = editDistance(featureId, feature.id);
102
+ if (distance < closestDistance) {
103
+ closest = feature.id;
104
+ closestDistance = distance;
105
+ }
106
+ }
107
+ const threshold = Math.max(2, Math.floor(featureId.length / 3));
108
+ return closest !== null && closestDistance <= threshold ? closest : null;
109
+ }
110
+ function resolveTargetDir(raw) {
111
+ return isAbsolute(raw) ? raw : resolve(raw);
112
+ }
113
+ function resolveFeatureDestPath(fileDest, route) {
114
+ const pages = "src/pages/";
115
+ const appPages = "src/pages/(app)/";
116
+ if (!fileDest.startsWith(pages) || fileDest.startsWith(appPages)) return fileDest;
117
+ const rest = fileDest.slice(pages.length);
118
+ return route?.protected === false ? `${appPages}${rest}` : `${appPages}(protected)/${rest}`;
119
+ }
120
+ function copyFile(src, dest) {
121
+ mkdirSync(dirname(dest), { recursive: true });
122
+ copyFileSync(src, dest);
123
+ }
124
+ function copyFileWithImportShift(src, dest, levels) {
125
+ mkdirSync(dirname(dest), { recursive: true });
126
+ const prefix = "../".repeat(levels);
127
+ const shifted = readFileSync2(src, "utf-8").replace(
128
+ /(from\s+|import\s+|import\()(['"])(\.\.\/)/g,
129
+ `$1$2${prefix}$3`
130
+ );
131
+ writeFileSync(dest, shifted);
132
+ }
133
+ function installFiles(packageRoot, config, targetDir, emit) {
134
+ let copied = 0;
135
+ let skipped = 0;
136
+ const created = [];
137
+ const preexisting = [];
138
+ const missing = [];
139
+ for (const file of config.files) {
140
+ const srcPath = join2(packageRoot, "features", config.id, file.src);
141
+ const resolvedDest = resolveFeatureDestPath(file.dest, config.route);
142
+ const destPath = join2(targetDir, resolvedDest);
143
+ const existed = existsSync2(destPath);
144
+ if (existed) preexisting.push(resolvedDest);
145
+ if (!existsSync2(srcPath)) {
146
+ emit(` Warning: source not found: ${file.src}`, "stderr");
147
+ if (!existed) missing.push(resolvedDest);
148
+ continue;
149
+ }
150
+ if (existed && !file.overwrite) {
151
+ emit(` Exists (skipped): ${resolvedDest}`);
152
+ skipped++;
153
+ continue;
154
+ }
155
+ const wasRedirected = resolvedDest !== file.dest;
156
+ const shiftLevels = resolvedDest.split("/").length - file.dest.split("/").length;
157
+ if (wasRedirected) copyFileWithImportShift(srcPath, destPath, shiftLevels);
158
+ else copyFile(srcPath, destPath);
159
+ emit(` ${existed ? "Overwrote" : "Copied"}: ${resolvedDest}`);
160
+ if (!existed) created.push(resolvedDest);
161
+ copied++;
162
+ }
163
+ return { copied, skipped, files: { created, preexisting, missing } };
164
+ }
165
+ function unresolvedSchema(schema, reason, emit) {
166
+ const entry = schema.spreadOperator ? `...${schema.exportName}` : schema.exportName;
167
+ emit("");
168
+ emit(" Add manually to src/schemas.ts:");
169
+ emit(` import { ${schema.exportName} } from '${schema.importPath}'`);
170
+ emit(` // then add ${entry} to the schemas array`);
171
+ return {
172
+ area: "schema",
173
+ reason,
174
+ steps: [
175
+ `Add import { ${schema.exportName} } from '${schema.importPath}' to src/schemas.ts.`,
176
+ `Add ${entry} to the exported schemas array.`
177
+ ]
178
+ };
179
+ }
180
+ function integrateSchema(config, targetDir, emit) {
181
+ if (!config.schema) return [];
182
+ const schemasPath = join2(targetDir, "src", "schemas.ts");
183
+ if (!existsSync2(schemasPath)) {
184
+ return [unresolvedSchema(config.schema, "src/schemas.ts was not found.", emit)];
185
+ }
186
+ let content = readFileSync2(schemasPath, "utf-8");
187
+ const { exportName, importPath, spreadOperator } = config.schema;
188
+ const importPattern = new RegExp(`^import\\s+\\{[^}]*\\b${exportName}\\b`, "m");
189
+ if (importPattern.test(content)) {
190
+ emit(` Schema already present: ${exportName}`);
191
+ return [];
192
+ }
193
+ if (!content.includes("export const schemas") || !/export const schemas:\s*CollectionSchema\[\]\s*=\s*\[/.test(content)) {
194
+ const reason = "src/schemas.ts has an unexpected structure.";
195
+ emit(` Could not auto-integrate schema (${reason})`);
196
+ return [unresolvedSchema(config.schema, reason, emit)];
197
+ }
198
+ const importLine = `import { ${exportName} } from '${importPath}'`;
199
+ const withImport = content.replace(
200
+ /export const schemas/,
201
+ `${importLine}
202
+
203
+ export const schemas`
204
+ );
205
+ const schemaEntry = spreadOperator ? `...${exportName}` : exportName;
206
+ const withEntry = withImport.replace(
207
+ /export const schemas:\s*CollectionSchema\[\]\s*=\s*\[/,
208
+ `export const schemas: CollectionSchema[] = [
209
+ ${schemaEntry},`
210
+ );
211
+ if (withImport === content || withEntry === withImport) {
212
+ const reason = "src/schemas.ts could not be updated safely.";
213
+ emit(` Could not auto-integrate schema (${reason})`);
214
+ return [unresolvedSchema(config.schema, reason, emit)];
215
+ }
216
+ content = withEntry;
217
+ writeFileSync(schemasPath, content);
218
+ emit(` Schema integrated: ${exportName} -> schemas.ts`);
219
+ return [];
220
+ }
221
+ function unresolvedActions(actions, reason, emit) {
222
+ emit("");
223
+ emit(" Add manually to src/actions/index.ts:");
224
+ for (const action of actions) {
225
+ emit(` import { ${action.exportName} } from '${action.importPath}'`);
226
+ emit(` // then add '${action.registerAs}': ${action.exportName} to the actions object`);
227
+ }
228
+ return {
229
+ area: "actions",
230
+ reason,
231
+ steps: actions.flatMap((action) => [
232
+ `Add import { ${action.exportName} } from '${action.importPath}' to src/actions/index.ts.`,
233
+ `Register '${action.registerAs}': ${action.exportName} in the exported actions object.`
234
+ ])
235
+ };
236
+ }
237
+ function integrateActions(config, targetDir, emit) {
238
+ if (!config.actions?.length) return [];
239
+ const actionsPath = join2(targetDir, "src", "actions", "index.ts");
240
+ if (!existsSync2(actionsPath)) {
241
+ return [unresolvedActions(config.actions, "src/actions/index.ts was not found.", emit)];
242
+ }
243
+ let content = readFileSync2(actionsPath, "utf-8");
244
+ const registryPattern = /export const actions:\s*Record<string,\s*ActionHandler(?:<[^>]*>)?>\s*=\s*\{/;
245
+ if (!registryPattern.test(content)) {
246
+ const reason = "src/actions/index.ts has an unexpected structure.";
247
+ emit(` Could not auto-integrate actions (${reason})`);
248
+ return [unresolvedActions(config.actions, reason, emit)];
249
+ }
250
+ let integrated = 0;
251
+ const unresolved = [];
252
+ for (const action of config.actions) {
253
+ const importPattern = new RegExp(`^import\\s+\\{[^}]*\\b${action.exportName}\\b`, "m");
254
+ if (importPattern.test(content)) {
255
+ emit(` Action already present: ${action.registerAs}`);
256
+ continue;
257
+ }
258
+ const importLine = `import { ${action.exportName} } from '${action.importPath}'`;
259
+ const withImport = content.replace(
260
+ /export const actions:/,
261
+ `${importLine}
262
+
263
+ export const actions:`
264
+ );
265
+ const registryEntry = ` '${action.registerAs}': ${action.exportName},`;
266
+ const withEntry = withImport.replace(registryPattern, (match) => `${match}
267
+ ${registryEntry}`);
268
+ if (withImport === content || withEntry === withImport) {
269
+ emit(` Could not integrate action: ${action.registerAs}`);
270
+ unresolved.push(
271
+ unresolvedActions(
272
+ [action],
273
+ `Action '${action.registerAs}' could not be registered safely.`,
274
+ emit
275
+ )
276
+ );
277
+ continue;
278
+ }
279
+ content = withEntry;
280
+ integrated++;
281
+ emit(` Action integrated: ${action.registerAs} (${action.exportName})`);
282
+ }
283
+ if (integrated > 0) writeFileSync(actionsPath, content);
284
+ return unresolved;
285
+ }
286
+ function integrateCss(packageRoot, config, targetDir, emit) {
287
+ if (!config.css?.length) return [];
288
+ const stylesPath = join2(targetDir, "src", "styles.css");
289
+ if (!existsSync2(stylesPath)) {
290
+ emit(" Warning: Cannot integrate CSS \u2014 src/styles.css not found");
291
+ return [
292
+ {
293
+ area: "css",
294
+ reason: "src/styles.css was not found.",
295
+ steps: config.css.map(
296
+ (cssFile) => `Append node_modules/deepspace/features/${config.id}/${cssFile} to src/styles.css.`
297
+ )
298
+ }
299
+ ];
300
+ }
301
+ let styles = readFileSync2(stylesPath, "utf-8");
302
+ let integrated = 0;
303
+ const unresolved = [];
304
+ for (const cssFile of config.css) {
305
+ const srcPath = join2(packageRoot, "features", config.id, cssFile);
306
+ if (!existsSync2(srcPath)) {
307
+ emit(` Warning: CSS source not found: ${cssFile}`, "stderr");
308
+ unresolved.push({
309
+ area: "css",
310
+ reason: `Feature CSS source '${cssFile}' was not found in the installed package.`,
311
+ steps: ["Reinstall the current deepspace package, then retry this feature installation."]
312
+ });
313
+ continue;
314
+ }
315
+ const css = readFileSync2(srcPath, "utf-8");
316
+ const fingerprint = css.match(/\.([\w-]+)\s*\{|@keyframes\s+([\w-]+)/);
317
+ const marker = fingerprint?.[1] ?? fingerprint?.[2];
318
+ if (marker && styles.includes(marker)) {
319
+ emit(` CSS already present: ${cssFile} (found .${marker})`);
320
+ continue;
321
+ }
322
+ styles += `
323
+ ${css}`;
324
+ integrated++;
325
+ emit(` CSS integrated: ${cssFile} -> styles.css`);
326
+ }
327
+ if (integrated > 0) writeFileSync(stylesPath, styles);
328
+ return unresolved;
329
+ }
330
+ function unresolvedRoute(route, config, reason, emit) {
331
+ emit("");
332
+ emit(" Add manually to src/nav.ts:");
333
+ emit(` { path: '${route.path}', label: '${config.name}' },`);
334
+ return {
335
+ area: "nav",
336
+ reason,
337
+ steps: [`Add { path: '${route.path}', label: '${config.name}' } to the exported nav array.`]
338
+ };
339
+ }
340
+ function integrateRoute(config, targetDir, emit) {
341
+ if (!config.route) return [];
342
+ const navPath = join2(targetDir, "src", "nav.ts");
343
+ if (!existsSync2(navPath)) {
344
+ return [unresolvedRoute(config.route, config, "src/nav.ts was not found.", emit)];
345
+ }
346
+ let content = readFileSync2(navPath, "utf-8");
347
+ const routePath = config.route.path;
348
+ if (content.includes(`'${routePath}'`)) {
349
+ emit(` Nav already present: ${routePath}`);
350
+ return [];
351
+ }
352
+ if (!content.includes(NAV_MARKER)) {
353
+ emit(" Could not auto-wire nav (marker not found in nav.ts)");
354
+ return [
355
+ unresolvedRoute(
356
+ config.route,
357
+ config,
358
+ "The feature insertion marker was not found in src/nav.ts.",
359
+ emit
360
+ )
361
+ ];
362
+ }
363
+ const roles = config.route.protected === false ? "" : ", roles: ['member' as Role]";
364
+ const entry = ` { path: '${routePath}', label: '${config.name}'${roles} },`;
365
+ content = content.replace(NAV_MARKER, `${NAV_MARKER}
366
+ ${entry}`);
367
+ writeFileSync(navPath, content);
368
+ emit(` Nav wired: ${routePath} (${config.name})`);
369
+ emit(" Route: automatic (file-based routing via src/pages/)");
370
+ return [];
371
+ }
372
+ var INSTALL_DIAGNOSTIC_BYTES = 16 * 1024;
373
+ function readDiagnosticTail(outputFd) {
374
+ const size = fstatSync(outputFd).size;
375
+ if (size === 0) return void 0;
376
+ const length = Math.min(size, INSTALL_DIAGNOSTIC_BYTES);
377
+ const buffer = Buffer.alloc(length);
378
+ readSync(outputFd, buffer, 0, length, size - length);
379
+ const tail = buffer.toString("utf-8").trim();
380
+ if (!tail) return void 0;
381
+ return size > length ? `[last ${length} bytes]
382
+ ${tail}` : tail;
383
+ }
384
+ function runPackageInstall(packageManager, targetDir, outputVisible) {
385
+ const options = {
386
+ cwd: targetDir,
387
+ shell: process.platform === "win32"
388
+ };
389
+ if (outputVisible) {
390
+ const child = spawnSync(packageManager, ["install"], { ...options, stdio: "inherit" });
391
+ return { error: child.error, status: child.status };
392
+ }
393
+ const captureDir = mkdtempSync(join2(tmpdir(), "deepspace-add-install-"));
394
+ let outputFd;
395
+ try {
396
+ outputFd = openSync(join2(captureDir, "output.log"), "w+");
397
+ const child = spawnSync(packageManager, ["install"], {
398
+ ...options,
399
+ stdio: ["ignore", outputFd, outputFd]
400
+ });
401
+ return {
402
+ error: child.error,
403
+ status: child.status,
404
+ diagnosticTail: readDiagnosticTail(outputFd)
405
+ };
406
+ } finally {
407
+ if (outputFd !== void 0) closeSync(outputFd);
408
+ rmSync(captureDir, { recursive: true, force: true });
409
+ }
410
+ }
411
+ function integrateDependencies(config, targetDir, runInstall, outputVisible, emit) {
412
+ const packageManager = detectPackageManager(targetDir);
413
+ const declarations = [
414
+ { section: "dependencies", values: config.dependencies },
415
+ { section: "devDependencies", values: config.devDependencies }
416
+ ];
417
+ if (declarations.every(({ values }) => !values || Object.keys(values).length === 0)) {
418
+ return { added: [], installRan: false, packageManager };
419
+ }
420
+ const packagePath = join2(targetDir, "package.json");
421
+ if (!existsSync2(packagePath)) {
422
+ emit(" Could not patch package.json (file not found)");
423
+ return { added: [], installRan: false, packageManager };
424
+ }
425
+ let pkg;
426
+ try {
427
+ pkg = JSON.parse(readFileSync2(packagePath, "utf-8"));
428
+ } catch (error) {
429
+ emit(
430
+ ` Could not patch package.json (parse error: ${error instanceof Error ? error.message : String(error)})`
431
+ );
432
+ return { added: [], installRan: false, packageManager };
433
+ }
434
+ const added = [];
435
+ const skipped = [];
436
+ for (const { section, values } of declarations) {
437
+ if (!values) continue;
438
+ pkg[section] ??= {};
439
+ for (const [name, version] of Object.entries(values)) {
440
+ if (pkg.dependencies?.[name] || pkg.devDependencies?.[name]) {
441
+ if (!added.some((dependency) => dependency.name === name)) skipped.push(name);
442
+ continue;
443
+ }
444
+ pkg[section][name] = version;
445
+ added.push({ name, section, version });
446
+ }
447
+ }
448
+ if (added.length === 0) {
449
+ emit(" Dependencies already present");
450
+ } else {
451
+ writeFileSync(packagePath, `${JSON.stringify(pkg, null, 2)}
452
+ `, "utf-8");
453
+ emit(` Added to package.json: ${added.map(({ name }) => name).join(", ")}`);
454
+ if (skipped.length) emit(` Already present (kept your version): ${skipped.join(", ")}`);
455
+ }
456
+ if (!runInstall) return { added, installRan: false, packageManager };
457
+ emit("");
458
+ emit(` Installing dependencies with ${packageManager}...`);
459
+ const child = runPackageInstall(packageManager, targetDir, outputVisible);
460
+ if (child.error) {
461
+ const error = child.error;
462
+ const reason = error.code === "ENOENT" ? `${packageManager} not found on PATH` : error.message;
463
+ return {
464
+ added,
465
+ installRan: false,
466
+ installError: `Could not launch ${packageManager} (${reason}).`,
467
+ diagnosticTail: child.diagnosticTail,
468
+ packageManager
469
+ };
470
+ }
471
+ if (child.status !== 0) {
472
+ const diagnostic = child.diagnosticTail ? `
473
+ Diagnostic tail:
474
+ ${child.diagnosticTail}` : "";
475
+ return {
476
+ added,
477
+ installRan: false,
478
+ installError: `${packageManager} install did not complete (exit ${child.status ?? "unknown"}).${diagnostic}`,
479
+ diagnosticTail: child.diagnosticTail,
480
+ packageManager
481
+ };
482
+ }
483
+ return { added, installRan: true, packageManager };
484
+ }
485
+ function dependencySnapshot(targetDir) {
486
+ try {
487
+ const pkg = JSON.parse(readFileSync2(join2(targetDir, "package.json"), "utf-8"));
488
+ return {
489
+ dependencies: pkg.dependencies ?? {},
490
+ devDependencies: pkg.devDependencies ?? {}
491
+ };
492
+ } catch {
493
+ return { dependencies: {}, devDependencies: {} };
494
+ }
495
+ }
496
+ function installedNavFeatures(packageRoot, targetDir) {
497
+ return listFeatureConfigs(packageRoot).filter((feature) => feature.category === "nav" && feature.files.length > 0).filter((feature) => {
498
+ const signature = resolveFeatureDestPath(feature.files[0].dest, feature.route);
499
+ return existsSync2(join2(targetDir, signature));
500
+ }).map((feature) => feature.id);
501
+ }
502
+ function warnNavConflicts(packageRoot, config, targetDir, emit) {
503
+ if (config.category !== "nav") return;
504
+ const present = installedNavFeatures(packageRoot, targetDir);
505
+ if (present.length <= 1) return;
506
+ emit("");
507
+ emit(`\u26A0 Multiple navigation features present (${present.join(", ")}).`);
508
+ emit(" sidebar, topbar, and tree are mutually exclusive \u2014 keep one and remove the");
509
+ emit(" others to avoid competing navigation in your layout.");
510
+ }
511
+ function installFeature(options) {
512
+ const packageRoot = featurePackageRoot();
513
+ const targetDir = resolveTargetDir(options.targetDir);
514
+ const config = readFeatureConfig(options.feature, packageRoot);
515
+ if (!config) {
516
+ const suggestion = suggestFeature(options.feature, packageRoot);
517
+ return {
518
+ ok: false,
519
+ code: "unknown_feature",
520
+ error: [
521
+ `Unknown feature: ${options.feature}`,
522
+ ...suggestion ? [`Did you mean "${suggestion}"?`] : []
523
+ ].join("\n"),
524
+ data: {
525
+ requestedFeature: options.feature,
526
+ suggestedFeature: suggestion,
527
+ availableFeatures: featureCatalog(packageRoot)
528
+ }
529
+ };
530
+ }
531
+ const emit = options.emit ?? (() => void 0);
532
+ emit("");
533
+ emit(`Installing: ${config.name}`);
534
+ emit(` ${config.description ?? ""}`);
535
+ emit(` Target: ${targetDir}`);
536
+ const installedFiles = installFiles(packageRoot, config, targetDir, emit);
537
+ emit("");
538
+ emit(
539
+ `Copied ${installedFiles.copied} file(s)${installedFiles.skipped > 0 ? `, skipped ${installedFiles.skipped} existing` : ""}`
540
+ );
541
+ const unresolvedIntegrations = [
542
+ ...integrateSchema(config, targetDir, emit),
543
+ ...integrateActions(config, targetDir, emit),
544
+ ...integrateCss(packageRoot, config, targetDir, emit),
545
+ ...integrateRoute(config, targetDir, emit)
546
+ ];
547
+ const dependencyResult = integrateDependencies(
548
+ config,
549
+ targetDir,
550
+ options.installDependencies,
551
+ options.emit !== void 0,
552
+ emit
553
+ );
554
+ if (config.instructions?.length) {
555
+ emit("");
556
+ emit("--- Manual wiring needed ---");
557
+ emit("");
558
+ config.instructions.forEach((instruction, index) => {
559
+ emit(`${index + 1}. ${instruction}`);
560
+ emit("");
561
+ });
562
+ }
563
+ if (config.patterns?.length) {
564
+ emit("--- Key patterns ---");
565
+ emit("");
566
+ config.patterns.forEach((pattern) => emit(`- ${pattern}`));
567
+ emit("");
568
+ }
569
+ warnNavConflicts(packageRoot, config, targetDir, emit);
570
+ const dependenciesAfter = dependencySnapshot(targetDir);
571
+ const declaredDependencies = [
572
+ ...Object.keys(config.dependencies ?? {}),
573
+ ...Object.keys(config.devDependencies ?? {})
574
+ ];
575
+ const missingDeclarations = declaredDependencies.filter(
576
+ (name) => !(name in dependenciesAfter.dependencies) && !(name in dependenciesAfter.devDependencies)
577
+ );
578
+ const installed = installedFiles.files.missing.length === 0 && missingDeclarations.length === 0 && unresolvedIntegrations.length === 0 && dependencyResult.installError === void 0;
579
+ const dependencyInstallRequired = installed && dependencyResult.added.length > 0 && !options.installDependencies && !dependencyResult.installRan;
580
+ const data = {
581
+ feature: config.id,
582
+ dir: targetDir,
583
+ installed,
584
+ observedChanges: {
585
+ files: installedFiles.files,
586
+ dependencies: {
587
+ added: dependencyResult.added,
588
+ missingDeclarations,
589
+ installRequested: options.installDependencies,
590
+ ...dependencyResult.diagnosticTail ? { diagnosticTail: dependencyResult.diagnosticTail } : {}
591
+ }
592
+ },
593
+ unresolvedIntegrations,
594
+ manifestGuidance: {
595
+ instructions: config.instructions ?? [],
596
+ patterns: config.patterns ?? [],
597
+ agentNotes: config.agentNotes ?? []
598
+ }
599
+ };
600
+ if (dependencyResult.installError) {
601
+ return {
602
+ ok: false,
603
+ code: "dependency_install_failed",
604
+ error: dependencyResult.installError,
605
+ data
606
+ };
607
+ }
608
+ if (unresolvedIntegrations.length > 0) {
609
+ const count = unresolvedIntegrations.length;
610
+ return {
611
+ ok: false,
612
+ code: "manual_integration_required",
613
+ error: `${count} app integration${count === 1 ? "" : "s"} require manual completion. Follow the unresolved integration steps, then run your checks.`,
614
+ data
615
+ };
616
+ }
617
+ return {
618
+ ok: true,
619
+ data,
620
+ ...dependencyInstallRequired ? {
621
+ action: {
622
+ cwd: targetDir,
623
+ argv: [dependencyResult.packageManager, "install"]
624
+ }
625
+ } : {}
626
+ };
627
+ }
628
+ export {
629
+ featureCatalog,
630
+ featurePackageRoot,
631
+ hasFeatureCatalog,
632
+ installFeature,
633
+ listFeatureConfigs,
634
+ readFeatureConfig,
635
+ resolveFeatureDestPath,
636
+ suggestFeature
637
+ };
638
+ //# sourceMappingURL=feature-installer.js.map