archsync 1.0.0 ā 1.0.2
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 +67 -0
- package/dist/archsync.cjs +2 -0
- package/package.json +11 -7
- package/bin/cli.js +0 -91
- package/src/__tests__/e2e-workflow.test.js +0 -66
- package/src/__tests__/hashEngine.test.js +0 -109
- package/src/__tests__/impact.test.js +0 -137
- package/src/__tests__/parsers.test.js +0 -496
- package/src/__tests__/scan-pipeline.test.js +0 -332
- package/src/__tests__/schemaBuilder.test.js +0 -145
- package/src/__tests__/workspace.test.js +0 -178
- package/src/commands/backup.js +0 -54
- package/src/commands/connect.js +0 -129
- package/src/commands/diff.js +0 -228
- package/src/commands/export.js +0 -125
- package/src/commands/impactReport.js +0 -50
- package/src/commands/import.js +0 -126
- package/src/commands/init.js +0 -80
- package/src/commands/login.js +0 -116
- package/src/commands/plugin.js +0 -28
- package/src/commands/push.js +0 -194
- package/src/commands/register.js +0 -127
- package/src/commands/scan.js +0 -498
- package/src/commands/serve.js +0 -133
- package/src/commands/setup.js +0 -233
- package/src/commands/status.js +0 -56
- package/src/commands/validate.js +0 -245
- package/src/commands/watch.js +0 -70
- package/src/core/credentialStore.js +0 -76
- package/src/core/hashEngine.js +0 -34
- package/src/core/impactEngine.js +0 -192
- package/src/core/monorepoDetector.js +0 -41
- package/src/core/pluginManager.js +0 -40
- package/src/core/relationshipEngine.js +0 -917
- package/src/core/requestSigning.js +0 -16
- package/src/core/schemaBuilder.js +0 -230
- package/src/core/schemaDeduplicator.js +0 -54
- package/src/core/supabaseClient.js +0 -68
- package/src/core/workspaceDetector.js +0 -113
- package/src/parsers/astParser.js +0 -274
- package/src/parsers/configParser.js +0 -49
- package/src/parsers/dependencyGraph.js +0 -31
- package/src/parsers/flutterParser.js +0 -98
- package/src/parsers/goParser.js +0 -99
- package/src/parsers/index.js +0 -211
- package/src/parsers/javaParser.js +0 -89
- package/src/parsers/nodeParser.js +0 -429
- package/src/parsers/pythonParser.js +0 -109
- package/src/parsers/reactParser.js +0 -368
- package/src/parsers/smartComment.js +0 -144
package/src/commands/scan.js
DELETED
|
@@ -1,498 +0,0 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
|
-
import ora from 'ora';
|
|
3
|
-
import fs from 'fs';
|
|
4
|
-
import path from 'path';
|
|
5
|
-
import { execSync } from 'child_process';
|
|
6
|
-
import { glob } from 'glob';
|
|
7
|
-
import { readConfig } from '../core/supabaseClient.js';
|
|
8
|
-
import { parseFile, moduleFallbackEntity } from '../parsers/index.js';
|
|
9
|
-
import { buildSchema } from '../core/schemaBuilder.js';
|
|
10
|
-
import { buildRelationships } from '../core/relationshipEngine.js';
|
|
11
|
-
import { hashSchema } from '../core/hashEngine.js';
|
|
12
|
-
import {
|
|
13
|
-
detectWorkspaceProjects,
|
|
14
|
-
includePatternsFor,
|
|
15
|
-
WORKSPACE_EXCLUDES,
|
|
16
|
-
} from '../core/workspaceDetector.js';
|
|
17
|
-
import { analyzeImpact, embedWarnings } from '../core/impactEngine.js';
|
|
18
|
-
import { printImpactWarnings } from './impactReport.js';
|
|
19
|
-
|
|
20
|
-
const LOCAL_SCHEMA_FILE = '.archsync-schema.json';
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Run `git diff --name-only HEAD~1` in cwd and return absolute paths of changed files.
|
|
24
|
-
* Returns null if git is unavailable or there is no history.
|
|
25
|
-
*/
|
|
26
|
-
function getGitChangedFiles(cwd) {
|
|
27
|
-
try {
|
|
28
|
-
const output = execSync('git diff --name-only HEAD~1', { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
29
|
-
return output
|
|
30
|
-
.split('\n')
|
|
31
|
-
.map(f => f.trim())
|
|
32
|
-
.filter(Boolean)
|
|
33
|
-
.map(f => path.resolve(cwd, f));
|
|
34
|
-
} catch {
|
|
35
|
-
// No git history or git not available
|
|
36
|
-
return null;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export async function scanCommand(options) {
|
|
41
|
-
const debug = options.debug || false;
|
|
42
|
-
const incremental = options.incremental || false;
|
|
43
|
-
|
|
44
|
-
console.log(chalk.blue.bold('\nš ArchSync CLI ā Scanning Codebase\n'));
|
|
45
|
-
|
|
46
|
-
if (options.workspace) {
|
|
47
|
-
return workspaceScan(options);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const config = readConfig(options.dir);
|
|
51
|
-
if (!config) {
|
|
52
|
-
// No project config ā if this directory holds several projects
|
|
53
|
-
// (backend + app + admin ā¦), fall through to workspace mode instead
|
|
54
|
-
// of demanding an init nobody ran yet.
|
|
55
|
-
const projects = detectWorkspaceProjects(path.resolve(options.dir || '.'));
|
|
56
|
-
if (projects.length >= 2) {
|
|
57
|
-
console.log(chalk.cyan('No .archsync.json found, but this looks like a multi-project workspace:'));
|
|
58
|
-
for (const p of projects) {
|
|
59
|
-
console.log(chalk.gray(` ⢠${p.name} (${p.framework}) ā system "${p.system}"`));
|
|
60
|
-
}
|
|
61
|
-
console.log(chalk.cyan('Running workspace scan. Use `archsync scan --workspace` to do this explicitly.\n'));
|
|
62
|
-
return workspaceScan(options);
|
|
63
|
-
}
|
|
64
|
-
console.log(chalk.red('ā No .archsync.json found. Run `archsync init` first.'));
|
|
65
|
-
process.exit(1);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const cwd = path.resolve(options.dir || '.');
|
|
69
|
-
const schemaPath = path.resolve(cwd, LOCAL_SCHEMA_FILE);
|
|
70
|
-
const spinner = ora('Discovering files...').start();
|
|
71
|
-
|
|
72
|
-
const includePatterns = config.scan?.include || [options.include || '**/*.{js,ts,jsx,tsx,dart,py,go,java,kt}'];
|
|
73
|
-
const excludePatterns = config.scan?.exclude || [options.exclude || '**/node_modules/**'];
|
|
74
|
-
|
|
75
|
-
// āāā Incremental mode āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
76
|
-
let incrementalMode = false;
|
|
77
|
-
let cachedSchema = null;
|
|
78
|
-
let changedFiles = null;
|
|
79
|
-
|
|
80
|
-
if (incremental) {
|
|
81
|
-
changedFiles = getGitChangedFiles(cwd);
|
|
82
|
-
if (!changedFiles) {
|
|
83
|
-
spinner.warn('No git history found ā falling back to full scan.');
|
|
84
|
-
} else if (changedFiles.length === 0) {
|
|
85
|
-
spinner.info('No changed files since last commit. Schema is up to date.');
|
|
86
|
-
return;
|
|
87
|
-
} else if (fs.existsSync(schemaPath)) {
|
|
88
|
-
try {
|
|
89
|
-
cachedSchema = JSON.parse(fs.readFileSync(schemaPath, 'utf-8'));
|
|
90
|
-
incrementalMode = true;
|
|
91
|
-
spinner.text = `Incremental: scanning ${changedFiles.length} changed file(s)...`;
|
|
92
|
-
} catch {
|
|
93
|
-
spinner.warn('Cached schema unreadable ā falling back to full scan.');
|
|
94
|
-
}
|
|
95
|
-
} else {
|
|
96
|
-
spinner.warn('No cached schema found ā falling back to full scan.');
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// āāā File discovery āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
101
|
-
let files = [];
|
|
102
|
-
|
|
103
|
-
if (incrementalMode) {
|
|
104
|
-
// Only scan the git-changed files that match include patterns
|
|
105
|
-
const includeExts = /\.(js|ts|jsx|tsx|dart|prisma)$/;
|
|
106
|
-
files = changedFiles.filter(f => {
|
|
107
|
-
if (!includeExts.test(f)) return false;
|
|
108
|
-
// Apply exclude patterns
|
|
109
|
-
const rel = path.relative(cwd, f);
|
|
110
|
-
const isExcluded = rel.includes('node_modules') || rel.includes('dist') || rel.includes('build');
|
|
111
|
-
return !isExcluded;
|
|
112
|
-
}).filter(f => fs.existsSync(f));
|
|
113
|
-
} else {
|
|
114
|
-
for (const pattern of includePatterns) {
|
|
115
|
-
const matched = await glob(pattern, {
|
|
116
|
-
cwd,
|
|
117
|
-
ignore: excludePatterns,
|
|
118
|
-
absolute: true,
|
|
119
|
-
});
|
|
120
|
-
files.push(...matched);
|
|
121
|
-
}
|
|
122
|
-
files = [...new Set(files)];
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
spinner.text = `Parsing ${files.length} files${incrementalMode ? ' (incremental)' : ''}...`;
|
|
126
|
-
|
|
127
|
-
const parsedFiles = [];
|
|
128
|
-
let errorCount = 0;
|
|
129
|
-
const errorFiles = [];
|
|
130
|
-
|
|
131
|
-
for (const file of files) {
|
|
132
|
-
try {
|
|
133
|
-
const result = parseFile(file, {
|
|
134
|
-
ast: options.ast || config.scan?.ast,
|
|
135
|
-
framework: config.framework,
|
|
136
|
-
});
|
|
137
|
-
// CodeSee-style: a source file with no recognised entities still
|
|
138
|
-
// becomes a `module` node so import arrows can reach it.
|
|
139
|
-
if (result.entities.length === 0) {
|
|
140
|
-
const fallback = moduleFallbackEntity(file, config.defaultSystem);
|
|
141
|
-
if (fallback) result.entities.push(fallback);
|
|
142
|
-
}
|
|
143
|
-
// `--system <label>` from init is AUTHORITATIVE for this project.
|
|
144
|
-
// Parsers guess systems from path heuristics ('app', 'web', ā¦) and
|
|
145
|
-
// would otherwise scatter one project across several systems.
|
|
146
|
-
// Genuine backend entities (route handlers, cloud functions) keep
|
|
147
|
-
// their label so cross-system API matching still works.
|
|
148
|
-
if (config.defaultSystem) {
|
|
149
|
-
for (const entity of result.entities) {
|
|
150
|
-
if (entity.system !== 'backend' || config.defaultSystem === 'backend') {
|
|
151
|
-
entity.system = config.defaultSystem;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
if (result.entities.length > 0 || result.relations.length > 0) {
|
|
156
|
-
parsedFiles.push(result);
|
|
157
|
-
}
|
|
158
|
-
} catch (err) {
|
|
159
|
-
errorCount++;
|
|
160
|
-
errorFiles.push({ file, error: err.message });
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
spinner.text = 'Building schema & relationships...';
|
|
165
|
-
|
|
166
|
-
let schema;
|
|
167
|
-
|
|
168
|
-
if (incrementalMode && cachedSchema) {
|
|
169
|
-
// Merge: remove stale entries from changed files, then add freshly parsed ones
|
|
170
|
-
const changedFileSet = new Set(changedFiles.map(f => f.replace(/\\/g, '/')));
|
|
171
|
-
|
|
172
|
-
const staleNodeIds = new Set(
|
|
173
|
-
(cachedSchema.nodes || [])
|
|
174
|
-
.filter(n => {
|
|
175
|
-
const src = n.metadata?.sourceFile || '';
|
|
176
|
-
return changedFileSet.has(src.replace(/\\/g, '/'));
|
|
177
|
-
})
|
|
178
|
-
.map(n => n.id)
|
|
179
|
-
);
|
|
180
|
-
|
|
181
|
-
const survivingNodes = (cachedSchema.nodes || []).filter(n => !staleNodeIds.has(n.id));
|
|
182
|
-
const survivingEdges = (cachedSchema.edges || []).filter(
|
|
183
|
-
e => !staleNodeIds.has(e.source) && !staleNodeIds.has(e.target)
|
|
184
|
-
);
|
|
185
|
-
|
|
186
|
-
// Build a mini-schema from only the changed files. Cross-file
|
|
187
|
-
// relations are resolved inside buildSchema against the fresh nodes.
|
|
188
|
-
const freshRelations = buildRelationships(parsedFiles, debug);
|
|
189
|
-
const freshSchema = buildSchema(parsedFiles, { ...config, debug }, freshRelations);
|
|
190
|
-
|
|
191
|
-
schema = {
|
|
192
|
-
...cachedSchema,
|
|
193
|
-
nodes: [...survivingNodes, ...freshSchema.nodes],
|
|
194
|
-
edges: [...survivingEdges, ...freshSchema.edges],
|
|
195
|
-
meta: {
|
|
196
|
-
...cachedSchema.meta,
|
|
197
|
-
nodeCount: survivingNodes.length + freshSchema.nodes.length,
|
|
198
|
-
edgeCount: survivingEdges.length + freshSchema.edges.length,
|
|
199
|
-
incrementalAt: new Date().toISOString(),
|
|
200
|
-
},
|
|
201
|
-
};
|
|
202
|
-
} else {
|
|
203
|
-
// Infer cross-file relationships (routeācontrollerāserviceāmodel)
|
|
204
|
-
// first ā PASS 0 composes public route paths that the schema builder
|
|
205
|
-
// must see ā then resolve every relation to node ids in buildSchema.
|
|
206
|
-
const inferredRelations = buildRelationships(parsedFiles, debug);
|
|
207
|
-
schema = buildSchema(parsedFiles, { ...config, debug }, inferredRelations);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
schema.hash = hashSchema(schema);
|
|
211
|
-
|
|
212
|
-
// Impact analysis against the previous snapshot. Warnings are embedded
|
|
213
|
-
// AFTER hashing so an unchanged architecture keeps a stable hash.
|
|
214
|
-
let previousSchema = null;
|
|
215
|
-
if (fs.existsSync(schemaPath)) {
|
|
216
|
-
try { previousSchema = JSON.parse(fs.readFileSync(schemaPath, 'utf-8')); } catch { /* corrupt cache ā skip */ }
|
|
217
|
-
}
|
|
218
|
-
const impactWarnings = analyzeImpact(previousSchema, schema);
|
|
219
|
-
embedWarnings(schema, impactWarnings);
|
|
220
|
-
|
|
221
|
-
// Save locally
|
|
222
|
-
fs.writeFileSync(schemaPath, JSON.stringify(schema, null, 2));
|
|
223
|
-
|
|
224
|
-
spinner.succeed(`Scan complete!${incrementalMode ? chalk.gray(' (incremental)') : ''}`);
|
|
225
|
-
|
|
226
|
-
// āāā Summary Report āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
227
|
-
console.log(chalk.gray('\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā'));
|
|
228
|
-
console.log(chalk.gray('ā') + chalk.white.bold(' Scan Results Summary ') + chalk.gray('ā'));
|
|
229
|
-
console.log(chalk.gray('ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā£'));
|
|
230
|
-
console.log(chalk.gray('ā') + chalk.white(` Files scanned: ${chalk.bold(String(files.length).padStart(6))} `) + chalk.gray('ā'));
|
|
231
|
-
console.log(chalk.gray('ā') + chalk.white(` Files parsed: ${chalk.bold(String(parsedFiles.length).padStart(6))} `) + chalk.gray('ā'));
|
|
232
|
-
console.log(chalk.gray('ā') + chalk.green(` Entities found: ${chalk.bold(String(schema.nodes.length).padStart(6))} `) + chalk.gray('ā'));
|
|
233
|
-
console.log(chalk.gray('ā') + chalk.blue(` Relations: ${chalk.bold(String(schema.edges.length).padStart(6))} `) + chalk.gray('ā'));
|
|
234
|
-
if (incrementalMode) {
|
|
235
|
-
console.log(chalk.gray('ā') + chalk.cyan(` Mode: incremental `) + chalk.gray('ā'));
|
|
236
|
-
}
|
|
237
|
-
if (errorCount > 0) {
|
|
238
|
-
console.log(chalk.gray('ā') + chalk.yellow(` Parse errors: ${chalk.bold(String(errorCount).padStart(6))} `) + chalk.gray('ā'));
|
|
239
|
-
}
|
|
240
|
-
console.log(chalk.gray('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā'));
|
|
241
|
-
|
|
242
|
-
// Entity breakdown by type
|
|
243
|
-
const typeCounts = {};
|
|
244
|
-
for (const node of schema.nodes) {
|
|
245
|
-
typeCounts[node.entityType] = (typeCounts[node.entityType] || 0) + 1;
|
|
246
|
-
}
|
|
247
|
-
console.log(chalk.gray('\nāāā Entity Breakdown āāāāāāāāāāāāāāā'));
|
|
248
|
-
const typeIcons = {
|
|
249
|
-
route: 'š£ ', controller: 'š®', service: 'āļø ', model: 'š¦', database: 'š ',
|
|
250
|
-
middleware: 'š” ', mount: 'š', screen: 'š±', widget: 'š§©', api: 'š',
|
|
251
|
-
worker: 'š·', trigger: 'ā”', config: 'š§', module: 'š',
|
|
252
|
-
};
|
|
253
|
-
for (const [type, count] of Object.entries(typeCounts).sort((a, b) => b[1] - a[1])) {
|
|
254
|
-
const icon = typeIcons[type] || ' ';
|
|
255
|
-
console.log(chalk.white(` ${icon} ${type.padEnd(15)} ${chalk.bold(count)}`));
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// Relation breakdown by type
|
|
259
|
-
const relCounts = {};
|
|
260
|
-
for (const edge of schema.edges) {
|
|
261
|
-
relCounts[edge.relation] = (relCounts[edge.relation] || 0) + 1;
|
|
262
|
-
}
|
|
263
|
-
if (Object.keys(relCounts).length > 0) {
|
|
264
|
-
console.log(chalk.gray('\nāāā Relation Breakdown āāāāāāāāāāāāā'));
|
|
265
|
-
const relIcons = {
|
|
266
|
-
handles: 'ā ', uses: 'ā ', queries: '⢠', extends: 'ā ',
|
|
267
|
-
protected_by: 'š” ', calls: 'š”', consumes: 'ā ', stored_in: 'š¾',
|
|
268
|
-
};
|
|
269
|
-
for (const [rel, count] of Object.entries(relCounts).sort((a, b) => b[1] - a[1])) {
|
|
270
|
-
const icon = relIcons[rel] || ' ';
|
|
271
|
-
console.log(chalk.white(` ${icon} ${rel.padEnd(15)} ${chalk.bold(count)}`));
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// Entity by system
|
|
276
|
-
const sysCounts = {};
|
|
277
|
-
for (const node of schema.nodes) {
|
|
278
|
-
sysCounts[node.system] = (sysCounts[node.system] || 0) + 1;
|
|
279
|
-
}
|
|
280
|
-
if (Object.keys(sysCounts).length > 1) {
|
|
281
|
-
console.log(chalk.gray('\nāāā Entities by System āāāāāāāāāāāāā'));
|
|
282
|
-
for (const [sys, count] of Object.entries(sysCounts).sort((a, b) => b[1] - a[1])) {
|
|
283
|
-
console.log(chalk.white(` ${sys.padEnd(15)} ${chalk.bold(count)}`));
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// Debug output
|
|
288
|
-
if (debug) {
|
|
289
|
-
console.log(chalk.gray('\nāāā Debug: Parse Errors āāāāāāāāāāāā'));
|
|
290
|
-
if (errorFiles.length > 0) {
|
|
291
|
-
for (const { file, error } of errorFiles) {
|
|
292
|
-
console.log(chalk.red(` ā ${path.relative('.', file)}: ${error}`));
|
|
293
|
-
}
|
|
294
|
-
} else {
|
|
295
|
-
console.log(chalk.green(' No parse errors'));
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
console.log(chalk.gray('\nāāā Debug: Files with Entities āāāāā'));
|
|
299
|
-
for (const pf of parsedFiles.slice(0, 50)) {
|
|
300
|
-
const entTypes = [...new Set((pf.entities || []).map(e => e.entityType))].join(', ');
|
|
301
|
-
console.log(chalk.gray(` ${path.relative('.', pf.filePath)} ā ${chalk.white(entTypes)} (${pf.entities.length})`));
|
|
302
|
-
}
|
|
303
|
-
if (parsedFiles.length > 50) {
|
|
304
|
-
console.log(chalk.gray(` ... and ${parsedFiles.length - 50} more files`));
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
console.log(chalk.gray('\nāāā Debug: Sample Relations āāāāāāāā'));
|
|
308
|
-
for (const edge of schema.edges.slice(0, 30)) {
|
|
309
|
-
const src = schema.nodes.find(n => n.id === edge.source);
|
|
310
|
-
const tgt = schema.nodes.find(n => n.id === edge.target);
|
|
311
|
-
console.log(chalk.gray(` ${src?.text || '?'} ā ${tgt?.text || '?'} [${edge.relation}]`));
|
|
312
|
-
}
|
|
313
|
-
if (schema.edges.length > 30) {
|
|
314
|
-
console.log(chalk.gray(` ... and ${schema.edges.length - 30} more edges`));
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
printImpactWarnings(impactWarnings, { heading: 'Impact since last scan' });
|
|
319
|
-
|
|
320
|
-
console.log(chalk.gray(`\n Schema saved to: ${schemaPath}`));
|
|
321
|
-
console.log(chalk.gray(` Hash: ${schema.hash}`));
|
|
322
|
-
|
|
323
|
-
if (schema.edges.length === 0) {
|
|
324
|
-
console.log(chalk.yellow('\nā No relations detected. Try running with --debug for details.'));
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
console.log(chalk.blue('\nNext: ') + chalk.gray('archsync push -b main'));
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
331
|
-
// WORKSPACE SCAN ā scan several sibling projects (Node backend, Flutter
|
|
332
|
-
// app, React admin, ā¦) in one pass and cross-link them.
|
|
333
|
-
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
334
|
-
async function workspaceScan(options) {
|
|
335
|
-
const debug = options.debug || false;
|
|
336
|
-
const cwd = path.resolve(options.dir || '.');
|
|
337
|
-
|
|
338
|
-
const projects = detectWorkspaceProjects(cwd);
|
|
339
|
-
if (projects.length === 0) {
|
|
340
|
-
console.log(chalk.red('ā No projects found in this workspace.'));
|
|
341
|
-
console.log(chalk.gray(' A project is a folder with a package.json or pubspec.yaml.'));
|
|
342
|
-
process.exit(1);
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
console.log(chalk.white.bold('Workspace projects:'));
|
|
346
|
-
for (const p of projects) {
|
|
347
|
-
console.log(chalk.white(` š ${p.name.padEnd(20)} ${chalk.gray(p.framework.padEnd(8))} ā system ${chalk.cyan(p.system)}`));
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
const spinner = ora('Scanning projects...').start();
|
|
351
|
-
const allParsed = [];
|
|
352
|
-
let totalFiles = 0;
|
|
353
|
-
let errorCount = 0;
|
|
354
|
-
const errorFiles = [];
|
|
355
|
-
|
|
356
|
-
for (const project of projects) {
|
|
357
|
-
spinner.text = `Scanning ${project.name} (${project.framework})...`;
|
|
358
|
-
|
|
359
|
-
let files = [];
|
|
360
|
-
for (const pattern of includePatternsFor(project.framework)) {
|
|
361
|
-
const matched = await glob(pattern, {
|
|
362
|
-
cwd: project.path,
|
|
363
|
-
ignore: WORKSPACE_EXCLUDES,
|
|
364
|
-
absolute: true,
|
|
365
|
-
});
|
|
366
|
-
files.push(...matched);
|
|
367
|
-
}
|
|
368
|
-
files = [...new Set(files)];
|
|
369
|
-
totalFiles += files.length;
|
|
370
|
-
|
|
371
|
-
for (const file of files) {
|
|
372
|
-
try {
|
|
373
|
-
const result = parseFile(file, {
|
|
374
|
-
ast: options.ast,
|
|
375
|
-
framework: project.framework,
|
|
376
|
-
});
|
|
377
|
-
// CodeSee-style: every source file becomes at least a `module` node
|
|
378
|
-
if (result.entities.length === 0) {
|
|
379
|
-
const fallback = moduleFallbackEntity(file, project.system);
|
|
380
|
-
if (fallback) result.entities.push(fallback);
|
|
381
|
-
}
|
|
382
|
-
if (result.entities.length > 0 || result.relations.length > 0) {
|
|
383
|
-
// The workspace layout is authoritative for system labels ā
|
|
384
|
-
// path heuristics inside parsers can't know that ./admin is
|
|
385
|
-
// the React admin and ./mobile_app the Flutter app.
|
|
386
|
-
for (const entity of result.entities) entity.system = project.system;
|
|
387
|
-
allParsed.push(result);
|
|
388
|
-
}
|
|
389
|
-
} catch (err) {
|
|
390
|
-
errorCount++;
|
|
391
|
-
errorFiles.push({ file, error: err.message });
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
spinner.text = 'Linking systems...';
|
|
397
|
-
const relations = buildRelationships(allParsed, debug);
|
|
398
|
-
const schema = buildSchema(allParsed, { defaultSystem: 'backend', debug }, relations);
|
|
399
|
-
schema.meta.workspace = {
|
|
400
|
-
root: cwd,
|
|
401
|
-
projects: projects.map(p => ({ name: p.name, framework: p.framework, system: p.system })),
|
|
402
|
-
};
|
|
403
|
-
schema.hash = hashSchema(schema);
|
|
404
|
-
|
|
405
|
-
// Impact analysis against the previous combined snapshot (embedded after
|
|
406
|
-
// hashing so an unchanged architecture keeps a stable hash). Because the
|
|
407
|
-
// workspace graph spans systems, a removed backend route flags the
|
|
408
|
-
// Flutter screens and React pages that call it.
|
|
409
|
-
const combinedPath = path.join(cwd, LOCAL_SCHEMA_FILE);
|
|
410
|
-
let previousSchema = null;
|
|
411
|
-
if (fs.existsSync(combinedPath)) {
|
|
412
|
-
try { previousSchema = JSON.parse(fs.readFileSync(combinedPath, 'utf-8')); } catch { /* corrupt cache ā skip */ }
|
|
413
|
-
}
|
|
414
|
-
const impactWarnings = analyzeImpact(previousSchema, schema);
|
|
415
|
-
embedWarnings(schema, impactWarnings);
|
|
416
|
-
|
|
417
|
-
// Combined schema (multiSystem: true ā canvas honours per-node systems)
|
|
418
|
-
fs.writeFileSync(combinedPath, JSON.stringify(schema, null, 2));
|
|
419
|
-
|
|
420
|
-
// Per-system schemas for the canvas's multi-file workspace import
|
|
421
|
-
const perSystemPaths = [];
|
|
422
|
-
for (const system of schema.systems) {
|
|
423
|
-
const sysNodes = schema.nodes.filter(n => n.system === system);
|
|
424
|
-
const nodeIds = new Set(sysNodes.map(n => n.id));
|
|
425
|
-
const sysEdges = schema.edges.filter(e => nodeIds.has(e.source) && nodeIds.has(e.target));
|
|
426
|
-
const sysSchema = {
|
|
427
|
-
system,
|
|
428
|
-
generatedBy: schema.meta?.source || 'archsync-cli',
|
|
429
|
-
nodes: sysNodes,
|
|
430
|
-
edges: sysEdges,
|
|
431
|
-
// Warnings that originate in or impact this system, so a
|
|
432
|
-
// single-system import still shows them in the right place.
|
|
433
|
-
warnings: (schema.warnings || []).filter(
|
|
434
|
-
w => w.system === system || (w.affectedSystems || []).includes(system)
|
|
435
|
-
),
|
|
436
|
-
};
|
|
437
|
-
const sysPath = path.join(cwd, `${system}.archsync-schema.json`);
|
|
438
|
-
fs.writeFileSync(sysPath, JSON.stringify(sysSchema, null, 2));
|
|
439
|
-
perSystemPaths.push(sysPath);
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
spinner.succeed('Workspace scan complete!');
|
|
443
|
-
|
|
444
|
-
const crossEdges = schema.edges.filter(e => e.isCrossSystem);
|
|
445
|
-
|
|
446
|
-
console.log(chalk.gray('\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā'));
|
|
447
|
-
console.log(chalk.gray('ā') + chalk.white.bold(' Workspace Scan Summary ') + chalk.gray('ā'));
|
|
448
|
-
console.log(chalk.gray('ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā£'));
|
|
449
|
-
console.log(chalk.gray('ā') + chalk.white(` Projects: ${chalk.bold(String(projects.length).padStart(5))} `) + chalk.gray('ā'));
|
|
450
|
-
console.log(chalk.gray('ā') + chalk.white(` Files scanned: ${chalk.bold(String(totalFiles).padStart(5))} `) + chalk.gray('ā'));
|
|
451
|
-
console.log(chalk.gray('ā') + chalk.green(` Entities found: ${chalk.bold(String(schema.nodes.length).padStart(5))} `) + chalk.gray('ā'));
|
|
452
|
-
console.log(chalk.gray('ā') + chalk.blue(` Relations: ${chalk.bold(String(schema.edges.length).padStart(5))} `) + chalk.gray('ā'));
|
|
453
|
-
console.log(chalk.gray('ā') + chalk.magenta(` Cross-system: ${chalk.bold(String(crossEdges.length).padStart(5))} `) + chalk.gray('ā'));
|
|
454
|
-
if (errorCount > 0) {
|
|
455
|
-
console.log(chalk.gray('ā') + chalk.yellow(` Parse errors: ${chalk.bold(String(errorCount).padStart(5))} `) + chalk.gray('ā'));
|
|
456
|
-
}
|
|
457
|
-
console.log(chalk.gray('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā'));
|
|
458
|
-
|
|
459
|
-
// Per-system entity counts
|
|
460
|
-
console.log(chalk.gray('\nāāā Entities by System āāāāāāāāāāāāā'));
|
|
461
|
-
for (const system of schema.systems) {
|
|
462
|
-
const count = schema.nodes.filter(n => n.system === system).length;
|
|
463
|
-
console.log(chalk.white(` ${system.padEnd(15)} ${chalk.bold(count)}`));
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
// Cross-system links are the headline feature ā always show them
|
|
467
|
-
if (crossEdges.length > 0) {
|
|
468
|
-
console.log(chalk.gray('\nāāā Cross-System Links āāāāāāāāāāāāā'));
|
|
469
|
-
const nodeById = new Map(schema.nodes.map(n => [n.id, n]));
|
|
470
|
-
for (const edge of crossEdges.slice(0, 20)) {
|
|
471
|
-
const src = nodeById.get(edge.source);
|
|
472
|
-
const tgt = nodeById.get(edge.target);
|
|
473
|
-
console.log(chalk.magenta(` ${src?.system}:${src?.text} ā ${tgt?.system}:${tgt?.text} [${edge.relation}]`));
|
|
474
|
-
}
|
|
475
|
-
if (crossEdges.length > 20) {
|
|
476
|
-
console.log(chalk.gray(` ... and ${crossEdges.length - 20} more`));
|
|
477
|
-
}
|
|
478
|
-
} else {
|
|
479
|
-
console.log(chalk.yellow('\nā No cross-system links found. Frontend API calls may not match any backend route.'));
|
|
480
|
-
console.log(chalk.gray(' Run with --debug to trace endpoint matching.'));
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
printImpactWarnings(impactWarnings, { heading: 'Impact since last scan' });
|
|
484
|
-
|
|
485
|
-
if (debug && errorFiles.length > 0) {
|
|
486
|
-
console.log(chalk.gray('\nāāā Debug: Parse Errors āāāāāāāāāāāā'));
|
|
487
|
-
for (const { file, error } of errorFiles) {
|
|
488
|
-
console.log(chalk.red(` ā ${path.relative(cwd, file)}: ${error}`));
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
console.log(chalk.gray(`\n Combined schema: ${combinedPath}`));
|
|
493
|
-
for (const p of perSystemPaths) {
|
|
494
|
-
console.log(chalk.gray(` Per-system schema: ${p}`));
|
|
495
|
-
}
|
|
496
|
-
console.log(chalk.gray(` Hash: ${schema.hash}`));
|
|
497
|
-
console.log(chalk.blue('\nNext: ') + chalk.gray('open the ArchSync canvas and import the schema file(s), or `archsync push`.'));
|
|
498
|
-
}
|
package/src/commands/serve.js
DELETED
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import http from 'http';
|
|
2
|
-
import fs from 'fs';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import chalk from 'chalk';
|
|
5
|
-
|
|
6
|
-
const LOCAL_SCHEMA_FILE = '.archsync-schema.json';
|
|
7
|
-
const DEFAULT_PORT = 4317;
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* archsync serve ā the localhost code bridge.
|
|
11
|
-
*
|
|
12
|
-
* Lets the ArchSync canvas show REAL code for any scanned member without the
|
|
13
|
-
* source ever leaving this machine:
|
|
14
|
-
*
|
|
15
|
-
* GET /v1/health ā { ok, root }
|
|
16
|
-
* GET /v1/schema ā the local .archsync-schema.json
|
|
17
|
-
* GET /v1/file?path=<abs>&from=<n>&to=<n> ā { path, from, to, total, code }
|
|
18
|
-
*
|
|
19
|
-
* Security model (zero-trust by construction):
|
|
20
|
-
* ⢠Binds strictly to 127.0.0.1 ā never reachable from another machine.
|
|
21
|
-
* ⢠Only serves files that appear in the scanned schema (allowlist),
|
|
22
|
-
* so even local callers can't read arbitrary paths.
|
|
23
|
-
* ⢠Read-only: no mutation endpoints exist.
|
|
24
|
-
* ⢠CORS restricted to localhost origins (the canvas dev/prod ports).
|
|
25
|
-
*/
|
|
26
|
-
export async function serveCommand(options) {
|
|
27
|
-
const dir = path.resolve(options.dir || '.');
|
|
28
|
-
const port = parseInt(options.port || DEFAULT_PORT, 10);
|
|
29
|
-
|
|
30
|
-
// āā Build the file allowlist from the scanned schema āāāāāāāāāāāāāā
|
|
31
|
-
const schemaPath = path.join(dir, LOCAL_SCHEMA_FILE);
|
|
32
|
-
let schema = null;
|
|
33
|
-
const allowedFiles = new Set();
|
|
34
|
-
|
|
35
|
-
const loadSchema = () => {
|
|
36
|
-
try {
|
|
37
|
-
schema = JSON.parse(fs.readFileSync(schemaPath, 'utf-8'));
|
|
38
|
-
allowedFiles.clear();
|
|
39
|
-
for (const n of schema.nodes || []) {
|
|
40
|
-
const sf = n.metadata?.sourceFile;
|
|
41
|
-
if (sf) allowedFiles.add(path.resolve(sf));
|
|
42
|
-
}
|
|
43
|
-
return true;
|
|
44
|
-
} catch {
|
|
45
|
-
return false;
|
|
46
|
-
}
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
if (!loadSchema()) {
|
|
50
|
-
console.log(chalk.red(`\nā No ${LOCAL_SCHEMA_FILE} found in ${dir}`));
|
|
51
|
-
console.log(chalk.gray(' Run `archsync scan` first.\n'));
|
|
52
|
-
process.exitCode = 1;
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Re-read the schema when it changes (after a re-scan)
|
|
57
|
-
fs.watchFile(schemaPath, { interval: 2000 }, () => {
|
|
58
|
-
if (loadSchema()) console.log(chalk.gray(' schema reloaded'));
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
const isLocalOrigin = (origin) =>
|
|
62
|
-
!origin || /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/.test(origin);
|
|
63
|
-
|
|
64
|
-
const server = http.createServer((req, res) => {
|
|
65
|
-
const origin = req.headers.origin;
|
|
66
|
-
// CORS ā localhost origins only
|
|
67
|
-
if (isLocalOrigin(origin)) {
|
|
68
|
-
res.setHeader('Access-Control-Allow-Origin', origin || '*');
|
|
69
|
-
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
|
|
70
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
71
|
-
}
|
|
72
|
-
if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; }
|
|
73
|
-
if (req.method !== 'GET') { res.writeHead(405); res.end(); return; }
|
|
74
|
-
|
|
75
|
-
const url = new URL(req.url, `http://127.0.0.1:${port}`);
|
|
76
|
-
const json = (status, body) => {
|
|
77
|
-
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
78
|
-
res.end(JSON.stringify(body));
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
if (url.pathname === '/v1/health') {
|
|
82
|
-
return json(200, { ok: true, root: dir, files: allowedFiles.size });
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (url.pathname === '/v1/schema') {
|
|
86
|
-
return json(200, schema);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (url.pathname === '/v1/file') {
|
|
90
|
-
const reqPath = url.searchParams.get('path') || '';
|
|
91
|
-
const resolved = path.resolve(reqPath);
|
|
92
|
-
// Allowlist check ā only scanned files, nothing else, ever.
|
|
93
|
-
if (!allowedFiles.has(resolved)) {
|
|
94
|
-
return json(403, { error: 'File is not part of the scanned schema' });
|
|
95
|
-
}
|
|
96
|
-
let source;
|
|
97
|
-
try { source = fs.readFileSync(resolved, 'utf-8'); }
|
|
98
|
-
catch { return json(404, { error: 'File not readable' }); }
|
|
99
|
-
|
|
100
|
-
const lines = source.split('\n');
|
|
101
|
-
const from = Math.max(1, parseInt(url.searchParams.get('from') || '1', 10));
|
|
102
|
-
const to = Math.min(lines.length, parseInt(url.searchParams.get('to') || String(from + 30), 10));
|
|
103
|
-
return json(200, {
|
|
104
|
-
path: resolved,
|
|
105
|
-
from,
|
|
106
|
-
to,
|
|
107
|
-
total: lines.length,
|
|
108
|
-
code: lines.slice(from - 1, to).join('\n'),
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
json(404, { error: 'Not found' });
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
// Strictly localhost ā reject everything else by construction
|
|
116
|
-
server.listen(port, '127.0.0.1', () => {
|
|
117
|
-
console.log(chalk.blue.bold('\nš ArchSync local code bridge\n'));
|
|
118
|
-
console.log(` Listening on ${chalk.white(`http://127.0.0.1:${port}`)} ${chalk.gray('(localhost only)')}`);
|
|
119
|
-
console.log(` Serving ${chalk.white(allowedFiles.size)} scanned files ${chalk.gray('(allowlisted, read-only)')}`);
|
|
120
|
-
console.log(` Schema ${chalk.gray(schemaPath)}`);
|
|
121
|
-
console.log(chalk.gray('\n The canvas at localhost:5173 will now show live code.'));
|
|
122
|
-
console.log(chalk.gray(' Press Ctrl+C to stop.\n'));
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
server.on('error', (err) => {
|
|
126
|
-
if (err.code === 'EADDRINUSE') {
|
|
127
|
-
console.log(chalk.yellow(`\nā Port ${port} is in use ā is another serve running? Try --port ${port + 1}\n`));
|
|
128
|
-
} else {
|
|
129
|
-
console.log(chalk.red(`\nā ${err.message}\n`));
|
|
130
|
-
}
|
|
131
|
-
process.exitCode = 1;
|
|
132
|
-
});
|
|
133
|
-
}
|