@veloxts/cli 0.7.2 → 0.7.4
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/CHANGELOG.md +26 -5
- package/dist/cli.js +2 -0
- package/dist/commands/sync.d.ts +22 -0
- package/dist/commands/sync.js +96 -0
- package/dist/generators/fields/types.d.ts +7 -0
- package/dist/generators/fields/types.js +29 -0
- package/dist/generators/generators/namespace.js +7 -2
- package/dist/generators/templates/namespace.d.ts +3 -0
- package/dist/generators/templates/namespace.js +85 -1
- package/dist/generators/utils/prisma-schema.d.ts +18 -0
- package/dist/generators/utils/prisma-schema.js +53 -2
- package/dist/sync/analyzer.d.ts +20 -0
- package/dist/sync/analyzer.js +277 -0
- package/dist/sync/detector.d.ts +18 -0
- package/dist/sync/detector.js +94 -0
- package/dist/sync/index.d.ts +21 -0
- package/dist/sync/index.js +302 -0
- package/dist/sync/planner.d.ts +24 -0
- package/dist/sync/planner.js +75 -0
- package/dist/sync/procedure-generator.d.ts +20 -0
- package/dist/sync/procedure-generator.js +253 -0
- package/dist/sync/prompter.d.ts +18 -0
- package/dist/sync/prompter.js +312 -0
- package/dist/sync/schema-generator.d.ts +24 -0
- package/dist/sync/schema-generator.js +213 -0
- package/dist/sync/types.d.ts +219 -0
- package/dist/sync/types.js +9 -0
- package/package.json +6 -6
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync Analyzer
|
|
3
|
+
*
|
|
4
|
+
* Reads a Prisma schema and produces rich `SyncModelInfo[]` metadata
|
|
5
|
+
* for every model. This is stage 1 of the `velox sync` pipeline.
|
|
6
|
+
*
|
|
7
|
+
* The analyzer re-parses the raw schema text (rather than relying solely
|
|
8
|
+
* on the existing `analyzePrismaSchema` helper) because the helper skips
|
|
9
|
+
* relation fields that carry `@relation(fields: [...])` -- exactly the
|
|
10
|
+
* ones we need to classify belongsTo relations and detect foreign keys.
|
|
11
|
+
*/
|
|
12
|
+
import { readFileSync } from 'node:fs';
|
|
13
|
+
import { analyzePrismaSchema, findPrismaSchema } from '../generators/utils/prisma-schema.js';
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// Constants
|
|
16
|
+
// ============================================================================
|
|
17
|
+
/** Field names considered auto-managed by the framework */
|
|
18
|
+
const AUTO_MANAGED_NAMES = new Set(['id', 'createdAt', 'updatedAt', 'deletedAt']);
|
|
19
|
+
/** Field names considered sensitive (checked case-insensitively) */
|
|
20
|
+
const SENSITIVE_PATTERNS = [
|
|
21
|
+
'password',
|
|
22
|
+
'passwordhash',
|
|
23
|
+
'secret',
|
|
24
|
+
'secretkey',
|
|
25
|
+
'token',
|
|
26
|
+
'hash',
|
|
27
|
+
'apikey',
|
|
28
|
+
];
|
|
29
|
+
// ============================================================================
|
|
30
|
+
// Public API
|
|
31
|
+
// ============================================================================
|
|
32
|
+
/**
|
|
33
|
+
* Analyze a Prisma schema and produce sync metadata for every model.
|
|
34
|
+
*
|
|
35
|
+
* @param projectRoot - Absolute path to the project root directory
|
|
36
|
+
* @returns Array of `SyncModelInfo` objects, one per Prisma model
|
|
37
|
+
* @throws If no Prisma schema can be found
|
|
38
|
+
*/
|
|
39
|
+
export function analyzeSchema(projectRoot) {
|
|
40
|
+
const schemaPath = findPrismaSchema(projectRoot);
|
|
41
|
+
if (!schemaPath) {
|
|
42
|
+
throw new Error('Prisma schema not found. Ensure prisma/schema.prisma exists in your project.');
|
|
43
|
+
}
|
|
44
|
+
// Use the existing parser to discover model names
|
|
45
|
+
const analysis = analyzePrismaSchema(schemaPath);
|
|
46
|
+
const modelNames = analysis.models;
|
|
47
|
+
// Read the raw schema content for our own deeper parsing
|
|
48
|
+
const content = readFileSync(schemaPath, 'utf-8');
|
|
49
|
+
// Extract raw model bodies keyed by model name
|
|
50
|
+
const modelBodies = extractModelBodies(content);
|
|
51
|
+
const results = [];
|
|
52
|
+
for (const modelName of modelNames) {
|
|
53
|
+
const body = modelBodies.get(modelName);
|
|
54
|
+
if (!body)
|
|
55
|
+
continue;
|
|
56
|
+
const rawLines = parseRawLines(body);
|
|
57
|
+
const fields = buildFieldInfos(rawLines, modelNames);
|
|
58
|
+
const relations = buildRelationInfos(rawLines, modelNames);
|
|
59
|
+
const uniqueConstraints = parseUniqueConstraints(body);
|
|
60
|
+
// Detect user foreign keys: for every belongsTo pointing to 'User',
|
|
61
|
+
// mark the corresponding FK scalar field.
|
|
62
|
+
const userFkNames = new Set();
|
|
63
|
+
for (const rel of relations) {
|
|
64
|
+
if (rel.kind === 'belongsTo' && rel.relatedModel === 'User' && rel.foreignKey) {
|
|
65
|
+
userFkNames.add(rel.foreignKey);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Patch isUserForeignKey on fields
|
|
69
|
+
const patchedFields = fields.map((f) => (userFkNames.has(f.name) ? { ...f, isUserForeignKey: true } : f));
|
|
70
|
+
// Collect FK field names from all belongsTo relations
|
|
71
|
+
const fkFieldNames = new Set();
|
|
72
|
+
for (const rel of relations) {
|
|
73
|
+
if (rel.kind === 'belongsTo' && rel.foreignKey) {
|
|
74
|
+
fkFieldNames.add(rel.foreignKey);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const isJoinTable = detectJoinTable(patchedFields, fkFieldNames, uniqueConstraints);
|
|
78
|
+
const hasCreatedAt = patchedFields.some((f) => f.name === 'createdAt');
|
|
79
|
+
const hasUpdatedAt = patchedFields.some((f) => f.name === 'updatedAt');
|
|
80
|
+
results.push({
|
|
81
|
+
name: modelName,
|
|
82
|
+
fields: patchedFields,
|
|
83
|
+
relations,
|
|
84
|
+
isJoinTable,
|
|
85
|
+
hasTimestamps: hasCreatedAt && hasUpdatedAt,
|
|
86
|
+
uniqueConstraints,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return results;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Extract the body text for each model in the schema.
|
|
93
|
+
* Returns a Map from model name to the text between `{` and `}`.
|
|
94
|
+
*/
|
|
95
|
+
function extractModelBodies(content) {
|
|
96
|
+
const bodies = new Map();
|
|
97
|
+
const modelRegex = /^model\s+(\w+)\s*\{/gm;
|
|
98
|
+
for (const match of content.matchAll(modelRegex)) {
|
|
99
|
+
const modelName = match[1];
|
|
100
|
+
const openBrace = content.indexOf('{', match.index ?? 0);
|
|
101
|
+
const closeBrace = findClosingBrace(content, openBrace);
|
|
102
|
+
bodies.set(modelName, content.slice(openBrace + 1, closeBrace));
|
|
103
|
+
}
|
|
104
|
+
return bodies;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Find the matching closing `}` for an opening `{`.
|
|
108
|
+
*/
|
|
109
|
+
function findClosingBrace(content, openIndex) {
|
|
110
|
+
let depth = 0;
|
|
111
|
+
for (let i = openIndex; i < content.length; i++) {
|
|
112
|
+
if (content[i] === '{')
|
|
113
|
+
depth++;
|
|
114
|
+
else if (content[i] === '}') {
|
|
115
|
+
depth--;
|
|
116
|
+
if (depth === 0)
|
|
117
|
+
return i;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return content.length;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Parse every field-like line from a model body into `RawFieldLine` objects.
|
|
124
|
+
* Skips blank lines, comments, and `@@` directives.
|
|
125
|
+
*/
|
|
126
|
+
function parseRawLines(body) {
|
|
127
|
+
const lines = [];
|
|
128
|
+
for (const line of body.split('\n')) {
|
|
129
|
+
const trimmed = line.trim();
|
|
130
|
+
if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('@@'))
|
|
131
|
+
continue;
|
|
132
|
+
const fieldMatch = trimmed.match(/^(\w+)\s+(\w+)(\[\])?\??/);
|
|
133
|
+
if (!fieldMatch)
|
|
134
|
+
continue;
|
|
135
|
+
const name = fieldMatch[1];
|
|
136
|
+
const baseType = fieldMatch[2];
|
|
137
|
+
const isArray = fieldMatch[3] === '[]';
|
|
138
|
+
// Optional `?` can appear after `[]` or directly after type
|
|
139
|
+
const isOptional = /^\w+\s+\w+(\[\])?\?/.test(trimmed);
|
|
140
|
+
lines.push({ name, baseType, isArray, isOptional, rawLine: trimmed });
|
|
141
|
+
}
|
|
142
|
+
return lines;
|
|
143
|
+
}
|
|
144
|
+
// ============================================================================
|
|
145
|
+
// Internal: Field classification
|
|
146
|
+
// ============================================================================
|
|
147
|
+
/**
|
|
148
|
+
* Build `SyncFieldInfo[]` for all scalar (non-relation) fields.
|
|
149
|
+
*/
|
|
150
|
+
function buildFieldInfos(rawLines, modelNames) {
|
|
151
|
+
const fields = [];
|
|
152
|
+
for (const line of rawLines) {
|
|
153
|
+
// Skip relation fields (type matches a known model name)
|
|
154
|
+
if (modelNames.has(line.baseType))
|
|
155
|
+
continue;
|
|
156
|
+
const hasDefault = /@default\(/.test(line.rawLine) || /@updatedAt/.test(line.rawLine);
|
|
157
|
+
const isId = /@id\b/.test(line.rawLine);
|
|
158
|
+
const isUnique = /@unique\b/.test(line.rawLine);
|
|
159
|
+
const defaultValue = extractDefaultValue(line.rawLine);
|
|
160
|
+
fields.push({
|
|
161
|
+
name: line.name,
|
|
162
|
+
type: line.baseType,
|
|
163
|
+
isOptional: line.isOptional,
|
|
164
|
+
isId,
|
|
165
|
+
isUnique,
|
|
166
|
+
hasDefault,
|
|
167
|
+
defaultValue,
|
|
168
|
+
isAutoManaged: AUTO_MANAGED_NAMES.has(line.name),
|
|
169
|
+
isSensitive: SENSITIVE_PATTERNS.includes(line.name.toLowerCase()),
|
|
170
|
+
isUserForeignKey: false, // patched later
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
return fields;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Extract the value string from `@default(VALUE)`.
|
|
177
|
+
* Handles nested parentheses like `@default(uuid())`.
|
|
178
|
+
* Returns `undefined` when there is no `@default(...)`.
|
|
179
|
+
*/
|
|
180
|
+
function extractDefaultValue(rawLine) {
|
|
181
|
+
const marker = '@default(';
|
|
182
|
+
const start = rawLine.indexOf(marker);
|
|
183
|
+
if (start === -1)
|
|
184
|
+
return undefined;
|
|
185
|
+
const valueStart = start + marker.length;
|
|
186
|
+
let depth = 1;
|
|
187
|
+
let i = valueStart;
|
|
188
|
+
while (i < rawLine.length && depth > 0) {
|
|
189
|
+
if (rawLine[i] === '(')
|
|
190
|
+
depth++;
|
|
191
|
+
else if (rawLine[i] === ')')
|
|
192
|
+
depth--;
|
|
193
|
+
if (depth > 0)
|
|
194
|
+
i++;
|
|
195
|
+
}
|
|
196
|
+
return rawLine.slice(valueStart, i);
|
|
197
|
+
}
|
|
198
|
+
// ============================================================================
|
|
199
|
+
// Internal: Relation classification
|
|
200
|
+
// ============================================================================
|
|
201
|
+
/**
|
|
202
|
+
* Build `SyncRelationInfo[]` for all relation fields.
|
|
203
|
+
*
|
|
204
|
+
* Unlike the existing parser, this does NOT skip `@relation(fields: [...])`
|
|
205
|
+
* lines -- those are classified as `belongsTo`.
|
|
206
|
+
*/
|
|
207
|
+
function buildRelationInfos(rawLines, modelNames) {
|
|
208
|
+
const relations = [];
|
|
209
|
+
for (const line of rawLines) {
|
|
210
|
+
if (!modelNames.has(line.baseType))
|
|
211
|
+
continue;
|
|
212
|
+
const hasFkRelation = /@relation\([^)]*fields:\s*\[/.test(line.rawLine);
|
|
213
|
+
if (hasFkRelation) {
|
|
214
|
+
// belongsTo: this model owns the FK
|
|
215
|
+
const fkMatch = line.rawLine.match(/@relation\([^)]*fields:\s*\[(\w+)\]/);
|
|
216
|
+
const foreignKey = fkMatch ? fkMatch[1] : undefined;
|
|
217
|
+
relations.push({
|
|
218
|
+
name: line.name,
|
|
219
|
+
relatedModel: line.baseType,
|
|
220
|
+
kind: 'belongsTo',
|
|
221
|
+
foreignKey,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
else if (line.isArray) {
|
|
225
|
+
// hasMany
|
|
226
|
+
relations.push({
|
|
227
|
+
name: line.name,
|
|
228
|
+
relatedModel: line.baseType,
|
|
229
|
+
kind: 'hasMany',
|
|
230
|
+
foreignKey: undefined,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
// hasOne (non-array, no FK annotation)
|
|
235
|
+
relations.push({
|
|
236
|
+
name: line.name,
|
|
237
|
+
relatedModel: line.baseType,
|
|
238
|
+
kind: 'hasOne',
|
|
239
|
+
foreignKey: undefined,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return relations;
|
|
244
|
+
}
|
|
245
|
+
// ============================================================================
|
|
246
|
+
// Internal: Unique constraints & join table detection
|
|
247
|
+
// ============================================================================
|
|
248
|
+
/**
|
|
249
|
+
* Parse `@@unique([field1, field2])` directives from a model body.
|
|
250
|
+
*/
|
|
251
|
+
function parseUniqueConstraints(body) {
|
|
252
|
+
const constraints = [];
|
|
253
|
+
const regex = /@@unique\(\[([^\]]+)\]\)/g;
|
|
254
|
+
for (const match of body.matchAll(regex)) {
|
|
255
|
+
const fieldNames = match[1]
|
|
256
|
+
.split(',')
|
|
257
|
+
.map((s) => s.trim())
|
|
258
|
+
.filter((s) => s.length > 0);
|
|
259
|
+
constraints.push(fieldNames);
|
|
260
|
+
}
|
|
261
|
+
return constraints;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Determine if a model is a many-to-many join table.
|
|
265
|
+
*
|
|
266
|
+
* A model is a join table when:
|
|
267
|
+
* 1. Every non-auto-managed scalar field is a foreign key
|
|
268
|
+
* 2. There is at least one `@@unique` compound constraint
|
|
269
|
+
*/
|
|
270
|
+
function detectJoinTable(fields, fkFieldNames, uniqueConstraints) {
|
|
271
|
+
if (uniqueConstraints.length === 0)
|
|
272
|
+
return false;
|
|
273
|
+
const nonAutoManagedScalars = fields.filter((f) => !f.isAutoManaged);
|
|
274
|
+
if (nonAutoManagedScalars.length === 0)
|
|
275
|
+
return false;
|
|
276
|
+
return nonAutoManagedScalars.every((f) => fkFieldNames.has(f.name));
|
|
277
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync Detector
|
|
3
|
+
*
|
|
4
|
+
* Scans a project's `src/procedures/` and `src/schemas/` directories to
|
|
5
|
+
* discover existing generated files. The result is an `ExistingCodeMap`
|
|
6
|
+
* consumed by the prompter stage to decide whether each model needs
|
|
7
|
+
* generation, regeneration, or can be skipped.
|
|
8
|
+
*/
|
|
9
|
+
import type { ExistingCodeMap, SyncModelInfo } from './types.js';
|
|
10
|
+
/**
|
|
11
|
+
* Detect existing procedure and schema files that correspond to known
|
|
12
|
+
* Prisma models.
|
|
13
|
+
*
|
|
14
|
+
* @param projectRoot - Absolute path to the project root directory
|
|
15
|
+
* @param models - Model metadata produced by the analyzer stage
|
|
16
|
+
* @returns Map of model names to their existing file paths
|
|
17
|
+
*/
|
|
18
|
+
export declare function detectExisting(projectRoot: string, models: readonly SyncModelInfo[]): ExistingCodeMap;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync Detector
|
|
3
|
+
*
|
|
4
|
+
* Scans a project's `src/procedures/` and `src/schemas/` directories to
|
|
5
|
+
* discover existing generated files. The result is an `ExistingCodeMap`
|
|
6
|
+
* consumed by the prompter stage to decide whether each model needs
|
|
7
|
+
* generation, regeneration, or can be skipped.
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { singularize, toPascalCase } from '../generators/utils/naming.js';
|
|
12
|
+
/**
|
|
13
|
+
* Detect existing procedure and schema files that correspond to known
|
|
14
|
+
* Prisma models.
|
|
15
|
+
*
|
|
16
|
+
* @param projectRoot - Absolute path to the project root directory
|
|
17
|
+
* @param models - Model metadata produced by the analyzer stage
|
|
18
|
+
* @returns Map of model names to their existing file paths
|
|
19
|
+
*/
|
|
20
|
+
export function detectExisting(projectRoot, models) {
|
|
21
|
+
const modelNames = new Set(models.map((m) => m.name));
|
|
22
|
+
const procedures = scanProcedures(projectRoot, modelNames);
|
|
23
|
+
const schemas = scanSchemas(projectRoot, modelNames);
|
|
24
|
+
return { procedures, schemas };
|
|
25
|
+
}
|
|
26
|
+
// ============================================================================
|
|
27
|
+
// Internal Helpers
|
|
28
|
+
// ============================================================================
|
|
29
|
+
/**
|
|
30
|
+
* Scan `src/procedures/` for `.ts` files that match known model names.
|
|
31
|
+
*
|
|
32
|
+
* Excludes `index.ts` and anything inside `__tests__/`.
|
|
33
|
+
* Converts kebab-case/plural filenames to PascalCase singular for matching.
|
|
34
|
+
*/
|
|
35
|
+
function scanProcedures(projectRoot, modelNames) {
|
|
36
|
+
const proceduresDir = join(projectRoot, 'src', 'procedures');
|
|
37
|
+
const result = new Map();
|
|
38
|
+
if (!existsSync(proceduresDir)) {
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
const entries = readdirSync(proceduresDir, { withFileTypes: true });
|
|
42
|
+
for (const entry of entries) {
|
|
43
|
+
// Skip directories (including __tests__/) and non-.ts files
|
|
44
|
+
if (entry.isDirectory()) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const fileName = entry.name;
|
|
48
|
+
// Skip non-.ts files and index.ts
|
|
49
|
+
if (!fileName.endsWith('.ts') || fileName === 'index.ts') {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
// Extract base name: "users.ts" -> "users", "friend-requests.ts" -> "friend-requests"
|
|
53
|
+
const baseName = fileName.slice(0, -3);
|
|
54
|
+
// Convert to PascalCase singular: "users" -> "User", "friend-requests" -> "FriendRequest"
|
|
55
|
+
const modelName = toPascalCase(singularize(baseName));
|
|
56
|
+
if (modelNames.has(modelName)) {
|
|
57
|
+
result.set(modelName, join(proceduresDir, fileName));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Scan `src/schemas/` for `.schema.ts` files that match known model names.
|
|
64
|
+
*
|
|
65
|
+
* Excludes `index.ts` and anything inside `__tests__/`.
|
|
66
|
+
* Strips the `.schema.ts` suffix, then converts to PascalCase singular.
|
|
67
|
+
*/
|
|
68
|
+
function scanSchemas(projectRoot, modelNames) {
|
|
69
|
+
const schemasDir = join(projectRoot, 'src', 'schemas');
|
|
70
|
+
const result = new Map();
|
|
71
|
+
if (!existsSync(schemasDir)) {
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
const entries = readdirSync(schemasDir, { withFileTypes: true });
|
|
75
|
+
for (const entry of entries) {
|
|
76
|
+
// Skip directories (including __tests__/) and non-.schema.ts files
|
|
77
|
+
if (entry.isDirectory()) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const fileName = entry.name;
|
|
81
|
+
// Skip files that are not *.schema.ts, and skip index.ts
|
|
82
|
+
if (!fileName.endsWith('.schema.ts') || fileName === 'index.ts') {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
// Extract base name: "post.schema.ts" -> "post", "friend-requests.schema.ts" -> "friend-requests"
|
|
86
|
+
const baseName = fileName.slice(0, -'.schema.ts'.length);
|
|
87
|
+
// Convert to PascalCase singular: "posts" -> "Post", "friend-requests" -> "FriendRequest"
|
|
88
|
+
const modelName = toPascalCase(singularize(baseName));
|
|
89
|
+
if (modelNames.has(modelName)) {
|
|
90
|
+
result.set(modelName, join(schemasDir, fileName));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync Orchestrator
|
|
3
|
+
*
|
|
4
|
+
* Wires together the 5-stage `velox sync` pipeline:
|
|
5
|
+
* 1. Analyze Prisma schema -> SyncModelInfo[]
|
|
6
|
+
* 2. Detect existing code -> ExistingCodeMap
|
|
7
|
+
* 3. Prompt user for choices -> ModelChoices[]
|
|
8
|
+
* 4. Build generation plan -> SyncPlan
|
|
9
|
+
* 5. Generate files + register in router
|
|
10
|
+
*
|
|
11
|
+
* Supports --dry-run, --force, and --skip-registration flags.
|
|
12
|
+
*/
|
|
13
|
+
import type { SyncCommandOptions, SyncResult } from './types.js';
|
|
14
|
+
/**
|
|
15
|
+
* Execute the full sync pipeline.
|
|
16
|
+
*
|
|
17
|
+
* @param projectRoot - Absolute path to the project root directory
|
|
18
|
+
* @param options - CLI flags (dryRun, force, skipRegistration)
|
|
19
|
+
* @returns Summary of files created, overwritten, skipped, registered, and errors
|
|
20
|
+
*/
|
|
21
|
+
export declare function executeSync(projectRoot: string, options: SyncCommandOptions): Promise<SyncResult>;
|