@mrpatronz/nexusflow 0.2.13 → 0.2.18

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 (228) hide show
  1. package/.agents/skills/ccc/SKILL.md +71 -0
  2. package/.agents/skills/ccc/references/management.md +110 -0
  3. package/.agents/skills/ccc/references/settings.md +126 -0
  4. package/.github/workflows/ci.yml +44 -0
  5. package/.github/workflows/dependabot-merge.yml +53 -0
  6. package/.github/workflows/release-desktop.yml +67 -0
  7. package/.github/workflows/release-npm.yml +65 -0
  8. package/.github/workflows/release-vscode.yml +61 -0
  9. package/GETTING_STARTED.md +5 -0
  10. package/README.md +53 -0
  11. package/desktop/build-installer.js +228 -0
  12. package/desktop/build.js +118 -0
  13. package/desktop/installer/nexusflow.iss +35 -0
  14. package/desktop/neutralino.config.json +39 -0
  15. package/desktop/package-lock.json +1060 -0
  16. package/desktop/package.json +14 -0
  17. package/dist/commands/adapter.d.ts +27 -0
  18. package/dist/commands/adapter.d.ts.map +1 -0
  19. package/dist/commands/adapter.js +318 -0
  20. package/dist/commands/adapter.js.map +1 -0
  21. package/dist/commands/add-repo.d.ts.map +1 -1
  22. package/dist/commands/add-repo.js +0 -1
  23. package/dist/commands/add-repo.js.map +1 -1
  24. package/dist/commands/commands.test.js +2 -2
  25. package/dist/commands/commands.test.js.map +1 -1
  26. package/dist/commands/config.d.ts +22 -0
  27. package/dist/commands/config.d.ts.map +1 -0
  28. package/dist/commands/config.js +50 -0
  29. package/dist/commands/config.js.map +1 -0
  30. package/dist/commands/create.d.ts.map +1 -1
  31. package/dist/commands/create.js +14 -17
  32. package/dist/commands/create.js.map +1 -1
  33. package/dist/commands/desktop.d.ts +12 -0
  34. package/dist/commands/desktop.d.ts.map +1 -0
  35. package/dist/commands/desktop.js +119 -0
  36. package/dist/commands/desktop.js.map +1 -0
  37. package/dist/commands/doctor.d.ts.map +1 -1
  38. package/dist/commands/doctor.js +11 -0
  39. package/dist/commands/doctor.js.map +1 -1
  40. package/dist/commands/refresh.d.ts +1 -0
  41. package/dist/commands/refresh.d.ts.map +1 -1
  42. package/dist/commands/refresh.js +2 -14
  43. package/dist/commands/refresh.js.map +1 -1
  44. package/dist/commands/sync.d.ts.map +1 -1
  45. package/dist/commands/sync.js +1 -8
  46. package/dist/commands/sync.js.map +1 -1
  47. package/dist/commands/tui.d.ts +8 -0
  48. package/dist/commands/tui.d.ts.map +1 -0
  49. package/dist/commands/tui.js +439 -0
  50. package/dist/commands/tui.js.map +1 -0
  51. package/dist/commands/ui.d.ts +3 -1
  52. package/dist/commands/ui.d.ts.map +1 -1
  53. package/dist/commands/ui.js +76 -19
  54. package/dist/commands/ui.js.map +1 -1
  55. package/dist/core/adapters/local-storage.d.ts +14 -0
  56. package/dist/core/adapters/local-storage.d.ts.map +1 -0
  57. package/dist/core/adapters/local-storage.js +58 -0
  58. package/dist/core/adapters/local-storage.js.map +1 -0
  59. package/dist/core/adapters/obsidian-storage.d.ts +28 -0
  60. package/dist/core/adapters/obsidian-storage.d.ts.map +1 -0
  61. package/dist/core/adapters/obsidian-storage.js +135 -0
  62. package/dist/core/adapters/obsidian-storage.js.map +1 -0
  63. package/dist/core/adapters/registry.d.ts +8 -0
  64. package/dist/core/adapters/registry.d.ts.map +1 -0
  65. package/dist/core/adapters/registry.js +37 -0
  66. package/dist/core/adapters/registry.js.map +1 -0
  67. package/dist/core/adapters/storage.test.d.ts +2 -0
  68. package/dist/core/adapters/storage.test.d.ts.map +1 -0
  69. package/dist/core/adapters/storage.test.js +106 -0
  70. package/dist/core/adapters/storage.test.js.map +1 -0
  71. package/dist/core/adapters/vault-storage.d.ts +16 -0
  72. package/dist/core/adapters/vault-storage.d.ts.map +1 -0
  73. package/dist/core/adapters/vault-storage.js +77 -0
  74. package/dist/core/adapters/vault-storage.js.map +1 -0
  75. package/dist/core/config.d.ts.map +1 -1
  76. package/dist/core/config.js +28 -4
  77. package/dist/core/config.js.map +1 -1
  78. package/dist/core/plugins/index.d.ts +14 -0
  79. package/dist/core/plugins/index.d.ts.map +1 -0
  80. package/dist/core/plugins/index.js +2 -0
  81. package/dist/core/plugins/index.js.map +1 -0
  82. package/dist/core/plugins/loader.d.ts +2 -0
  83. package/dist/core/plugins/loader.d.ts.map +1 -0
  84. package/dist/core/plugins/loader.js +34 -0
  85. package/dist/core/plugins/loader.js.map +1 -0
  86. package/dist/core/ports/storage.d.ts +59 -0
  87. package/dist/core/ports/storage.d.ts.map +1 -0
  88. package/dist/core/ports/storage.js +10 -0
  89. package/dist/core/ports/storage.js.map +1 -0
  90. package/dist/core/storage.d.ts +10 -0
  91. package/dist/core/storage.d.ts.map +1 -0
  92. package/dist/core/storage.js +29 -0
  93. package/dist/core/storage.js.map +1 -0
  94. package/dist/core/workspace.d.ts.map +1 -1
  95. package/dist/core/workspace.js +110 -16
  96. package/dist/core/workspace.js.map +1 -1
  97. package/dist/generators/base.d.ts.map +1 -1
  98. package/dist/generators/base.js +19 -6
  99. package/dist/generators/base.js.map +1 -1
  100. package/dist/generators/diff-context.d.ts +6 -0
  101. package/dist/generators/diff-context.d.ts.map +1 -0
  102. package/dist/generators/diff-context.js +68 -0
  103. package/dist/generators/diff-context.js.map +1 -0
  104. package/dist/generators/index.d.ts +1 -1
  105. package/dist/generators/index.d.ts.map +1 -1
  106. package/dist/generators/index.js +75 -18
  107. package/dist/generators/index.js.map +1 -1
  108. package/dist/generators/map-generator.d.ts.map +1 -1
  109. package/dist/generators/map-generator.js +7 -11
  110. package/dist/generators/map-generator.js.map +1 -1
  111. package/dist/generators/plan-generator.d.ts.map +1 -1
  112. package/dist/generators/plan-generator.js +3 -5
  113. package/dist/generators/plan-generator.js.map +1 -1
  114. package/dist/gui/app_logo.png +0 -0
  115. package/dist/gui/assets/index-CB-jWded.css +1 -0
  116. package/dist/gui/assets/index-DDXpZZu3.js +25 -0
  117. package/dist/gui/index.html +2 -2
  118. package/dist/index.js +174 -20
  119. package/dist/index.js.map +1 -1
  120. package/dist/server.d.ts.map +1 -1
  121. package/dist/server.js +212 -28
  122. package/dist/server.js.map +1 -1
  123. package/dist/server.test.js +173 -38
  124. package/dist/server.test.js.map +1 -1
  125. package/dist/types.d.ts +11 -2
  126. package/dist/types.d.ts.map +1 -1
  127. package/dist/utils/git.d.ts +16 -0
  128. package/dist/utils/git.d.ts.map +1 -1
  129. package/dist/utils/git.js +52 -0
  130. package/dist/utils/git.js.map +1 -1
  131. package/dist/utils/local-ai.d.ts.map +1 -1
  132. package/dist/utils/local-ai.js +7 -3
  133. package/dist/utils/local-ai.js.map +1 -1
  134. package/dist/utils/multi-git.d.ts +2 -0
  135. package/dist/utils/multi-git.d.ts.map +1 -1
  136. package/dist/utils/multi-git.js +5 -2
  137. package/dist/utils/multi-git.js.map +1 -1
  138. package/dist/utils/update-check.d.ts +3 -1
  139. package/dist/utils/update-check.d.ts.map +1 -1
  140. package/dist/utils/update-check.js +23 -34
  141. package/dist/utils/update-check.js.map +1 -1
  142. package/dist/utils/workflow-advisor.d.ts +16 -0
  143. package/dist/utils/workflow-advisor.d.ts.map +1 -0
  144. package/dist/utils/workflow-advisor.js +100 -0
  145. package/dist/utils/workflow-advisor.js.map +1 -0
  146. package/dist/utils/workflows.d.ts +11 -0
  147. package/dist/utils/workflows.d.ts.map +1 -0
  148. package/dist/utils/workflows.js +156 -0
  149. package/dist/utils/workflows.js.map +1 -0
  150. package/dist/utils/workflows.test.d.ts +2 -0
  151. package/dist/utils/workflows.test.d.ts.map +1 -0
  152. package/dist/utils/workflows.test.js +86 -0
  153. package/dist/utils/workflows.test.js.map +1 -0
  154. package/extension/media/icon.svg +1 -0
  155. package/extension/package-lock.json +4893 -1660
  156. package/extension/package.json +58 -11
  157. package/extension/src/extension.ts +476 -27
  158. package/extension/tsconfig.json +4 -2
  159. package/gui/e2e/wizard.spec.ts +19 -0
  160. package/gui/package-lock.json +342 -381
  161. package/gui/package.json +12 -12
  162. package/gui/public/app_logo.png +0 -0
  163. package/gui/src/App.tsx +991 -81
  164. package/gui/src/features/workspace/WorkspaceList.tsx +30 -30
  165. package/gui/src/index.css +64 -22
  166. package/gui/src/main.tsx +89 -5
  167. package/package.json +11 -12
  168. package/resources/workflows/plan-implement-review.md +8 -0
  169. package/resources/workflows/research-verify.md +6 -0
  170. package/resources/workflows/solo-developer.md +3 -0
  171. package/skills-lock.json +11 -0
  172. package/src/commands/adapter.ts +348 -0
  173. package/src/commands/add-repo.ts +0 -1
  174. package/src/commands/commands.test.ts +3 -2
  175. package/src/commands/config.ts +52 -0
  176. package/src/commands/create.ts +14 -17
  177. package/src/commands/desktop.ts +128 -0
  178. package/src/commands/doctor.ts +13 -0
  179. package/src/commands/mcp.ts +2 -0
  180. package/src/commands/refresh.ts +4 -14
  181. package/src/commands/sync.ts +2 -8
  182. package/src/commands/tui.ts +484 -0
  183. package/src/commands/ui.ts +83 -19
  184. package/src/core/adapters/local-storage.ts +66 -0
  185. package/src/core/adapters/obsidian-storage.ts +151 -0
  186. package/src/core/adapters/registry.ts +44 -0
  187. package/src/core/adapters/storage.test.ts +174 -0
  188. package/src/core/adapters/vault-storage.ts +86 -0
  189. package/src/core/config.ts +29 -4
  190. package/src/core/plugins/index.ts +17 -0
  191. package/src/core/plugins/loader.ts +36 -0
  192. package/src/core/ports/storage.ts +71 -0
  193. package/src/core/storage.ts +37 -0
  194. package/src/core/workspace.ts +122 -15
  195. package/src/generators/base.ts +21 -5
  196. package/src/generators/diff-context.ts +75 -0
  197. package/src/generators/index.ts +93 -21
  198. package/src/generators/map-generator.ts +7 -10
  199. package/src/generators/plan-generator.ts +6 -4
  200. package/src/index.ts +178 -21
  201. package/src/server.test.ts +193 -39
  202. package/src/server.ts +238 -28
  203. package/src/types.ts +14 -2
  204. package/src/utils/git.ts +56 -0
  205. package/src/utils/local-ai.ts +8 -3
  206. package/src/utils/multi-git.ts +16 -9
  207. package/src/utils/update-check.ts +42 -31
  208. package/src/utils/workflow-advisor.ts +118 -0
  209. package/src/utils/workflows.test.ts +114 -0
  210. package/src/utils/workflows.ts +178 -0
  211. package/.github/workflows/release.yml +0 -100
  212. package/dist/commands/pack.d.ts +0 -7
  213. package/dist/commands/pack.d.ts.map +0 -1
  214. package/dist/commands/pack.js +0 -64
  215. package/dist/commands/pack.js.map +0 -1
  216. package/dist/core/packer.d.ts +0 -14
  217. package/dist/core/packer.d.ts.map +0 -1
  218. package/dist/core/packer.js +0 -87
  219. package/dist/core/packer.js.map +0 -1
  220. package/dist/core/packer.test.d.ts +0 -2
  221. package/dist/core/packer.test.d.ts.map +0 -1
  222. package/dist/core/packer.test.js +0 -82
  223. package/dist/core/packer.test.js.map +0 -1
  224. package/dist/gui/assets/index-CFWwqFux.js +0 -23
  225. package/dist/gui/assets/index-Dvc9QMvl.css +0 -2
  226. package/src/commands/pack.ts +0 -73
  227. package/src/core/packer.test.ts +0 -99
  228. package/src/core/packer.ts +0 -107
