@mrpatronz/nexusflow 0.2.4 → 0.2.6

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 (48) hide show
  1. package/dist/commands/commands.test.d.ts +2 -0
  2. package/dist/commands/commands.test.d.ts.map +1 -0
  3. package/dist/commands/commands.test.js +132 -0
  4. package/dist/commands/commands.test.js.map +1 -0
  5. package/dist/commands/doctor.d.ts +7 -0
  6. package/dist/commands/doctor.d.ts.map +1 -0
  7. package/dist/commands/doctor.js +286 -0
  8. package/dist/commands/doctor.js.map +1 -0
  9. package/dist/commands/handoff.d.ts +8 -0
  10. package/dist/commands/handoff.d.ts.map +1 -0
  11. package/dist/commands/handoff.js +235 -0
  12. package/dist/commands/handoff.js.map +1 -0
  13. package/dist/commands/refresh.d.ts +11 -0
  14. package/dist/commands/refresh.d.ts.map +1 -0
  15. package/dist/commands/refresh.js +121 -0
  16. package/dist/commands/refresh.js.map +1 -0
  17. package/dist/core/workspace.d.ts.map +1 -1
  18. package/dist/core/workspace.js +12 -0
  19. package/dist/core/workspace.js.map +1 -1
  20. package/dist/core/worktree.js +2 -2
  21. package/dist/core/worktree.js.map +1 -1
  22. package/dist/generators/base.d.ts.map +1 -1
  23. package/dist/generators/base.js +17 -8
  24. package/dist/generators/base.js.map +1 -1
  25. package/dist/generators/index.d.ts +1 -1
  26. package/dist/generators/index.d.ts.map +1 -1
  27. package/dist/generators/index.js +4 -1
  28. package/dist/generators/index.js.map +1 -1
  29. package/dist/generators/map-generator.d.ts.map +1 -1
  30. package/dist/generators/map-generator.js +33 -6
  31. package/dist/generators/map-generator.js.map +1 -1
  32. package/dist/generators/plan-generator.d.ts.map +1 -1
  33. package/dist/generators/plan-generator.js +3 -0
  34. package/dist/generators/plan-generator.js.map +1 -1
  35. package/dist/index.js +43 -0
  36. package/dist/index.js.map +1 -1
  37. package/package.json +1 -1
  38. package/src/commands/commands.test.ts +155 -0
  39. package/src/commands/doctor.ts +301 -0
  40. package/src/commands/handoff.ts +266 -0
  41. package/src/commands/refresh.ts +139 -0
  42. package/src/core/workspace.ts +16 -0
  43. package/src/core/worktree.ts +2 -2
  44. package/src/generators/base.ts +15 -7
  45. package/src/generators/index.ts +4 -0
  46. package/src/generators/map-generator.ts +32 -6
  47. package/src/generators/plan-generator.ts +3 -0
  48. package/src/index.ts +43 -0
