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
@@ -1,54 +0,0 @@
1
- // archsync backup / restore commands - Task 142
2
- const fs = require('fs');
3
- const path = require('path');
4
-
5
- async function backupCommand(options) {
6
- const configPath = path.join(process.cwd(), '.archsync.json');
7
- if (!fs.existsSync(configPath)) {
8
- console.error('No .archsync.json found. Run archsync init first.');
9
- process.exit(1);
10
- }
11
- const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
12
- const schemaPath = path.join(process.cwd(), '.archsync-schema.json');
13
- if (!fs.existsSync(schemaPath)) {
14
- console.error('No schema found. Run archsync scan first.');
15
- process.exit(1);
16
- }
17
-
18
- const schema = JSON.parse(fs.readFileSync(schemaPath, 'utf8'));
19
- const backup = {
20
- id: Date.now().toString(),
21
- projectId: config.projectId || 'unknown',
22
- createdAt: new Date().toISOString(),
23
- nodeCount: schema.nodes?.length || 0,
24
- edgeCount: schema.edges?.length || 0,
25
- data: schema,
26
- };
27
-
28
- const outputPath = options.output || `archsync-backup-${backup.id}.json`;
29
- fs.writeFileSync(outputPath, JSON.stringify(backup, null, 2));
30
- console.log(`✓ Backup saved to ${outputPath} (${backup.nodeCount} nodes, ${backup.edgeCount} edges)`);
31
- }
32
-
33
- async function restoreCommand(options) {
34
- if (!options.file) {
35
- console.error('--file is required');
36
- process.exit(1);
37
- }
38
- if (!fs.existsSync(options.file)) {
39
- console.error(`File not found: ${options.file}`);
40
- process.exit(1);
41
- }
42
-
43
- const backup = JSON.parse(fs.readFileSync(options.file, 'utf8'));
44
- if (!backup.data?.nodes) {
45
- console.error('Invalid backup file: missing data.nodes');
46
- process.exit(1);
47
- }
48
-
49
- const outputPath = '.archsync-schema.json';
50
- fs.writeFileSync(outputPath, JSON.stringify(backup.data, null, 2));
51
- console.log(`✓ Restored ${backup.data.nodes.length} nodes from backup (created ${backup.createdAt})`);
52
- }
53
-
54
- module.exports = { backupCommand, restoreCommand };
@@ -1,129 +0,0 @@
1
- import chalk from 'chalk';
2
- import ora from 'ora';
3
- import fs from 'fs';
4
- import path from 'path';
5
- import os from 'os';
6
- import { createInterface } from 'readline';
7
- import { createClient } from '@supabase/supabase-js';
8
- import { readConfig, writeConfig } from '../core/supabaseClient.js';
9
-
10
- const ARCHSYNC_DIR = path.join(os.homedir(), '.archsync');
11
- const GLOBAL_CONFIG_FILE = path.join(ARCHSYNC_DIR, 'config.json');
12
-
13
- function ensureDir() {
14
- if (!fs.existsSync(ARCHSYNC_DIR)) {
15
- fs.mkdirSync(ARCHSYNC_DIR, { recursive: true, mode: 0o700 });
16
- }
17
- }
18
-
19
- function saveGlobalConfig(cfg) {
20
- ensureDir();
21
- fs.writeFileSync(GLOBAL_CONFIG_FILE, JSON.stringify(cfg, null, 2), { mode: 0o600 });
22
- }
23
-
24
- function loadGlobalConfig() {
25
- if (!fs.existsSync(GLOBAL_CONFIG_FILE)) return null;
26
- try {
27
- return JSON.parse(fs.readFileSync(GLOBAL_CONFIG_FILE, 'utf-8'));
28
- } catch {
29
- return null;
30
- }
31
- }
32
-
33
- function prompt(rl, question) {
34
- return new Promise(resolve => rl.question(question, resolve));
35
- }
36
-
37
- async function validateSupabaseConnection(url, key) {
38
- try {
39
- const client = createClient(url, key);
40
- // Lightweight health check — just attempt to reach the REST API
41
- const { error } = await client.from('schema_commits').select('id').limit(1);
42
- // PGRST116 = no rows, which means the table exists and connection works
43
- if (error && error.code !== 'PGRST116' && error.code !== '42P01') {
44
- // 42P01 = table doesn't exist yet — still means connection is healthy
45
- return { ok: false, message: error.message };
46
- }
47
- return { ok: true };
48
- } catch (err) {
49
- return { ok: false, message: err.message };
50
- }
51
- }
52
-
53
- export async function connectCommand(options) {
54
- console.log(chalk.blue.bold('\n🔗 ArchSync CLI — Connect Database\n'));
55
-
56
- let supabaseUrl = options.supabaseUrl;
57
- let supabaseKey = options.supabaseKey;
58
- const postgresUrl = options.postgresUrl;
59
-
60
- const rl = createInterface({ input: process.stdin, output: process.stdout });
61
-
62
- try {
63
- if (!supabaseUrl) {
64
- supabaseUrl = await prompt(rl, chalk.white('Supabase URL (e.g. https://xxx.supabase.co): '));
65
- }
66
- if (!supabaseKey) {
67
- supabaseKey = await prompt(rl, chalk.white('Supabase Anon Key: '));
68
- }
69
- } finally {
70
- rl.close();
71
- }
72
-
73
- if (!supabaseUrl || !supabaseKey) {
74
- console.log(chalk.red('❌ Supabase URL and key are required.'));
75
- process.exit(1);
76
- }
77
-
78
- // Normalise URL — strip trailing slash
79
- supabaseUrl = supabaseUrl.trim().replace(/\/$/, '');
80
- supabaseKey = supabaseKey.trim();
81
-
82
- // Basic URL validation
83
- if (!supabaseUrl.startsWith('https://') && !supabaseUrl.startsWith('http://')) {
84
- console.log(chalk.red('❌ Supabase URL must start with https://'));
85
- process.exit(1);
86
- }
87
-
88
- const spinner = ora('Validating connection...').start();
89
-
90
- const { ok, message } = await validateSupabaseConnection(supabaseUrl, supabaseKey);
91
-
92
- if (!ok) {
93
- spinner.fail('Connection validation failed');
94
- console.log(chalk.red(`\n Error: ${message}`));
95
- console.log(chalk.yellow('\n Check that:'));
96
- console.log(chalk.white(' 1. Your Supabase URL is correct'));
97
- console.log(chalk.white(' 2. Your anon key is correct'));
98
- console.log(chalk.white(' 3. Your Supabase project is not paused'));
99
- process.exit(1);
100
- }
101
-
102
- spinner.succeed('Connection validated!');
103
-
104
- // Build the config object to persist
105
- const dbConfig = {
106
- supabaseUrl,
107
- supabaseKey,
108
- ...(postgresUrl ? { postgresUrl } : {}),
109
- connectedAt: new Date().toISOString(),
110
- };
111
-
112
- // Save to ~/.archsync/config.json
113
- saveGlobalConfig(dbConfig);
114
- console.log(chalk.gray(`\n Config saved to: ${GLOBAL_CONFIG_FILE}`));
115
-
116
- // Also update the local .archsync.json if it exists
117
- const localConfig = readConfig();
118
- if (localConfig) {
119
- localConfig.supabase = localConfig.supabase || {};
120
- localConfig.supabase.url = supabaseUrl;
121
- localConfig.supabase.anonKey = supabaseKey;
122
- if (postgresUrl) localConfig.supabase.postgresUrl = postgresUrl;
123
- writeConfig(localConfig);
124
- console.log(chalk.gray(` Also updated: .archsync.json`));
125
- }
126
-
127
- console.log(chalk.green('\n Database connected successfully.'));
128
- console.log(chalk.blue('\nNext: ') + chalk.gray('archsync login'));
129
- }
@@ -1,228 +0,0 @@
1
- import chalk from 'chalk';
2
- import ora from 'ora';
3
- import fs from 'fs';
4
- import path from 'path';
5
- import { readConfig, getSupabaseClient, fetchLatestSchema } from '../core/supabaseClient.js';
6
- import { diffSchemas } from '../core/schemaBuilder.js';
7
- import { analyzeImpact, summarizeWarnings } from '../core/impactEngine.js';
8
- import { printImpactWarnings } from './impactReport.js';
9
-
10
- const LOCAL_SCHEMA_FILE = '.archsync-schema.json';
11
-
12
- // ─── Visual diff renderer ─────────────────────────────────────
13
-
14
- function visualDiff(diff, branch, localSchema, remoteSchema) {
15
- const { added, modified, deleted, unchanged } = diff;
16
-
17
- const RESET = '\x1b[0m';
18
- const BOLD = '\x1b[1m';
19
- const DIM = '\x1b[2m';
20
- const GREEN = '\x1b[32m';
21
- const RED = '\x1b[31m';
22
- const YELLOW = '\x1b[33m';
23
- const CYAN = '\x1b[36m';
24
- const GRAY = '\x1b[90m';
25
- const WHITE = '\x1b[37m';
26
-
27
- const line = (color, prefix, node, extra = '') => {
28
- const type = `[${(node.entityType || 'unknown').padEnd(12)}]`;
29
- const sys = node.system ? `{${node.system}}` : '';
30
- const name = node.text || node.name || node.id || '?';
31
- return `${color}${BOLD}${prefix}${RESET}${color} ${type} ${WHITE}${name}${RESET}${GRAY} ${sys}${extra}${RESET}`;
32
- };
33
-
34
- console.log(`\n${BOLD}${CYAN}╔══════════════════════════════════════════════════╗${RESET}`);
35
- console.log(`${BOLD}${CYAN}║${RESET}${BOLD} Visual Diff — local vs ${branch.padEnd(19)}${CYAN}║${RESET}`);
36
- console.log(`${BOLD}${CYAN}╚══════════════════════════════════════════════════╝${RESET}`);
37
-
38
- // ── Node changes ──────────────────────────────────────────
39
- if (added.length > 0) {
40
- console.log(`\n${BOLD}${GREEN} ADDED NODES (${added.length})${RESET}`);
41
- console.log(`${GRAY} ${'─'.repeat(48)}${RESET}`);
42
- for (const node of added) {
43
- console.log(' ' + line(GREEN, '+', node));
44
- }
45
- }
46
-
47
- if (deleted.length > 0) {
48
- console.log(`\n${BOLD}${RED} REMOVED NODES (${deleted.length})${RESET}`);
49
- console.log(`${GRAY} ${'─'.repeat(48)}${RESET}`);
50
- for (const node of deleted) {
51
- console.log(' ' + line(RED, '-', node));
52
- }
53
- }
54
-
55
- if (modified.length > 0) {
56
- console.log(`\n${BOLD}${YELLOW} MODIFIED NODES (${modified.length})${RESET}`);
57
- console.log(`${GRAY} ${'─'.repeat(48)}${RESET}`);
58
- for (const node of modified) {
59
- console.log(' ' + line(YELLOW, '~', node));
60
- }
61
- }
62
-
63
- if (unchanged.length > 0) {
64
- console.log(`\n${GRAY} UNCHANGED NODES (${unchanged.length})${RESET}`);
65
- }
66
-
67
- // ── Edge changes ──────────────────────────────────────────
68
- const localEdgeKeys = new Set((localSchema.edges || []).map(e => `${e.source}→${e.target}:${e.relation}`));
69
- const remoteEdgeKeys = new Set((remoteSchema?.edges || []).map(e => `${e.source}→${e.target}:${e.relation}`));
70
-
71
- const addedEdges = (localSchema.edges || []).filter(e => !remoteEdgeKeys.has(`${e.source}→${e.target}:${e.relation}`));
72
- const removedEdges = (remoteSchema?.edges || []).filter(e => !localEdgeKeys.has(`${e.source}→${e.target}:${e.relation}`));
73
-
74
- const nodeNameById = (schema, id) => {
75
- const n = (schema?.nodes || []).find(n => n.id === id);
76
- return n?.text || n?.name || id || '?';
77
- };
78
-
79
- if (addedEdges.length > 0 || removedEdges.length > 0) {
80
- console.log(`\n${BOLD}${CYAN} EDGE CHANGES${RESET}`);
81
- console.log(`${GRAY} ${'─'.repeat(48)}${RESET}`);
82
-
83
- for (const edge of addedEdges.slice(0, 20)) {
84
- const src = nodeNameById(localSchema, edge.source);
85
- const tgt = nodeNameById(localSchema, edge.target);
86
- console.log(` ${GREEN}${BOLD}+${RESET}${GREEN} ${src} ${DIM}─[${edge.relation}]→${RESET}${GREEN} ${tgt}${RESET}`);
87
- }
88
- if (addedEdges.length > 20) {
89
- console.log(` ${GRAY} ... and ${addedEdges.length - 20} more added edges${RESET}`);
90
- }
91
-
92
- for (const edge of removedEdges.slice(0, 20)) {
93
- const src = nodeNameById(remoteSchema, edge.source);
94
- const tgt = nodeNameById(remoteSchema, edge.target);
95
- console.log(` ${RED}${BOLD}-${RESET}${RED} ${src} ${DIM}─[${edge.relation}]→${RESET}${RED} ${tgt}${RESET}`);
96
- }
97
- if (removedEdges.length > 20) {
98
- console.log(` ${GRAY} ... and ${removedEdges.length - 20} more removed edges${RESET}`);
99
- }
100
- }
101
-
102
- // ── Summary bar ───────────────────────────────────────────
103
- const total = added.length + deleted.length + modified.length + addedEdges.length + removedEdges.length;
104
-
105
- console.log(`\n${BOLD}${CYAN}╔══════════════════════════════════════════════════╗${RESET}`);
106
- console.log(`${CYAN}║${RESET} ${BOLD}Summary${RESET}`);
107
- console.log(`${CYAN}║${RESET} ${GREEN}${BOLD}+${RESET} ${added.length.toString().padStart(4)} nodes added ${GREEN}${'█'.repeat(Math.min(added.length, 20))}${RESET}`);
108
- console.log(`${CYAN}║${RESET} ${RED}${BOLD}-${RESET} ${deleted.length.toString().padStart(4)} nodes removed ${RED}${'█'.repeat(Math.min(deleted.length, 20))}${RESET}`);
109
- console.log(`${CYAN}║${RESET} ${YELLOW}~${RESET} ${modified.length.toString().padStart(4)} nodes modified ${YELLOW}${'█'.repeat(Math.min(modified.length, 20))}${RESET}`);
110
- console.log(`${CYAN}║${RESET} ${GREEN}+${RESET} ${addedEdges.length.toString().padStart(4)} edges added`);
111
- console.log(`${CYAN}║${RESET} ${RED}-${RESET} ${removedEdges.length.toString().padStart(4)} edges removed`);
112
- console.log(`${CYAN}║${RESET} ${GRAY}=${RESET} ${unchanged.length.toString().padStart(4)} unchanged`);
113
- console.log(`${CYAN}╠══════════════════════════════════════════════════╣${RESET}`);
114
- if (total === 0) {
115
- console.log(`${CYAN}║${RESET} ${BOLD}No changes detected.${RESET}`);
116
- } else {
117
- console.log(`${CYAN}║${RESET} ${BOLD}${total} total change(s)${RESET}`);
118
- }
119
- console.log(`${BOLD}${CYAN}╚══════════════════════════════════════════════════╝${RESET}\n`);
120
- }
121
-
122
- // ─── Command ──────────────────────────────────────────────────
123
-
124
- export async function diffCommand(options) {
125
- console.log(chalk.blue.bold('\n🔀 ArchSync CLI — Schema Diff\n'));
126
-
127
- const config = readConfig();
128
- if (!config) {
129
- console.log(chalk.red('❌ No .archsync.json found. Run `archsync init` first.'));
130
- process.exit(1);
131
- }
132
-
133
- const schemaPath = path.resolve(LOCAL_SCHEMA_FILE);
134
- if (!fs.existsSync(schemaPath)) {
135
- console.log(chalk.red('❌ No local schema found. Run `archsync scan` first.'));
136
- process.exit(1);
137
- }
138
-
139
- const localSchema = JSON.parse(fs.readFileSync(schemaPath, 'utf-8'));
140
- const branch = options.branch || 'main';
141
-
142
- if (!config.projectId || !config.supabase?.url) {
143
- // Offline diff — compare with last known schema
144
- console.log(chalk.yellow('⚠ No remote configured. Showing local schema summary.'));
145
- console.log(chalk.gray(`\n Nodes: ${localSchema.nodes.length}`));
146
- console.log(chalk.gray(` Edges: ${localSchema.edges.length}`));
147
- console.log(chalk.gray(` Hash: ${localSchema.hash || 'N/A'}`));
148
- return;
149
- }
150
-
151
- const spinner = ora('Fetching remote schema...').start();
152
-
153
- try {
154
- const client = getSupabaseClient(config);
155
- const remoteSchema = await fetchLatestSchema(client, config.projectId, branch);
156
-
157
- if (!remoteSchema) {
158
- spinner.info('No remote schema found. Everything is new.');
159
- if (options.json) {
160
- console.log(JSON.stringify({ added: localSchema.nodes.length, modified: 0, deleted: 0 }, null, 2));
161
- }
162
- return;
163
- }
164
-
165
- spinner.succeed('Diff computed!');
166
-
167
- const diff = diffSchemas(remoteSchema, localSchema);
168
- const impactWarnings = analyzeImpact(remoteSchema, localSchema);
169
-
170
- if (options.json) {
171
- console.log(JSON.stringify({
172
- branch,
173
- added: diff.added.length,
174
- modified: diff.modified.length,
175
- deleted: diff.deleted.length,
176
- unchanged: diff.unchanged.length,
177
- breakingChanges: summarizeWarnings(impactWarnings.filter(w => w.severity === 'breaking')),
178
- details: {
179
- added: diff.added.map(n => ({ id: n.id, type: n.entityType, name: n.text })),
180
- modified: diff.modified.map(n => ({ id: n.id, type: n.entityType, name: n.text })),
181
- deleted: diff.deleted.map(n => ({ id: n.id, type: n.entityType, name: n.text })),
182
- },
183
- }, null, 2));
184
- return;
185
- }
186
-
187
- if (options.visual) {
188
- visualDiff(diff, branch, localSchema, remoteSchema);
189
- printImpactWarnings(impactWarnings, { heading: `Impact on "${branch}"` });
190
- return;
191
- }
192
-
193
- // Default text output
194
- console.log(chalk.gray(`\n─── Diff: local vs ${branch} ────────────`));
195
- console.log(chalk.green(` + Added: ${diff.added.length}`));
196
- console.log(chalk.yellow(` ~ Modified: ${diff.modified.length}`));
197
- console.log(chalk.red(` - Deleted: ${diff.deleted.length}`));
198
- console.log(chalk.gray(` = Unchanged: ${diff.unchanged.length}`));
199
-
200
- if (diff.added.length > 0) {
201
- console.log(chalk.green('\n Added:'));
202
- for (const n of diff.added.slice(0, 10)) {
203
- console.log(chalk.green(` + [${n.entityType}] ${n.text}`));
204
- }
205
- if (diff.added.length > 10) console.log(chalk.gray(` ... and ${diff.added.length - 10} more`));
206
- }
207
-
208
- if (diff.modified.length > 0) {
209
- console.log(chalk.yellow('\n Modified:'));
210
- for (const n of diff.modified.slice(0, 10)) {
211
- console.log(chalk.yellow(` ~ [${n.entityType}] ${n.text}`));
212
- }
213
- }
214
-
215
- if (diff.deleted.length > 0) {
216
- console.log(chalk.red('\n Deleted:'));
217
- for (const n of diff.deleted.slice(0, 10)) {
218
- console.log(chalk.red(` - [${n.entityType}] ${n.text}`));
219
- }
220
- }
221
-
222
- printImpactWarnings(impactWarnings, { heading: `Impact on "${branch}"` });
223
-
224
- } catch (err) {
225
- spinner.fail(`Diff failed: ${err.message}`);
226
- process.exit(1);
227
- }
228
- }
@@ -1,125 +0,0 @@
1
- import chalk from 'chalk';
2
- import ora from 'ora';
3
- import fs from 'fs';
4
- import path from 'path';
5
- import { readConfig, getSupabaseClient } from '../core/supabaseClient.js';
6
- import { getApiKey } from '../core/credentialStore.js';
7
-
8
- /**
9
- * archsync export --format json|sql --output <filename> [--project <projectId>]
10
- *
11
- * Fetches all nodes, edges, and project metadata from the configured Supabase
12
- * project and writes the result to a file.
13
- */
14
- export async function exportCommand(options) {
15
- console.log(chalk.blue.bold('\n📤 ArchSync CLI — Export\n'));
16
-
17
- const config = readConfig(options.dir);
18
- if (!config) {
19
- console.log(chalk.red('❌ No .archsync.json found. Run `archsync init` first.'));
20
- process.exit(1);
21
- }
22
-
23
- const projectId = options.project || config.projectId;
24
- if (!projectId) {
25
- console.log(chalk.red('❌ No project ID. Pass --project <id> or set projectId in .archsync.json.'));
26
- process.exit(1);
27
- }
28
-
29
- const format = (options.format || 'json').toLowerCase();
30
- if (!['json', 'sql'].includes(format)) {
31
- console.log(chalk.red('❌ --format must be json or sql'));
32
- process.exit(1);
33
- }
34
-
35
- const defaultFile = `archsync-export-${projectId.slice(0, 8)}.${format}`;
36
- const outputFile = path.resolve(options.output || defaultFile);
37
-
38
- const spinner = ora('Connecting to database…').start();
39
-
40
- let client;
41
- try {
42
- client = getSupabaseClient(config);
43
- } catch (err) {
44
- spinner.fail('Failed to create database client');
45
- console.log(chalk.red(` ${err.message}`));
46
- process.exit(1);
47
- }
48
-
49
- // ─── Fetch data ──────────────────────────────────────────────────────
50
- spinner.text = 'Fetching project data…';
51
-
52
- const [projectRes, nodesRes, edgesRes] = await Promise.all([
53
- client.from('projects').select('*').eq('id', projectId).single(),
54
- client.from('nodes').select('*').eq('project_id', projectId),
55
- client.from('edges').select('*').eq('project_id', projectId),
56
- ]);
57
-
58
- if (projectRes.error) {
59
- spinner.fail('Failed to fetch project');
60
- console.log(chalk.red(` ${projectRes.error.message}`));
61
- process.exit(1);
62
- }
63
-
64
- const exportData = {
65
- __archsync_export: true,
66
- version: '1.0',
67
- exportedAt: new Date().toISOString(),
68
- project: projectRes.data,
69
- nodes: nodesRes.data || [],
70
- edges: edgesRes.data || [],
71
- };
72
-
73
- spinner.text = `Writing ${format.toUpperCase()} file…`;
74
-
75
- // ─── Format output ───────────────────────────────────────────────────
76
- let output;
77
-
78
- if (format === 'json') {
79
- output = JSON.stringify(exportData, null, 2);
80
- } else {
81
- // SQL
82
- const escSql = (val) => {
83
- if (val === null || val === undefined) return 'NULL';
84
- if (typeof val === 'boolean') return val ? 'TRUE' : 'FALSE';
85
- if (typeof val === 'number') return String(val);
86
- if (typeof val === 'object') return `'${JSON.stringify(val).replace(/'/g, "''")}'`;
87
- return `'${String(val).replace(/'/g, "''")}'`;
88
- };
89
-
90
- const rowToInsert = (table, row) => {
91
- const cols = Object.keys(row).join(', ');
92
- const vals = Object.values(row).map(escSql).join(', ');
93
- return `INSERT INTO ${table} (${cols}) VALUES (${vals});`;
94
- };
95
-
96
- const lines = [
97
- `-- ArchSync SQL export`,
98
- `-- Project: ${projectId}`,
99
- `-- Generated: ${exportData.exportedAt}`,
100
- '',
101
- '-- Project',
102
- rowToInsert('projects', exportData.project),
103
- '',
104
- '-- Nodes',
105
- ...exportData.nodes.map(n => rowToInsert('nodes', n)),
106
- '',
107
- '-- Edges',
108
- ...exportData.edges.map(e => rowToInsert('edges', e)),
109
- ];
110
-
111
- output = lines.join('\n');
112
- }
113
-
114
- fs.writeFileSync(outputFile, output, 'utf-8');
115
-
116
- spinner.succeed('Export complete!');
117
-
118
- console.log(chalk.gray('\n─── Export Summary ─────────────────'));
119
- console.log(chalk.white(` Project: ${chalk.bold(exportData.project?.name || projectId)}`));
120
- console.log(chalk.white(` Nodes: ${chalk.bold(exportData.nodes.length)}`));
121
- console.log(chalk.white(` Edges: ${chalk.bold(exportData.edges.length)}`));
122
- console.log(chalk.white(` Format: ${chalk.bold(format.toUpperCase())}`));
123
- console.log(chalk.white(` Output: ${chalk.bold(outputFile)}`));
124
- console.log('');
125
- }
@@ -1,50 +0,0 @@
1
- import chalk from 'chalk';
2
-
3
- /**
4
- * Render impact warnings to the terminal — shared by scan / diff / push so
5
- * the developer sees the same story everywhere.
6
- */
7
- export function printImpactWarnings(warnings, { heading = 'Breaking-Change Impact' } = {}) {
8
- if (!warnings || warnings.length === 0) return;
9
-
10
- const breaking = warnings.filter(w => w.severity === 'breaking');
11
- const minor = warnings.filter(w => w.severity !== 'breaking');
12
-
13
- console.log(chalk.red.bold(`\n⚠ ${heading} — ${breaking.length} breaking, ${minor.length} minor\n`));
14
-
15
- for (const w of breaking.slice(0, 15)) {
16
- const icon = w.changeKind === 'removed' ? chalk.red('✖ removed ') : chalk.yellow('~ changed ');
17
- console.log(` ${icon} ${chalk.white.bold(`[${w.entityType}] ${w.nodeText}`)} ${chalk.gray(`{${w.system}}`)}`);
18
- console.log(chalk.gray(` ${w.message}`));
19
-
20
- if (w.affected.length > 0) {
21
- const bySystem = new Map();
22
- for (const a of w.affected) {
23
- if (!bySystem.has(a.system)) bySystem.set(a.system, []);
24
- bySystem.get(a.system).push(a);
25
- }
26
- const typeSummary = Object.entries(w.affectedByType)
27
- .map(([t, c]) => `${c} ${t}${c > 1 ? 's' : ''}`)
28
- .join(', ');
29
- console.log(chalk.red(` affects ${w.affected.length} dependent(s) across ${w.affectedSystems.length} system(s): ${typeSummary}`));
30
- for (const [system, deps] of bySystem) {
31
- const names = deps.slice(0, 4).map(d => `${d.text} (${d.entityType})`).join(', ');
32
- const more = deps.length > 4 ? ` … +${deps.length - 4} more` : '';
33
- console.log(chalk.gray(` ${chalk.cyan(system)}: ${names}${more}`));
34
- }
35
- }
36
- console.log('');
37
- }
38
- if (breaking.length > 15) {
39
- console.log(chalk.gray(` … and ${breaking.length - 15} more breaking change(s)\n`));
40
- }
41
-
42
- if (minor.length > 0) {
43
- console.log(chalk.yellow(` ${minor.length} change(s) without known dependents:`));
44
- for (const w of minor.slice(0, 8)) {
45
- console.log(chalk.gray(` • ${w.message} {${w.system}}`));
46
- }
47
- if (minor.length > 8) console.log(chalk.gray(` … and ${minor.length - 8} more`));
48
- console.log('');
49
- }
50
- }