archsync 1.0.0 → 1.0.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.
Files changed (50) hide show
  1. package/README.md +67 -0
  2. package/dist/archsync.cjs +2 -0
  3. package/package.json +8 -4
  4. package/bin/cli.js +0 -91
  5. package/src/__tests__/e2e-workflow.test.js +0 -66
  6. package/src/__tests__/hashEngine.test.js +0 -109
  7. package/src/__tests__/impact.test.js +0 -137
  8. package/src/__tests__/parsers.test.js +0 -496
  9. package/src/__tests__/scan-pipeline.test.js +0 -332
  10. package/src/__tests__/schemaBuilder.test.js +0 -145
  11. package/src/__tests__/workspace.test.js +0 -178
  12. package/src/commands/backup.js +0 -54
  13. package/src/commands/connect.js +0 -129
  14. package/src/commands/diff.js +0 -228
  15. package/src/commands/export.js +0 -125
  16. package/src/commands/impactReport.js +0 -50
  17. package/src/commands/import.js +0 -126
  18. package/src/commands/init.js +0 -80
  19. package/src/commands/login.js +0 -116
  20. package/src/commands/plugin.js +0 -28
  21. package/src/commands/push.js +0 -194
  22. package/src/commands/register.js +0 -127
  23. package/src/commands/scan.js +0 -498
  24. package/src/commands/serve.js +0 -133
  25. package/src/commands/setup.js +0 -233
  26. package/src/commands/status.js +0 -56
  27. package/src/commands/validate.js +0 -245
  28. package/src/commands/watch.js +0 -70
  29. package/src/core/credentialStore.js +0 -76
  30. package/src/core/hashEngine.js +0 -34
  31. package/src/core/impactEngine.js +0 -192
  32. package/src/core/monorepoDetector.js +0 -41
  33. package/src/core/pluginManager.js +0 -40
  34. package/src/core/relationshipEngine.js +0 -917
  35. package/src/core/requestSigning.js +0 -16
  36. package/src/core/schemaBuilder.js +0 -230
  37. package/src/core/schemaDeduplicator.js +0 -54
  38. package/src/core/supabaseClient.js +0 -68
  39. package/src/core/workspaceDetector.js +0 -113
  40. package/src/parsers/astParser.js +0 -274
  41. package/src/parsers/configParser.js +0 -49
  42. package/src/parsers/dependencyGraph.js +0 -31
  43. package/src/parsers/flutterParser.js +0 -98
  44. package/src/parsers/goParser.js +0 -99
  45. package/src/parsers/index.js +0 -211
  46. package/src/parsers/javaParser.js +0 -89
  47. package/src/parsers/nodeParser.js +0 -429
  48. package/src/parsers/pythonParser.js +0 -109
  49. package/src/parsers/reactParser.js +0 -368
  50. package/src/parsers/smartComment.js +0 -144
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "archsync",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "ArchSync CLI — Sync codebase to architecture graph",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -23,20 +23,24 @@
23
23
  "archsync"
24
24
  ],
25
25
  "bin": {
26
- "archsync": "./bin/cli.js"
26
+ "archsync": "dist/archsync.cjs"
27
27
  },
28
28
  "files": [
29
- "bin",
30
- "src"
29
+ "dist",
30
+ "README.md"
31
31
  ],
32
32
  "scripts": {
33
33
  "start": "node bin/cli.js",
34
34
  "dev": "node bin/cli.js",
35
+ "build": "node build.mjs",
36
+ "prepublishOnly": "npm run build",
35
37
  "test": "vitest",
36
38
  "test:run": "vitest run",
37
39
  "coverage": "vitest run --coverage"
38
40
  },
39
41
  "devDependencies": {
42
+ "esbuild": "^0.24.0",
43
+ "javascript-obfuscator": "^4.1.1",
40
44
  "vitest": "^3.2.0",
41
45
  "@vitest/coverage-v8": "^3.2.0"
42
46
  },