@@ -0,0 +1,139 @@
1
+ import chalk from 'chalk';
2
+ import { select } from '@inquirer/prompts';
3
+ import * as path from 'node:path';
4
+ import * as fs from 'node:fs/promises';
5
+
6
+ import { loadConfig } from '../core/config.js';
7
+ import { listWorkspaces, loadFeatureConfig } from '../core/workspace.js';
8
+ import { getWorkspaceRepos } from '../utils/multi-git.js';
9
+ import { analyzeAllRepos } from '../analyzers/index.js';
10
+ import { generateContextFiles } from '../generators/index.js';
11
+ import type { WorkspaceContext } from '../types.js';
12
+
13
+ /**
14
+ * Runs the refresh command.
15
+ * Updates context files, maps, and plans.
16
+ *
17
+ * @param options - CLI options, e.g. { repo: 'API_CoworkerFacade' }.
18
+ * @param workspaceArg - Optional workspace path.
19
+ */
20
+ export async function refreshCommand(
21
+ options: { repo?: string },
22
+ workspaceArg?: string,
23
+ ): Promise<void> {
24
+ console.log(chalk.bold.cyan('\nšŸ”„ NexusFlow — Refresh Workspace Context\n'));
25
+
26
+ const workspacePath = await resolveWorkspace(workspaceArg);
27
+ if (!workspacePath) return;
28
+
29
+ const feature = await loadFeatureConfig(workspacePath);
30
+ if (!feature) {
31
+ console.error(chalk.red('āœ– Failed to load workspace configuration. Ensure nexusflow.json exists.'));
32
+ return;
33
+ }
34
+
35
+ const onlyRepo = options.repo;
36
+ if (onlyRepo) {
37
+ const hasRepo = feature.repos.some(r => path.basename(r) === onlyRepo);
38
+ if (!hasRepo) {
39
+ console.error(chalk.red(`āœ– Repository "${onlyRepo}" is not part of this workspace.`));
40
+ console.log(chalk.dim(` Available repos: ${feature.repos.map(r => path.basename(r)).join(', ')}`));
41
+ return;
42
+ }
43
+ console.log(`Refreshing context for repository: ${chalk.bold(onlyRepo)}`);
44
+ } else {
45
+ console.log('Refreshing context for all repositories...');
46
+ }
47
+
48
+ const allRepos = await Promise.all(
49
+ feature.repos.map(async (r) => {
50
+ const repoName = path.basename(r);
51
+ return {
52
+ name: repoName,
53
+ path: r,
54
+ defaultBranch: 'main',
55
+ };
56
+ })
57
+ );
58
+
59
+ console.log(chalk.cyan('Running project analysis...'));
60
+ const analysis = await analyzeAllRepos(allRepos);
61
+
62
+ const ctx: WorkspaceContext = {
63
+ feature,
64
+ repos: allRepos,
65
+ analysis,
66
+ };
67
+
68
+ console.log(chalk.cyan('Regenerating context files and maps...'));
69
+ await generateContextFiles(ctx, feature.assistants, workspacePath, onlyRepo);
70
+
71
+ // If repopack context packing is enabled
72
+ const config = await loadConfig();
73
+ if (config.packContextXml) {
74
+ const { packWorkspace } = await import('../core/packer.js');
75
+ console.log(chalk.cyan('Re-packing workspace context...'));
76
+ try {
77
+ await packWorkspace(workspacePath);
78
+ } catch (error) {
79
+ console.warn(chalk.yellow(` ⚠ Failed to repack context: ${error}`));
80
+ }
81
+ }
82
+
83
+ // If handoff file exists, refresh it automatically too!
84
+ const handoffPath = path.join(workspacePath, 'nexusflow-handoff.md');
85
+ let hasHandoff = false;
86
+ try {
87
+ await fs.access(handoffPath);
88
+ hasHandoff = true;
89
+ } catch {}
90
+
91
+ if (hasHandoff) {
92
+ console.log(chalk.cyan('Refreshing handoff bundle...'));
93
+ try {
94
+ const { handoffCommand } = await import('./handoff.js');
95
+ await handoffCommand(workspacePath);
96
+ } catch (error) {
97
+ console.warn(chalk.yellow(` ⚠ Failed to refresh handoff bundle: ${error}`));
98
+ }
99
+ }
100
+
101
+ console.log(chalk.bold.green('\nāœ… Workspace context successfully refreshed!\n'));
102
+ }
103
+
104
+ /**
105
+ * Resolves a workspace path.
106
+ */
107
+ async function resolveWorkspace(workspaceArg?: string): Promise<string | null> {
108
+ if (workspaceArg) {
109
+ const absolutePath = path.resolve(workspaceArg);
110
+ try {
111
+ await fs.access(path.join(absolutePath, 'nexusflow.json'));
112
+ return absolutePath;
113
+ } catch {
114
+ console.error(chalk.red(`āœ– Invalid workspace: No nexusflow.json found at ${absolutePath}`));
115
+ return null;
116
+ }
117
+ }
118
+
119
+ const cwdFeature = await loadFeatureConfig(process.cwd());
120
+ if (cwdFeature) return process.cwd();
121
+
122
+ const config = await loadConfig();
123
+ const workspaces = await listWorkspaces(config.workspacesDir);
124
+
125
+ if (workspaces.length === 0) {
126
+ console.log(chalk.yellow('No workspaces found.\n'));
127
+ return null;
128
+ }
129
+
130
+ const selected = await select({
131
+ message: 'Select a workspace to refresh:',
132
+ choices: workspaces.map((ws) => ({
133
+ name: `${ws.branchName} ${chalk.dim(`(${ws.repos.length} repos)`)}`,
134
+ value: ws.workspacePath,
135
+ })),
136
+ });
137
+
138
+ return selected;
139
+ }
@@ -66,6 +66,22 @@ export async function createWorkspace(
66
66
  console.warn('Warning: Failed to initialize git repository at workspace root:', error);
67
67
  }
