@aion0/forge 0.5.48 → 0.5.49
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/CLAUDE.md +0 -1
- package/RELEASE_NOTES.md +8 -3
- package/app/api/tasks/[id]/log/entry/route.ts +13 -0
- package/app/api/tasks/[id]/log/route.ts +23 -0
- package/app/api/tasks/route.ts +2 -2
- package/components/ProjectDetail.tsx +1 -16
- package/components/TaskDetail.tsx +201 -51
- package/lib/help-docs/CLAUDE.md +0 -2
- package/lib/task-manager.ts +110 -0
- package/package.json +1 -1
- package/src/types/index.ts +7 -0
- package/app/api/migration/config/route.ts +0 -19
- package/app/api/migration/discover/route.ts +0 -26
- package/app/api/migration/failures/route.ts +0 -35
- package/app/api/migration/fix/route.ts +0 -82
- package/app/api/migration/run/route.ts +0 -22
- package/app/api/migration/run-batch/route.ts +0 -86
- package/components/MigrationCockpit.tsx +0 -541
- package/lib/help-docs/14-migration.md +0 -154
- package/lib/migration/differ.ts +0 -193
- package/lib/migration/discoverer.ts +0 -363
- package/lib/migration/openapi.ts +0 -137
- package/lib/migration/runner.ts +0 -219
- package/lib/migration/store.ts +0 -89
- package/lib/migration/types.ts +0 -115
package/lib/migration/runner.ts
DELETED
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
// HTTP runner — fires the same path against legacy + new and compares.
|
|
2
|
-
// diffMode controls how comparison happens:
|
|
3
|
-
// exact = fire both, deep-equal JSON
|
|
4
|
-
// shape = fire only `next`, validate response against OpenAPI schema (legacy not needed)
|
|
5
|
-
// both = fire both + schema validation
|
|
6
|
-
|
|
7
|
-
import type { Endpoint, MigrationConfig, RunResult, SideResult } from './types';
|
|
8
|
-
import { diff, validateAgainstSchema, type SchemaViolation } from './differ';
|
|
9
|
-
|
|
10
|
-
function compileIgnore(patterns: string[]): RegExp[] {
|
|
11
|
-
return patterns.map(p => {
|
|
12
|
-
const body = p.startsWith('$') ? p.slice(1) : p;
|
|
13
|
-
const escaped = body
|
|
14
|
-
.replace(/\./g, '\\.')
|
|
15
|
-
.replace(/\[\*\]/g, '\\[\\d+\\]')
|
|
16
|
-
.replace(/\[(\d+)\]/g, '\\[$1\\]');
|
|
17
|
-
return new RegExp(`^\\$${escaped}$`);
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
|
-
import { loadOpenApi, lookup, getResponseSchema, type OpenApiDoc } from './openapi';
|
|
21
|
-
|
|
22
|
-
function substitutePath(path: string, subs: Record<string, string> = {}): string {
|
|
23
|
-
return path.replace(/\{([^}]+)\}/g, (_, name) => subs[name] ?? subs.id ?? '1');
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function buildHeaders(config: MigrationConfig): Record<string, string> {
|
|
27
|
-
const headers: Record<string, string> = { Accept: 'application/json' };
|
|
28
|
-
if (config.auth.mode === 'bearer' && config.auth.tokenEnv) {
|
|
29
|
-
const tok = process.env[config.auth.tokenEnv];
|
|
30
|
-
if (tok) headers['Authorization'] = `Bearer ${tok}`;
|
|
31
|
-
} else if (config.auth.mode === 'basic' && config.auth.username && config.auth.passwordEnv) {
|
|
32
|
-
const pw = process.env[config.auth.passwordEnv];
|
|
33
|
-
if (pw) {
|
|
34
|
-
const b64 = Buffer.from(`${config.auth.username}:${pw}`).toString('base64');
|
|
35
|
-
headers['Authorization'] = `Basic ${b64}`;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
return headers;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async function fetchOne(baseUrl: string, ep: Endpoint, config: MigrationConfig, timeout: number): Promise<SideResult> {
|
|
42
|
-
const path = substitutePath(ep.path, config.pathSubstitutions);
|
|
43
|
-
const url = baseUrl.replace(/\/+$/, '') + path;
|
|
44
|
-
const start = Date.now();
|
|
45
|
-
const ctrl = new AbortController();
|
|
46
|
-
const t = setTimeout(() => ctrl.abort(), timeout);
|
|
47
|
-
try {
|
|
48
|
-
const resp = await fetch(url, {
|
|
49
|
-
method: ep.method,
|
|
50
|
-
headers: buildHeaders(config),
|
|
51
|
-
signal: ctrl.signal,
|
|
52
|
-
});
|
|
53
|
-
const text = await resp.text();
|
|
54
|
-
let bodyJson: any = undefined;
|
|
55
|
-
try { bodyJson = text ? JSON.parse(text) : undefined; } catch {}
|
|
56
|
-
return {
|
|
57
|
-
url, status: resp.status, ok: resp.ok,
|
|
58
|
-
bodyExcerpt: text.slice(0, 4096),
|
|
59
|
-
bodyJson,
|
|
60
|
-
durationMs: Date.now() - start,
|
|
61
|
-
};
|
|
62
|
-
} catch (e: any) {
|
|
63
|
-
return {
|
|
64
|
-
url, status: 0, ok: false,
|
|
65
|
-
error: e?.name === 'AbortError' ? `timeout after ${timeout}ms` : (e?.message || String(e)),
|
|
66
|
-
durationMs: Date.now() - start,
|
|
67
|
-
};
|
|
68
|
-
} finally {
|
|
69
|
-
clearTimeout(t);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Convert SchemaViolation[] → DiffEntry[] so the UI can show them uniformly.
|
|
74
|
-
function violationsToDiffs(vios: SchemaViolation[]): RunResult['diff'] {
|
|
75
|
-
return vios.slice(0, 50).map(v => ({
|
|
76
|
-
jsonPath: v.jsonPath,
|
|
77
|
-
legacy: v.expected,
|
|
78
|
-
next: v.actual,
|
|
79
|
-
reason: v.reason === 'missing-required' ? 'missing-in-next' :
|
|
80
|
-
v.reason === 'type-mismatch' ? 'type-mismatch' : 'value',
|
|
81
|
-
}));
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export async function runEndpoint(ep: Endpoint, config: MigrationConfig, openApi?: OpenApiDoc | null): Promise<RunResult> {
|
|
85
|
-
const startedAt = new Date().toISOString();
|
|
86
|
-
const t0 = Date.now();
|
|
87
|
-
const mode = config.diffMode || 'exact';
|
|
88
|
-
|
|
89
|
-
let legacy: SideResult;
|
|
90
|
-
let next: SideResult;
|
|
91
|
-
|
|
92
|
-
if (mode === 'shape') {
|
|
93
|
-
// Skip legacy entirely — use a synthesized "n/a" SideResult
|
|
94
|
-
legacy = { url: '(skipped — shape mode)', status: 0, ok: true, durationMs: 0 };
|
|
95
|
-
next = await fetchOne(config.next.baseUrl, ep, config, config.healthCheck.newTimeout);
|
|
96
|
-
} else {
|
|
97
|
-
[legacy, next] = await Promise.all([
|
|
98
|
-
fetchOne(config.legacy.baseUrl, ep, config, config.healthCheck.legacyTimeout),
|
|
99
|
-
fetchOne(config.next.baseUrl, ep, config, config.healthCheck.newTimeout),
|
|
100
|
-
]);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
let match: RunResult['match'] = 'pass';
|
|
104
|
-
let errorType: string | undefined;
|
|
105
|
-
let errorMessage: string | undefined;
|
|
106
|
-
let diffEntries: RunResult['diff'];
|
|
107
|
-
|
|
108
|
-
// ── Stubbed: only the new side matters; expect 501 ──
|
|
109
|
-
if (ep.isStubbed) {
|
|
110
|
-
if (next.error) {
|
|
111
|
-
match = 'error'; errorType = 'new-unreachable'; errorMessage = next.error;
|
|
112
|
-
} else if (next.status === 501) {
|
|
113
|
-
match = 'stub-ok';
|
|
114
|
-
} else {
|
|
115
|
-
match = 'fail'; errorType = 'stub-not-501'; errorMessage = `Expected 501, got ${next.status}`;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
// ── Shape mode: validate new against OpenAPI schema ──
|
|
119
|
-
else if (mode === 'shape') {
|
|
120
|
-
if (next.error) {
|
|
121
|
-
match = 'error'; errorType = 'new-unreachable'; errorMessage = next.error;
|
|
122
|
-
} else if (!(next.status >= 200 && next.status < 300)) {
|
|
123
|
-
match = 'fail'; errorType = 'http-status'; errorMessage = `HTTP ${next.status}`;
|
|
124
|
-
} else {
|
|
125
|
-
const schema = openApi ? getOpResponseSchema(openApi, ep) : null;
|
|
126
|
-
if (!schema) {
|
|
127
|
-
// No schema — treat as smoke pass (endpoint responded 2xx with no body check)
|
|
128
|
-
match = 'pass';
|
|
129
|
-
} else {
|
|
130
|
-
const vios = validateAgainstSchema(next.bodyJson, schema, '$', [], compileIgnore(config.ignorePaths));
|
|
131
|
-
if (vios.length > 0) {
|
|
132
|
-
match = 'fail';
|
|
133
|
-
errorType = 'schema-violation';
|
|
134
|
-
errorMessage = `${vios.length} schema violations`;
|
|
135
|
-
diffEntries = violationsToDiffs(vios);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
// ── Exact / both: deep-equal both sides + optional schema validation ──
|
|
141
|
-
else {
|
|
142
|
-
if (legacy.error || next.error) {
|
|
143
|
-
match = 'error';
|
|
144
|
-
errorType = legacy.error ? 'legacy-unreachable' : 'new-unreachable';
|
|
145
|
-
errorMessage = legacy.error || next.error;
|
|
146
|
-
} else if (legacy.status !== next.status) {
|
|
147
|
-
match = 'fail';
|
|
148
|
-
errorType = 'http-status-mismatch';
|
|
149
|
-
errorMessage = `legacy=${legacy.status} new=${next.status}`;
|
|
150
|
-
} else {
|
|
151
|
-
const d = diff(legacy.bodyJson, next.bodyJson, config.ignorePaths, { sortArrays: true });
|
|
152
|
-
if (d.length > 0) {
|
|
153
|
-
match = 'fail';
|
|
154
|
-
errorType = 'json-diff';
|
|
155
|
-
errorMessage = `${d.length} differences`;
|
|
156
|
-
diffEntries = d.slice(0, 50);
|
|
157
|
-
}
|
|
158
|
-
// In `both` mode also run schema validation on the new side
|
|
159
|
-
if (mode === 'both' && openApi) {
|
|
160
|
-
const schema = getOpResponseSchema(openApi, ep);
|
|
161
|
-
if (schema) {
|
|
162
|
-
const vios = validateAgainstSchema(next.bodyJson, schema, '$', [], compileIgnore(config.ignorePaths));
|
|
163
|
-
if (vios.length > 0 && match === 'pass') {
|
|
164
|
-
match = 'fail';
|
|
165
|
-
errorType = 'schema-violation';
|
|
166
|
-
errorMessage = `${vios.length} schema violations`;
|
|
167
|
-
diffEntries = violationsToDiffs(vios);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
return {
|
|
175
|
-
endpointId: ep.id,
|
|
176
|
-
startedAt,
|
|
177
|
-
durationMs: Date.now() - t0,
|
|
178
|
-
legacy, next, match,
|
|
179
|
-
diff: diffEntries,
|
|
180
|
-
errorType,
|
|
181
|
-
errorMessage,
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function getOpResponseSchema(openApi: OpenApiDoc, ep: Endpoint): any | null {
|
|
186
|
-
const op = lookup(openApi, ep.method, ep.path);
|
|
187
|
-
if (!op) return null;
|
|
188
|
-
return getResponseSchema(op, openApi);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
export async function runEndpoints(eps: Endpoint[], config: MigrationConfig, opts: {
|
|
192
|
-
concurrency?: number;
|
|
193
|
-
onProgress?: (done: number, total: number, last: RunResult) => void;
|
|
194
|
-
projectPath?: string;
|
|
195
|
-
} = {}): Promise<RunResult[]> {
|
|
196
|
-
const concurrency = Math.max(1, opts.concurrency ?? 4);
|
|
197
|
-
const results: RunResult[] = [];
|
|
198
|
-
let done = 0;
|
|
199
|
-
let i = 0;
|
|
200
|
-
|
|
201
|
-
// Load OpenAPI once for the whole batch (shape/both modes)
|
|
202
|
-
let openApi: OpenApiDoc | null = null;
|
|
203
|
-
if ((config.diffMode === 'shape' || config.diffMode === 'both') && config.endpointSource.openApiSpec && opts.projectPath) {
|
|
204
|
-
openApi = loadOpenApi(opts.projectPath, config.endpointSource.openApiSpec);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
async function worker() {
|
|
208
|
-
while (true) {
|
|
209
|
-
const idx = i++;
|
|
210
|
-
if (idx >= eps.length) return;
|
|
211
|
-
const r = await runEndpoint(eps[idx], config, openApi);
|
|
212
|
-
results[idx] = r;
|
|
213
|
-
done++;
|
|
214
|
-
opts.onProgress?.(done, eps.length, r);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
await Promise.all(Array.from({ length: Math.min(concurrency, eps.length) }, worker));
|
|
218
|
-
return results;
|
|
219
|
-
}
|
package/lib/migration/store.ts
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
// Per-project file storage at <project>/.forge/migration/
|
|
2
|
-
|
|
3
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from 'node:fs';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
import * as YAML from 'yaml';
|
|
6
|
-
import type { Endpoint, MigrationConfig, RunResult, Failure } from './types';
|
|
7
|
-
import { DEFAULT_CONFIG } from './types';
|
|
8
|
-
|
|
9
|
-
function migrationDir(projectPath: string): string {
|
|
10
|
-
return join(projectPath, '.forge', 'migration');
|
|
11
|
-
}
|
|
12
|
-
function ensureDir(dir: string) {
|
|
13
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// ── Config ──────────────────────────────────────────────
|
|
17
|
-
export function loadConfig(projectPath: string): MigrationConfig {
|
|
18
|
-
const file = join(migrationDir(projectPath), 'config.yaml');
|
|
19
|
-
if (!existsSync(file)) return structuredClone(DEFAULT_CONFIG);
|
|
20
|
-
try {
|
|
21
|
-
const parsed = YAML.parse(readFileSync(file, 'utf8')) || {};
|
|
22
|
-
return { ...DEFAULT_CONFIG, ...parsed,
|
|
23
|
-
auth: { ...DEFAULT_CONFIG.auth, ...(parsed.auth || {}) },
|
|
24
|
-
healthCheck: { ...DEFAULT_CONFIG.healthCheck, ...(parsed.healthCheck || {}) },
|
|
25
|
-
endpointSource: { ...DEFAULT_CONFIG.endpointSource, ...(parsed.endpointSource || {}) },
|
|
26
|
-
pathSubstitutions: { ...DEFAULT_CONFIG.pathSubstitutions, ...(parsed.pathSubstitutions || {}) },
|
|
27
|
-
};
|
|
28
|
-
} catch {
|
|
29
|
-
return structuredClone(DEFAULT_CONFIG);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function saveConfig(projectPath: string, config: MigrationConfig): void {
|
|
34
|
-
const dir = migrationDir(projectPath);
|
|
35
|
-
ensureDir(dir);
|
|
36
|
-
writeFileSync(join(dir, 'config.yaml'), YAML.stringify(config), 'utf8');
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// ── Endpoints ───────────────────────────────────────────
|
|
40
|
-
export function loadEndpoints(projectPath: string): Endpoint[] {
|
|
41
|
-
const file = join(migrationDir(projectPath), 'endpoints.json');
|
|
42
|
-
if (!existsSync(file)) return [];
|
|
43
|
-
try { return JSON.parse(readFileSync(file, 'utf8')); } catch { return []; }
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function saveEndpoints(projectPath: string, endpoints: Endpoint[]): void {
|
|
47
|
-
const dir = migrationDir(projectPath);
|
|
48
|
-
ensureDir(dir);
|
|
49
|
-
writeFileSync(join(dir, 'endpoints.json'), JSON.stringify(endpoints, null, 2), 'utf8');
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// ── Runs ────────────────────────────────────────────────
|
|
53
|
-
export function saveRun(projectPath: string, results: RunResult[]): string {
|
|
54
|
-
const runsDir = join(migrationDir(projectPath), 'runs');
|
|
55
|
-
ensureDir(runsDir);
|
|
56
|
-
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
57
|
-
const file = join(runsDir, `${ts}.json`);
|
|
58
|
-
writeFileSync(file, JSON.stringify(results, null, 2), 'utf8');
|
|
59
|
-
return file;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function listRuns(projectPath: string): { name: string; path: string; mtime: number }[] {
|
|
63
|
-
const runsDir = join(migrationDir(projectPath), 'runs');
|
|
64
|
-
if (!existsSync(runsDir)) return [];
|
|
65
|
-
return readdirSync(runsDir)
|
|
66
|
-
.filter(f => f.endsWith('.json'))
|
|
67
|
-
.map(f => {
|
|
68
|
-
const path = join(runsDir, f);
|
|
69
|
-
return { name: f, path, mtime: 0 };
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export function loadRun(file: string): RunResult[] {
|
|
74
|
-
if (!existsSync(file)) return [];
|
|
75
|
-
try { return JSON.parse(readFileSync(file, 'utf8')); } catch { return []; }
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// ── Failures ────────────────────────────────────────────
|
|
79
|
-
export function saveFailures(projectPath: string, failures: Failure[]): void {
|
|
80
|
-
const dir = join(migrationDir(projectPath), 'failures');
|
|
81
|
-
ensureDir(dir);
|
|
82
|
-
writeFileSync(join(dir, 'current.json'), JSON.stringify(failures, null, 2), 'utf8');
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export function loadFailures(projectPath: string): Failure[] {
|
|
86
|
-
const file = join(migrationDir(projectPath), 'failures', 'current.json');
|
|
87
|
-
if (!existsSync(file)) return [];
|
|
88
|
-
try { return JSON.parse(readFileSync(file, 'utf8')); } catch { return []; }
|
|
89
|
-
}
|
package/lib/migration/types.ts
DELETED
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
// Migration Cockpit types — API parity testing for legacy → new module migrations.
|
|
2
|
-
|
|
3
|
-
export type EndpointStatus = 'pending' | 'in-progress' | 'migrated' | 'tested' | 'skip' | 'defer';
|
|
4
|
-
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';
|
|
5
|
-
|
|
6
|
-
export interface Endpoint {
|
|
7
|
-
id: string; // stable hash of `${method} ${path}`
|
|
8
|
-
controller: string; // tag from OpenAPI or controller name from doc
|
|
9
|
-
file?: string; // legacy or migrated file path
|
|
10
|
-
method: HttpMethod;
|
|
11
|
-
path: string; // raw path with `{id}` style placeholders
|
|
12
|
-
status: EndpointStatus;
|
|
13
|
-
expectedHttpStatus: number; // 200 normally, 501 for stubbed
|
|
14
|
-
isStubbed: boolean;
|
|
15
|
-
source: string; // primary source (OpenAPI / doc file)
|
|
16
|
-
notes?: string;
|
|
17
|
-
acceptance?: string[];
|
|
18
|
-
// OpenAPI cross-reference (when OpenAPI is the primary source)
|
|
19
|
-
operationId?: string;
|
|
20
|
-
tag?: string;
|
|
21
|
-
summary?: string;
|
|
22
|
-
hasResponseSchema?: boolean;
|
|
23
|
-
// Migration doc cross-reference
|
|
24
|
-
docFile?: string; // docs/migration/<X>.java.md if matched
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface RunResult {
|
|
28
|
-
endpointId: string;
|
|
29
|
-
startedAt: string;
|
|
30
|
-
durationMs: number;
|
|
31
|
-
legacy: SideResult;
|
|
32
|
-
next: SideResult;
|
|
33
|
-
match: 'pass' | 'fail' | 'stub-ok' | 'error';
|
|
34
|
-
diff?: DiffEntry[];
|
|
35
|
-
errorType?: string;
|
|
36
|
-
errorMessage?: string;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export interface SideResult {
|
|
40
|
-
url: string;
|
|
41
|
-
status: number;
|
|
42
|
-
ok: boolean;
|
|
43
|
-
bodyExcerpt?: string;
|
|
44
|
-
bodyJson?: any;
|
|
45
|
-
error?: string;
|
|
46
|
-
durationMs: number;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export interface DiffEntry {
|
|
50
|
-
jsonPath: string;
|
|
51
|
-
legacy: any;
|
|
52
|
-
next: any;
|
|
53
|
-
reason: 'value' | 'missing-in-next' | 'missing-in-legacy' | 'type-mismatch';
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export interface Failure {
|
|
57
|
-
endpointId: string;
|
|
58
|
-
controller: string;
|
|
59
|
-
method: HttpMethod;
|
|
60
|
-
path: string;
|
|
61
|
-
errorType: string; // e.g. "http-status-mismatch", "json-diff", "exception"
|
|
62
|
-
errorMessage: string;
|
|
63
|
-
lastSeenAt: string;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export interface FailureCluster {
|
|
67
|
-
errorType: string;
|
|
68
|
-
count: number;
|
|
69
|
-
controllers: { controller: string; failures: Failure[] }[];
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export type DiffMode = 'exact' | 'shape' | 'both';
|
|
73
|
-
|
|
74
|
-
export interface MigrationConfig {
|
|
75
|
-
legacy: { baseUrl: string };
|
|
76
|
-
next: { baseUrl: string; sourceDir?: string };
|
|
77
|
-
auth: {
|
|
78
|
-
mode: 'skip' | 'bearer' | 'basic';
|
|
79
|
-
tokenEnv?: string;
|
|
80
|
-
username?: string;
|
|
81
|
-
passwordEnv?: string;
|
|
82
|
-
};
|
|
83
|
-
ignorePaths: string[];
|
|
84
|
-
healthCheck: {
|
|
85
|
-
legacyTimeout: number;
|
|
86
|
-
newTimeout: number;
|
|
87
|
-
skipUnhealthy: boolean;
|
|
88
|
-
};
|
|
89
|
-
clusterMode: 'simple' | 'ai';
|
|
90
|
-
diffMode: DiffMode; // exact = compare both sides; shape = validate new against schema; both = both
|
|
91
|
-
endpointSource: {
|
|
92
|
-
type: 'docs' | 'openapi' | 'source-scan' | 'mixed';
|
|
93
|
-
primary: string; // dir for per-controller docs
|
|
94
|
-
fallback?: string; // history file
|
|
95
|
-
openApiSpec?: string; // path to OpenAPI 3 JSON (preferred when set)
|
|
96
|
-
};
|
|
97
|
-
pathSubstitutions?: Record<string, string>; // {id} → "1" etc
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export const DEFAULT_CONFIG: MigrationConfig = {
|
|
101
|
-
legacy: { baseUrl: 'http://localhost:8080' },
|
|
102
|
-
next: { baseUrl: 'http://localhost:9090' },
|
|
103
|
-
auth: { mode: 'skip' },
|
|
104
|
-
ignorePaths: ['$.timestamp', '$.requestId', '$.traceId'],
|
|
105
|
-
healthCheck: { legacyTimeout: 2000, newTimeout: 2000, skipUnhealthy: true },
|
|
106
|
-
clusterMode: 'simple',
|
|
107
|
-
diffMode: 'shape',
|
|
108
|
-
endpointSource: {
|
|
109
|
-
type: 'mixed',
|
|
110
|
-
primary: 'docs/migration',
|
|
111
|
-
fallback: 'docs/lead/migration-history.md',
|
|
112
|
-
openApiSpec: 'docs/fnac-rest-schema-7.6.json',
|
|
113
|
-
},
|
|
114
|
-
pathSubstitutions: { id: '1', dbid: '1', ip: '127.0.0.1', mac: '00:00:00:00:00:00' },
|
|
115
|
-
};
|