package/bin/cli.js DELETED
@@ -1,91 +0,0 @@
1
- #!/usr/bin/env node
2
- import { Command } from 'commander';
3
- import chalk from 'chalk';
4
- import { initCommand } from '../src/commands/init.js';
5
- import { scanCommand } from '../src/commands/scan.js';
6
- import { pushCommand } from '../src/commands/push.js';
7
- import { diffCommand } from '../src/commands/diff.js';
8
- import { watchCommand } from '../src/commands/watch.js';
9
- import { statusCommand } from '../src/commands/status.js';
10
- import { serveCommand } from '../src/commands/serve.js';
11
-
12
- const program = new Command();
13
-
14
- program
15
- .name('archsync')
16
- .description(chalk.bold.blue('ArchSync CLI') + ' — Sync codebase to architecture graph')
17
- .version('1.0.0');
18
-
19
- program
20
- .command('serve')
21
- .description('Start the localhost-only code bridge so the canvas can show live source code')
22
- .option('-d, --dir <path>', 'Project directory (must contain .archsync-schema.json)', '.')
23
- .option('-p, --port <port>', 'Port to bind on 127.0.0.1', '4317')
24
- .action(serveCommand);
25
-
26
- program
27
- .command('init')
28
- .description('Initialize ArchSync in the current project')
29
- .option('-p, --project <id>', 'Supabase project ID to link')
30
- .option('-f, --framework <type>', 'Framework: node, flutter, react, express, nestjs, nextjs')
31
- .option('-s, --system <label>', 'System label for cross-system merging: backend, mobile, web, admin (overrides the framework default)')
32
- .action(initCommand);
33
-
34
- program
35
- .command('scan')
36
- .description('Scan codebase and build schema from source')
37
- .option('-d, --dir <path>', 'Directory to scan', '.')
38
- .option('--include <patterns>', 'Glob patterns to include', '**/*.{js,ts,jsx,tsx,dart,py,go,java,kt}')
39
- .option('--exclude <patterns>', 'Glob patterns to exclude', '**/node_modules/**')
40
- .option('--ast', 'Use AST parsing (slower, more accurate)', false)
41
- .option('--debug', 'Show detailed debug output (entity sources, relation tracing)', false)
42
- .option('-w, --workspace', 'Scan a multi-project workspace (e.g. backend + flutter app + react admin) and cross-link systems', false)
43
- .action(scanCommand);
44
-
45
- program
46
- .command('push')
47
- .description('Push local schema to Supabase')
48
- .option('-b, --branch <name>', 'Branch name', 'main')
49
- .option('-m, --message <msg>', 'Commit message')
50
- .option('--dry-run', 'Show what would be pushed without pushing')
51
- .action(pushCommand);
52
-
53
- program
54
- .command('diff')
55
- .description('Show diff between local and remote schema')
56
- .option('-b, --branch <name>', 'Branch to compare against', 'main')
57
- .option('--json', 'Output as JSON')
58
- .action(diffCommand);
59
-
60
- program
61
- .command('watch')
62
- .description('Watch for file changes and auto-sync')
63
- .option('-d, --dir <path>', 'Directory to watch', '.')
64
- .option('--debounce <ms>', 'Debounce interval in ms', '2000')
65
- .action(watchCommand);
66
-
67
- program
68
- .command('status')
69
- .description('Show current sync status')
70
- .action(statusCommand);
71
-
72
- // Don't exit with code 1 when no command is provided — just show help
73
- program.exitOverride();
74
- try {
75
- if (!process.argv.slice(2).length) {
76
- program.outputHelp();
77
- } else {
78
- program.parse();
79
- }
80
- } catch (err) {
81
- // Commander throws on --help / --version — treat as clean exits
82
- const cleanCodes = ['commander.help', 'commander.helpDisplayed', 'commander.version'];
83
- if (cleanCodes.includes(err.code)) {
84
- process.exit(0);
85
- }
86
- if (err.code === 'commander.unknownCommand') {
87
- console.error(chalk.red(`Unknown command. Run 'archsync --help' to see available commands.`));
88
- process.exit(1);
89
- }
90
- throw err;
91
- }
@@ -1,66 +0,0 @@
1
- import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2
- import fs from 'fs';
3
- import path from 'path';
4
- import os from 'os';
5
-
6
- let tmpDir;
7
-
8
- beforeAll(() => {
9
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'archsync-e2e-'));
10
- // Create fixture files
11
- fs.writeFileSync(path.join(tmpDir, 'App.jsx'), `
12
- import React from 'react';
13
- import UserService from './UserService';
14
- export default function App() { return <div>Hello</div>; }
15
- `);
16
- fs.writeFileSync(path.join(tmpDir, 'UserService.js'), `
17
- const db = require('./db');
18
- class UserService {
19
- async getUser(id) { return db.users.findById(id); }
20
- }
21
- module.exports = UserService;
22
- `);
23
- fs.writeFileSync(path.join(tmpDir, 'server.js'), `
24
- const express = require('express');
25
- const app = express();
26
- app.get('/api/users', (req, res) => res.json([]));
27
- app.post('/api/users', (req, res) => res.json({}));
28
- module.exports = app;
29
- `);
30
- });
31
-
32
- afterAll(() => {
33
- fs.rmSync(tmpDir, { recursive: true, force: true });
34
- });
35
-
36
- describe('CLI Scan → Build Pipeline E2E', () => {
37
- it('scans fixture directory and produces schema', async () => {
38
- // Import parsers
39
- const { parseFile } = await import('../../src/parsers/index.js');
40
- const { buildSchema } = await import('../../src/core/schemaBuilder.js');
41
-
42
- const files = fs.readdirSync(tmpDir).map(f => path.join(tmpDir, f));
43
- const parsed = files.flatMap(f => {
44
- try {
45
- const content = fs.readFileSync(f, 'utf8');
46
- const result = parseFile(f, content);
47
- return result ? [result] : [];
48
- } catch { return []; }
49
- });
50
-
51
- const schema = buildSchema(parsed, tmpDir);
52
- expect(schema).toBeDefined();
53
- expect(schema.nodes).toBeDefined();
54
- expect(Array.isArray(schema.nodes)).toBe(true);
55
- });
56
-
57
- it('produces deterministic schema hash', async () => {
58
- const { hashSchema } = await import('../../src/core/hashEngine.js');
59
- const schema = { nodes: [{ id: '1', name: 'Test', type: 'service' }], edges: [] };
60
- const hash1 = hashSchema(schema);
61
- const hash2 = hashSchema(schema);
62
- expect(hash1).toBe(hash2);
63
- expect(typeof hash1).toBe('string');
64
- expect(hash1.length).toBeGreaterThan(0);
65
- });
66
- });
@@ -1,109 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { hashNode, hashSchema, schemasEqual } from '../core/hashEngine.js';
3
-
4
- // ─── hashNode ────────────────────────────────────────────────
5
- describe('hashNode', () => {
6
- it('produces a 16-character hex string', () => {
7
- const node = { entityType: 'service', system: 'backend', text: 'AuthService', data: {}, metadata: {} };
8
- const hash = hashNode(node);
9
- expect(typeof hash).toBe('string');
10
- expect(hash).toHaveLength(16);
11
- expect(hash).toMatch(/^[0-9a-f]+$/);
12
- });
13
-
14
- it('identical nodes produce the same hash', () => {
15
- const node = { entityType: 'service', system: 'backend', text: 'AuthService', data: {}, metadata: {} };
16
- expect(hashNode(node)).toBe(hashNode(node));
17
- });
18
-
19
- it('different nodes produce different hashes', () => {
20
- const a = { entityType: 'service', system: 'backend', text: 'AuthService', data: {}, metadata: {} };
21
- const b = { entityType: 'service', system: 'backend', text: 'PaymentService', data: {}, metadata: {} };
22
- expect(hashNode(a)).not.toBe(hashNode(b));
23
- });
24
-
25
- it('missing data/metadata defaults to empty objects and still hashes', () => {
26
- const node = { entityType: 'model', system: 'backend', text: 'User' };
27
- expect(() => hashNode(node)).not.toThrow();
28
- });
29
- });
30
-
31
- // ─── hashSchema ──────────────────────────────────────────────
32
- describe('hashSchema', () => {
33
- const baseSchema = {
34
- nodes: [
35
- { entityType: 'service', system: 'backend', text: 'AuthService', data: {}, metadata: {} },
36
- ],
37
- edges: [
38
- { source: 'n1', relation: 'calls', target: 'n2' },
39
- ],
40
- };
41
-
42
- it('identical schemas produce the same hash', () => {
43
- const hash1 = hashSchema(baseSchema);
44
- const hash2 = hashSchema(baseSchema);
45
- expect(hash1).toBe(hash2);
46
- });
47
-
48
- it('adding a node changes the schema hash', () => {
49
- const extended = {
50
- ...baseSchema,
51
- nodes: [
52
- ...baseSchema.nodes,
53
- { entityType: 'model', system: 'backend', text: 'UserModel', data: {}, metadata: {} },
54
- ],
55
- };
56
- expect(hashSchema(baseSchema)).not.toBe(hashSchema(extended));
57
- });
58
-
59
- it('hash is stable regardless of node property insertion order', () => {
60
- const schema1 = {
61
- nodes: [{ entityType: 'service', system: 'backend', text: 'A', data: {}, metadata: {} }],
62
- edges: [],
63
- };
64
- // Same logical content, different property order when serialised
65
- const schema2 = {
66
- nodes: [{ text: 'A', entityType: 'service', system: 'backend', metadata: {}, data: {} }],
67
- edges: [],
68
- };
69
- // hashNode uses JSON.stringify with fixed key order — both schemas are equivalent
70
- expect(hashSchema(schema1)).toBe(hashSchema(schema2));
71
- });
72
-
73
- it('produces a non-empty string for an empty schema', () => {
74
- const hash = hashSchema({ nodes: [], edges: [] });
75
- expect(typeof hash).toBe('string');
76
- expect(hash.length).toBeGreaterThan(0);
77
- });
78
-
79
- it('handles schema without edges gracefully', () => {
80
- const schema = { nodes: [{ entityType: 'service', system: 'backend', text: 'X', data: {}, metadata: {} }] };
81
- expect(() => hashSchema(schema)).not.toThrow();
82
- });
83
- });
84
-
85
- // ─── schemasEqual ────────────────────────────────────────────
86
- describe('schemasEqual', () => {
87
- it('two null schemas are equal', () => {
88
- expect(schemasEqual(null, null)).toBe(true);
89
- });
90
-
91
- it('null vs non-null schema is not equal', () => {
92
- expect(schemasEqual(null, { nodes: [], edges: [] })).toBe(false);
93
- expect(schemasEqual({ nodes: [], edges: [] }, null)).toBe(false);
94
- });
95
-
96
- it('identical schemas are equal', () => {
97
- const schema = {
98
- nodes: [{ entityType: 'service', system: 'backend', text: 'Svc', data: {}, metadata: {} }],
99
- edges: [],
100
- };
101
- expect(schemasEqual(schema, schema)).toBe(true);
102
- });
103
-
104
- it('schemas with different nodes are not equal', () => {
105
- const a = { nodes: [{ entityType: 'service', system: 'backend', text: 'A', data: {}, metadata: {} }], edges: [] };
106
- const b = { nodes: [{ entityType: 'service', system: 'backend', text: 'B', data: {}, metadata: {} }], edges: [] };
107
- expect(schemasEqual(a, b)).toBe(false);
108
- });
109
- });
@@ -1,137 +0,0 @@
1
- /**
2
- * Tests for the impact engine: breaking-change classification and
3
- * transitive, cross-system dependent discovery.
4
- */
5
-
6
- import { describe, it, expect } from 'vitest';
7
- import { analyzeImpact, findDependents, embedWarnings } from '../core/impactEngine.js';
8
-
9
- // screen(app) → api(app) → route(backend) → controller(backend)
10
- // service(backend) → model(backend)
11
- function makeSchema() {
12
- const nodes = [
13
- { id: 'screen1', entityType: 'screen', system: 'app', text: 'LoginScreen', data: {} },
14
- { id: 'api1', entityType: 'api', system: 'app', text: 'POST /api/login', data: { method: 'POST', path: '/api/login' } },
15
- { id: 'route1', entityType: 'route', system: 'backend', text: 'POST /api/login', data: { method: 'POST', path: '/login', fullPath: '/api/login' } },
16
- { id: 'ctrl1', entityType: 'controller', system: 'backend', text: 'login', data: {} },
17
- { id: 'svc1', entityType: 'service', system: 'backend', text: 'UserService', data: {} },
18
- { id: 'model1', entityType: 'model', system: 'backend', text: 'User', data: {} },
19
- ];
20
- const edges = [
21
- { id: 'e1', source: 'screen1', target: 'api1', relation: 'consumes' },
22
- { id: 'e2', source: 'api1', target: 'route1', relation: 'calls' },
23
- { id: 'e3', source: 'route1', target: 'ctrl1', relation: 'handles' },
24
- { id: 'e4', source: 'ctrl1', target: 'svc1', relation: 'uses' },
25
- { id: 'e5', source: 'svc1', target: 'model1', relation: 'queries' },
26
- ];
27
- return { system: 'backend', nodes, edges };
28
- }
29
-
30
- function withoutNode(schema, id) {
31
- return {
32
- ...schema,
33
- nodes: schema.nodes.filter(n => n.id !== id),
34
- edges: schema.edges.filter(e => e.source !== id && e.target !== id),
35
- };
36
- }
37
-
38
- describe('findDependents', () => {
39
- it('walks the dependency graph transitively across systems', () => {
40
- const schema = makeSchema();
41
- const deps = findDependents(schema, 'model1');
42
- const ids = deps.map(d => d.id);
43
- // model ← service ← controller ← route ← api ← screen
44
- expect(ids).toEqual(['svc1', 'ctrl1', 'route1', 'api1', 'screen1']);
45
- expect(deps.find(d => d.id === 'svc1').depth).toBe(1);
46
- expect(deps.find(d => d.id === 'screen1').depth).toBe(5);
47
- expect(deps.find(d => d.id === 'screen1').system).toBe('app');
48
- });
49
-
50
- it('returns nothing for a leaf with no dependents', () => {
51
- expect(findDependents(makeSchema(), 'screen1')).toEqual([]);
52
- });
53
- });
54
-
55
- describe('analyzeImpact', () => {
56
- it('returns no warnings without a previous snapshot', () => {
57
- expect(analyzeImpact(null, makeSchema())).toEqual([]);
58
- });
59
-
60
- it('flags a removed route as breaking and lists cross-system dependents', () => {
61
- const oldSchema = makeSchema();
62
- const newSchema = withoutNode(oldSchema, 'route1');
63
-
64
- const warnings = analyzeImpact(oldSchema, newSchema);
65
- expect(warnings).toHaveLength(1);
66
-
67
- const w = warnings[0];
68
- expect(w.severity).toBe('breaking');
69
- expect(w.changeKind).toBe('removed');
70
- expect(w.nodeText).toBe('POST /api/login');
71
- expect(w.affected.map(a => a.id)).toEqual(['api1', 'screen1']);
72
- expect(w.affectedSystems).toEqual(['app']);
73
- expect(w.affectedByType).toEqual({ api: 1, screen: 1 });
74
- });
75
-
76
- it('flags a contract change (path) on a node with dependents', () => {
77
- const oldSchema = makeSchema();
78
- const newSchema = makeSchema();
79
- const route = newSchema.nodes.find(n => n.id === 'route1');
80
- route.data = { ...route.data, fullPath: '/api/v2/login' };
81
-
82
- const warnings = analyzeImpact(oldSchema, newSchema);
83
- expect(warnings).toHaveLength(1);
84
- expect(warnings[0].changeKind).toBe('modified');
85
- expect(warnings[0].message).toContain('fullPath');
86
- expect(warnings[0].severity).toBe('breaking');
87
- });
88
-
89
- it('ignores cosmetic changes (position, metadata)', () => {
90
- const oldSchema = makeSchema();
91
- const newSchema = makeSchema();
92
- const route = newSchema.nodes.find(n => n.id === 'route1');
93
- route.x = 999;
94
- route.metadata = { parsedAt: 'now' };
95
-
96
- expect(analyzeImpact(oldSchema, newSchema)).toEqual([]);
97
- });
98
-
99
- it('skips modified nodes nobody depends on', () => {
100
- const oldSchema = makeSchema();
101
- const newSchema = makeSchema();
102
- const screen = newSchema.nodes.find(n => n.id === 'screen1');
103
- screen.data = { parentClass: 'HookWidget' };
104
-
105
- expect(analyzeImpact(oldSchema, newSchema)).toEqual([]);
106
- });
107
- });
108
-
109
- describe('embedWarnings', () => {
110
- it('attaches schema-level warnings and node-level flags', () => {
111
- const oldSchema = makeSchema();
112
- const newSchema = withoutNode(oldSchema, 'route1');
113
- const warnings = analyzeImpact(oldSchema, newSchema);
114
-
115
- embedWarnings(newSchema, warnings);
116
-
117
- expect(newSchema.warnings).toHaveLength(1);
118
- const api = newSchema.nodes.find(n => n.id === 'api1');
119
- const screen = newSchema.nodes.find(n => n.id === 'screen1');
120
- expect(api.impactedBy).toEqual(['route1']);
121
- expect(screen.impactedBy).toEqual(['route1']);
122
- // the removed node is gone — survivors only carry impactedBy
123
- expect(newSchema.nodes.every(n => !n.warning)).toBe(true);
124
- });
125
-
126
- it('flags the changed node itself when it survives', () => {
127
- const oldSchema = makeSchema();
128
- const newSchema = makeSchema();
129
- const route = newSchema.nodes.find(n => n.id === 'route1');
130
- route.data = { ...route.data, fullPath: '/api/v2/login' };
131
-
132
- const warnings = analyzeImpact(oldSchema, newSchema);
133
- embedWarnings(newSchema, warnings);
134
-
135
- expect(newSchema.nodes.find(n => n.id === 'route1').warning).toMatchObject({ severity: 'breaking' });
136
- });
137
- });