68
68
 
69
+ // Create .vscode/settings.json to allow VS Code search to query inside ignored sub-repos
70
+ try {
71
+ const vscodeDir = path.join(workspacePath, '.vscode');
72
+ await fs.mkdir(vscodeDir, { recursive: true });
73
+ const settings = {
74
+ "search.useIgnoreFiles": false
75
+ };
76
+ await fs.writeFile(
77
+ path.join(vscodeDir, 'settings.json'),
78
+ JSON.stringify(settings, null, 2) + '\n',
79
+ 'utf-8'
80
+ );
81
+ } catch (error) {
82
+ console.warn('Warning: Failed to create .vscode/settings.json:', error);
83
+ }
84
+
69
85
  // Create a worktree for each repo inside the workspace.
70
86
  for (const repo of repos) {
71
87
  const worktreeTarget = path.join(workspacePath, repo.name);
@@ -78,10 +78,10 @@ export async function createWorktree(
78
78
  { cwd: repoPath },
79
79
  );
80
80
  } else {
81
- // If the branch does not exist, create a new branch based on the start point
81
+ // If the branch does not exist, create a new branch based on the start point without tracking it
82
82
  await execa(
83
83
  'git',
84
- ['worktree', 'add', targetPath, '-b', branchName, startPoint],
84
+ ['worktree', 'add', '--no-track', targetPath, '-b', branchName, startPoint],
85
85
  { cwd: repoPath },
86
86
  );
87
87
  }
@@ -151,14 +151,20 @@ ${parts.join('\n')}
151
151
 
152
152
  const knowledgePath = path.join(workspacePath, 'nexusflow-knowledge.md').replace(/\\/g, '/');
153
153
 
154
- const taskSection = `## First Steps
155
-
156
- Your very first task upon entering this workspace is to explore the codebase and align with the user:
154
+ let setupDone = false;
155
+ try {
156
+ const realKnowledgePath = path.join(workspacePath, 'nexusflow-knowledge.md');
157
+ if (fs.existsSync(realKnowledgePath)) {
158
+ const content = fs.readFileSync(realKnowledgePath, 'utf-8');
159
+ if (!content.includes('No assumptions recorded yet') && !content.includes('AI assistant to populate')) {
160
+ setupDone = true;
161
+ }
162
+ }
163
+ } catch {}
157
164
 
158
- 1. **Verify Assumptions**: Open [nexusflow-knowledge.md](file:///${knowledgePath}) and fill in the **Project Assumptions** section with a brief description of what each project does, its tech stack, and responsibilities.
159
- 2. **Raise Questions**: Document any outstanding uncertainties or architectural questions in the **Clarifying Questions for the User** section.
160
- 3. **Obtain Approval**: Ask the user to confirm your assumptions and answer your questions before writing any code.
161
- `;
165
+ const taskSection = setupDone
166
+ ? `## Setup Status\n\nāœ… **Setup Completed**: Project assumptions and initial questions have been addressed. Refer to [nexusflow-knowledge.md](file:///${knowledgePath}) for persistent session details.`
167
+ : `## First Steps\n\nYour very first task upon entering this workspace is to explore the codebase and align with the user:\n\n1. **Verify Assumptions**: Open [nexusflow-knowledge.md](file:///${knowledgePath}) and fill in the **Project Assumptions** section with a brief description of what each project does, its tech stack, and responsibilities.\n2. **Raise Questions**: Document any outstanding uncertainties or architectural questions in the **Clarifying Questions for the User** section.\n3. **Obtain Approval**: Ask the user to confirm your assumptions and answer your questions before writing any code.`;
162
168
 
