@md2do/cli 0.4.0 → 0.5.0
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 +22 -0
- package/README.md +25 -0
- package/coverage/coverage-final.json +3 -2
- package/coverage/index.html +11 -11
- package/coverage/lcov-report/index.html +11 -11
- package/coverage/lcov-report/src/cli.ts.html +10 -4
- package/coverage/lcov-report/src/commands/config.ts.html +2269 -0
- package/coverage/lcov-report/src/commands/index.html +22 -7
- package/coverage/lcov-report/src/commands/index.ts.html +7 -4
- package/coverage/lcov-report/src/commands/list.ts.html +1 -1
- package/coverage/lcov-report/src/commands/stats.ts.html +1 -1
- package/coverage/lcov-report/src/commands/todoist.ts.html +1 -1
- package/coverage/lcov-report/src/formatters/index.html +1 -1
- package/coverage/lcov-report/src/formatters/json.ts.html +1 -1
- package/coverage/lcov-report/src/formatters/pretty.ts.html +1 -1
- package/coverage/lcov-report/src/index.html +5 -5
- package/coverage/lcov-report/src/index.ts.html +1 -1
- package/coverage/lcov-report/src/scanner.ts.html +1 -1
- package/coverage/lcov.info +746 -3
- package/coverage/src/cli.ts.html +10 -4
- package/coverage/src/commands/config.ts.html +2269 -0
- package/coverage/src/commands/index.html +22 -7
- package/coverage/src/commands/index.ts.html +7 -4
- package/coverage/src/commands/list.ts.html +1 -1
- package/coverage/src/commands/stats.ts.html +1 -1
- package/coverage/src/commands/todoist.ts.html +1 -1
- package/coverage/src/formatters/index.html +1 -1
- package/coverage/src/formatters/json.ts.html +1 -1
- package/coverage/src/formatters/pretty.ts.html +1 -1
- package/coverage/src/index.html +5 -5
- package/coverage/src/index.ts.html +1 -1
- package/coverage/src/scanner.ts.html +1 -1
- package/dist/cli.js +466 -6
- package/dist/index.d.ts +6 -1
- package/dist/index.js +461 -0
- package/package.json +5 -2
- package/src/cli.ts +2 -0
- package/src/commands/config.ts +731 -0
- package/src/commands/index.ts +1 -0
|
@@ -0,0 +1,731 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { loadConfig, validateConfig, type Config } from '@md2do/config';
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { homedir } from 'os';
|
|
6
|
+
import { execSync } from 'child_process';
|
|
7
|
+
import yaml from 'js-yaml';
|
|
8
|
+
|
|
9
|
+
interface ConfigInitOptions {
|
|
10
|
+
global?: boolean;
|
|
11
|
+
format?: 'json' | 'yaml' | 'js';
|
|
12
|
+
defaultAssignee?: string;
|
|
13
|
+
workdayStart?: string;
|
|
14
|
+
workdayEnd?: string;
|
|
15
|
+
defaultDueTime?: 'start' | 'end';
|
|
16
|
+
outputFormat?: 'pretty' | 'table' | 'json';
|
|
17
|
+
colors?: boolean;
|
|
18
|
+
warnings?: 'recommended' | 'strict' | 'off';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface ConfigSetOptions {
|
|
22
|
+
global?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface ConfigGetOptions {
|
|
26
|
+
global?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface ConfigListOptions {
|
|
30
|
+
showOrigin?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface ConfigEditOptions {
|
|
34
|
+
global?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Create the config command group
|
|
39
|
+
*/
|
|
40
|
+
export function createConfigCommand(): Command {
|
|
41
|
+
const command = new Command('config');
|
|
42
|
+
|
|
43
|
+
command.description('Manage md2do configuration');
|
|
44
|
+
|
|
45
|
+
// Add subcommands
|
|
46
|
+
command.addCommand(createConfigInitCommand());
|
|
47
|
+
command.addCommand(createConfigSetCommand());
|
|
48
|
+
command.addCommand(createConfigGetCommand());
|
|
49
|
+
command.addCommand(createConfigListCommand());
|
|
50
|
+
command.addCommand(createConfigEditCommand());
|
|
51
|
+
command.addCommand(createConfigValidateCommand());
|
|
52
|
+
|
|
53
|
+
return command;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Create the 'config init' subcommand
|
|
58
|
+
*/
|
|
59
|
+
function createConfigInitCommand(): Command {
|
|
60
|
+
const command = new Command('init');
|
|
61
|
+
|
|
62
|
+
command
|
|
63
|
+
.description('Initialize md2do configuration with interactive prompts')
|
|
64
|
+
.option('-g, --global', 'Create global configuration in home directory')
|
|
65
|
+
.option(
|
|
66
|
+
'--format <type>',
|
|
67
|
+
'Config file format (json|yaml|js)',
|
|
68
|
+
'json' as 'json' | 'yaml' | 'js',
|
|
69
|
+
)
|
|
70
|
+
.option('--default-assignee <username>', 'Default assignee username')
|
|
71
|
+
.option('--workday-start <time>', 'Work day start time (HH:MM)')
|
|
72
|
+
.option('--workday-end <time>', 'Work day end time (HH:MM)')
|
|
73
|
+
.option(
|
|
74
|
+
'--default-due-time <when>',
|
|
75
|
+
'Default due time (start|end)',
|
|
76
|
+
'end' as 'start' | 'end',
|
|
77
|
+
)
|
|
78
|
+
.option(
|
|
79
|
+
'--output-format <type>',
|
|
80
|
+
'Output format (pretty|table|json)',
|
|
81
|
+
'pretty' as 'pretty' | 'table' | 'json',
|
|
82
|
+
)
|
|
83
|
+
.option('--no-colors', 'Disable colored output')
|
|
84
|
+
.option(
|
|
85
|
+
'--warnings <level>',
|
|
86
|
+
'Warning level (recommended|strict|off)',
|
|
87
|
+
'recommended' as 'recommended' | 'strict' | 'off',
|
|
88
|
+
)
|
|
89
|
+
.action(async (options: ConfigInitOptions) => {
|
|
90
|
+
try {
|
|
91
|
+
await configInitAction(options);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
if (error instanceof Error && error.message === 'canceled') {
|
|
94
|
+
const p = await import('@clack/prompts');
|
|
95
|
+
p.cancel('Configuration canceled');
|
|
96
|
+
process.exit(0);
|
|
97
|
+
}
|
|
98
|
+
console.error(
|
|
99
|
+
'Error:',
|
|
100
|
+
error instanceof Error ? error.message : String(error),
|
|
101
|
+
);
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
return command;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Action handler for 'config init' command
|
|
111
|
+
*/
|
|
112
|
+
async function configInitAction(options: ConfigInitOptions): Promise<void> {
|
|
113
|
+
// Dynamic import for ESM-only @clack/prompts
|
|
114
|
+
const p = await import('@clack/prompts');
|
|
115
|
+
|
|
116
|
+
p.intro('Welcome to md2do configuration!');
|
|
117
|
+
|
|
118
|
+
// Determine if we're in interactive mode
|
|
119
|
+
const hasAnyOption =
|
|
120
|
+
options.defaultAssignee !== undefined ||
|
|
121
|
+
options.workdayStart !== undefined ||
|
|
122
|
+
options.workdayEnd !== undefined ||
|
|
123
|
+
options.defaultDueTime !== undefined ||
|
|
124
|
+
options.outputFormat !== undefined ||
|
|
125
|
+
options.colors !== undefined ||
|
|
126
|
+
options.warnings !== undefined;
|
|
127
|
+
|
|
128
|
+
const interactive = !hasAnyOption;
|
|
129
|
+
|
|
130
|
+
const config: Partial<Config> = {};
|
|
131
|
+
|
|
132
|
+
if (interactive) {
|
|
133
|
+
// Interactive prompts
|
|
134
|
+
const defaultAssignee = await p.text({
|
|
135
|
+
message: 'Your username (for filtering tasks):',
|
|
136
|
+
placeholder: 'Leave empty to skip',
|
|
137
|
+
validate: (value) => {
|
|
138
|
+
if (value && !/^[a-zA-Z0-9_-]+$/.test(value)) {
|
|
139
|
+
return 'Username should only contain letters, numbers, dashes, and underscores';
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const workdayStart = await p.text({
|
|
145
|
+
message: 'Work day start time (HH:MM):',
|
|
146
|
+
initialValue: '08:00',
|
|
147
|
+
validate: (value) => {
|
|
148
|
+
if (typeof value === 'string' && !/^\d{2}:\d{2}$/.test(value)) {
|
|
149
|
+
return 'Time must be in HH:MM format';
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const workdayEnd = await p.text({
|
|
155
|
+
message: 'Work day end time (HH:MM):',
|
|
156
|
+
initialValue: '17:00',
|
|
157
|
+
validate: (value) => {
|
|
158
|
+
if (typeof value === 'string' && !/^\d{2}:\d{2}$/.test(value)) {
|
|
159
|
+
return 'Time must be in HH:MM format';
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const defaultDueTime = await p.select({
|
|
165
|
+
message: 'Default due time:',
|
|
166
|
+
options: [
|
|
167
|
+
{ value: 'end', label: 'End of day' },
|
|
168
|
+
{ value: 'start', label: 'Start of day' },
|
|
169
|
+
],
|
|
170
|
+
initialValue: 'end',
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const outputFormat = await p.select({
|
|
174
|
+
message: 'Output format:',
|
|
175
|
+
options: [
|
|
176
|
+
{ value: 'pretty', label: 'Pretty (human-readable)' },
|
|
177
|
+
{ value: 'table', label: 'Table (structured)' },
|
|
178
|
+
{ value: 'json', label: 'JSON (machine-readable)' },
|
|
179
|
+
],
|
|
180
|
+
initialValue: 'pretty',
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const colors = await p.confirm({
|
|
184
|
+
message: 'Enable colored output?',
|
|
185
|
+
initialValue: true,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const warnings = await p.select({
|
|
189
|
+
message: 'Warning level:',
|
|
190
|
+
options: [
|
|
191
|
+
{
|
|
192
|
+
value: 'recommended',
|
|
193
|
+
label: 'Recommended (validates format, metadata optional)',
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
value: 'strict',
|
|
197
|
+
label: 'Strict (enforces complete metadata)',
|
|
198
|
+
},
|
|
199
|
+
{ value: 'off', label: 'Off (no warnings)' },
|
|
200
|
+
],
|
|
201
|
+
initialValue: 'recommended',
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// Build config object
|
|
205
|
+
if (defaultAssignee && typeof defaultAssignee === 'string') {
|
|
206
|
+
config.defaultAssignee = defaultAssignee;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
config.workday = {
|
|
210
|
+
startTime: typeof workdayStart === 'string' ? workdayStart : '08:00',
|
|
211
|
+
endTime: typeof workdayEnd === 'string' ? workdayEnd : '17:00',
|
|
212
|
+
defaultDueTime:
|
|
213
|
+
typeof defaultDueTime === 'string'
|
|
214
|
+
? (defaultDueTime as 'start' | 'end')
|
|
215
|
+
: 'end',
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
config.output = {
|
|
219
|
+
format:
|
|
220
|
+
typeof outputFormat === 'string'
|
|
221
|
+
? (outputFormat as 'pretty' | 'table' | 'json')
|
|
222
|
+
: 'pretty',
|
|
223
|
+
colors: typeof colors === 'boolean' ? colors : true,
|
|
224
|
+
paths: true,
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
if (typeof warnings === 'string' && warnings === 'off') {
|
|
228
|
+
config.warnings = { enabled: false };
|
|
229
|
+
} else if (typeof warnings === 'string' && warnings === 'strict') {
|
|
230
|
+
config.warnings = {
|
|
231
|
+
enabled: true,
|
|
232
|
+
rules: {
|
|
233
|
+
'unsupported-bullet': 'warn',
|
|
234
|
+
'malformed-checkbox': 'warn',
|
|
235
|
+
'missing-space-after': 'warn',
|
|
236
|
+
'missing-space-before': 'warn',
|
|
237
|
+
'relative-date-no-context': 'warn',
|
|
238
|
+
'missing-due-date': 'warn',
|
|
239
|
+
'missing-completed-date': 'warn',
|
|
240
|
+
'duplicate-todoist-id': 'error',
|
|
241
|
+
'file-read-error': 'error',
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
// recommended is the default, no need to set explicitly
|
|
246
|
+
} else {
|
|
247
|
+
// Non-interactive mode: use CLI options
|
|
248
|
+
if (options.defaultAssignee) {
|
|
249
|
+
config.defaultAssignee = options.defaultAssignee;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
config.workday = {
|
|
253
|
+
startTime: options.workdayStart || '08:00',
|
|
254
|
+
endTime: options.workdayEnd || '17:00',
|
|
255
|
+
defaultDueTime: options.defaultDueTime || 'end',
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
config.output = {
|
|
259
|
+
format: options.outputFormat || 'pretty',
|
|
260
|
+
colors: options.colors ?? true,
|
|
261
|
+
paths: true,
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
if (options.warnings === 'off') {
|
|
265
|
+
config.warnings = { enabled: false };
|
|
266
|
+
} else if (options.warnings === 'strict') {
|
|
267
|
+
config.warnings = {
|
|
268
|
+
enabled: true,
|
|
269
|
+
rules: {
|
|
270
|
+
'unsupported-bullet': 'warn',
|
|
271
|
+
'malformed-checkbox': 'warn',
|
|
272
|
+
'missing-space-after': 'warn',
|
|
273
|
+
'missing-space-before': 'warn',
|
|
274
|
+
'relative-date-no-context': 'warn',
|
|
275
|
+
'missing-due-date': 'warn',
|
|
276
|
+
'missing-completed-date': 'warn',
|
|
277
|
+
'duplicate-todoist-id': 'error',
|
|
278
|
+
'file-read-error': 'error',
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Validate config
|
|
285
|
+
try {
|
|
286
|
+
validateConfig(config);
|
|
287
|
+
} catch (error) {
|
|
288
|
+
p.cancel(
|
|
289
|
+
`Invalid configuration: ${error instanceof Error ? error.message : String(error)}`,
|
|
290
|
+
);
|
|
291
|
+
process.exit(1);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Determine config file path
|
|
295
|
+
const format = options.format || 'json';
|
|
296
|
+
const configPath = getConfigPath(options.global || false, format);
|
|
297
|
+
|
|
298
|
+
// Check if config already exists
|
|
299
|
+
if (existsSync(configPath)) {
|
|
300
|
+
if (interactive) {
|
|
301
|
+
const overwrite = await p.confirm({
|
|
302
|
+
message: `Configuration file already exists at ${configPath}. Overwrite?`,
|
|
303
|
+
initialValue: false,
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
if (!overwrite) {
|
|
307
|
+
p.cancel('Configuration canceled');
|
|
308
|
+
process.exit(0);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Write config file
|
|
314
|
+
writeConfigFile(configPath, config, format);
|
|
315
|
+
|
|
316
|
+
p.outro(`✓ Configuration saved to ${configPath}`);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Create the 'config set' subcommand
|
|
321
|
+
*/
|
|
322
|
+
function createConfigSetCommand(): Command {
|
|
323
|
+
const command = new Command('set');
|
|
324
|
+
|
|
325
|
+
command
|
|
326
|
+
.description('Set a configuration value')
|
|
327
|
+
.argument('<key>', 'Configuration key (e.g., workday.startTime)')
|
|
328
|
+
.argument('<value>', 'Configuration value')
|
|
329
|
+
.option('-g, --global', 'Set in global configuration')
|
|
330
|
+
.action((key: string, value: string, options: ConfigSetOptions) => {
|
|
331
|
+
try {
|
|
332
|
+
configSetAction(key, value, options);
|
|
333
|
+
} catch (error) {
|
|
334
|
+
console.error(
|
|
335
|
+
'Error:',
|
|
336
|
+
error instanceof Error ? error.message : String(error),
|
|
337
|
+
);
|
|
338
|
+
process.exit(1);
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
return command;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Action handler for 'config set' command
|
|
347
|
+
*/
|
|
348
|
+
function configSetAction(
|
|
349
|
+
key: string,
|
|
350
|
+
value: string,
|
|
351
|
+
options: ConfigSetOptions,
|
|
352
|
+
): void {
|
|
353
|
+
const isGlobal = options.global || false;
|
|
354
|
+
const configPath = findExistingConfigPath(isGlobal);
|
|
355
|
+
|
|
356
|
+
let config: Partial<Config> = {};
|
|
357
|
+
let format: 'json' | 'yaml' | 'js' = 'json';
|
|
358
|
+
|
|
359
|
+
// Load existing config if exists
|
|
360
|
+
if (configPath) {
|
|
361
|
+
format = getConfigFormat(configPath);
|
|
362
|
+
const content = readFileSync(configPath, 'utf-8');
|
|
363
|
+
config = parseConfigFile(content, format);
|
|
364
|
+
} else {
|
|
365
|
+
// Create new config with default format
|
|
366
|
+
format = 'json';
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Parse and set value
|
|
370
|
+
const parsedValue = parseValue(value);
|
|
371
|
+
setNestedValue(config, key, parsedValue);
|
|
372
|
+
|
|
373
|
+
// Validate
|
|
374
|
+
try {
|
|
375
|
+
validateConfig(config);
|
|
376
|
+
} catch (error) {
|
|
377
|
+
console.error(
|
|
378
|
+
`Invalid configuration: ${error instanceof Error ? error.message : String(error)}`,
|
|
379
|
+
);
|
|
380
|
+
process.exit(1);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Write back
|
|
384
|
+
const finalPath = configPath || getConfigPath(isGlobal, format);
|
|
385
|
+
writeConfigFile(finalPath, config, format);
|
|
386
|
+
|
|
387
|
+
console.log(`✓ Set ${key} = ${value} in ${finalPath}`);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Create the 'config get' subcommand
|
|
392
|
+
*/
|
|
393
|
+
function createConfigGetCommand(): Command {
|
|
394
|
+
const command = new Command('get');
|
|
395
|
+
|
|
396
|
+
command
|
|
397
|
+
.description('Get a configuration value')
|
|
398
|
+
.argument('<key>', 'Configuration key (e.g., workday.startTime)')
|
|
399
|
+
.option('-g, --global', 'Get from global configuration only')
|
|
400
|
+
.action(async (key: string, options: ConfigGetOptions) => {
|
|
401
|
+
try {
|
|
402
|
+
await configGetAction(key, options);
|
|
403
|
+
} catch (error) {
|
|
404
|
+
console.error(
|
|
405
|
+
'Error:',
|
|
406
|
+
error instanceof Error ? error.message : String(error),
|
|
407
|
+
);
|
|
408
|
+
process.exit(1);
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
return command;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Action handler for 'config get' command
|
|
417
|
+
*/
|
|
418
|
+
async function configGetAction(
|
|
419
|
+
key: string,
|
|
420
|
+
options: ConfigGetOptions,
|
|
421
|
+
): Promise<void> {
|
|
422
|
+
const config = options.global
|
|
423
|
+
? await loadConfig({ loadGlobal: true, loadEnv: false, cwd: homedir() })
|
|
424
|
+
: await loadConfig();
|
|
425
|
+
|
|
426
|
+
const value = getNestedValue(config, key);
|
|
427
|
+
|
|
428
|
+
if (value === undefined) {
|
|
429
|
+
console.log('(not set)');
|
|
430
|
+
} else if (typeof value === 'object') {
|
|
431
|
+
console.log(JSON.stringify(value, null, 2));
|
|
432
|
+
} else {
|
|
433
|
+
console.log(value);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Create the 'config list' subcommand
|
|
439
|
+
*/
|
|
440
|
+
function createConfigListCommand(): Command {
|
|
441
|
+
const command = new Command('list');
|
|
442
|
+
|
|
443
|
+
command
|
|
444
|
+
.description('Show all configuration values')
|
|
445
|
+
.option('--show-origin', 'Show where each value comes from')
|
|
446
|
+
.action(async (options: ConfigListOptions) => {
|
|
447
|
+
try {
|
|
448
|
+
await configListAction(options);
|
|
449
|
+
} catch (error) {
|
|
450
|
+
console.error(
|
|
451
|
+
'Error:',
|
|
452
|
+
error instanceof Error ? error.message : String(error),
|
|
453
|
+
);
|
|
454
|
+
process.exit(1);
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
return command;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Action handler for 'config list' command
|
|
463
|
+
*/
|
|
464
|
+
async function configListAction(options: ConfigListOptions): Promise<void> {
|
|
465
|
+
const config = await loadConfig();
|
|
466
|
+
|
|
467
|
+
console.log('');
|
|
468
|
+
console.log('Current configuration:');
|
|
469
|
+
console.log('');
|
|
470
|
+
console.log(JSON.stringify(config, null, 2));
|
|
471
|
+
console.log('');
|
|
472
|
+
|
|
473
|
+
if (options.showOrigin) {
|
|
474
|
+
console.log('Configuration sources:');
|
|
475
|
+
console.log(' 1. Default values (built-in)');
|
|
476
|
+
console.log(` 2. Global config (${getConfigPath(true, 'json')})`);
|
|
477
|
+
console.log(` 3. Project config (${getConfigPath(false, 'json')})`);
|
|
478
|
+
console.log(' 4. Environment variables');
|
|
479
|
+
console.log('');
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Create the 'config edit' subcommand
|
|
485
|
+
*/
|
|
486
|
+
function createConfigEditCommand(): Command {
|
|
487
|
+
const command = new Command('edit');
|
|
488
|
+
|
|
489
|
+
command
|
|
490
|
+
.description('Open configuration file in your default editor')
|
|
491
|
+
.option('-g, --global', 'Edit global configuration')
|
|
492
|
+
.action((options: ConfigEditOptions) => {
|
|
493
|
+
try {
|
|
494
|
+
configEditAction(options);
|
|
495
|
+
} catch (error) {
|
|
496
|
+
console.error(
|
|
497
|
+
'Error:',
|
|
498
|
+
error instanceof Error ? error.message : String(error),
|
|
499
|
+
);
|
|
500
|
+
process.exit(1);
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
return command;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Action handler for 'config edit' command
|
|
509
|
+
*/
|
|
510
|
+
function configEditAction(options: ConfigEditOptions): void {
|
|
511
|
+
const isGlobal = options.global || false;
|
|
512
|
+
let configPath = findExistingConfigPath(isGlobal);
|
|
513
|
+
|
|
514
|
+
// Create config if it doesn't exist
|
|
515
|
+
if (!configPath) {
|
|
516
|
+
configPath = getConfigPath(isGlobal, 'json');
|
|
517
|
+
writeConfigFile(configPath, {}, 'json');
|
|
518
|
+
console.log(`Created new configuration file: ${configPath}`);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Get editor
|
|
522
|
+
const editor =
|
|
523
|
+
process.env.VISUAL ||
|
|
524
|
+
process.env.EDITOR ||
|
|
525
|
+
(process.platform === 'win32' ? 'notepad' : 'vi');
|
|
526
|
+
|
|
527
|
+
console.log(`Opening ${configPath} in ${editor}...`);
|
|
528
|
+
|
|
529
|
+
try {
|
|
530
|
+
execSync(`${editor} '${configPath}'`, { stdio: 'inherit' });
|
|
531
|
+
} catch (error) {
|
|
532
|
+
console.error('Failed to open editor');
|
|
533
|
+
console.error(`You can manually edit the file at: ${configPath}`);
|
|
534
|
+
process.exit(1);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Create the 'config validate' subcommand
|
|
540
|
+
*/
|
|
541
|
+
function createConfigValidateCommand(): Command {
|
|
542
|
+
const command = new Command('validate');
|
|
543
|
+
|
|
544
|
+
command.description('Validate current configuration').action(async () => {
|
|
545
|
+
try {
|
|
546
|
+
await configValidateAction();
|
|
547
|
+
} catch (error) {
|
|
548
|
+
console.error(
|
|
549
|
+
'Error:',
|
|
550
|
+
error instanceof Error ? error.message : String(error),
|
|
551
|
+
);
|
|
552
|
+
process.exit(1);
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
return command;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Action handler for 'config validate' command
|
|
561
|
+
*/
|
|
562
|
+
async function configValidateAction(): Promise<void> {
|
|
563
|
+
try {
|
|
564
|
+
const config = await loadConfig();
|
|
565
|
+
validateConfig(config);
|
|
566
|
+
console.log('✓ Configuration is valid');
|
|
567
|
+
} catch (error) {
|
|
568
|
+
console.error(
|
|
569
|
+
`Invalid configuration: ${error instanceof Error ? error.message : String(error)}`,
|
|
570
|
+
);
|
|
571
|
+
process.exit(1);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Helper: Get config file path
|
|
577
|
+
*/
|
|
578
|
+
function getConfigPath(
|
|
579
|
+
isGlobal: boolean,
|
|
580
|
+
format: 'json' | 'yaml' | 'js',
|
|
581
|
+
): string {
|
|
582
|
+
const base = isGlobal ? homedir() : process.cwd();
|
|
583
|
+
const fileName =
|
|
584
|
+
format === 'json'
|
|
585
|
+
? '.md2do.json'
|
|
586
|
+
: format === 'yaml'
|
|
587
|
+
? '.md2do.yaml'
|
|
588
|
+
: '.md2do.js';
|
|
589
|
+
return join(base, fileName);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Helper: Find existing config file path
|
|
594
|
+
*/
|
|
595
|
+
function findExistingConfigPath(isGlobal: boolean): string | null {
|
|
596
|
+
const base = isGlobal ? homedir() : process.cwd();
|
|
597
|
+
const possibleFiles = [
|
|
598
|
+
'.md2do.json',
|
|
599
|
+
'.md2do.yaml',
|
|
600
|
+
'.md2do.yml',
|
|
601
|
+
'.md2do.js',
|
|
602
|
+
'.md2do.cjs',
|
|
603
|
+
'md2do.config.js',
|
|
604
|
+
'md2do.config.cjs',
|
|
605
|
+
];
|
|
606
|
+
|
|
607
|
+
for (const file of possibleFiles) {
|
|
608
|
+
const path = join(base, file);
|
|
609
|
+
if (existsSync(path)) {
|
|
610
|
+
return path;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
return null;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Helper: Get format from config path
|
|
619
|
+
*/
|
|
620
|
+
function getConfigFormat(path: string): 'json' | 'yaml' | 'js' {
|
|
621
|
+
if (path.endsWith('.json')) return 'json';
|
|
622
|
+
if (path.endsWith('.yaml') || path.endsWith('.yml')) return 'yaml';
|
|
623
|
+
return 'js';
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Helper: Parse config file content
|
|
628
|
+
*/
|
|
629
|
+
function parseConfigFile(
|
|
630
|
+
content: string,
|
|
631
|
+
format: 'json' | 'yaml' | 'js',
|
|
632
|
+
): Partial<Config> {
|
|
633
|
+
if (format === 'json') {
|
|
634
|
+
return JSON.parse(content) as Partial<Config>;
|
|
635
|
+
} else if (format === 'yaml') {
|
|
636
|
+
const loaded = yaml.load(content);
|
|
637
|
+
return (loaded || {}) as Partial<Config>;
|
|
638
|
+
} else {
|
|
639
|
+
// For JS files, we'd need to eval or require, which is complex
|
|
640
|
+
// For now, just parse as JSON
|
|
641
|
+
return JSON.parse(content) as Partial<Config>;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Helper: Write config file
|
|
647
|
+
*/
|
|
648
|
+
function writeConfigFile(
|
|
649
|
+
path: string,
|
|
650
|
+
config: Partial<Config>,
|
|
651
|
+
format: 'json' | 'yaml' | 'js',
|
|
652
|
+
): void {
|
|
653
|
+
let content: string;
|
|
654
|
+
|
|
655
|
+
if (format === 'json') {
|
|
656
|
+
content = JSON.stringify(config, null, 2) + '\n';
|
|
657
|
+
} else if (format === 'yaml') {
|
|
658
|
+
content = yaml.dump(config);
|
|
659
|
+
} else {
|
|
660
|
+
content = `module.exports = ${JSON.stringify(config, null, 2)};\n`;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
writeFileSync(path, content, 'utf-8');
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Helper: Parse value from string
|
|
668
|
+
*/
|
|
669
|
+
function parseValue(value: string): unknown {
|
|
670
|
+
// Try boolean
|
|
671
|
+
if (value === 'true') return true;
|
|
672
|
+
if (value === 'false') return false;
|
|
673
|
+
|
|
674
|
+
// Try number
|
|
675
|
+
const num = Number(value);
|
|
676
|
+
if (!isNaN(num)) return num;
|
|
677
|
+
|
|
678
|
+
// Try JSON
|
|
679
|
+
try {
|
|
680
|
+
return JSON.parse(value);
|
|
681
|
+
} catch {
|
|
682
|
+
// Return as string
|
|
683
|
+
return value;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Helper: Get nested value from object
|
|
689
|
+
*/
|
|
690
|
+
function getNestedValue(obj: unknown, path: string): unknown {
|
|
691
|
+
const keys = path.split('.');
|
|
692
|
+
let current: unknown = obj;
|
|
693
|
+
|
|
694
|
+
for (const key of keys) {
|
|
695
|
+
if (
|
|
696
|
+
current === null ||
|
|
697
|
+
current === undefined ||
|
|
698
|
+
typeof current !== 'object'
|
|
699
|
+
) {
|
|
700
|
+
return undefined;
|
|
701
|
+
}
|
|
702
|
+
current = (current as Record<string, unknown>)[key];
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
return current;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Helper: Set nested value in object
|
|
710
|
+
*/
|
|
711
|
+
function setNestedValue(
|
|
712
|
+
obj: Record<string, unknown>,
|
|
713
|
+
path: string,
|
|
714
|
+
value: unknown,
|
|
715
|
+
): void {
|
|
716
|
+
const keys = path.split('.');
|
|
717
|
+
const lastKey = keys.pop();
|
|
718
|
+
|
|
719
|
+
if (!lastKey) return;
|
|
720
|
+
|
|
721
|
+
let current: Record<string, unknown> = obj;
|
|
722
|
+
|
|
723
|
+
for (const key of keys) {
|
|
724
|
+
if (!(key in current) || typeof current[key] !== 'object') {
|
|
725
|
+
current[key] = {};
|
|
726
|
+
}
|
|
727
|
+
current = current[key] as Record<string, unknown>;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
current[lastKey] = value;
|
|
731
|
+
}
|