@veloxts/cli 0.7.3 → 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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # @veloxts/cli
2
2
 
3
+ ## 0.7.4
4
+
5
+ ### Patch Changes
6
+
7
+ - feat(cli): add velox sync command for whole-schema Prisma-to-TypeScript generation
8
+ - Updated dependencies
9
+ - @veloxts/auth@0.7.4
10
+ - @veloxts/core@0.7.4
11
+ - @veloxts/orm@0.7.4
12
+ - @veloxts/router@0.7.4
13
+ - @veloxts/validation@0.7.4
14
+
3
15
  ## 0.7.3
4
16
 
5
17
  ### Patch Changes
package/dist/cli.js CHANGED
@@ -18,6 +18,7 @@ import { createMigrateCommand } from './commands/migrate.js';
18
18
  import { createOpenApiCommand } from './commands/openapi.js';
19
19
  import { createProceduresCommand } from './commands/procedures.js';
20
20
  import { createScheduleCommand } from './commands/schedule.js';
21
+ import { createSyncCommand } from './commands/sync.js';
21
22
  import { createTenantCommand } from './commands/tenant.js';
22
23
  import { CLI_VERSION } from './index.js';
23
24
  /**
@@ -40,6 +41,7 @@ function createCLI() {
40
41
  program.addCommand(createOpenApiCommand());
41
42
  program.addCommand(createProceduresCommand());
42
43
  program.addCommand(createScheduleCommand());
44
+ program.addCommand(createSyncCommand());
43
45
  program.addCommand(createTenantCommand());
44
46
  return program;
45
47
  }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Sync command - Generate Zod schemas + CRUD procedures from Prisma schema
3
+ *
4
+ * Usage:
5
+ * velox sync [options]
6
+ *
7
+ * Options:
8
+ * --dry-run Preview changes without writing files
9
+ * --force Skip prompts, generate all models with defaults
10
+ * --skip-registration Skip auto-registering procedures in router
11
+ *
12
+ * Examples:
13
+ * velox sync Interactive sync for all models
14
+ * velox sync --dry-run Preview what would be generated
15
+ * velox sync --force Generate all models with defaults
16
+ * velox sync --skip-registration Skip router registration
17
+ */
18
+ import { Command } from 'commander';
19
+ /**
20
+ * Create the `velox sync` command.
21
+ */
22
+ export declare function createSyncCommand(): Command;
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Sync command - Generate Zod schemas + CRUD procedures from Prisma schema
3
+ *
4
+ * Usage:
5
+ * velox sync [options]
6
+ *
7
+ * Options:
8
+ * --dry-run Preview changes without writing files
9
+ * --force Skip prompts, generate all models with defaults
10
+ * --skip-registration Skip auto-registering procedures in router
11
+ *
12
+ * Examples:
13
+ * velox sync Interactive sync for all models
14
+ * velox sync --dry-run Preview what would be generated
15
+ * velox sync --force Generate all models with defaults
16
+ * velox sync --skip-registration Skip router registration
17
+ */
18
+ import * as p from '@clack/prompts';
19
+ import { Command } from 'commander';
20
+ import pc from 'picocolors';
21
+ import { ensureVeloxProject } from '../generators/index.js';
22
+ import { findPrismaSchema } from '../generators/utils/prisma-schema.js';
23
+ import { executeSync } from '../sync/index.js';
24
+ // ============================================================================
25
+ // Command Creation
26
+ // ============================================================================
27
+ /**
28
+ * Create the `velox sync` command.
29
+ */
30
+ export function createSyncCommand() {
31
+ const cmd = new Command('sync')
32
+ .description('Generate Zod schemas and CRUD procedures from Prisma schema')
33
+ .option('-d, --dry-run', 'Preview changes without writing files', false)
34
+ .option('-f, --force', 'Skip prompts and generate all models with defaults', false)
35
+ .option('--skip-registration', 'Skip auto-registering procedures in router', false)
36
+ .action(async (options) => {
37
+ await runSync(options);
38
+ });
39
+ return cmd;
40
+ }
41
+ // ============================================================================
42
+ // Command Execution
43
+ // ============================================================================
44
+ /**
45
+ * Run the sync pipeline.
46
+ */
47
+ async function runSync(options) {
48
+ const projectRoot = process.cwd();
49
+ try {
50
+ // Verify we're in a VeloxTS project
51
+ await ensureVeloxProject(projectRoot);
52
+ // Verify Prisma schema exists
53
+ const schemaPath = findPrismaSchema(projectRoot);
54
+ if (!schemaPath) {
55
+ p.log.error('Prisma schema not found. Ensure prisma/schema.prisma exists in your project.');
56
+ process.exit(1);
57
+ }
58
+ p.intro(pc.bgCyan(pc.black(' VeloxTS Sync ')));
59
+ const result = await executeSync(projectRoot, {
60
+ dryRun: options.dryRun,
61
+ force: options.force,
62
+ skipRegistration: options.skipRegistration,
63
+ });
64
+ // Show final summary
65
+ const totalCreated = result.created.length;
66
+ const totalOverwritten = result.overwritten.length;
67
+ const totalRegistered = result.registered.length;
68
+ const totalErrors = result.errors.length;
69
+ if (totalErrors > 0) {
70
+ p.outro(pc.yellow(`Sync completed with ${totalErrors} error${totalErrors === 1 ? '' : 's'}. ` +
71
+ `${totalCreated} created, ${totalOverwritten} overwritten.`));
72
+ process.exit(1);
73
+ }
74
+ if (totalCreated === 0 && totalOverwritten === 0) {
75
+ p.outro(pc.dim('Nothing to generate.'));
76
+ }
77
+ else {
78
+ const parts = [];
79
+ if (totalCreated > 0) {
80
+ parts.push(`${totalCreated} created`);
81
+ }
82
+ if (totalOverwritten > 0) {
83
+ parts.push(`${totalOverwritten} overwritten`);
84
+ }
85
+ if (totalRegistered > 0) {
86
+ parts.push(`${totalRegistered} registered`);
87
+ }
88
+ p.outro(pc.green(`Sync complete: ${parts.join(', ')}.`));
89
+ }
90
+ }
91
+ catch (err) {
92
+ const message = err instanceof Error ? err.message : String(err);
93
+ p.log.error(message);
94
+ process.exit(1);
95
+ }
96
+ }
@@ -0,0 +1,20 @@
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 type { SyncModelInfo } from './types.js';
13
+ /**
14
+ * Analyze a Prisma schema and produce sync metadata for every model.
15
+ *
16
+ * @param projectRoot - Absolute path to the project root directory
17
+ * @returns Array of `SyncModelInfo` objects, one per Prisma model
18
+ * @throws If no Prisma schema can be found
19
+ */
20
+ export declare function analyzeSchema(projectRoot: string): SyncModelInfo[];
@@ -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>;