musubi-sdd 3.6.1 → 3.7.1
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/bin/musubi-analyze.js +275 -3
- package/bin/musubi-checkpoint.js +396 -0
- package/bin/musubi-convert.js +40 -1
- package/bin/musubi-costs.js +344 -0
- package/bin/musubi-init.js +44 -2
- package/package.json +1 -1
- package/src/converters/index.js +39 -0
- package/src/converters/parsers/openapi-parser.js +350 -0
- package/src/gui/public/index.html +670 -3
- package/src/gui/server.js +195 -0
- package/src/gui/services/index.js +2 -0
- package/src/gui/services/replanning-service.js +276 -0
- package/src/gui/services/traceability-service.js +29 -0
- package/src/llm-providers/index.js +20 -1
- package/src/llm-providers/ollama-provider.js +401 -0
- package/src/managers/checkpoint-manager.js +556 -0
- package/src/managers/index.js +30 -0
- package/src/monitoring/cost-tracker.js +510 -0
- package/src/monitoring/index.js +5 -1
- package/src/templates/index.js +15 -0
- package/src/templates/locale-manager.js +288 -0
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview MUSUBI Checkpoint CLI
|
|
5
|
+
* @description Manage development checkpoints
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
const { Command } = require('commander');
|
|
11
|
+
const chalk = require('chalk');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const { CheckpointManager, CheckpointState } = require('../src/managers/checkpoint-manager');
|
|
14
|
+
|
|
15
|
+
const program = new Command();
|
|
16
|
+
|
|
17
|
+
// Initialize checkpoint manager
|
|
18
|
+
function getManager(options = {}) {
|
|
19
|
+
return new CheckpointManager({
|
|
20
|
+
workspaceDir: options.workspace || process.cwd(),
|
|
21
|
+
maxCheckpoints: options.maxCheckpoints || 50,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
program
|
|
26
|
+
.name('musubi-checkpoint')
|
|
27
|
+
.description('Manage development state checkpoints')
|
|
28
|
+
.version('1.0.0');
|
|
29
|
+
|
|
30
|
+
// Create checkpoint
|
|
31
|
+
program
|
|
32
|
+
.command('create')
|
|
33
|
+
.alias('save')
|
|
34
|
+
.description('Create a new checkpoint')
|
|
35
|
+
.option('-n, --name <name>', 'Checkpoint name')
|
|
36
|
+
.option('-d, --description <description>', 'Checkpoint description')
|
|
37
|
+
.option('-t, --tags <tags>', 'Comma-separated tags')
|
|
38
|
+
.option('-w, --workspace <dir>', 'Workspace directory')
|
|
39
|
+
.action(async (options) => {
|
|
40
|
+
try {
|
|
41
|
+
const manager = getManager(options);
|
|
42
|
+
await manager.initialize();
|
|
43
|
+
|
|
44
|
+
const checkpoint = await manager.create({
|
|
45
|
+
name: options.name,
|
|
46
|
+
description: options.description,
|
|
47
|
+
tags: options.tags ? options.tags.split(',').map(t => t.trim()) : [],
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
console.log(chalk.green('✓ Checkpoint created successfully'));
|
|
51
|
+
console.log();
|
|
52
|
+
console.log(chalk.bold('ID:'), checkpoint.id);
|
|
53
|
+
console.log(chalk.bold('Name:'), checkpoint.name);
|
|
54
|
+
console.log(chalk.bold('Files:'), checkpoint.stats.filesCount);
|
|
55
|
+
console.log(chalk.bold('Size:'), formatSize(checkpoint.stats.totalSize));
|
|
56
|
+
console.log(chalk.bold('Timestamp:'), new Date(checkpoint.timestamp).toLocaleString());
|
|
57
|
+
|
|
58
|
+
manager.stopAutoCheckpoint();
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error(chalk.red('Error:'), error.message);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// List checkpoints
|
|
66
|
+
program
|
|
67
|
+
.command('list')
|
|
68
|
+
.alias('ls')
|
|
69
|
+
.description('List all checkpoints')
|
|
70
|
+
.option('-t, --tags <tags>', 'Filter by tags (comma-separated)')
|
|
71
|
+
.option('-s, --state <state>', 'Filter by state')
|
|
72
|
+
.option('-l, --limit <n>', 'Maximum results', parseInt)
|
|
73
|
+
.option('-w, --workspace <dir>', 'Workspace directory')
|
|
74
|
+
.option('--json', 'Output as JSON')
|
|
75
|
+
.action(async (options) => {
|
|
76
|
+
try {
|
|
77
|
+
const manager = getManager(options);
|
|
78
|
+
await manager.initialize();
|
|
79
|
+
|
|
80
|
+
const checkpoints = manager.list({
|
|
81
|
+
tags: options.tags ? options.tags.split(',').map(t => t.trim()) : undefined,
|
|
82
|
+
state: options.state,
|
|
83
|
+
limit: options.limit,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (options.json) {
|
|
87
|
+
console.log(JSON.stringify(checkpoints, null, 2));
|
|
88
|
+
manager.stopAutoCheckpoint();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (checkpoints.length === 0) {
|
|
93
|
+
console.log(chalk.yellow('No checkpoints found'));
|
|
94
|
+
manager.stopAutoCheckpoint();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
console.log(chalk.bold(`Checkpoints (${checkpoints.length}):`));
|
|
99
|
+
console.log();
|
|
100
|
+
|
|
101
|
+
for (const cp of checkpoints) {
|
|
102
|
+
const stateColor = getStateColor(cp.state);
|
|
103
|
+
const current = cp.id === manager.currentCheckpoint ? chalk.cyan(' [current]') : '';
|
|
104
|
+
|
|
105
|
+
console.log(`${chalk.bold(cp.id)}${current}`);
|
|
106
|
+
console.log(` Name: ${cp.name}`);
|
|
107
|
+
console.log(` State: ${stateColor(cp.state)}`);
|
|
108
|
+
console.log(` Files: ${cp.stats.filesCount} | Size: ${formatSize(cp.stats.totalSize)}`);
|
|
109
|
+
console.log(` Time: ${new Date(cp.timestamp).toLocaleString()}`);
|
|
110
|
+
if (cp.tags.length > 0) {
|
|
111
|
+
console.log(` Tags: ${cp.tags.map(t => chalk.blue(`#${t}`)).join(' ')}`);
|
|
112
|
+
}
|
|
113
|
+
console.log();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
manager.stopAutoCheckpoint();
|
|
117
|
+
} catch (error) {
|
|
118
|
+
console.error(chalk.red('Error:'), error.message);
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Show checkpoint details
|
|
124
|
+
program
|
|
125
|
+
.command('show <id>')
|
|
126
|
+
.description('Show checkpoint details')
|
|
127
|
+
.option('-w, --workspace <dir>', 'Workspace directory')
|
|
128
|
+
.option('--json', 'Output as JSON')
|
|
129
|
+
.action(async (id, options) => {
|
|
130
|
+
try {
|
|
131
|
+
const manager = getManager(options);
|
|
132
|
+
await manager.initialize();
|
|
133
|
+
|
|
134
|
+
const checkpoint = manager.get(id);
|
|
135
|
+
if (!checkpoint) {
|
|
136
|
+
console.error(chalk.red('Checkpoint not found:'), id);
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (options.json) {
|
|
141
|
+
console.log(JSON.stringify(checkpoint, null, 2));
|
|
142
|
+
manager.stopAutoCheckpoint();
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
console.log(chalk.bold('Checkpoint Details:'));
|
|
147
|
+
console.log();
|
|
148
|
+
console.log(chalk.bold('ID:'), checkpoint.id);
|
|
149
|
+
console.log(chalk.bold('Name:'), checkpoint.name);
|
|
150
|
+
console.log(chalk.bold('Description:'), checkpoint.description || '(none)');
|
|
151
|
+
console.log(chalk.bold('State:'), getStateColor(checkpoint.state)(checkpoint.state));
|
|
152
|
+
console.log(chalk.bold('Files:'), checkpoint.stats.filesCount);
|
|
153
|
+
console.log(chalk.bold('Size:'), formatSize(checkpoint.stats.totalSize));
|
|
154
|
+
console.log(chalk.bold('Created:'), new Date(checkpoint.timestamp).toLocaleString());
|
|
155
|
+
console.log(chalk.bold('Tags:'), checkpoint.tags.length > 0
|
|
156
|
+
? checkpoint.tags.map(t => chalk.blue(`#${t}`)).join(' ')
|
|
157
|
+
: '(none)');
|
|
158
|
+
|
|
159
|
+
if (Object.keys(checkpoint.context).length > 0) {
|
|
160
|
+
console.log(chalk.bold('Context:'));
|
|
161
|
+
console.log(JSON.stringify(checkpoint.context, null, 2));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
manager.stopAutoCheckpoint();
|
|
165
|
+
} catch (error) {
|
|
166
|
+
console.error(chalk.red('Error:'), error.message);
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Restore checkpoint
|
|
172
|
+
program
|
|
173
|
+
.command('restore <id>')
|
|
174
|
+
.description('Restore a checkpoint')
|
|
175
|
+
.option('--no-backup', 'Skip creating backup before restore')
|
|
176
|
+
.option('-w, --workspace <dir>', 'Workspace directory')
|
|
177
|
+
.action(async (id, options) => {
|
|
178
|
+
try {
|
|
179
|
+
const manager = getManager(options);
|
|
180
|
+
await manager.initialize();
|
|
181
|
+
|
|
182
|
+
console.log(chalk.yellow('⚠ Restoring checkpoint will overwrite current files'));
|
|
183
|
+
|
|
184
|
+
const checkpoint = await manager.restore(id, {
|
|
185
|
+
backup: options.backup !== false,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
console.log(chalk.green('✓ Checkpoint restored successfully'));
|
|
189
|
+
console.log();
|
|
190
|
+
console.log(chalk.bold('ID:'), checkpoint.id);
|
|
191
|
+
console.log(chalk.bold('Name:'), checkpoint.name);
|
|
192
|
+
console.log(chalk.bold('Files restored:'), checkpoint.stats.filesCount);
|
|
193
|
+
|
|
194
|
+
if (options.backup !== false) {
|
|
195
|
+
console.log(chalk.cyan('ℹ Backup checkpoint created before restore'));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
manager.stopAutoCheckpoint();
|
|
199
|
+
} catch (error) {
|
|
200
|
+
console.error(chalk.red('Error:'), error.message);
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// Delete checkpoint
|
|
206
|
+
program
|
|
207
|
+
.command('delete <id>')
|
|
208
|
+
.alias('rm')
|
|
209
|
+
.description('Delete a checkpoint')
|
|
210
|
+
.option('-w, --workspace <dir>', 'Workspace directory')
|
|
211
|
+
.action(async (id, options) => {
|
|
212
|
+
try {
|
|
213
|
+
const manager = getManager(options);
|
|
214
|
+
await manager.initialize();
|
|
215
|
+
|
|
216
|
+
const deleted = await manager.delete(id);
|
|
217
|
+
if (deleted) {
|
|
218
|
+
console.log(chalk.green('✓ Checkpoint deleted:'), id);
|
|
219
|
+
} else {
|
|
220
|
+
console.error(chalk.red('Checkpoint not found:'), id);
|
|
221
|
+
process.exit(1);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
manager.stopAutoCheckpoint();
|
|
225
|
+
} catch (error) {
|
|
226
|
+
console.error(chalk.red('Error:'), error.message);
|
|
227
|
+
process.exit(1);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// Archive checkpoint
|
|
232
|
+
program
|
|
233
|
+
.command('archive <id>')
|
|
234
|
+
.description('Archive a checkpoint')
|
|
235
|
+
.option('-w, --workspace <dir>', 'Workspace directory')
|
|
236
|
+
.action(async (id, options) => {
|
|
237
|
+
try {
|
|
238
|
+
const manager = getManager(options);
|
|
239
|
+
await manager.initialize();
|
|
240
|
+
|
|
241
|
+
const checkpoint = await manager.archive(id);
|
|
242
|
+
console.log(chalk.green('✓ Checkpoint archived:'), checkpoint.name);
|
|
243
|
+
|
|
244
|
+
manager.stopAutoCheckpoint();
|
|
245
|
+
} catch (error) {
|
|
246
|
+
console.error(chalk.red('Error:'), error.message);
|
|
247
|
+
process.exit(1);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// Compare checkpoints
|
|
252
|
+
program
|
|
253
|
+
.command('compare <id1> <id2>')
|
|
254
|
+
.alias('diff')
|
|
255
|
+
.description('Compare two checkpoints')
|
|
256
|
+
.option('-w, --workspace <dir>', 'Workspace directory')
|
|
257
|
+
.option('--json', 'Output as JSON')
|
|
258
|
+
.action(async (id1, id2, options) => {
|
|
259
|
+
try {
|
|
260
|
+
const manager = getManager(options);
|
|
261
|
+
await manager.initialize();
|
|
262
|
+
|
|
263
|
+
const comparison = await manager.compare(id1, id2);
|
|
264
|
+
|
|
265
|
+
if (options.json) {
|
|
266
|
+
console.log(JSON.stringify(comparison, null, 2));
|
|
267
|
+
manager.stopAutoCheckpoint();
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
console.log(chalk.bold('Checkpoint Comparison:'));
|
|
272
|
+
console.log();
|
|
273
|
+
console.log(chalk.bold('Checkpoint 1:'), comparison.checkpoint1.name);
|
|
274
|
+
console.log(` ID: ${comparison.checkpoint1.id}`);
|
|
275
|
+
console.log(` Time: ${new Date(comparison.checkpoint1.timestamp).toLocaleString()}`);
|
|
276
|
+
console.log();
|
|
277
|
+
console.log(chalk.bold('Checkpoint 2:'), comparison.checkpoint2.name);
|
|
278
|
+
console.log(` ID: ${comparison.checkpoint2.id}`);
|
|
279
|
+
console.log(` Time: ${new Date(comparison.checkpoint2.timestamp).toLocaleString()}`);
|
|
280
|
+
console.log();
|
|
281
|
+
|
|
282
|
+
console.log(chalk.bold('Changes:'));
|
|
283
|
+
console.log(` ${chalk.green('Added:')} ${comparison.changes.added} files`);
|
|
284
|
+
console.log(` ${chalk.red('Removed:')} ${comparison.changes.removed} files`);
|
|
285
|
+
console.log(` ${chalk.yellow('Modified:')} ${comparison.changes.modified} files`);
|
|
286
|
+
console.log(` ${chalk.gray('Unchanged:')} ${comparison.changes.unchanged} files`);
|
|
287
|
+
|
|
288
|
+
if (comparison.files.added.length > 0) {
|
|
289
|
+
console.log();
|
|
290
|
+
console.log(chalk.green('Added files:'));
|
|
291
|
+
comparison.files.added.forEach(f => console.log(` + ${f}`));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (comparison.files.removed.length > 0) {
|
|
295
|
+
console.log();
|
|
296
|
+
console.log(chalk.red('Removed files:'));
|
|
297
|
+
comparison.files.removed.forEach(f => console.log(` - ${f}`));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (comparison.files.modified.length > 0) {
|
|
301
|
+
console.log();
|
|
302
|
+
console.log(chalk.yellow('Modified files:'));
|
|
303
|
+
comparison.files.modified.forEach(f => console.log(` ~ ${f}`));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
manager.stopAutoCheckpoint();
|
|
307
|
+
} catch (error) {
|
|
308
|
+
console.error(chalk.red('Error:'), error.message);
|
|
309
|
+
process.exit(1);
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// Tag checkpoint
|
|
314
|
+
program
|
|
315
|
+
.command('tag <id> <tags...>')
|
|
316
|
+
.description('Add tags to a checkpoint')
|
|
317
|
+
.option('-w, --workspace <dir>', 'Workspace directory')
|
|
318
|
+
.action(async (id, tags, options) => {
|
|
319
|
+
try {
|
|
320
|
+
const manager = getManager(options);
|
|
321
|
+
await manager.initialize();
|
|
322
|
+
|
|
323
|
+
const checkpoint = await manager.addTags(id, tags);
|
|
324
|
+
console.log(chalk.green('✓ Tags added to checkpoint:'), checkpoint.name);
|
|
325
|
+
console.log(chalk.bold('Tags:'), checkpoint.tags.map(t => chalk.blue(`#${t}`)).join(' '));
|
|
326
|
+
|
|
327
|
+
manager.stopAutoCheckpoint();
|
|
328
|
+
} catch (error) {
|
|
329
|
+
console.error(chalk.red('Error:'), error.message);
|
|
330
|
+
process.exit(1);
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// Current checkpoint
|
|
335
|
+
program
|
|
336
|
+
.command('current')
|
|
337
|
+
.description('Show current checkpoint')
|
|
338
|
+
.option('-w, --workspace <dir>', 'Workspace directory')
|
|
339
|
+
.action(async (options) => {
|
|
340
|
+
try {
|
|
341
|
+
const manager = getManager(options);
|
|
342
|
+
await manager.initialize();
|
|
343
|
+
|
|
344
|
+
const current = manager.getCurrent();
|
|
345
|
+
if (!current) {
|
|
346
|
+
console.log(chalk.yellow('No current checkpoint'));
|
|
347
|
+
manager.stopAutoCheckpoint();
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
console.log(chalk.bold('Current Checkpoint:'));
|
|
352
|
+
console.log(chalk.bold('ID:'), current.id);
|
|
353
|
+
console.log(chalk.bold('Name:'), current.name);
|
|
354
|
+
console.log(chalk.bold('Created:'), new Date(current.timestamp).toLocaleString());
|
|
355
|
+
|
|
356
|
+
manager.stopAutoCheckpoint();
|
|
357
|
+
} catch (error) {
|
|
358
|
+
console.error(chalk.red('Error:'), error.message);
|
|
359
|
+
process.exit(1);
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// Helper functions
|
|
364
|
+
function formatSize(bytes) {
|
|
365
|
+
const units = ['B', 'KB', 'MB', 'GB'];
|
|
366
|
+
let i = 0;
|
|
367
|
+
let size = bytes;
|
|
368
|
+
while (size >= 1024 && i < units.length - 1) {
|
|
369
|
+
size /= 1024;
|
|
370
|
+
i++;
|
|
371
|
+
}
|
|
372
|
+
return `${size.toFixed(2)} ${units[i]}`;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function getStateColor(state) {
|
|
376
|
+
switch (state) {
|
|
377
|
+
case CheckpointState.CREATED:
|
|
378
|
+
return chalk.green;
|
|
379
|
+
case CheckpointState.ACTIVE:
|
|
380
|
+
return chalk.cyan;
|
|
381
|
+
case CheckpointState.RESTORED:
|
|
382
|
+
return chalk.yellow;
|
|
383
|
+
case CheckpointState.ARCHIVED:
|
|
384
|
+
return chalk.gray;
|
|
385
|
+
default:
|
|
386
|
+
return chalk.white;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Parse and execute
|
|
391
|
+
program.parse(process.argv);
|
|
392
|
+
|
|
393
|
+
// Show help if no command
|
|
394
|
+
if (process.argv.length <= 2) {
|
|
395
|
+
program.help();
|
|
396
|
+
}
|
package/bin/musubi-convert.js
CHANGED
|
@@ -19,7 +19,8 @@ const {
|
|
|
19
19
|
convertFromSpeckit,
|
|
20
20
|
convertToSpeckit,
|
|
21
21
|
validateFormat,
|
|
22
|
-
testRoundtrip
|
|
22
|
+
testRoundtrip,
|
|
23
|
+
convertFromOpenAPI,
|
|
23
24
|
} = require('../src/converters');
|
|
24
25
|
const packageJson = require('../package.json');
|
|
25
26
|
|
|
@@ -137,6 +138,44 @@ program
|
|
|
137
138
|
}
|
|
138
139
|
});
|
|
139
140
|
|
|
141
|
+
program
|
|
142
|
+
.command('from-openapi <specPath>')
|
|
143
|
+
.description('Convert OpenAPI/Swagger specification to MUSUBI requirements')
|
|
144
|
+
.option('-o, --output <dir>', 'Output directory', '.')
|
|
145
|
+
.option('--dry-run', 'Preview changes without writing')
|
|
146
|
+
.option('-v, --verbose', 'Verbose output')
|
|
147
|
+
.option('-f, --force', 'Overwrite existing files')
|
|
148
|
+
.option('--feature <name>', 'Create as single feature with given name')
|
|
149
|
+
.action(async (specPath, options) => {
|
|
150
|
+
try {
|
|
151
|
+
console.log('🔄 Converting OpenAPI → MUSUBI...\n');
|
|
152
|
+
|
|
153
|
+
const result = await convertFromOpenAPI(specPath, {
|
|
154
|
+
output: options.output,
|
|
155
|
+
dryRun: options.dryRun,
|
|
156
|
+
force: options.force,
|
|
157
|
+
verbose: options.verbose,
|
|
158
|
+
featureName: options.feature,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
console.log(`\n✅ Conversion complete!`);
|
|
162
|
+
console.log(` Features created: ${result.featuresCreated}`);
|
|
163
|
+
console.log(` Requirements: ${result.requirementsCreated}`);
|
|
164
|
+
console.log(` Output: ${result.outputPath}`);
|
|
165
|
+
|
|
166
|
+
if (result.warnings.length > 0) {
|
|
167
|
+
console.log(`\n⚠️ Warnings (${result.warnings.length}):`);
|
|
168
|
+
result.warnings.forEach(w => console.log(` - ${w}`));
|
|
169
|
+
}
|
|
170
|
+
} catch (error) {
|
|
171
|
+
console.error(`\n❌ Conversion failed: ${error.message}`);
|
|
172
|
+
if (options.verbose) {
|
|
173
|
+
console.error(error.stack);
|
|
174
|
+
}
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
140
179
|
program
|
|
141
180
|
.command('roundtrip <path>')
|
|
142
181
|
.description('Test roundtrip conversion (A → B → A\')')
|