@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.
- package/dist/commands/commands.test.d.ts +2 -0
- package/dist/commands/commands.test.d.ts.map +1 -0
- package/dist/commands/commands.test.js +132 -0
- package/dist/commands/commands.test.js.map +1 -0
- package/dist/commands/doctor.d.ts +7 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +286 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/handoff.d.ts +8 -0
- package/dist/commands/handoff.d.ts.map +1 -0
- package/dist/commands/handoff.js +235 -0
- package/dist/commands/handoff.js.map +1 -0
- package/dist/commands/refresh.d.ts +11 -0
- package/dist/commands/refresh.d.ts.map +1 -0
- package/dist/commands/refresh.js +121 -0
- package/dist/commands/refresh.js.map +1 -0
- package/dist/core/workspace.d.ts.map +1 -1
- package/dist/core/workspace.js +12 -0
- package/dist/core/workspace.js.map +1 -1
- package/dist/core/worktree.js +2 -2
- package/dist/core/worktree.js.map +1 -1
- package/dist/generators/base.d.ts.map +1 -1
- package/dist/generators/base.js +17 -8
- package/dist/generators/base.js.map +1 -1
- package/dist/generators/index.d.ts +1 -1
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +4 -1
- package/dist/generators/index.js.map +1 -1
- package/dist/generators/map-generator.d.ts.map +1 -1
- package/dist/generators/map-generator.js +33 -6
- package/dist/generators/map-generator.js.map +1 -1
- package/dist/generators/plan-generator.d.ts.map +1 -1
- package/dist/generators/plan-generator.js +3 -0
- package/dist/generators/plan-generator.js.map +1 -1
- package/dist/index.js +43 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/commands.test.ts +155 -0
- package/src/commands/doctor.ts +301 -0
- package/src/commands/handoff.ts +266 -0
- package/src/commands/refresh.ts +139 -0
- package/src/core/workspace.ts +16 -0
- package/src/core/worktree.ts +2 -2
- package/src/generators/base.ts +15 -7
- package/src/generators/index.ts +4 -0
- package/src/generators/map-generator.ts +32 -6
- package/src/generators/plan-generator.ts +3 -0
- 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
|
+
}
|
package/src/core/workspace.ts
CHANGED
|
@@ -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);
|
package/src/core/worktree.ts
CHANGED
|
@@ -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
|
}
|
package/src/generators/base.ts
CHANGED
|
@@ -151,14 +151,20 @@ ${parts.join('\n')}
|
|
|
151
151
|
|
|
152
152
|
const knowledgePath = path.join(workspacePath, 'nexusflow-knowledge.md').replace(/\\/g, '/');
|
|
153
153
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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.
|
package/src/generators/index.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
500
|
-
|
|
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
|