ultra-dex 2.2.0 → 2.2.1
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/README.md +12 -6
- package/assets/agents/0-orchestration/orchestrator.md +2 -2
- package/assets/docs/QUICK-REFERENCE.md +3 -3
- package/assets/docs/ROADMAP.md +5 -5
- package/assets/docs/WORKFLOW-DIAGRAMS.md +1 -1
- package/assets/templates/README.md +1 -1
- package/bin/ultra-dex.js +1 -2095
- package/lib/commands/agents.js +3 -6
- package/lib/commands/init.js +3 -5
- package/lib/commands/serve.js +128 -11
- package/lib/commands/sync.js +35 -0
- package/lib/templates/context.js +2 -2
- package/lib/utils/fallback.js +4 -2
- package/lib/utils/files.js +0 -41
- package/lib/utils/sync.js +216 -0
- package/package.json +1 -1
package/lib/commands/agents.js
CHANGED
|
@@ -3,6 +3,7 @@ import fs from 'fs/promises';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { ASSETS_ROOT, ROOT_FALLBACK } from '../config/paths.js';
|
|
5
5
|
import { githubBlobUrl } from '../config/urls.js';
|
|
6
|
+
import { readWithFallback } from '../utils/fallback.js';
|
|
6
7
|
|
|
7
8
|
export const AGENTS = [
|
|
8
9
|
{ name: 'cto', description: 'Architecture & tech decisions', file: '1-leadership/cto.md', tier: 'Leadership' },
|
|
@@ -24,12 +25,8 @@ export const AGENTS = [
|
|
|
24
25
|
|
|
25
26
|
async function readAgentPrompt(agent) {
|
|
26
27
|
const agentPath = path.join(ASSETS_ROOT, 'agents', agent.file);
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
} catch (err) {
|
|
30
|
-
const fallbackPath = path.join(ROOT_FALLBACK, 'agents', agent.file);
|
|
31
|
-
return await fs.readFile(fallbackPath, 'utf-8');
|
|
32
|
-
}
|
|
28
|
+
const fallbackPath = path.join(ROOT_FALLBACK, 'agents', agent.file);
|
|
29
|
+
return readWithFallback(agentPath, fallbackPath, 'utf-8');
|
|
33
30
|
}
|
|
34
31
|
|
|
35
32
|
export function registerAgentsCommand(program) {
|
package/lib/commands/init.js
CHANGED
|
@@ -207,12 +207,10 @@ ${answers.ideaWhat} for ${answers.ideaFor}.
|
|
|
207
207
|
const cursorRulesPath = path.join(ASSETS_ROOT, 'cursor-rules');
|
|
208
208
|
const fallbackRulesPath = path.join(ROOT_FALLBACK, 'cursor-rules');
|
|
209
209
|
try {
|
|
210
|
-
const ruleFiles = await listWithFallback(cursorRulesPath, fallbackRulesPath);
|
|
211
|
-
const sourcePath = ruleFiles ? cursorRulesPath : fallbackRulesPath;
|
|
210
|
+
const { files: ruleFiles, sourcePath } = await listWithFallback(cursorRulesPath, fallbackRulesPath);
|
|
212
211
|
for (const file of ruleFiles.filter(f => f.endsWith('.mdc'))) {
|
|
213
|
-
await
|
|
212
|
+
await fs.copyFile(
|
|
214
213
|
path.join(sourcePath, file),
|
|
215
|
-
null,
|
|
216
214
|
path.join(rulesDir, file)
|
|
217
215
|
);
|
|
218
216
|
}
|
|
@@ -330,7 +328,7 @@ ${answers.ideaWhat} for ${answers.ideaFor}.
|
|
|
330
328
|
console.log(chalk.blue(` ${githubWebUrl()}`));
|
|
331
329
|
} catch (error) {
|
|
332
330
|
spinner.fail(chalk.red('Failed to create project'));
|
|
333
|
-
console.error(error);
|
|
331
|
+
console.error(`[init] ${error?.message ?? error}`);
|
|
334
332
|
process.exit(1);
|
|
335
333
|
}
|
|
336
334
|
});
|
package/lib/commands/serve.js
CHANGED
|
@@ -1,17 +1,69 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import fs from 'fs/promises';
|
|
3
2
|
import http from 'http';
|
|
4
|
-
import
|
|
3
|
+
import fs from 'fs/promises';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { readFileSafe } from '../utils/files.js';
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
// State management helpers
|
|
8
|
+
async function loadState() {
|
|
7
9
|
try {
|
|
8
|
-
const content = await fs.readFile(
|
|
9
|
-
return
|
|
10
|
+
const content = await fs.readFile(path.resolve(process.cwd(), '.ultra/state.json'), 'utf8');
|
|
11
|
+
return JSON.parse(content);
|
|
10
12
|
} catch {
|
|
11
|
-
return
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function computeState() {
|
|
18
|
+
const state = {
|
|
19
|
+
version: '2.1.0',
|
|
20
|
+
updatedAt: new Date().toISOString(),
|
|
21
|
+
project: { name: path.basename(process.cwd()) },
|
|
22
|
+
files: {},
|
|
23
|
+
sections: { total: 34, completed: 0, list: [] },
|
|
24
|
+
score: 0
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const coreFiles = ['CONTEXT.md', 'IMPLEMENTATION-PLAN.md', 'CHECKLIST.md', 'QUICK-START.md'];
|
|
28
|
+
for (const file of coreFiles) {
|
|
29
|
+
try {
|
|
30
|
+
const stat = await fs.stat(path.resolve(process.cwd(), file));
|
|
31
|
+
state.files[file] = { exists: true, size: stat.size };
|
|
32
|
+
} catch {
|
|
33
|
+
state.files[file] = { exists: false };
|
|
34
|
+
}
|
|
12
35
|
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const plan = await fs.readFile(path.resolve(process.cwd(), 'IMPLEMENTATION-PLAN.md'), 'utf8');
|
|
39
|
+
const sectionRegex = /^##\s+(\d+)\.\s+(.+)$/gm;
|
|
40
|
+
let match;
|
|
41
|
+
while ((match = sectionRegex.exec(plan)) !== null) {
|
|
42
|
+
state.sections.list.push({ number: parseInt(match[1]), title: match[2].trim() });
|
|
43
|
+
}
|
|
44
|
+
state.sections.completed = state.sections.list.length;
|
|
45
|
+
} catch { /* no plan */ }
|
|
46
|
+
|
|
47
|
+
const fileScore = Object.values(state.files).filter(f => f.exists).length / coreFiles.length * 40;
|
|
48
|
+
const sectionScore = state.sections.completed / state.sections.total * 60;
|
|
49
|
+
state.score = Math.round(fileScore + sectionScore);
|
|
50
|
+
|
|
51
|
+
return state;
|
|
13
52
|
}
|
|
14
53
|
|
|
54
|
+
const BUILD_AGENTS = [
|
|
55
|
+
{ name: 'planner', tier: 'architect', task: 'Break down requirements into tasks' },
|
|
56
|
+
{ name: 'cto', tier: 'architect', task: 'Technical decisions & architecture' },
|
|
57
|
+
{ name: 'backend', tier: 'core', task: 'API, business logic, services' },
|
|
58
|
+
{ name: 'frontend', tier: 'core', task: 'UI components, pages, styling' },
|
|
59
|
+
{ name: 'database', tier: 'core', task: 'Schema design, migrations, queries' },
|
|
60
|
+
{ name: 'auth', tier: 'specialist', task: 'Authentication & authorization' },
|
|
61
|
+
{ name: 'security', tier: 'specialist', task: 'Security audit & hardening' },
|
|
62
|
+
{ name: 'testing', tier: 'specialist', task: 'Test strategy & implementation' },
|
|
63
|
+
{ name: 'reviewer', tier: 'quality', task: 'Code review & best practices' },
|
|
64
|
+
{ name: 'devops', tier: 'quality', task: 'CI/CD, deployment, infrastructure' }
|
|
65
|
+
];
|
|
66
|
+
|
|
15
67
|
export function registerServeCommand(program) {
|
|
16
68
|
program
|
|
17
69
|
.command('serve')
|
|
@@ -25,13 +77,24 @@ export function registerServeCommand(program) {
|
|
|
25
77
|
}
|
|
26
78
|
|
|
27
79
|
const server = http.createServer(async (req, res) => {
|
|
80
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
81
|
+
|
|
28
82
|
if (!req.url || req.url === '/') {
|
|
29
|
-
res.writeHead(200, { 'Content-Type': '
|
|
30
|
-
res.end(
|
|
83
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
84
|
+
res.end(JSON.stringify({
|
|
85
|
+
name: 'Ultra-Dex MCP Server',
|
|
86
|
+
version: '2.1.0',
|
|
87
|
+
endpoints: ['/context', '/state', '/score', '/agents', '/agent/:name', '/refresh']
|
|
88
|
+
}));
|
|
31
89
|
return;
|
|
32
90
|
}
|
|
33
91
|
|
|
34
92
|
if (req.url === '/context') {
|
|
93
|
+
const meta = {
|
|
94
|
+
protocol: 'mcp-lite',
|
|
95
|
+
version: '0.1',
|
|
96
|
+
generatedAt: new Date().toISOString(),
|
|
97
|
+
};
|
|
35
98
|
const [context, plan, quickStart] = await Promise.all([
|
|
36
99
|
readFileSafe('CONTEXT.md', 'CONTEXT.md'),
|
|
37
100
|
readFileSafe('IMPLEMENTATION-PLAN.md', 'IMPLEMENTATION-PLAN.md'),
|
|
@@ -39,7 +102,54 @@ export function registerServeCommand(program) {
|
|
|
39
102
|
]);
|
|
40
103
|
|
|
41
104
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
42
|
-
res.end(JSON.stringify({ files: [context, plan, quickStart] }));
|
|
105
|
+
res.end(JSON.stringify({ meta, files: [context, plan, quickStart] }));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// /state - returns .ultra/state.json
|
|
110
|
+
if (req.url === '/state') {
|
|
111
|
+
let state = await loadState();
|
|
112
|
+
if (!state) state = await computeState();
|
|
113
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
114
|
+
res.end(JSON.stringify(state));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// /score - quick alignment score
|
|
119
|
+
if (req.url === '/score') {
|
|
120
|
+
const state = await computeState();
|
|
121
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
122
|
+
res.end(JSON.stringify({ score: state.score, sections: state.sections.completed, total: 34 }));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// /agents - list available agents
|
|
127
|
+
if (req.url === '/agents') {
|
|
128
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
129
|
+
res.end(JSON.stringify({ agents: BUILD_AGENTS }));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// /agent/:name - get specific agent prompt
|
|
134
|
+
if (req.url.startsWith('/agent/')) {
|
|
135
|
+
const agentName = req.url.replace('/agent/', '');
|
|
136
|
+
try {
|
|
137
|
+
const agentPath = path.resolve(process.cwd(), `agents/${agentName}.md`);
|
|
138
|
+
const content = await fs.readFile(agentPath, 'utf8');
|
|
139
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
140
|
+
res.end(JSON.stringify({ agent: agentName, prompt: content }));
|
|
141
|
+
} catch {
|
|
142
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
143
|
+
res.end(JSON.stringify({ error: `Agent ${agentName} not found` }));
|
|
144
|
+
}
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// /refresh - force state refresh
|
|
149
|
+
if (req.url === '/refresh') {
|
|
150
|
+
const state = await computeState();
|
|
151
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
152
|
+
res.end(JSON.stringify({ refreshed: true, score: state.score }));
|
|
43
153
|
return;
|
|
44
154
|
}
|
|
45
155
|
|
|
@@ -49,8 +159,15 @@ export function registerServeCommand(program) {
|
|
|
49
159
|
|
|
50
160
|
server.listen(port, () => {
|
|
51
161
|
console.log(chalk.green(`\n✅ Ultra-Dex MCP server running on http://localhost:${port}`));
|
|
52
|
-
console.log(chalk.
|
|
53
|
-
console.log(chalk.gray(' GET /
|
|
162
|
+
console.log(chalk.bold('\n📡 Endpoints:'));
|
|
163
|
+
console.log(chalk.gray(' GET / → Server info & endpoint list'));
|
|
164
|
+
console.log(chalk.gray(' GET /context → All context files'));
|
|
165
|
+
console.log(chalk.gray(' GET /state → Full project state'));
|
|
166
|
+
console.log(chalk.gray(' GET /score → Quick alignment score'));
|
|
167
|
+
console.log(chalk.gray(' GET /agents → List available agents'));
|
|
168
|
+
console.log(chalk.gray(' GET /agent/:n → Get specific agent prompt'));
|
|
169
|
+
console.log(chalk.gray(' GET /refresh → Force state refresh'));
|
|
170
|
+
console.log(chalk.cyan('\n💡 Connect your AI tool to this server for live context.\n'));
|
|
54
171
|
});
|
|
55
172
|
});
|
|
56
173
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { snapshotContext } from '../utils/sync.js';
|
|
4
|
+
|
|
5
|
+
export function registerSyncCommand(program) {
|
|
6
|
+
program
|
|
7
|
+
.command('sync')
|
|
8
|
+
.description('Auto-sync CONTEXT.md with current codebase')
|
|
9
|
+
.option('-d, --dir <directory>', 'Project directory', '.')
|
|
10
|
+
.action(async (options) => {
|
|
11
|
+
const rootDir = path.resolve(options.dir);
|
|
12
|
+
console.log(chalk.cyan('\n🔁 Ultra-Dex Context Sync\n'));
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const result = await snapshotContext(rootDir);
|
|
16
|
+
if (result.missingContext) {
|
|
17
|
+
console.log(chalk.red('❌ CONTEXT.md not found. Run `ultra-dex init` first.'));
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (result.updated) {
|
|
22
|
+
console.log(chalk.green('✅ CONTEXT.md updated with latest snapshot.'));
|
|
23
|
+
} else {
|
|
24
|
+
console.log(chalk.yellow('⚠️ CONTEXT.md already up to date.'));
|
|
25
|
+
}
|
|
26
|
+
console.log(chalk.gray(`Files scanned: ${result.summary.fileCount}`));
|
|
27
|
+
console.log(chalk.gray(`Stack guess: ${result.summary.stack}`));
|
|
28
|
+
console.log(chalk.gray(`Changes since last sync: +${result.diff.added} / -${result.diff.removed}\n`));
|
|
29
|
+
} catch (error) {
|
|
30
|
+
console.log(chalk.red('❌ Sync failed.'));
|
|
31
|
+
console.error(error);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
package/lib/templates/context.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { githubBlobUrl,
|
|
1
|
+
import { githubBlobUrl, githubWebUrl } from '../config/urls.js';
|
|
2
2
|
|
|
3
3
|
export const CONTEXT_TEMPLATE = `# {{PROJECT_NAME}} - Context
|
|
4
4
|
|
|
@@ -21,6 +21,6 @@ export const CONTEXT_TEMPLATE = `# {{PROJECT_NAME}} - Context
|
|
|
21
21
|
Setting up the implementation plan.
|
|
22
22
|
|
|
23
23
|
## Resources
|
|
24
|
-
- [Ultra-Dex Template](${
|
|
24
|
+
- [Ultra-Dex Template](${githubWebUrl()})
|
|
25
25
|
- [TaskFlow Example](${githubBlobUrl('@%20Ultra%20DeX/Saas%20plan/Examples/TaskFlow-Complete.md')})
|
|
26
26
|
`;
|
package/lib/utils/fallback.js
CHANGED
|
@@ -26,11 +26,13 @@ export async function copyWithFallback(primaryPath, fallbackPath, destinationPat
|
|
|
26
26
|
|
|
27
27
|
export async function listWithFallback(primaryPath, fallbackPath) {
|
|
28
28
|
try {
|
|
29
|
-
|
|
29
|
+
const files = await fs.readdir(primaryPath);
|
|
30
|
+
return { files, sourcePath: primaryPath };
|
|
30
31
|
} catch (primaryError) {
|
|
31
32
|
if (!fallbackPath) {
|
|
32
33
|
throw primaryError;
|
|
33
34
|
}
|
|
34
|
-
|
|
35
|
+
const files = await fs.readdir(fallbackPath);
|
|
36
|
+
return { files, sourcePath: fallbackPath };
|
|
35
37
|
}
|
|
36
38
|
}
|
package/lib/utils/files.js
CHANGED
|
@@ -21,47 +21,6 @@ export async function pathExists(targetPath, type = 'file') {
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
export async function copyWithFallback({ primary, fallback, destination, onPrimaryMissing }) {
|
|
25
|
-
try {
|
|
26
|
-
await fs.copyFile(primary, destination);
|
|
27
|
-
return 'primary';
|
|
28
|
-
} catch (primaryError) {
|
|
29
|
-
if (onPrimaryMissing) {
|
|
30
|
-
onPrimaryMissing(primaryError);
|
|
31
|
-
}
|
|
32
|
-
if (!fallback) {
|
|
33
|
-
throw primaryError;
|
|
34
|
-
}
|
|
35
|
-
await fs.copyFile(fallback, destination);
|
|
36
|
-
return 'fallback';
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export async function readWithFallback({ primary, fallback, encoding = 'utf-8' }) {
|
|
41
|
-
try {
|
|
42
|
-
return await fs.readFile(primary, encoding);
|
|
43
|
-
} catch (primaryError) {
|
|
44
|
-
if (!fallback) {
|
|
45
|
-
throw primaryError;
|
|
46
|
-
}
|
|
47
|
-
return await fs.readFile(fallback, encoding);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
24
|
export function resolveAssetPath(basePath, relativePath) {
|
|
52
25
|
return path.join(basePath, relativePath);
|
|
53
26
|
}
|
|
54
|
-
|
|
55
|
-
export async function copyDirectory(srcDir, destDir) {
|
|
56
|
-
await fs.mkdir(destDir, { recursive: true });
|
|
57
|
-
const entries = await fs.readdir(srcDir, { withFileTypes: true });
|
|
58
|
-
for (const entry of entries) {
|
|
59
|
-
const srcPath = path.join(srcDir, entry.name);
|
|
60
|
-
const destPath = path.join(destDir, entry.name);
|
|
61
|
-
if (entry.isDirectory()) {
|
|
62
|
-
await copyDirectory(srcPath, destPath);
|
|
63
|
-
} else if (entry.isFile()) {
|
|
64
|
-
await fs.copyFile(srcPath, destPath);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
const IGNORED_DIRS = new Set([
|
|
5
|
+
'.git',
|
|
6
|
+
'node_modules',
|
|
7
|
+
'.next',
|
|
8
|
+
'dist',
|
|
9
|
+
'build',
|
|
10
|
+
'out',
|
|
11
|
+
'.turbo',
|
|
12
|
+
'.cache',
|
|
13
|
+
'.ultra-dex',
|
|
14
|
+
'.cursor',
|
|
15
|
+
'.agents',
|
|
16
|
+
'coverage',
|
|
17
|
+
'.idea',
|
|
18
|
+
'.vscode',
|
|
19
|
+
]);
|
|
20
|
+
const IGNORED_FILES = new Set(['CONTEXT.md', '.DS_Store']);
|
|
21
|
+
const SNAPSHOT_DIR = '.ultra-dex';
|
|
22
|
+
const SNAPSHOT_FILE = 'context-snapshot.json';
|
|
23
|
+
const AUTO_SYNC_HEADER = '## Auto-Sync Snapshot';
|
|
24
|
+
const SCHEMA_PATTERNS = [
|
|
25
|
+
/schema\.prisma$/i,
|
|
26
|
+
/drizzle\/schema/i,
|
|
27
|
+
/supabase\/migrations/i,
|
|
28
|
+
/migrations\/.*\.(sql|ts|js)$/i,
|
|
29
|
+
/db\/schema/i,
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
async function listFilesRecursive(rootDir, baseDir = rootDir) {
|
|
33
|
+
let results = [];
|
|
34
|
+
const entries = await fs.readdir(rootDir, { withFileTypes: true });
|
|
35
|
+
for (const entry of entries) {
|
|
36
|
+
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
37
|
+
const fullPath = path.join(rootDir, entry.name);
|
|
38
|
+
const relativePath = path.relative(baseDir, fullPath);
|
|
39
|
+
if (entry.isDirectory()) {
|
|
40
|
+
results = results.concat(await listFilesRecursive(fullPath, baseDir));
|
|
41
|
+
} else if (entry.isFile()) {
|
|
42
|
+
if (IGNORED_FILES.has(entry.name)) continue;
|
|
43
|
+
results.push(relativePath);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return results;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function inferStackFromFiles(fileList) {
|
|
50
|
+
if (fileList.some((file) => file.includes('package.json'))) {
|
|
51
|
+
if (fileList.some((file) => file.includes('next.config'))) return 'Next.js';
|
|
52
|
+
if (fileList.some((file) => file.includes('remix.config'))) return 'Remix';
|
|
53
|
+
if (fileList.some((file) => file.includes('svelte.config'))) return 'SvelteKit';
|
|
54
|
+
return 'Node.js';
|
|
55
|
+
}
|
|
56
|
+
if (fileList.some((file) => file.includes('pyproject.toml') || file.includes('requirements.txt'))) {
|
|
57
|
+
return 'Python';
|
|
58
|
+
}
|
|
59
|
+
return 'Unknown';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function classifyFilePaths(files) {
|
|
63
|
+
const appFiles = [];
|
|
64
|
+
const apiFiles = [];
|
|
65
|
+
const schemaFiles = [];
|
|
66
|
+
const configFiles = [];
|
|
67
|
+
|
|
68
|
+
for (const file of files) {
|
|
69
|
+
if (SCHEMA_PATTERNS.some((pattern) => pattern.test(file))) {
|
|
70
|
+
schemaFiles.push(file);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (/^app\/api\//i.test(file) || /api\/.*\.(ts|js)$/i.test(file)) {
|
|
74
|
+
apiFiles.push(file);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (/\.(tsx|jsx|svelte|vue)$/i.test(file) || /^app\//i.test(file)) {
|
|
78
|
+
appFiles.push(file);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (/(config|\.config)\.(js|ts|json)$/i.test(file) || /\.(env|toml|yaml|yml)$/i.test(file)) {
|
|
82
|
+
configFiles.push(file);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { appFiles, apiFiles, schemaFiles, configFiles };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function buildAutoSyncSection(summary) {
|
|
90
|
+
const lines = [];
|
|
91
|
+
lines.push(AUTO_SYNC_HEADER);
|
|
92
|
+
lines.push('');
|
|
93
|
+
lines.push(`- Last synced: ${summary.generatedAt}`);
|
|
94
|
+
lines.push(`- Project root: ${summary.root}`);
|
|
95
|
+
lines.push(`- Stack guess: ${summary.stack}`);
|
|
96
|
+
lines.push(`- Total files scanned: ${summary.fileCount}`);
|
|
97
|
+
lines.push(`- App/UI files: ${summary.appCount}`);
|
|
98
|
+
lines.push(`- API files: ${summary.apiCount}`);
|
|
99
|
+
lines.push(`- Schema files: ${summary.schemaCount}`);
|
|
100
|
+
lines.push(`- Config files: ${summary.configCount}`);
|
|
101
|
+
lines.push('');
|
|
102
|
+
if (summary.appFiles.length > 0) {
|
|
103
|
+
lines.push('### App/UI Files');
|
|
104
|
+
lines.push(...summary.appFiles.map((file) => `- ${file}`));
|
|
105
|
+
lines.push('');
|
|
106
|
+
}
|
|
107
|
+
if (summary.apiFiles.length > 0) {
|
|
108
|
+
lines.push('### API Files');
|
|
109
|
+
lines.push(...summary.apiFiles.map((file) => `- ${file}`));
|
|
110
|
+
lines.push('');
|
|
111
|
+
}
|
|
112
|
+
if (summary.schemaFiles.length > 0) {
|
|
113
|
+
lines.push('### Schema Files');
|
|
114
|
+
lines.push(...summary.schemaFiles.map((file) => `- ${file}`));
|
|
115
|
+
lines.push('');
|
|
116
|
+
}
|
|
117
|
+
if (summary.configFiles.length > 0) {
|
|
118
|
+
lines.push('### Config Files');
|
|
119
|
+
lines.push(...summary.configFiles.map((file) => `- ${file}`));
|
|
120
|
+
lines.push('');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return lines.join('\n').trim();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function summarizeDiff(previous, next) {
|
|
127
|
+
if (!previous) {
|
|
128
|
+
return {
|
|
129
|
+
added: next.fileCount,
|
|
130
|
+
removed: 0,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
const previousSet = new Set(previous.fileList || []);
|
|
134
|
+
const nextSet = new Set(next.fileList || []);
|
|
135
|
+
let added = 0;
|
|
136
|
+
let removed = 0;
|
|
137
|
+
for (const file of nextSet) {
|
|
138
|
+
if (!previousSet.has(file)) added++;
|
|
139
|
+
}
|
|
140
|
+
for (const file of previousSet) {
|
|
141
|
+
if (!nextSet.has(file)) removed++;
|
|
142
|
+
}
|
|
143
|
+
return { added, removed };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export async function snapshotContext(rootDir) {
|
|
147
|
+
const files = await listFilesRecursive(rootDir);
|
|
148
|
+
const { appFiles, apiFiles, schemaFiles, configFiles } = classifyFilePaths(files);
|
|
149
|
+
const summary = {
|
|
150
|
+
generatedAt: new Date().toISOString(),
|
|
151
|
+
root: rootDir,
|
|
152
|
+
fileCount: files.length,
|
|
153
|
+
appCount: appFiles.length,
|
|
154
|
+
apiCount: apiFiles.length,
|
|
155
|
+
schemaCount: schemaFiles.length,
|
|
156
|
+
configCount: configFiles.length,
|
|
157
|
+
appFiles: appFiles.slice(0, 25),
|
|
158
|
+
apiFiles: apiFiles.slice(0, 25),
|
|
159
|
+
schemaFiles: schemaFiles.slice(0, 25),
|
|
160
|
+
configFiles: configFiles.slice(0, 25),
|
|
161
|
+
stack: inferStackFromFiles(files),
|
|
162
|
+
fileList: files,
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const snapshotDir = path.join(rootDir, SNAPSHOT_DIR);
|
|
166
|
+
await fs.mkdir(snapshotDir, { recursive: true });
|
|
167
|
+
const snapshotPath = path.join(snapshotDir, SNAPSHOT_FILE);
|
|
168
|
+
let previous = null;
|
|
169
|
+
try {
|
|
170
|
+
const previousRaw = await fs.readFile(snapshotPath, 'utf-8');
|
|
171
|
+
previous = JSON.parse(previousRaw);
|
|
172
|
+
} catch {
|
|
173
|
+
previous = null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
await fs.writeFile(snapshotPath, JSON.stringify(summary, null, 2));
|
|
177
|
+
|
|
178
|
+
const contextPath = path.join(rootDir, 'CONTEXT.md');
|
|
179
|
+
let contextContent = null;
|
|
180
|
+
try {
|
|
181
|
+
contextContent = await fs.readFile(contextPath, 'utf-8');
|
|
182
|
+
} catch {
|
|
183
|
+
return {
|
|
184
|
+
summary,
|
|
185
|
+
updated: false,
|
|
186
|
+
missingContext: true,
|
|
187
|
+
diff: summarizeDiff(previous, summary),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const section = buildAutoSyncSection(summary);
|
|
192
|
+
let updatedContext = contextContent;
|
|
193
|
+
if (contextContent.includes(AUTO_SYNC_HEADER)) {
|
|
194
|
+
const pattern = new RegExp(`${AUTO_SYNC_HEADER}[\\s\\S]*?(?=^##\\s|\\n##\\s|$)`, 'm');
|
|
195
|
+
updatedContext = contextContent.replace(pattern, `${section}\n\n`);
|
|
196
|
+
} else {
|
|
197
|
+
updatedContext = `${contextContent.trim()}\n\n${section}\n`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (updatedContext !== contextContent) {
|
|
201
|
+
await fs.writeFile(contextPath, updatedContext);
|
|
202
|
+
return {
|
|
203
|
+
summary,
|
|
204
|
+
updated: true,
|
|
205
|
+
missingContext: false,
|
|
206
|
+
diff: summarizeDiff(previous, summary),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
summary,
|
|
212
|
+
updated: false,
|
|
213
|
+
missingContext: false,
|
|
214
|
+
diff: summarizeDiff(previous, summary),
|
|
215
|
+
};
|
|
216
|
+
}
|