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/backup.js
DELETED
|
@@ -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 };
|
package/src/commands/connect.js
DELETED
|
@@ -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
|
-
}
|
package/src/commands/diff.js
DELETED
|
@@ -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
|
-
}
|
package/src/commands/export.js
DELETED
|
@@ -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
|
-
}
|