@syncular/cli 0.0.0-44 → 0.0.0-46

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.
@@ -1,133 +0,0 @@
1
- import { existsSync } from 'node:fs';
2
- import { dirname, join } from 'node:path';
3
- import process from 'node:process';
4
- import { Box, renderToString, Text } from 'ink';
5
- import type { ReactElement } from 'react';
6
-
7
- interface DoctorCheck {
8
- name: 'bun' | 'workspace' | 'git';
9
- label: string;
10
- ok: boolean;
11
- detail: string;
12
- hint: string;
13
- }
14
-
15
- function runDoctorChecks(cwd: string): DoctorCheck[] {
16
- const checks: DoctorCheck[] = [];
17
- const hasBun = typeof process.versions.bun === 'string';
18
- const hasWorkspacePackageJson = existsSync(join(cwd, 'package.json'));
19
-
20
- checks.push({
21
- name: 'bun',
22
- label: 'Bun runtime',
23
- ok: hasBun,
24
- detail: hasBun ? `Bun ${process.versions.bun}` : 'Bun runtime not detected',
25
- hint: 'Run the CLI with Bun, for example: bun syncular doctor',
26
- });
27
-
28
- checks.push({
29
- name: 'workspace',
30
- label: 'Workspace package',
31
- ok: hasWorkspacePackageJson,
32
- detail: hasWorkspacePackageJson
33
- ? 'package.json found'
34
- : 'package.json not found in current directory',
35
- hint: 'Run this command in the project root or package directory.',
36
- });
37
-
38
- let hasGitRepo = false;
39
- let currentDir = cwd;
40
- for (let index = 0; index < 8; index += 1) {
41
- if (existsSync(join(currentDir, '.git'))) {
42
- hasGitRepo = true;
43
- break;
44
- }
45
- const parentDir = dirname(currentDir);
46
- if (parentDir === currentDir) {
47
- break;
48
- }
49
- currentDir = parentDir;
50
- }
51
-
52
- checks.push({
53
- name: 'git',
54
- label: 'Git repository',
55
- ok: hasGitRepo,
56
- detail: hasGitRepo
57
- ? 'git repository detected'
58
- : 'git repository not detected near current directory',
59
- hint: 'Initialize a git repository with: git init',
60
- });
61
-
62
- return checks;
63
- }
64
-
65
- function renderDoctorView(args: {
66
- cwd: string;
67
- checks: DoctorCheck[];
68
- }): ReactElement {
69
- const passedCount = args.checks.reduce(
70
- (count, check) => count + (check.ok ? 1 : 0),
71
- 0
72
- );
73
- const failedCount = args.checks.length - passedCount;
74
- const labelWidth = args.checks.reduce(
75
- (maxWidth, check) => Math.max(maxWidth, check.label.length),
76
- 0
77
- );
78
- const allPassed = failedCount === 0;
79
-
80
- return (
81
- <Box flexDirection="column">
82
- <Text color="cyanBright" bold>
83
- syncular doctor
84
- </Text>
85
- <Text color="gray">cwd: {args.cwd}</Text>
86
- <Box marginTop={1} flexDirection="column">
87
- {args.checks.map((check) => (
88
- <Box key={check.name} flexDirection="column">
89
- <Box>
90
- <Text color={check.ok ? 'greenBright' : 'redBright'}>
91
- {check.ok ? 'OK ' : 'FAIL'}
92
- </Text>
93
- <Text> {check.label.padEnd(labelWidth)} </Text>
94
- <Text color={check.ok ? 'gray' : 'white'}>{check.detail}</Text>
95
- </Box>
96
- {check.ok ? null : (
97
- <Box marginLeft={6}>
98
- <Text color="yellow">hint: </Text>
99
- <Text color="yellowBright">{check.hint}</Text>
100
- </Box>
101
- )}
102
- </Box>
103
- ))}
104
- </Box>
105
- <Box marginTop={1}>
106
- <Text color={allPassed ? 'greenBright' : 'yellowBright'}>
107
- {allPassed
108
- ? `All checks passed (${passedCount}/${args.checks.length}).`
109
- : `Checks passed: ${passedCount}/${args.checks.length}.`}
110
- </Text>
111
- </Box>
112
- </Box>
113
- );
114
- }
115
-
116
- export async function runDoctor(): Promise<number> {
117
- const cwd = process.cwd();
118
- const checks = runDoctorChecks(cwd);
119
- const rendered = renderToString(
120
- renderDoctorView({
121
- cwd,
122
- checks,
123
- }),
124
- {
125
- columns: Math.max(process.stdout.columns ?? 120, 120),
126
- }
127
- );
128
- process.stdout.write(rendered);
129
- if (!rendered.endsWith('\n')) {
130
- process.stdout.write('\n');
131
- }
132
- return checks.every((check) => check.ok) ? 0 : 1;
133
- }
@@ -1,312 +0,0 @@
1
- import { readFile } from 'node:fs/promises';
2
- import { dirname, resolve } from 'node:path';
3
- import { pathToFileURL } from 'node:url';
4
- import { printError } from '../output';
5
-
6
- type ChecksumMismatchMode = 'error' | 'reset';
7
-
8
- interface MigrationStatusInput {
9
- cwd: string;
10
- }
11
-
12
- interface MigrationUpInput extends MigrationStatusInput {
13
- onChecksumMismatch: ChecksumMismatchMode;
14
- dryRun: boolean;
15
- }
16
-
17
- interface MigrationStatusResult {
18
- currentVersion: number;
19
- targetVersion: number;
20
- pendingVersions: number[];
21
- trackingTable?: string;
22
- }
23
-
24
- interface MigrationUpResult {
25
- appliedVersions: number[];
26
- currentVersion: number;
27
- wasReset?: boolean;
28
- dryRun?: boolean;
29
- }
30
-
31
- interface MigrationAdapter {
32
- status(input: MigrationStatusInput): Promise<MigrationStatusResult>;
33
- up(input: MigrationUpInput): Promise<MigrationUpResult>;
34
- }
35
-
36
- interface SyncularCliConfig {
37
- migrate?: {
38
- adapter: string;
39
- export?: string;
40
- };
41
- }
42
-
43
- const DEFAULT_CONFIG_PATH = 'syncular.config.json';
44
- const DEFAULT_MIGRATE_EXPORT = 'migrationRunner';
45
-
46
- function isRecord(value: unknown): value is Record<string, unknown> {
47
- return typeof value === 'object' && value !== null;
48
- }
49
-
50
- function parseConfig(value: unknown): SyncularCliConfig | { error: string } {
51
- if (!isRecord(value)) {
52
- return { error: 'Config must be a JSON object.' };
53
- }
54
-
55
- const config: SyncularCliConfig = {};
56
- const migrateValue = value.migrate;
57
- if (migrateValue !== undefined) {
58
- if (!isRecord(migrateValue)) {
59
- return { error: 'Config field "migrate" must be an object.' };
60
- }
61
-
62
- const adapter = migrateValue.adapter;
63
- const exportName = migrateValue.export;
64
- if (typeof adapter !== 'string' || adapter.length === 0) {
65
- return {
66
- error: 'Config field "migrate.adapter" must be a non-empty string.',
67
- };
68
- }
69
- if (
70
- exportName !== undefined &&
71
- (typeof exportName !== 'string' || exportName.length === 0)
72
- ) {
73
- return {
74
- error:
75
- 'Config field "migrate.export" must be a non-empty string when provided.',
76
- };
77
- }
78
-
79
- config.migrate = {
80
- adapter,
81
- ...(exportName ? { export: exportName } : {}),
82
- };
83
- }
84
-
85
- return config;
86
- }
87
-
88
- async function loadConfig(
89
- configPath: string
90
- ): Promise<{ config: SyncularCliConfig } | { error: string }> {
91
- let raw = '';
92
- try {
93
- raw = await readFile(configPath, 'utf8');
94
- } catch {
95
- return {
96
- error: `Config not found at ${configPath}. Run "syncular create --template syncular-libraries" first.`,
97
- };
98
- }
99
-
100
- let parsed: unknown;
101
- try {
102
- parsed = JSON.parse(raw);
103
- } catch {
104
- return { error: `Config at ${configPath} is not valid JSON.` };
105
- }
106
-
107
- const normalized = parseConfig(parsed);
108
- if ('error' in normalized) {
109
- return { error: `${normalized.error} (path: ${configPath})` };
110
- }
111
-
112
- return { config: normalized };
113
- }
114
-
115
- function isMigrationAdapter(value: unknown): value is MigrationAdapter {
116
- if (!isRecord(value)) {
117
- return false;
118
- }
119
- return typeof value.status === 'function' && typeof value.up === 'function';
120
- }
121
-
122
- async function loadMigrationAdapter(args: {
123
- configPath: string;
124
- }): Promise<{ adapter: MigrationAdapter } | { error: string }> {
125
- const loaded = await loadConfig(args.configPath);
126
- if ('error' in loaded) {
127
- return loaded;
128
- }
129
-
130
- const migrate = loaded.config.migrate;
131
- if (!migrate) {
132
- return {
133
- error:
134
- 'Config does not define "migrate". Run "syncular create --template syncular-libraries" to scaffold adapter files.',
135
- };
136
- }
137
-
138
- const adapterModulePath = resolve(dirname(args.configPath), migrate.adapter);
139
- const adapterExportName = migrate.export ?? DEFAULT_MIGRATE_EXPORT;
140
-
141
- let moduleValue: unknown;
142
- try {
143
- moduleValue = await import(pathToFileURL(adapterModulePath).href);
144
- } catch (error: unknown) {
145
- const message = error instanceof Error ? error.message : String(error);
146
- return {
147
- error: `Failed to load migrate adapter module (${adapterModulePath}): ${message}`,
148
- };
149
- }
150
-
151
- if (!isRecord(moduleValue)) {
152
- return {
153
- error: `Adapter module ${adapterModulePath} did not load as an object module.`,
154
- };
155
- }
156
-
157
- const exportValue = moduleValue[adapterExportName];
158
- if (!isMigrationAdapter(exportValue)) {
159
- return {
160
- error:
161
- `Adapter export "${adapterExportName}" in ${adapterModulePath} is missing ` +
162
- 'required methods: status() and up().',
163
- };
164
- }
165
-
166
- return { adapter: exportValue };
167
- }
168
-
169
- function optionalFlag(
170
- flagValues: Map<string, string>,
171
- flag: string,
172
- fallback: string
173
- ): string {
174
- const value = flagValues.get(flag)?.trim();
175
- return value && value.length > 0 ? value : fallback;
176
- }
177
-
178
- function optionalBooleanFlag(
179
- flagValues: Map<string, string>,
180
- flag: string,
181
- fallback: boolean
182
- ): boolean {
183
- const value = flagValues.get(flag);
184
- if (!value) {
185
- return fallback;
186
- }
187
-
188
- const normalized = value.trim().toLowerCase();
189
- if (
190
- normalized === '1' ||
191
- normalized === 'true' ||
192
- normalized === 'yes' ||
193
- normalized === 'on'
194
- ) {
195
- return true;
196
- }
197
- if (
198
- normalized === '0' ||
199
- normalized === 'false' ||
200
- normalized === 'no' ||
201
- normalized === 'off'
202
- ) {
203
- return false;
204
- }
205
-
206
- throw new Error(
207
- `Invalid ${flag} value "${value}". Use true/false, yes/no, on/off, or 1/0.`
208
- );
209
- }
210
-
211
- function parseChecksumMode(
212
- flagValues: Map<string, string>
213
- ): ChecksumMismatchMode | { error: string } {
214
- const value = flagValues.get('--on-checksum-mismatch');
215
- if (!value) {
216
- return 'error';
217
- }
218
- if (value === 'error' || value === 'reset') {
219
- return value;
220
- }
221
- return {
222
- error: 'Invalid value for --on-checksum-mismatch. Use "error" or "reset".',
223
- };
224
- }
225
-
226
- function resolveConfigPath(
227
- flagValues: Map<string, string>,
228
- cwd: string
229
- ): string {
230
- const configPath = optionalFlag(flagValues, '--config', DEFAULT_CONFIG_PATH);
231
- return resolve(cwd, configPath);
232
- }
233
-
234
- export async function runMigrateStatus(
235
- flagValues: Map<string, string>
236
- ): Promise<number> {
237
- try {
238
- const cwd = process.cwd();
239
- const configPath = resolveConfigPath(flagValues, cwd);
240
- const loaded = await loadMigrationAdapter({ configPath });
241
- if ('error' in loaded) {
242
- throw new Error(loaded.error);
243
- }
244
-
245
- const result = await loaded.adapter.status({ cwd });
246
- const pendingVersions =
247
- result.pendingVersions.length === 0
248
- ? '(none)'
249
- : result.pendingVersions.join(', ');
250
-
251
- console.log(`Current version: ${result.currentVersion}`);
252
- console.log(`Target version: ${result.targetVersion}`);
253
- console.log(`Pending versions: ${pendingVersions}`);
254
- if (result.trackingTable) {
255
- console.log(`Tracking table: ${result.trackingTable}`);
256
- }
257
- return 0;
258
- } catch (error: unknown) {
259
- printError(
260
- error instanceof Error ? error.message : 'Migration status failed.'
261
- );
262
- return 1;
263
- }
264
- }
265
-
266
- export async function runMigrateUp(
267
- flagValues: Map<string, string>
268
- ): Promise<number> {
269
- try {
270
- const checksumMode = parseChecksumMode(flagValues);
271
- if (typeof checksumMode !== 'string') {
272
- throw new Error(checksumMode.error);
273
- }
274
-
275
- const confirmReset = optionalBooleanFlag(flagValues, '--yes', false);
276
- if (checksumMode === 'reset' && !confirmReset) {
277
- throw new Error(
278
- 'Reset mode requires explicit confirmation. Re-run with --on-checksum-mismatch reset --yes true.'
279
- );
280
- }
281
-
282
- const cwd = process.cwd();
283
- const dryRun = optionalBooleanFlag(flagValues, '--dry-run', false);
284
- const configPath = resolveConfigPath(flagValues, cwd);
285
- const loaded = await loadMigrationAdapter({ configPath });
286
- if ('error' in loaded) {
287
- throw new Error(loaded.error);
288
- }
289
-
290
- const result = await loaded.adapter.up({
291
- cwd,
292
- onChecksumMismatch: checksumMode,
293
- dryRun,
294
- });
295
- const appliedVersions =
296
- result.appliedVersions.length === 0
297
- ? '(none)'
298
- : result.appliedVersions.join(', ');
299
-
300
- console.log(`Applied versions: ${appliedVersions}`);
301
- console.log(`Current version: ${result.currentVersion}`);
302
- console.log(`Checksum mismatch mode: ${checksumMode}`);
303
- console.log(`Reset occurred: ${result.wasReset ? 'yes' : 'no'}`);
304
- if (result.dryRun || dryRun) {
305
- console.log('Dry run: yes');
306
- }
307
- return 0;
308
- } catch (error: unknown) {
309
- printError(error instanceof Error ? error.message : 'Migration up failed.');
310
- return 1;
311
- }
312
- }