163
169
  return `# Multi-Repo Workspace Context
164
170
 
@@ -194,6 +200,8 @@ ${taskSection}
194
200
  - \`nexusflow diff\` — view changes across all sub-repositories.
195
201
  - \`nexusflow commit\` — commit and push changes across modified repositories.
196
202
  - \`nexusflow sync\` — rebase all repositories with their default base branches.
203
+ - \`nexusflow refresh\` — regenerate maps, context files and plans without rebasing.
204
+ - \`nexusflow doctor\` — run diagnostics to verify workspace health.
197
205
  - **Workspace Knowledge**: Read \`nexusflow-knowledge.md\` at the start of every session. It serves as the persistent memory for this feature. Before ending your session, append significant architecture decisions, discovered gotchas, and checklist progress to \`nexusflow-knowledge.md\`. Never delete or overwrite existing knowledge/decisions — only append.
198
206
  - **Implementation Plan**: Refer to \`nexusflow-plan.md\` for the suggested implementation order based on dependency analysis. Follow the phased implementation order to avoid blocking yourself on cross-repo dependencies.
199
207
  - Read each project's existing \`README.md\` and any doc files before proposing changes.
@@ -117,6 +117,7 @@ export async function generateContextFiles(
117
117
  ctx: WorkspaceContext,
118
118
  assistants: AIAssistant[],
119
119
  workspacePath: string,
120
+ onlyRepo?: string,
120
121
  ): Promise<void> {
121
122
  // Always generate a universal WORKSPACE.md at the workspace root
122
123
  try {
@@ -159,6 +160,9 @@ export async function generateContextFiles(
159
160
  }
160
161
 
161
162
  for (const repo of ctx.repos) {
163
+ if (onlyRepo && repo.name !== onlyRepo) {
164
+ continue;
165
+ }
162
166
  const a = ctx.analysis.get(repo.path);
163
167
  if (a) {
164
168
  try {
@@ -129,7 +129,7 @@ export async function generateRepoMap(
129
129
  md.push('');
130
130
  md.push(`> **Repository Path**: \`${worktreePath}\``);
131
131
  md.push(`> **Generated At**: ${new Date().toISOString()} (UTC)`);
132
- md.push(`> **Regeneration Command**: Run \`nexusflow sync\` to update this map and workspace planning files.`);
132
+ md.push(`> **Regeneration Command**: Run \`nexusflow refresh\` to update this map.`);
133
133
  md.push(`> **Note**: Maps are advisory snapshots of the codebase. Always verify route parameters, patterns, and filenames before relying on them.`);
134
134
  md.push('');
135
135
 