@@ -7,6 +7,7 @@
7
7
  import path from 'node:path';
8
8
  import fse from 'fs-extra';
9
9
  import chalk from 'chalk';
10
+ import { writeWorkspaceFile } from '../core/storage.js';
10
11
  import type {
11
12
  WorkspaceContext,
12
13
  ProjectAnalysis,
@@ -187,8 +188,10 @@ export async function generateImplementationPlan(
187
188
  .map((n) => `- ${n}`),
188
189
  '',
189
190
  ];
190
- await fse.outputFile(
191
- path.join(workspacePath, 'nexusflow-plan.md'),
191
+ await writeWorkspaceFile(
192
+ workspacePath,
193
+ feature.id,
194
+ 'nexusflow-plan.md',
192
195
  lines.join('\n'),
193
196
  );
194
197
  console.log(chalk.green(' ✔'), 'Generated nexusflow-plan.md');
@@ -420,8 +423,7 @@ export async function generateImplementationPlan(
420
423
  md.push('');
421
424
 
422
425
  // ── Write file ──────────────────────────────────────────────────────
423
- const outPath = path.join(workspacePath, 'nexusflow-plan.md');
424
- await fse.outputFile(outPath, md.join('\n'));
426
+ await writeWorkspaceFile(workspacePath, feature.id, 'nexusflow-plan.md', md.join('\n'));
425
427
  console.log(chalk.green(' ✔'), 'Generated nexusflow-plan.md');
426
428
  } catch (error) {
427
429
  const message = error instanceof Error ? error.message : String(error);
package/src/index.ts CHANGED
@@ -20,16 +20,21 @@ import { stopCommand } from './commands/stop.js';
20
20
  import { logsCommand } from './commands/logs.js';
21
21
  import { statusCommand } from './commands/status.js';
22
22
  import { uiCommand } from './commands/ui.js';
23
+ import { tuiCommand } from './commands/tui.js';
23
24
  import { syncCommand } from './commands/sync.js';
24
25
  import { commitCommand } from './commands/commit.js';
25
26
  import { diffCommand } from './commands/diff.js';
26
- import { packCommand } from './commands/pack.js';
27
27
  import { removeCommand } from './commands/remove.js';
28
+ import { loadConfig } from './core/config.js';
29
+ import { loadPlugins } from './core/plugins/loader.js';
28
30
  import { addRepoCommand } from './commands/add-repo.js';
29
31
  import { mcpRunCommand, mcpSetupCommand } from './commands/mcp.js';
30
32
  import { handoffCommand } from './commands/handoff.js';
31
33
  import { refreshCommand } from './commands/refresh.js';
32
34
  import { doctorCommand } from './commands/doctor.js';
35
+ import { desktopCommand } from './commands/desktop.js';
36
+ import { configShowCommand, configGetCommand, configSetCommand } from './commands/config.js';
37
+ import { adapterListCommand, adapterUseCommand, adapterInfoCommand, adapterInitCommand } from './commands/adapter.js';
33
38
  import { getCurrentVersion, checkForUpdates, printUpdateBanner } from './utils/update-check.js';
34
39
 
35
40
  const program = new Command();
@@ -177,7 +182,9 @@ program
177
182
  .command('ui')
178
183
  .description('Open the web GUI dashboard for NexusFlow')
179
184
  .option('-p, --port <number>', 'Port to run the dashboard server on', '3000')
180
- .action(async (options: { port?: string }) => {
185
+ .option('-d, --daemon', 'Run the dashboard server in the background (daemon mode)')
186
+ .option('--server-only', 'Start the dashboard server without opening the browser')
187
+ .action(async (options: { port?: string; daemon?: boolean; serverOnly?: boolean }) => {
181
188
  try {
182
189
  await uiCommand(options);
183
190
  } catch (error) {
@@ -186,6 +193,34 @@ program
186
193
  }
187
194
  });
188
195
 
196
+ program
197
+ .command('dashboard')
198
+ .alias('dash')
199
+ .description('Instantly open the web GUI dashboard in your default browser')
200
+ .option('-p, --port <number>', 'Port to run the dashboard server on', '3000')
201
+ .action(async (options: { port?: string }) => {
202
+ try {
203
+ await uiCommand({ ...options, daemon: true });
204
+ } catch (error) {
205
+ console.error(error);
206
+ process.exit(1);
207
+ }
208
+ });
209
+
210
+
211
+ program
212
+ .command('tui')
213
+ .description('Open the interactive terminal GUI (TUI) dashboard')
214
+ .argument('[workspace]', 'Path to workspace (auto-detects from CWD)')
215
+ .action(async (workspace?: string) => {
216
+ try {
217
+ await tuiCommand({ workspace });
218
+ } catch (error) {
219
+ console.error(error);
220
+ process.exit(1);
221
+ }
222
+ });
223
+
189
224
  program
190
225
  .command('sync')
191
226
  .description('Sync all repositories in a workspace')
@@ -240,23 +275,7 @@ program
240
275
  }
241
276
  });
242
277
 
243
- program
244
- .command('pack')
245
- .description('Pack the workspace codebase into a single token-efficient XML file for AI consumption')
246
- .argument('[workspace]', 'Path to workspace (auto-detects from CWD)')
247
- .option('--no-compress', 'Do not compress files (strip comments, empty lines)')
248
- .action(async (workspace: string | undefined, options: { compress?: boolean }) => {
249
- try {
250
- await packCommand(workspace, options);
251
- } catch (error) {
252
- if (error instanceof Error && error.message.includes('User force closed')) {
253
- console.log('\nCancelled.');
254
- process.exit(0);
255
- }
256
- console.error(error);
257
- process.exit(1);
258
- }
259
- });
278
+ // Pack command removed.
260
279
 
261
280
  program
262
281
  .command('remove')
@@ -313,7 +332,8 @@ program
313
332
  .description('Refresh workspace context, maps, plans and handoff files')
314
333
  .argument('[workspace]', 'Path to workspace (auto-detects from CWD)')
315
334
  .option('-r, --repo <repo>', 'Only refresh the map for a specific repository')
316
- .action(async (workspace: string | undefined, options: { repo?: string }) => {
335
+ .option('-b, --base', 'Only refresh base-layer maps and codebase knowledge from main')
336
+ .action(async (workspace: string | undefined, options: { repo?: string; base?: boolean }) => {
317
337
  try {
318
338
  await refreshCommand(options, workspace);
319
339
  } catch (error) {
@@ -335,6 +355,133 @@ program
335
355
  }
336
356
  });
337
357
 
358
+ program
359
+ .command('desktop')
360
+ .description('Launch the NexusFlow desktop application')
361
+ .action(async () => {
362
+ try {
363
+ await desktopCommand();
364
+ } catch (error) {
365
+ console.error(error);
366
+ process.exit(1);
367
+ }
368
+ });
369
+ // Config command group
370
+ const configCmd = program.command('config').description('View and update NexusFlow configuration');
371
+
372
+ configCmd
373
+ .command('show')
374
+ .description('Display the current configuration')
375
+ .action(async () => {
376
+ try {
377
+ await configShowCommand();
378
+ } catch (error) {
379
+ console.error(error);
380
+ process.exit(1);
381
+ }
382
+ });
383
+
384
+ configCmd
385
+ .command('get')
386
+ .description('Get a specific configuration key')
387
+ .argument('<key>', 'Configuration key to read')
388
+ .action(async (key: string) => {
389
+ try {
390
+ await configGetCommand(key);
391
+ } catch (error) {
392
+ console.error(error);
393
+ process.exit(1);
394
+ }
395
+ });
396
+
397
+ configCmd
398
+ .command('set')
399
+ .description('Set a configuration key to a value')
400
+ .argument('<key>', 'Configuration key to set')
401
+ .argument('<value>', 'Value to assign')
402
+ .action(async (key: string, value: string) => {
403
+ try {
404
+ await configSetCommand(key, value);
405
+ } catch (error) {
406
+ console.error(error);
407
+ process.exit(1);
408
+ }
409
+ });
410
+
411
+ // Default action when 'nexusflow config' is run without a subcommand
412
+ configCmd.action(async () => {
413
+ try {
414
+ await configShowCommand();
415
+ } catch (error) {
416
+ console.error(error);
417
+ process.exit(1);
418
+ }
419
+ });
420
+
421
+ // Adapter command group
422
+ const adapterCmd = program.command('adapter').description('Manage storage adapters — list, switch, configure, or create new ones');
423
+
424
+ adapterCmd
425
+ .command('list')
426
+ .description('List all available storage adapters')
427
+ .action(async () => {
428
+ try {
429
+ await adapterListCommand();
430
+ } catch (error) {
431
+ console.error(error);
432
+ process.exit(1);
433
+ }
434
+ });
435
+
436
+ adapterCmd
437
+ .command('use')
438
+ .description('Switch to a different storage adapter (prompts for config if needed)')
439
+ .argument('<name>', 'Adapter name to activate')
440
+ .action(async (name: string) => {
441
+ try {
442
+ await adapterUseCommand(name);
443
+ } catch (error) {
444
+ console.error(error);
445
+ process.exit(1);
446
+ }
447
+ });
448
+
449
+ adapterCmd
450
+ .command('info')
451
+ .description('Show detailed information about an adapter')
452
+ .argument('<name>', 'Adapter name to inspect')
453
+ .action(async (name: string) => {
454
+ try {
455
+ await adapterInfoCommand(name);
456
+ } catch (error) {
457
+ console.error(error);
458
+ process.exit(1);
459
+ }
460
+ });
461
+
462
+ adapterCmd
463
+ .command('init')
464
+ .description('Scaffold a new adapter plugin project')
465
+ .argument('<name>', 'Name for the new adapter')
466
+ .action(async (name: string) => {
467
+ try {
468
+ await adapterInitCommand(name);
469
+ } catch (error) {
470
+ console.error(error);
471
+ process.exit(1);
472
+ }
473
+ });
474
+
475
+ // Default action when 'nexusflow adapter' is run without a subcommand
476
+ adapterCmd.action(async () => {
477
+ try {
478
+ await adapterListCommand();
479
+ } catch (error) {
480
+ console.error(error);
481
+ process.exit(1);
482
+ }
483
+ });
484
+
338
485
  const mcp = program.command('mcp').description('Manage the NexusFlow MCP Server for AI assistants');
339
486
 
340
487
  mcp
@@ -378,5 +525,15 @@ program.hook('postAction', async (thisCommand, actionCommand) => {
378
525
  }
379
526
  });
380
527
 
381
- program.parse();
528
+ async function bootstrap() {
529
+ try {
530
+ const config = await loadConfig();
531
+ if (config.plugins && config.plugins.length > 0) {
532
+ await loadPlugins(program, config.plugins);
533
+ }
534
+ } catch {}
535
+ await program.parseAsync(process.argv);
536
+ }
537
+
538
+ bootstrap();
382
539
 
@@ -9,7 +9,8 @@ import * as localAi from './utils/local-ai.js';
9
9
  import * as updateCheck from './utils/update-check.js';
10
10
  import * as analyzers from './analyzers/index.js';
11
11
  import * as generators from './generators/index.js';
12
- import * as packer from './core/packer.js';
12
+ import * as workflows from './utils/workflows.js';
13
+ import * as detectAi from './utils/detect-ai.js';
13
14
 
14
15
  // Mock dependencies
15
16
  vi.mock('node:fs/promises');
@@ -21,7 +22,10 @@ vi.mock('./utils/local-ai.js');
21
22
  vi.mock('./utils/update-check.js');
22
23
  vi.mock('./analyzers/index.js');
23
24
  vi.mock('./generators/index.js');
24
- vi.mock('./core/packer.js');
25
+ vi.mock('./utils/workflows.js');
26
+ vi.mock('./utils/detect-ai.js', () => ({
27
+ detectAIAssistants: vi.fn().mockResolvedValue([])
28
+ }));
25
29
 
26
30
  describe('Server API Endpoints Unit Tests', () => {
27
31
  beforeEach(() => {
@@ -200,6 +204,25 @@ describe('Server API Endpoints Unit Tests', () => {
200
204
  });
201
205
  });
202
206
 
207
+ describe('GET /api/adapters', () => {
208
+ it('should return all registered storage adapters with meta', async () => {
209
+ const response = await app.request('/api/adapters');
210
+ expect(response.status).toBe(200);
211
+ const data = await response.json();
212
+ expect(data.adapters).toBeDefined();
213
+ expect(Array.isArray(data.adapters)).toBe(true);
214
+ // It should include at least 'local', 'central-vault', and 'obsidian'
215
+ const names = data.adapters.map((a: any) => a.name);
216
+ expect(names).toContain('local');
217
+ expect(names).toContain('central-vault');
218
+ expect(names).toContain('obsidian');
219
+
220
+ const obsidian = data.adapters.find((a: any) => a.name === 'obsidian');
221
+ expect(obsidian.displayName).toBe('Obsidian Vault');
222
+ expect(obsidian.configFields.length).toBeGreaterThan(0);
223
+ });
224
+ });
225
+
203
226
  describe('POST /api/config', () => {
204
227
  it('should validate endpoint domain safety', async () => {
205
228
  const response = await app.request('/api/config', {
@@ -215,7 +238,7 @@ describe('Server API Endpoints Unit Tests', () => {
215
238
 
216
239
  expect(response.status).toBe(400);
217
240
  const data = await response.json();
218
- expect(data.error).toContain('Local AI endpoint must be localhost, 127.0.0.1, or a private LAN IP.');
241
+ expect(data.error).toContain('Local AI endpoint must be HTTPS, localhost, 127.0.0.1, or a private LAN IP.');
219
242
  });
220
243
 
221
244
  it('should save safe endpoint and config', async () => {
@@ -360,22 +383,22 @@ describe('Server API Endpoints Unit Tests', () => {
360
383
  const response = await app.request('/api/updates/install', {
361
384
  method: 'POST',
362
385
  headers: { 'Content-Type': 'application/json' },
363
- body: JSON.stringify({ toolId: 'repomix' })
386
+ body: JSON.stringify({ toolId: 'nexusflow' })
364
387
  });
365
388
 
366
389
  expect(response.status).toBe(200);
367
390
  const data = await response.json();
368
391
  expect(data.success).toBe(true);
369
392
  expect(data.output).toBe('Successfully updated');
370
- expect(execa).toHaveBeenCalledWith('npm', ['install', '-g', 'repomix'], expect.any(Object));
393
+ expect(execa).toHaveBeenCalledWith('npm', ['install', '-g', '@mrpatronz/nexusflow'], expect.any(Object));
371
394
  });
372
395
  });
373
396
 
374
397
  describe('POST /api/workspace', () => {
375
- it('should complete workspace creation successfully when packContextXml is false', async () => {
398
+ it('should complete workspace creation successfully', async () => {
376
399
  vi.spyOn(config, 'loadConfig').mockResolvedValue({
377
400
  workspacesDir: '/mock/workspaces',
378
- packContextXml: false
401
+ storageProvider: 'local'
379
402
  } as any);
380
403
 
381
404
  vi.spyOn(workspace, 'createWorkspace').mockResolvedValue('/mock/workspaces/test-ws-creation-no-pack');
@@ -413,52 +436,183 @@ describe('Server API Endpoints Unit Tests', () => {
413
436
  expect(text).toContain('"progress":100');
414
437
  });
415
438
 
416
- it('should complete workspace creation successfully when packContextXml is true', async () => {
417
- vi.spyOn(config, 'loadConfig').mockResolvedValue({
418
- workspacesDir: '/mock/workspaces',
419
- packContextXml: true
420
- } as any);
439
+ // XML context packing tests removed.
440
+ });
421
441
 
422
- vi.spyOn(workspace, 'createWorkspace').mockResolvedValue('/mock/workspaces/test-ws-creation-with-pack');
423
- vi.spyOn(analyzers, 'analyzeAllRepos').mockResolvedValue(new Map());
424
- vi.spyOn(generators, 'generateContextFiles').mockResolvedValue(undefined);
425
- vi.spyOn(packer, 'packWorkspace').mockResolvedValue({
426
- outputPath: '/mock/workspace/nexusflow-context.xml',
427
- totalFiles: 5,
428
- totalCharacters: 1000,
429
- fileSize: 5120
442
+ describe('Workflows Templates API', () => {
443
+ it('GET /api/workflows/templates should return list of templates', async () => {
444
+ vi.spyOn(workflows, 'getWorkflowTemplates').mockResolvedValue([
445
+ { id: 'test-id', name: 'Test Name', description: 'Test Desc', content: 'Test Content', custom: false }
446
+ ]);
447
+
448
+ const response = await app.request('/api/workflows/templates');
449
+ expect(response.status).toBe(200);
450
+ const data = await response.json();
451
+ expect(data).toEqual({
452
+ templates: [
453
+ { id: 'test-id', name: 'Test Name', description: 'Test Desc', content: 'Test Content', custom: false }
454
+ ]
430
455
  });
456
+ });
431
457
 
432
- const response = await app.request('/api/workspace', {
458
+ it('POST /api/workflows/templates should create or update template', async () => {
459
+ vi.spyOn(workflows, 'saveWorkflowTemplate').mockResolvedValue({
460
+ id: 'test-id',
461
+ name: 'Test Name',
462
+ description: 'Test Desc',
463
+ content: 'Test Content',
464
+ custom: true
465
+ });
466
+
467
+ const response = await app.request('/api/workflows/templates', {
468
+ method: 'POST',
469
+ headers: { 'Content-Type': 'application/json' },
470
+ body: JSON.stringify({ id: 'old-id', name: 'Test Name', content: 'Test Content' })
471
+ });
472
+
473
+ expect(response.status).toBe(200);
474
+ const data = await response.json();
475
+ expect(data.success).toBe(true);
476
+ expect(data.template.id).toBe('test-id');
477
+ expect(workflows.saveWorkflowTemplate).toHaveBeenCalledWith('Test Name', 'Test Content', 'old-id');
478
+ });
479
+
480
+ it('DELETE /api/workflows/templates/:id should delete template', async () => {
481
+ vi.spyOn(workflows, 'getWorkflowTemplates').mockResolvedValue([
482
+ { id: 'test-id', name: 'Test Name', description: 'Test Desc', content: 'Test Content', custom: true }
483
+ ]);
484
+ vi.spyOn(workflows, 'deleteWorkflowTemplate').mockResolvedValue(undefined);
485
+
486
+ const response = await app.request('/api/workflows/templates/test-id', {
487
+ method: 'DELETE'
488
+ });
489
+
490
+ expect(response.status).toBe(200);
491
+ const data = await response.json();
492
+ expect(data.success).toBe(true);
493
+ expect(workflows.deleteWorkflowTemplate).toHaveBeenCalledWith('test-id');
494
+ });
495
+
496
+ it('POST /api/workflows/templates/:id/analyze should run inspection with comment', async () => {
497
+ vi.mocked(detectAi.detectAIAssistants).mockResolvedValue([
498
+ { name: 'antigravity', displayName: 'Antigravity', detected: true, command: 'agy' }
499
+ ]);
500
+
501
+ vi.mocked(execa).mockResolvedValue({
502
+ exitCode: 0,
503
+ stdout: 'Review result: Success\n=== SUGGESTED IMPROVEMENT START ===\n# Refined Strategy\n=== SUGGESTED IMPROVEMENT END ==='
504
+ } as any);
505
+
506
+ const response = await app.request('/api/workflows/templates/test-id/analyze', {
433
507
  method: 'POST',
434
508
  headers: { 'Content-Type': 'application/json' },
435
509
  body: JSON.stringify({
436
- branchName: 'test-ws-creation-with-pack',
437
- description: 'A test workspace',
438
- repos: [{ name: 'repo-1', path: '/mock/repo-1' }],
439
- assistants: ['antigravity']
510
+ content: 'My Strategy guidelines',
511
+ assistant: 'antigravity',
512
+ comment: 'Check for timeouts'
440
513
  })
441
514
  });
442
515
 
443
516
  expect(response.status).toBe(200);
444
517
  const data = await response.json();
445
- expect(data.success).toBe(true);
446
- expect(data.jobId).toBe('test-ws-creation-with-pack');
518
+ expect(data.analysis).toContain('Review result: Success');
519
+ expect(data.suggestedImprovement).toBe('# Refined Strategy');
520
+ expect(execa).toHaveBeenCalled();
521
+
522
+ const calledArgs = vi.mocked(execa).mock.calls[0][1];
523
+ expect(JSON.stringify(calledArgs)).toContain('Check for timeouts');
524
+ });
447
525
 
448
- // Wait a brief tick for the background job to execute
449
- await new Promise((resolve) => setTimeout(resolve, 50));
526
+ describe('POST /api/workspace/suggest-workflow', () => {
527
+ it('should suggest a workflow using heuristics when local LLM is disabled', async () => {
528
+ vi.spyOn(config, 'loadConfig').mockResolvedValue({
529
+ version: '1.0',
530
+ devDir: '/dev',
531
+ workspacesDir: '/dev/workspaces',
532
+ defaultAssistant: null,
533
+ scanDepth: 2,
534
+ localLlm: { enabled: false, provider: 'ollama', endpoint: 'http://localhost:11434', model: 'qwen' }
535
+ });
536
+
537
+ const response = await app.request('/api/workspace/suggest-workflow', {
538
+ method: 'POST',
539
+ headers: { 'Content-Type': 'application/json' },
540
+ body: JSON.stringify({
541
+ description: 'Fix a typo in README.md and update comments',
542
+ repos: [{ name: 'my-project', path: '/dev/my-project', defaultBranch: 'main' }]
543
+ })
544
+ });
545
+
546
+ expect(response.status).toBe(200);
547
+ const data = await response.json();
548
+ expect(data.success).toBe(true);
549
+ expect(data.difficulty).toBe('simple');
550
+ expect(data.suggestedWorkflowId).toBe('solo-developer');
551
+ expect(data.customInstructions).toContain('Solo Developer');
552
+ });
450
553
 
451
- expect(workspace.createWorkspace).toHaveBeenCalled();
452
- expect(analyzers.analyzeAllRepos).toHaveBeenCalled();
453
- expect(generators.generateContextFiles).toHaveBeenCalled();
454
- expect(packer.packWorkspace).toHaveBeenCalled();
554
+ it('should suggest a complex workflow when description contains complex keywords', async () => {
555
+ vi.spyOn(config, 'loadConfig').mockResolvedValue({
556
+ version: '1.0',
557
+ devDir: '/dev',
558
+ workspacesDir: '/dev/workspaces',
559
+ defaultAssistant: null,
560
+ scanDepth: 2,
561
+ localLlm: { enabled: false, provider: 'ollama', endpoint: 'http://localhost:11434', model: 'qwen' }
562
+ });
563
+
564
+ const response = await app.request('/api/workspace/suggest-workflow', {
565
+ method: 'POST',
566
+ headers: { 'Content-Type': 'application/json' },
567
+ body: JSON.stringify({
568
+ description: 'Refactor database schema and migrate data to postgres',
569
+ repos: [{ name: 'my-project', path: '/dev/my-project', defaultBranch: 'main' }]
570
+ })
571
+ });
572
+
573
+ expect(response.status).toBe(200);
574
+ const data = await response.json();
575
+ expect(data.success).toBe(true);
576
+ expect(data.difficulty).toBe('complex');
577
+ expect(data.suggestedWorkflowId).toBe('plan-implement-review');
578
+ expect(data.customInstructions).toContain('Plan, Implement, Review');
579
+ });
455
580
 
456
- // Read status via SSE stream route
457
- const streamResponse = await app.request('/api/workspace/create-stream/test-ws-creation-with-pack');
458
- expect(streamResponse.status).toBe(200);
459
- const text = await streamResponse.text();
460
- expect(text).toContain('"status":"completed"');
461
- expect(text).toContain('"progress":100');
581
+ it('should call local LLM and return suggested workflow when LLM is enabled', async () => {
582
+ vi.spyOn(config, 'loadConfig').mockResolvedValue({
583
+ version: '1.0',
584
+ devDir: '/dev',
585
+ workspacesDir: '/dev/workspaces',
586
+ defaultAssistant: null,
587
+ scanDepth: 2,
588
+ localLlm: { enabled: true, provider: 'ollama', endpoint: 'http://localhost:11434', model: 'qwen' }
589
+ });
590
+
591
+ vi.spyOn(localAi, 'callLocalLlm').mockResolvedValue(JSON.stringify({
592
+ difficulty: 'moderate',
593
+ rationale: 'LLM selected moderate strategy.',
594
+ suggestedWorkflowId: 'research-verify',
595
+ customInstructions: '# LLM Custom Instructions'
596
+ }));
597
+
598
+ const response = await app.request('/api/workspace/suggest-workflow', {
599
+ method: 'POST',
600
+ headers: { 'Content-Type': 'application/json' },
601
+ body: JSON.stringify({
602
+ description: 'Implement new UI component',
603
+ repos: [{ name: 'my-project', path: '/dev/my-project', defaultBranch: 'main' }]
604
+ })
605
+ });
606
+
607
+ expect(response.status).toBe(200);
608
+ const data = await response.json();
609
+ expect(data.success).toBe(true);
610
+ expect(data.difficulty).toBe('moderate');
611
+ expect(data.rationale).toBe('LLM selected moderate strategy.');
612
+ expect(data.suggestedWorkflowId).toBe('research-verify');
613
+ expect(data.customInstructions).toBe('# LLM Custom Instructions');
614
+ expect(localAi.callLocalLlm).toHaveBeenCalled();
615
+ });
462
616
  });
463
617
  });
464
618
  });