@@ -495,18 +495,44 @@ export async function generateRepoMap(
495
495
 
496
496
  const conventionsFile = path.join(workspacePath, `nexusflow-conventions-${repoName}.md`);
497
497
  let customConventions = '';
498
+
499
+ let conventionsExist = false;
498
500
  try {
499
- customConventions = await fs.readFile(conventionsFile, 'utf-8');
500
- // Strip the title from the old conventions file if present
501
- customConventions = customConventions.replace(/^#\s+.+\n?/, '').trim();
501
+ await fs.access(conventionsFile);
502
+ conventionsExist = true;
502
503
  } catch {}
503
504
 
505
+ if (!conventionsExist) {
506
+ const starterContent = `# Repository Conventions — ${repoName}
507
+
508
+ Use this file to record project-specific coding conventions, patterns, gotchas, and constraints learned during development.
509
+ Future AI assistant sessions will read these conventions from the repository map.
510
+
511
+ ## Coding Patterns
512
+ <!-- E.g., Use ErrorContent structure for errors instead of plain strings -->
513
+
514
+ - None recorded yet.
515
+
516
+ ## Gotchas & Watch-outs
517
+ <!-- E.g., Non-nullable enum required attributes gotchas -->
518
+
519
+ - None recorded yet.
520
+ `;
521
+ try {
522
+ await fs.writeFile(conventionsFile, starterContent, 'utf-8');
523
+ customConventions = starterContent.replace(/^#\s+.+\n?/, '').trim();
524
+ } catch {}
525
+ } else {
526
+ try {
527
+ customConventions = await fs.readFile(conventionsFile, 'utf-8');
528
+ customConventions = customConventions.replace(/^#\s+.+\n?/, '').trim();
529
+ } catch {}
530
+ }
531
+
504
532
  if (customConventions) {
505
533
  md.push(customConventions);
506
534
  md.push('');
507
535
  } else {
508
- md.push('<!-- AI assistants: Document any project-specific conventions, gotchas, or coding rules discovered here. -->');
509
- md.push('');
510
536
  md.push('- None recorded yet.');
511
537
  md.push('');
512
538
  }
@@ -204,6 +204,9 @@ export async function generateImplementationPlan(
204
204
 
205
205
  md.push(`# Implementation Plan — ${feature.id}`);
206
206
  md.push('');
207
+ md.push(`> **Generated At**: ${new Date().toISOString()} (UTC)`);
208
+ md.push(`> **Regeneration Command**: Run \`nexusflow refresh\` to update this plan.`);
209
+ md.push('');
207
210
  md.push(
208
211
  '> Auto-generated by NexusFlow based on dependency analysis between repos.',
209
212
  );
package/src/index.ts CHANGED
@@ -27,6 +27,9 @@ import { packCommand } from './commands/pack.js';
27
27
  import { removeCommand } from './commands/remove.js';
28
28
  import { addRepoCommand } from './commands/add-repo.js';
29
29
  import { mcpRunCommand, mcpSetupCommand } from './commands/mcp.js';
30
+ import { handoffCommand } from './commands/handoff.js';
31
+ import { refreshCommand } from './commands/refresh.js';
32
+ import { doctorCommand } from './commands/doctor.js';
30
33
  import { getCurrentVersion, checkForUpdates, printUpdateBanner } from './utils/update-check.js';
31
34
 
32
35
  const program = new Command();
@@ -292,6 +295,46 @@ program
292
295
  }
293
296
  });
294
297
 
298
+ program
299
+ .command('handoff')
300
+ .description('Generate a compact handoff bundle (nexusflow-handoff.md) for session resumption')
301
+ .argument('[workspace]', 'Path to workspace (auto-detects from CWD)')
302
+ .action(async (workspace?: string) => {
303
+ try {
304
+ await handoffCommand(workspace);
305
+ } catch (error) {
306
+ console.error(error);
307
+ process.exit(1);
308
+ }
309
+ });
310
+
311
+ program
312
+ .command('refresh')
313
+ .description('Refresh workspace context, maps, plans and handoff files')
314
+ .argument('[workspace]', 'Path to workspace (auto-detects from CWD)')
315
+ .option('-r, --repo <repo>', 'Only refresh the map for a specific repository')
316
+ .action(async (workspace: string | undefined, options: { repo?: string }) => {
317
+ try {
318
+ await refreshCommand(options, workspace);
319
+ } catch (error) {
320
+ console.error(error);
321
+ process.exit(1);
322
+ }
323
+ });
324
+
325
+ program
326
+ .command('doctor')
327
+ .description('Run diagnostics to verify workspace health and check for local loop issues')
328
+ .argument('[workspace]', 'Path to workspace (auto-detects from CWD)')
329
+ .action(async (workspace?: string) => {
330
+ try {
331
+ await doctorCommand(workspace);
332
+ } catch (error) {
333
+ console.error(error);
334
+ process.exit(1);
335
+ }
336
+ });
337
+
295
338
  const mcp = program.command('mcp').description('Manage the NexusFlow MCP Server for AI assistants');
296
339
 
297
340
  mcp