@hacksmith/doraval 0.2.11

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/doraval.js ADDED
@@ -0,0 +1,3578 @@
1
+ #!/usr/bin/env bun
2
+ // @bun
3
+ var __create = Object.create;
4
+ var __getProtoOf = Object.getPrototypeOf;
5
+ var __defProp = Object.defineProperty;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ function __accessProp(key) {
9
+ return this[key];
10
+ }
11
+ var __toESMCache_node;
12
+ var __toESMCache_esm;
13
+ var __toESM = (mod, isNodeMode, target) => {
14
+ var canCache = mod != null && typeof mod === "object";
15
+ if (canCache) {
16
+ var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
17
+ var cached = cache.get(mod);
18
+ if (cached)
19
+ return cached;
20
+ }
21
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
22
+ const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
23
+ for (let key of __getOwnPropNames(mod))
24
+ if (!__hasOwnProp.call(to, key))
25
+ __defProp(to, key, {
26
+ get: __accessProp.bind(mod, key),
27
+ enumerable: true
28
+ });
29
+ if (canCache)
30
+ cache.set(mod, to);
31
+ return to;
32
+ };
33
+ var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
34
+ var __returnValue = (v) => v;
35
+ function __exportSetter(name, newValue) {
36
+ this[name] = __returnValue.bind(null, newValue);
37
+ }
38
+ var __export = (target, all) => {
39
+ for (var name in all)
40
+ __defProp(target, name, {
41
+ get: all[name],
42
+ enumerable: true,
43
+ configurable: true,
44
+ set: __exportSetter.bind(all, name)
45
+ });
46
+ };
47
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
48
+ var __require = import.meta.require;
49
+
50
+ // node_modules/.bun/citty@0.2.2/node_modules/citty/dist/_chunks/libs/scule.mjs
51
+ function isUppercase(char = "") {
52
+ if (NUMBER_CHAR_RE.test(char))
53
+ return;
54
+ return char !== char.toLowerCase();
55
+ }
56
+ function splitByCase(str, separators) {
57
+ const splitters = separators ?? STR_SPLITTERS;
58
+ const parts = [];
59
+ if (!str || typeof str !== "string")
60
+ return parts;
61
+ let buff = "";
62
+ let previousUpper;
63
+ let previousSplitter;
64
+ for (const char of str) {
65
+ const isSplitter = splitters.includes(char);
66
+ if (isSplitter === true) {
67
+ parts.push(buff);
68
+ buff = "";
69
+ previousUpper = undefined;
70
+ continue;
71
+ }
72
+ const isUpper = isUppercase(char);
73
+ if (previousSplitter === false) {
74
+ if (previousUpper === false && isUpper === true) {
75
+ parts.push(buff);
76
+ buff = char;
77
+ previousUpper = isUpper;
78
+ continue;
79
+ }
80
+ if (previousUpper === true && isUpper === false && buff.length > 1) {
81
+ const lastChar = buff.at(-1);
82
+ parts.push(buff.slice(0, Math.max(0, buff.length - 1)));
83
+ buff = lastChar + char;
84
+ previousUpper = isUpper;
85
+ continue;
86
+ }
87
+ }
88
+ buff += char;
89
+ previousUpper = isUpper;
90
+ previousSplitter = isSplitter;
91
+ }
92
+ parts.push(buff);
93
+ return parts;
94
+ }
95
+ function upperFirst(str) {
96
+ return str ? str[0].toUpperCase() + str.slice(1) : "";
97
+ }
98
+ function lowerFirst(str) {
99
+ return str ? str[0].toLowerCase() + str.slice(1) : "";
100
+ }
101
+ function pascalCase(str, opts) {
102
+ return str ? (Array.isArray(str) ? str : splitByCase(str)).map((p) => upperFirst(opts?.normalize ? p.toLowerCase() : p)).join("") : "";
103
+ }
104
+ function camelCase(str, opts) {
105
+ return lowerFirst(pascalCase(str || "", opts));
106
+ }
107
+ function kebabCase(str, joiner) {
108
+ return str ? (Array.isArray(str) ? str : splitByCase(str)).map((p) => p.toLowerCase()).join(joiner ?? "-") : "";
109
+ }
110
+ function snakeCase(str) {
111
+ return kebabCase(str || "", "_");
112
+ }
113
+ var NUMBER_CHAR_RE, STR_SPLITTERS;
114
+ var init_scule = __esm(() => {
115
+ NUMBER_CHAR_RE = /\d/;
116
+ STR_SPLITTERS = [
117
+ "-",
118
+ "_",
119
+ "/",
120
+ "."
121
+ ];
122
+ });
123
+
124
+ // node_modules/.bun/citty@0.2.2/node_modules/citty/dist/index.mjs
125
+ import { parseArgs as parseArgs$1 } from "util";
126
+ function toArray(val) {
127
+ if (Array.isArray(val))
128
+ return val;
129
+ return val === undefined ? [] : [val];
130
+ }
131
+ function formatLineColumns(lines, linePrefix = "") {
132
+ const maxLength = [];
133
+ for (const line of lines)
134
+ for (const [i, element] of line.entries())
135
+ maxLength[i] = Math.max(maxLength[i] || 0, element.length);
136
+ return lines.map((l) => l.map((c, i) => linePrefix + c[i === 0 ? "padStart" : "padEnd"](maxLength[i])).join(" ")).join(`
137
+ `);
138
+ }
139
+ function resolveValue(input) {
140
+ return typeof input === "function" ? input() : input;
141
+ }
142
+ function parseRawArgs(args = [], opts = {}) {
143
+ const booleans = new Set(opts.boolean || []);
144
+ const strings = new Set(opts.string || []);
145
+ const aliasMap = opts.alias || {};
146
+ const defaults = opts.default || {};
147
+ const aliasToMain = /* @__PURE__ */ new Map;
148
+ const mainToAliases = /* @__PURE__ */ new Map;
149
+ for (const [key, value] of Object.entries(aliasMap)) {
150
+ const targets = value;
151
+ for (const target of targets) {
152
+ aliasToMain.set(key, target);
153
+ if (!mainToAliases.has(target))
154
+ mainToAliases.set(target, []);
155
+ mainToAliases.get(target).push(key);
156
+ aliasToMain.set(target, key);
157
+ if (!mainToAliases.has(key))
158
+ mainToAliases.set(key, []);
159
+ mainToAliases.get(key).push(target);
160
+ }
161
+ }
162
+ const options = {};
163
+ function getType(name) {
164
+ if (booleans.has(name))
165
+ return "boolean";
166
+ const aliases = mainToAliases.get(name) || [];
167
+ for (const alias of aliases)
168
+ if (booleans.has(alias))
169
+ return "boolean";
170
+ return "string";
171
+ }
172
+ function isStringType(name) {
173
+ if (strings.has(name))
174
+ return true;
175
+ const aliases = mainToAliases.get(name) || [];
176
+ for (const alias of aliases)
177
+ if (strings.has(alias))
178
+ return true;
179
+ return false;
180
+ }
181
+ const allOptions = new Set([
182
+ ...booleans,
183
+ ...strings,
184
+ ...Object.keys(aliasMap),
185
+ ...Object.values(aliasMap).flat(),
186
+ ...Object.keys(defaults)
187
+ ]);
188
+ for (const name of allOptions)
189
+ if (!options[name])
190
+ options[name] = {
191
+ type: getType(name),
192
+ default: defaults[name]
193
+ };
194
+ for (const [alias, main] of aliasToMain.entries())
195
+ if (alias.length === 1 && options[main] && !options[main].short)
196
+ options[main].short = alias;
197
+ const processedArgs = [];
198
+ const negatedFlags = {};
199
+ for (let i = 0;i < args.length; i++) {
200
+ const arg = args[i];
201
+ if (arg === "--") {
202
+ processedArgs.push(...args.slice(i));
203
+ break;
204
+ }
205
+ if (arg.startsWith("--no-")) {
206
+ const flagName = arg.slice(5);
207
+ negatedFlags[flagName] = true;
208
+ continue;
209
+ }
210
+ processedArgs.push(arg);
211
+ }
212
+ let parsed;
213
+ try {
214
+ parsed = parseArgs$1({
215
+ args: processedArgs,
216
+ options: Object.keys(options).length > 0 ? options : undefined,
217
+ allowPositionals: true,
218
+ strict: false
219
+ });
220
+ } catch {
221
+ parsed = {
222
+ values: {},
223
+ positionals: processedArgs
224
+ };
225
+ }
226
+ const out = { _: [] };
227
+ out._ = parsed.positionals;
228
+ for (const [key, value] of Object.entries(parsed.values)) {
229
+ let coerced = value;
230
+ if (getType(key) === "boolean" && typeof value === "string")
231
+ coerced = value !== "false";
232
+ else if (isStringType(key) && typeof value === "boolean")
233
+ coerced = "";
234
+ out[key] = coerced;
235
+ }
236
+ for (const [name] of Object.entries(negatedFlags)) {
237
+ out[name] = false;
238
+ const mainName = aliasToMain.get(name);
239
+ if (mainName)
240
+ out[mainName] = false;
241
+ const aliases = mainToAliases.get(name);
242
+ if (aliases)
243
+ for (const alias of aliases)
244
+ out[alias] = false;
245
+ }
246
+ for (const [alias, main] of aliasToMain.entries()) {
247
+ if (out[alias] !== undefined && out[main] === undefined)
248
+ out[main] = out[alias];
249
+ if (out[main] !== undefined && out[alias] === undefined)
250
+ out[alias] = out[main];
251
+ if (out[alias] !== out[main] && defaults[main] === out[main])
252
+ out[main] = out[alias];
253
+ }
254
+ return out;
255
+ }
256
+ function parseArgs(rawArgs, argsDef) {
257
+ const parseOptions = {
258
+ boolean: [],
259
+ string: [],
260
+ alias: {},
261
+ default: {}
262
+ };
263
+ const args = resolveArgs(argsDef);
264
+ for (const arg of args) {
265
+ if (arg.type === "positional")
266
+ continue;
267
+ if (arg.type === "string" || arg.type === "enum")
268
+ parseOptions.string.push(arg.name);
269
+ else if (arg.type === "boolean")
270
+ parseOptions.boolean.push(arg.name);
271
+ if (arg.default !== undefined)
272
+ parseOptions.default[arg.name] = arg.default;
273
+ if (arg.alias)
274
+ parseOptions.alias[arg.name] = arg.alias;
275
+ const camelName = camelCase(arg.name);
276
+ const kebabName = kebabCase(arg.name);
277
+ if (camelName !== arg.name || kebabName !== arg.name) {
278
+ const existingAliases = toArray(parseOptions.alias[arg.name] || []);
279
+ if (camelName !== arg.name && !existingAliases.includes(camelName))
280
+ existingAliases.push(camelName);
281
+ if (kebabName !== arg.name && !existingAliases.includes(kebabName))
282
+ existingAliases.push(kebabName);
283
+ if (existingAliases.length > 0)
284
+ parseOptions.alias[arg.name] = existingAliases;
285
+ }
286
+ }
287
+ const parsed = parseRawArgs(rawArgs, parseOptions);
288
+ const [...positionalArguments] = parsed._;
289
+ const parsedArgsProxy = new Proxy(parsed, { get(target, prop) {
290
+ return target[prop] ?? target[camelCase(prop)] ?? target[kebabCase(prop)];
291
+ } });
292
+ for (const [, arg] of args.entries())
293
+ if (arg.type === "positional") {
294
+ const nextPositionalArgument = positionalArguments.shift();
295
+ if (nextPositionalArgument !== undefined)
296
+ parsedArgsProxy[arg.name] = nextPositionalArgument;
297
+ else if (arg.default === undefined && arg.required !== false)
298
+ throw new CLIError(`Missing required positional argument: ${arg.name.toUpperCase()}`, "EARG");
299
+ else
300
+ parsedArgsProxy[arg.name] = arg.default;
301
+ } else if (arg.type === "enum") {
302
+ const argument = parsedArgsProxy[arg.name];
303
+ const options = arg.options || [];
304
+ if (argument !== undefined && options.length > 0 && !options.includes(argument))
305
+ throw new CLIError(`Invalid value for argument: ${cyan(`--${arg.name}`)} (${cyan(argument)}). Expected one of: ${options.map((o) => cyan(o)).join(", ")}.`, "EARG");
306
+ } else if (arg.required && parsedArgsProxy[arg.name] === undefined)
307
+ throw new CLIError(`Missing required argument: --${arg.name}`, "EARG");
308
+ return parsedArgsProxy;
309
+ }
310
+ function resolveArgs(argsDef) {
311
+ const args = [];
312
+ for (const [name, argDef] of Object.entries(argsDef || {}))
313
+ args.push({
314
+ ...argDef,
315
+ name,
316
+ alias: toArray(argDef.alias)
317
+ });
318
+ return args;
319
+ }
320
+ async function resolvePlugins(plugins) {
321
+ return Promise.all(plugins.map((p) => resolveValue(p)));
322
+ }
323
+ function defineCommand(def) {
324
+ return def;
325
+ }
326
+ async function runCommand(cmd, opts) {
327
+ const cmdArgs = await resolveValue(cmd.args || {});
328
+ const parsedArgs = parseArgs(opts.rawArgs, cmdArgs);
329
+ const context = {
330
+ rawArgs: opts.rawArgs,
331
+ args: parsedArgs,
332
+ data: opts.data,
333
+ cmd
334
+ };
335
+ const plugins = await resolvePlugins(cmd.plugins ?? []);
336
+ let result;
337
+ let runError;
338
+ try {
339
+ for (const plugin of plugins)
340
+ await plugin.setup?.(context);
341
+ if (typeof cmd.setup === "function")
342
+ await cmd.setup(context);
343
+ const subCommands = await resolveValue(cmd.subCommands);
344
+ if (subCommands && Object.keys(subCommands).length > 0) {
345
+ const subCommandArgIndex = findSubCommandIndex(opts.rawArgs, cmdArgs);
346
+ const explicitName = opts.rawArgs[subCommandArgIndex];
347
+ if (explicitName) {
348
+ const subCommand = await _findSubCommand(subCommands, explicitName);
349
+ if (!subCommand)
350
+ throw new CLIError(`Unknown command ${cyan(explicitName)}`, "E_UNKNOWN_COMMAND");
351
+ await runCommand(subCommand, { rawArgs: opts.rawArgs.slice(subCommandArgIndex + 1) });
352
+ } else {
353
+ const defaultSubCommand = await resolveValue(cmd.default);
354
+ if (defaultSubCommand) {
355
+ if (cmd.run)
356
+ throw new CLIError(`Cannot specify both 'run' and 'default' on the same command.`, "E_DEFAULT_CONFLICT");
357
+ const subCommand = await _findSubCommand(subCommands, defaultSubCommand);
358
+ if (!subCommand)
359
+ throw new CLIError(`Default sub command ${cyan(defaultSubCommand)} not found in subCommands.`, "E_UNKNOWN_COMMAND");
360
+ await runCommand(subCommand, { rawArgs: opts.rawArgs });
361
+ } else if (!cmd.run)
362
+ throw new CLIError(`No command specified.`, "E_NO_COMMAND");
363
+ }
364
+ }
365
+ if (typeof cmd.run === "function")
366
+ result = await cmd.run(context);
367
+ } catch (error) {
368
+ runError = error;
369
+ }
370
+ const cleanupErrors = [];
371
+ if (typeof cmd.cleanup === "function")
372
+ try {
373
+ await cmd.cleanup(context);
374
+ } catch (error) {
375
+ cleanupErrors.push(error);
376
+ }
377
+ for (const plugin of [...plugins].reverse())
378
+ try {
379
+ await plugin.cleanup?.(context);
380
+ } catch (error) {
381
+ cleanupErrors.push(error);
382
+ }
383
+ if (runError)
384
+ throw runError;
385
+ if (cleanupErrors.length === 1)
386
+ throw cleanupErrors[0];
387
+ if (cleanupErrors.length > 1)
388
+ throw new Error("Multiple cleanup errors", { cause: cleanupErrors });
389
+ return { result };
390
+ }
391
+ async function resolveSubCommand(cmd, rawArgs, parent) {
392
+ const subCommands = await resolveValue(cmd.subCommands);
393
+ if (subCommands && Object.keys(subCommands).length > 0) {
394
+ const subCommandArgIndex = findSubCommandIndex(rawArgs, await resolveValue(cmd.args || {}));
395
+ const subCommandName = rawArgs[subCommandArgIndex];
396
+ const subCommand = await _findSubCommand(subCommands, subCommandName);
397
+ if (subCommand)
398
+ return resolveSubCommand(subCommand, rawArgs.slice(subCommandArgIndex + 1), cmd);
399
+ }
400
+ return [cmd, parent];
401
+ }
402
+ async function _findSubCommand(subCommands, name) {
403
+ if (name in subCommands)
404
+ return resolveValue(subCommands[name]);
405
+ for (const sub of Object.values(subCommands)) {
406
+ const resolved = await resolveValue(sub);
407
+ const meta = await resolveValue(resolved?.meta);
408
+ if (meta?.alias) {
409
+ if (toArray(meta.alias).includes(name))
410
+ return resolved;
411
+ }
412
+ }
413
+ }
414
+ function findSubCommandIndex(rawArgs, argsDef) {
415
+ for (let i = 0;i < rawArgs.length; i++) {
416
+ const arg = rawArgs[i];
417
+ if (arg === "--")
418
+ return -1;
419
+ if (arg.startsWith("-")) {
420
+ if (!arg.includes("=") && _isValueFlag(arg, argsDef))
421
+ i++;
422
+ continue;
423
+ }
424
+ return i;
425
+ }
426
+ return -1;
427
+ }
428
+ function _isValueFlag(flag, argsDef) {
429
+ const name = flag.replace(/^-{1,2}/, "");
430
+ const normalized = camelCase(name);
431
+ for (const [key, def] of Object.entries(argsDef)) {
432
+ if (def.type !== "string" && def.type !== "enum")
433
+ continue;
434
+ if (normalized === camelCase(key))
435
+ return true;
436
+ if ((Array.isArray(def.alias) ? def.alias : def.alias ? [def.alias] : []).includes(name))
437
+ return true;
438
+ }
439
+ return false;
440
+ }
441
+ async function showUsage(cmd, parent) {
442
+ try {
443
+ console.log(await renderUsage(cmd, parent) + `
444
+ `);
445
+ } catch (error) {
446
+ console.error(error);
447
+ }
448
+ }
449
+ async function renderUsage(cmd, parent) {
450
+ const cmdMeta = await resolveValue(cmd.meta || {});
451
+ const cmdArgs = resolveArgs(await resolveValue(cmd.args || {}));
452
+ const parentMeta = await resolveValue(parent?.meta || {});
453
+ const commandName = `${parentMeta.name ? `${parentMeta.name} ` : ""}` + (cmdMeta.name || process.argv[1]);
454
+ const argLines = [];
455
+ const posLines = [];
456
+ const commandsLines = [];
457
+ const usageLine = [];
458
+ for (const arg of cmdArgs)
459
+ if (arg.type === "positional") {
460
+ const name = arg.name.toUpperCase();
461
+ const isRequired = arg.required !== false && arg.default === undefined;
462
+ posLines.push([cyan(name + renderValueHint(arg)), renderDescription(arg, isRequired)]);
463
+ usageLine.push(isRequired ? `<${name}>` : `[${name}]`);
464
+ } else {
465
+ const isRequired = arg.required === true && arg.default === undefined;
466
+ const argStr = [...(arg.alias || []).map((a) => `-${a}`), `--${arg.name}`].join(", ") + renderValueHint(arg);
467
+ argLines.push([cyan(argStr), renderDescription(arg, isRequired)]);
468
+ if (arg.type === "boolean" && (arg.default === true || arg.negativeDescription) && !negativePrefixRe.test(arg.name)) {
469
+ const negativeArgStr = [...(arg.alias || []).map((a) => `--no-${a}`), `--no-${arg.name}`].join(", ");
470
+ argLines.push([cyan(negativeArgStr), [arg.negativeDescription, isRequired ? gray("(Required)") : ""].filter(Boolean).join(" ")]);
471
+ }
472
+ if (isRequired)
473
+ usageLine.push(`--${arg.name}` + renderValueHint(arg));
474
+ }
475
+ if (cmd.subCommands) {
476
+ const commandNames = [];
477
+ const subCommands = await resolveValue(cmd.subCommands);
478
+ for (const [name, sub] of Object.entries(subCommands)) {
479
+ const meta = await resolveValue((await resolveValue(sub))?.meta);
480
+ if (meta?.hidden)
481
+ continue;
482
+ const aliases = toArray(meta?.alias);
483
+ const label = [name, ...aliases].join(", ");
484
+ commandsLines.push([cyan(label), meta?.description || ""]);
485
+ commandNames.push(name, ...aliases);
486
+ }
487
+ usageLine.push(commandNames.join("|"));
488
+ }
489
+ const usageLines = [];
490
+ const version = cmdMeta.version || parentMeta.version;
491
+ usageLines.push(gray(`${cmdMeta.description} (${commandName + (version ? ` v${version}` : "")})`), "");
492
+ const hasOptions = argLines.length > 0 || posLines.length > 0;
493
+ usageLines.push(`${underline(bold("USAGE"))} ${cyan(`${commandName}${hasOptions ? " [OPTIONS]" : ""} ${usageLine.join(" ")}`)}`, "");
494
+ if (posLines.length > 0) {
495
+ usageLines.push(underline(bold("ARGUMENTS")), "");
496
+ usageLines.push(formatLineColumns(posLines, " "));
497
+ usageLines.push("");
498
+ }
499
+ if (argLines.length > 0) {
500
+ usageLines.push(underline(bold("OPTIONS")), "");
501
+ usageLines.push(formatLineColumns(argLines, " "));
502
+ usageLines.push("");
503
+ }
504
+ if (commandsLines.length > 0) {
505
+ usageLines.push(underline(bold("COMMANDS")), "");
506
+ usageLines.push(formatLineColumns(commandsLines, " "));
507
+ usageLines.push("", `Use ${cyan(`${commandName} <command> --help`)} for more information about a command.`);
508
+ }
509
+ return usageLines.filter((l) => typeof l === "string").join(`
510
+ `);
511
+ }
512
+ function renderValueHint(arg) {
513
+ const valueHint = arg.valueHint ? `=<${arg.valueHint}>` : "";
514
+ const fallbackValueHint = valueHint || `=<${snakeCase(arg.name)}>`;
515
+ if (!arg.type || arg.type === "positional" || arg.type === "boolean")
516
+ return valueHint;
517
+ if (arg.type === "enum" && arg.options?.length)
518
+ return `=<${arg.options.join("|")}>`;
519
+ return fallbackValueHint;
520
+ }
521
+ function renderDescription(arg, required) {
522
+ const requiredHint = required ? gray("(Required)") : "";
523
+ const defaultHint = arg.default === undefined ? "" : gray(`(Default: ${arg.default})`);
524
+ return [
525
+ arg.description,
526
+ requiredHint,
527
+ defaultHint
528
+ ].filter(Boolean).join(" ");
529
+ }
530
+ async function runMain(cmd, opts = {}) {
531
+ const rawArgs = opts.rawArgs || process.argv.slice(2);
532
+ const showUsage$1 = opts.showUsage || showUsage;
533
+ try {
534
+ const builtinFlags = await _resolveBuiltinFlags(cmd);
535
+ if (builtinFlags.help.length > 0 && rawArgs.some((arg) => builtinFlags.help.includes(arg))) {
536
+ await showUsage$1(...await resolveSubCommand(cmd, rawArgs));
537
+ process.exit(0);
538
+ } else if (rawArgs.length === 1 && builtinFlags.version.includes(rawArgs[0])) {
539
+ const meta = typeof cmd.meta === "function" ? await cmd.meta() : await cmd.meta;
540
+ if (!meta?.version)
541
+ throw new CLIError("No version specified", "E_NO_VERSION");
542
+ console.log(meta.version);
543
+ } else
544
+ await runCommand(cmd, { rawArgs });
545
+ } catch (error) {
546
+ if (error instanceof CLIError) {
547
+ await showUsage$1(...await resolveSubCommand(cmd, rawArgs));
548
+ console.error(error.message);
549
+ } else
550
+ console.error(error, `
551
+ `);
552
+ process.exit(1);
553
+ }
554
+ }
555
+ async function _resolveBuiltinFlags(cmd) {
556
+ const argsDef = await resolveValue(cmd.args || {});
557
+ const userNames = /* @__PURE__ */ new Set;
558
+ const userAliases = /* @__PURE__ */ new Set;
559
+ for (const [name, def] of Object.entries(argsDef)) {
560
+ userNames.add(name);
561
+ for (const alias of toArray(def.alias))
562
+ userAliases.add(alias);
563
+ }
564
+ return {
565
+ help: _getBuiltinFlags("help", "h", userNames, userAliases),
566
+ version: _getBuiltinFlags("version", "v", userNames, userAliases)
567
+ };
568
+ }
569
+ function _getBuiltinFlags(long, short, userNames, userAliases) {
570
+ if (userNames.has(long) || userAliases.has(long))
571
+ return [];
572
+ if (userNames.has(short) || userAliases.has(short))
573
+ return [`--${long}`];
574
+ return [`--${long}`, `-${short}`];
575
+ }
576
+ var CLIError, noColor, _c = (c, r = 39) => (t) => noColor ? t : `\x1B[${c}m${t}\x1B[${r}m`, bold, cyan, gray, underline, negativePrefixRe;
577
+ var init_dist = __esm(() => {
578
+ init_scule();
579
+ CLIError = class extends Error {
580
+ code;
581
+ constructor(message, code) {
582
+ super(message);
583
+ this.name = "CLIError";
584
+ this.code = code;
585
+ }
586
+ };
587
+ noColor = /* @__PURE__ */ (() => {
588
+ const env = globalThis.process?.env ?? {};
589
+ return env.NO_COLOR === "1" || env.TERM === "dumb" || env.TEST || env.CI;
590
+ })();
591
+ bold = /* @__PURE__ */ _c(1, 22);
592
+ cyan = /* @__PURE__ */ _c(36);
593
+ gray = /* @__PURE__ */ _c(90);
594
+ underline = /* @__PURE__ */ _c(4, 24);
595
+ negativePrefixRe = /^no[-A-Z]/;
596
+ });
597
+
598
+ // node_modules/.bun/picocolors@1.1.1/node_modules/picocolors/picocolors.js
599
+ var require_picocolors = __commonJS((exports, module) => {
600
+ var p = process || {};
601
+ var argv = p.argv || [];
602
+ var env = p.env || {};
603
+ var isColorSupported = !(!!env.NO_COLOR || argv.includes("--no-color")) && (!!env.FORCE_COLOR || argv.includes("--color") || p.platform === "win32" || (p.stdout || {}).isTTY && env.TERM !== "dumb" || !!env.CI);
604
+ var formatter = (open, close, replace = open) => (input) => {
605
+ let string = "" + input, index = string.indexOf(close, open.length);
606
+ return ~index ? open + replaceClose(string, close, replace, index) + close : open + string + close;
607
+ };
608
+ var replaceClose = (string, close, replace, index) => {
609
+ let result = "", cursor = 0;
610
+ do {
611
+ result += string.substring(cursor, index) + replace;
612
+ cursor = index + close.length;
613
+ index = string.indexOf(close, cursor);
614
+ } while (~index);
615
+ return result + string.substring(cursor);
616
+ };
617
+ var createColors = (enabled = isColorSupported) => {
618
+ let f = enabled ? formatter : () => String;
619
+ return {
620
+ isColorSupported: enabled,
621
+ reset: f("\x1B[0m", "\x1B[0m"),
622
+ bold: f("\x1B[1m", "\x1B[22m", "\x1B[22m\x1B[1m"),
623
+ dim: f("\x1B[2m", "\x1B[22m", "\x1B[22m\x1B[2m"),
624
+ italic: f("\x1B[3m", "\x1B[23m"),
625
+ underline: f("\x1B[4m", "\x1B[24m"),
626
+ inverse: f("\x1B[7m", "\x1B[27m"),
627
+ hidden: f("\x1B[8m", "\x1B[28m"),
628
+ strikethrough: f("\x1B[9m", "\x1B[29m"),
629
+ black: f("\x1B[30m", "\x1B[39m"),
630
+ red: f("\x1B[31m", "\x1B[39m"),
631
+ green: f("\x1B[32m", "\x1B[39m"),
632
+ yellow: f("\x1B[33m", "\x1B[39m"),
633
+ blue: f("\x1B[34m", "\x1B[39m"),
634
+ magenta: f("\x1B[35m", "\x1B[39m"),
635
+ cyan: f("\x1B[36m", "\x1B[39m"),
636
+ white: f("\x1B[37m", "\x1B[39m"),
637
+ gray: f("\x1B[90m", "\x1B[39m"),
638
+ bgBlack: f("\x1B[40m", "\x1B[49m"),
639
+ bgRed: f("\x1B[41m", "\x1B[49m"),
640
+ bgGreen: f("\x1B[42m", "\x1B[49m"),
641
+ bgYellow: f("\x1B[43m", "\x1B[49m"),
642
+ bgBlue: f("\x1B[44m", "\x1B[49m"),
643
+ bgMagenta: f("\x1B[45m", "\x1B[49m"),
644
+ bgCyan: f("\x1B[46m", "\x1B[49m"),
645
+ bgWhite: f("\x1B[47m", "\x1B[49m"),
646
+ blackBright: f("\x1B[90m", "\x1B[39m"),
647
+ redBright: f("\x1B[91m", "\x1B[39m"),
648
+ greenBright: f("\x1B[92m", "\x1B[39m"),
649
+ yellowBright: f("\x1B[93m", "\x1B[39m"),
650
+ blueBright: f("\x1B[94m", "\x1B[39m"),
651
+ magentaBright: f("\x1B[95m", "\x1B[39m"),
652
+ cyanBright: f("\x1B[96m", "\x1B[39m"),
653
+ whiteBright: f("\x1B[97m", "\x1B[39m"),
654
+ bgBlackBright: f("\x1B[100m", "\x1B[49m"),
655
+ bgRedBright: f("\x1B[101m", "\x1B[49m"),
656
+ bgGreenBright: f("\x1B[102m", "\x1B[49m"),
657
+ bgYellowBright: f("\x1B[103m", "\x1B[49m"),
658
+ bgBlueBright: f("\x1B[104m", "\x1B[49m"),
659
+ bgMagentaBright: f("\x1B[105m", "\x1B[49m"),
660
+ bgCyanBright: f("\x1B[106m", "\x1B[49m"),
661
+ bgWhiteBright: f("\x1B[107m", "\x1B[49m")
662
+ };
663
+ };
664
+ module.exports = createColors();
665
+ module.exports.createColors = createColors;
666
+ });
667
+
668
+ // src/cli/out.ts
669
+ function write(s) {
670
+ process.stderr.write(s.endsWith(`
671
+ `) ? s : s + `
672
+ `);
673
+ }
674
+ var import_picocolors, ui;
675
+ var init_out = __esm(() => {
676
+ import_picocolors = __toESM(require_picocolors(), 1);
677
+ ui = {
678
+ write,
679
+ info: (s) => write(s),
680
+ dim: (s) => write(import_picocolors.default.dim(import_picocolors.default.gray(s))),
681
+ blank: () => write(""),
682
+ heading: (s) => write(`
683
+ ${import_picocolors.default.bold(import_picocolors.default.white(s))}
684
+ `),
685
+ success: (s) => write(` ${import_picocolors.default.green("\u2713")} ${import_picocolors.default.white(s)}`),
686
+ warn: (s) => write(` ${import_picocolors.default.yellow("\u26A0")} ${import_picocolors.default.white(s)}`),
687
+ fail: (s) => write(`${import_picocolors.default.red("\u2717")} ${import_picocolors.default.white(s)}`),
688
+ pass: (s) => write(` ${import_picocolors.default.green("\u2713")} ${import_picocolors.default.white(s)}`),
689
+ failItem: (s) => write(` ${import_picocolors.default.red("\u2717")} ${import_picocolors.default.white(s)}`),
690
+ warnItem: (s) => write(` ${import_picocolors.default.yellow("\u26A0")} ${import_picocolors.default.white(s)}`)
691
+ };
692
+ });
693
+
694
+ // src/core/frontmatter.ts
695
+ var {YAML } = globalThis.Bun;
696
+ function parseFrontmatter(raw) {
697
+ const match = raw.match(FRONTMATTER_RE);
698
+ if (!match) {
699
+ return { data: {}, content: raw };
700
+ }
701
+ const data = YAML.parse(match[1]);
702
+ return { data: data ?? {}, content: match[2] };
703
+ }
704
+ var FRONTMATTER_RE;
705
+ var init_frontmatter = __esm(() => {
706
+ FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
707
+ });
708
+
709
+ // src/core/skill-validate.ts
710
+ function checkFrontmatterPresence(model, _ctx) {
711
+ const keys = Object.keys(model.data);
712
+ if (keys.length === 0) {
713
+ return { warnings: ["YAML frontmatter is empty (description recommended for discoverability)"] };
714
+ }
715
+ return { passes: ["YAML frontmatter present and parseable"] };
716
+ }
717
+ function checkName(model, _ctx) {
718
+ if (!model.data.name) {
719
+ return { warnings: ['No "name" in frontmatter \u2014 directory name provides the /command (name is optional except for plugin-root skills)'] };
720
+ }
721
+ const name = String(model.data.name);
722
+ if (!NAME_REGEX.test(name)) {
723
+ return { errors: [`Invalid name format: "${name}" \u2014 should be kebab-case (a-z, 0-9, hyphens) for best compatibility`] };
724
+ }
725
+ if (name.length < 2 || name.length > 64) {
726
+ return { errors: [`Name length out of range: ${name.length} chars (recommended 2-64)`] };
727
+ }
728
+ return { passes: [`name: "${name}"`] };
729
+ }
730
+ function checkDescription(model, _ctx) {
731
+ if (!model.data.description) {
732
+ return { warnings: ['Missing "description" (recommended) \u2014 helps Claude decide when to load the skill automatically'] };
733
+ }
734
+ return { passes: ["description field present"] };
735
+ }
736
+ function checkBody(model, _ctx) {
737
+ if (!model.content.trim()) {
738
+ return { errors: ["Markdown body is empty"] };
739
+ }
740
+ return { passes: ["Markdown body is non-empty"] };
741
+ }
742
+ function checkAdvancedFields(model, _ctx) {
743
+ const advanced = Object.keys(model.data).filter((k) => KNOWN_FIELDS.has(k) && k !== "name" && k !== "description");
744
+ if (advanced.length > 0) {
745
+ return { passes: [`advanced frontmatter: ${advanced.join(", ")}`] };
746
+ }
747
+ return {};
748
+ }
749
+ function checkUnknownFields(model, _ctx) {
750
+ const warnings = Object.keys(model.data).filter((k) => !KNOWN_FIELDS.has(k)).map((k) => `Unknown frontmatter field: "${k}" (may be a typo or newer spec addition)`);
751
+ return { warnings };
752
+ }
753
+ function checkSupportingDirs(_model, ctx) {
754
+ const passes = SUPPORTING_DIRS.filter((dir) => ctx.existingDirs.includes(dir)).map((dir) => `${dir}/ directory exists`);
755
+ return { passes };
756
+ }
757
+ function checkDynamicInjection(model, _ctx) {
758
+ const passes = [];
759
+ if (/!\s*`[^`]+`/.test(model.content) || /```\s*!/.test(model.content)) {
760
+ passes.push("uses dynamic context injection (!`...` or ```! blocks)");
761
+ }
762
+ if (/\$ARGUMENTS|\$[0-9]|\$\{CLAUDE_/.test(model.content)) {
763
+ passes.push("uses argument / session substitutions ($ARGUMENTS, $0, ${CLAUDE_*})");
764
+ }
765
+ return { passes };
766
+ }
767
+ function validateSkillModel(model, context = { existingDirs: [] }) {
768
+ return checks.reduce((acc, check) => merge(acc, check(model, context)), EMPTY);
769
+ }
770
+ var merge = (a, b) => ({
771
+ errors: [...a.errors, ...b.errors ?? []],
772
+ warnings: [...a.warnings, ...b.warnings ?? []],
773
+ passes: [...a.passes, ...b.passes ?? []]
774
+ }), NAME_REGEX, KNOWN_FIELDS, SUPPORTING_DIRS, EMPTY, checks;
775
+ var init_skill_validate = __esm(() => {
776
+ NAME_REGEX = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
777
+ KNOWN_FIELDS = new Set([
778
+ "name",
779
+ "description",
780
+ "when_to_use",
781
+ "argument-hint",
782
+ "arguments",
783
+ "disable-model-invocation",
784
+ "user-invocable",
785
+ "allowed-tools",
786
+ "disallowed-tools",
787
+ "model",
788
+ "effort",
789
+ "context",
790
+ "agent",
791
+ "hooks",
792
+ "paths",
793
+ "shell"
794
+ ]);
795
+ SUPPORTING_DIRS = ["references", "scripts", "assets", "examples"];
796
+ EMPTY = { errors: [], warnings: [], passes: [] };
797
+ checks = [
798
+ checkFrontmatterPresence,
799
+ checkName,
800
+ checkDescription,
801
+ checkBody,
802
+ checkAdvancedFields,
803
+ checkUnknownFields,
804
+ checkSupportingDirs,
805
+ checkDynamicInjection
806
+ ];
807
+ });
808
+
809
+ // src/cli/commands/validate.ts
810
+ var exports_validate = {};
811
+ __export(exports_validate, {
812
+ default: () => validate_default
813
+ });
814
+ import { existsSync } from "fs";
815
+ import { resolve } from "path";
816
+ var import_picocolors2, OPTIONAL_DIRS, validate_default;
817
+ var init_validate = __esm(() => {
818
+ init_dist();
819
+ init_out();
820
+ init_frontmatter();
821
+ init_skill_validate();
822
+ import_picocolors2 = __toESM(require_picocolors(), 1);
823
+ OPTIONAL_DIRS = ["references", "scripts", "assets"];
824
+ validate_default = defineCommand({
825
+ meta: {
826
+ name: "validate",
827
+ description: "Validate structure and schema of a skill or plugin"
828
+ },
829
+ args: {
830
+ path: {
831
+ type: "positional",
832
+ description: "Path to skill directory or plugin root",
833
+ required: true
834
+ },
835
+ for: {
836
+ type: "string",
837
+ description: 'Target a provider ("claude") or specific validator ("claude:skill")'
838
+ },
839
+ format: {
840
+ type: "string",
841
+ alias: "f",
842
+ description: "Output format (json or table)",
843
+ default: "table"
844
+ },
845
+ verbose: {
846
+ type: "boolean",
847
+ alias: "v",
848
+ description: "Show detailed diagnostics",
849
+ default: false
850
+ },
851
+ ci: {
852
+ type: "boolean",
853
+ description: "Machine-friendly output, non-zero exit on issues",
854
+ default: false
855
+ }
856
+ },
857
+ async run({ args }) {
858
+ const targetPath = args.path;
859
+ const fullPath = resolve(targetPath);
860
+ if (!existsSync(fullPath)) {
861
+ ui.fail(`Path not found: ${targetPath}
862
+
863
+ Check that the path is correct and the directory exists.`);
864
+ process.exit(1);
865
+ }
866
+ const skillMd = resolve(fullPath, "SKILL.md");
867
+ if (!existsSync(skillMd)) {
868
+ ui.fail(`No skill or plugin found at ${targetPath}
869
+
870
+ Searched for:
871
+ \u2022 SKILL.md (Agent Skills spec)
872
+ \u2022 .claude-plugin/plugin.json (Claude Code plugin)
873
+
874
+ Try:
875
+ \u2022 Check the path points to a skill or plugin directory
876
+ \u2022 Use --for to target a specific validator`);
877
+ process.exit(1);
878
+ }
879
+ const raw = await Bun.file(skillMd).text();
880
+ let parsed;
881
+ try {
882
+ parsed = parseFrontmatter(raw);
883
+ } catch {
884
+ ui.fail(`Failed to parse YAML frontmatter in SKILL.md
885
+
886
+ Fix the YAML syntax and retry.`);
887
+ process.exit(1);
888
+ }
889
+ const existingDirs = OPTIONAL_DIRS.filter((dir) => existsSync(resolve(fullPath, dir)));
890
+ const { errors, warnings, passes } = validateSkillModel(parsed, {
891
+ existingDirs: [...existingDirs]
892
+ });
893
+ if (args.format === "json") {
894
+ const result = { path: targetPath, errors, warnings, passes };
895
+ console.log(JSON.stringify(result, null, 2));
896
+ } else {
897
+ ui.heading("dora skill validate \u2014 Structural validation");
898
+ ui.info(` Path: ${targetPath}
899
+ `);
900
+ for (const p of passes) {
901
+ ui.pass(p);
902
+ }
903
+ for (const w of warnings) {
904
+ ui.warnItem(w);
905
+ }
906
+ for (const e of errors) {
907
+ ui.failItem(e);
908
+ }
909
+ if (errors.length === 0 && warnings.length === 0) {
910
+ ui.write(`
911
+ ${import_picocolors2.default.green("\u2713")} ${import_picocolors2.default.white("All checks passed.")}
912
+ `);
913
+ } else {
914
+ ui.info(`
915
+ Result: ${errors.length} error(s), ${warnings.length} warning(s)
916
+ `);
917
+ }
918
+ }
919
+ if (errors.length > 0) {
920
+ process.exit(1);
921
+ }
922
+ process.exit(0);
923
+ }
924
+ });
925
+ });
926
+
927
+ // src/core/skill-drift.ts
928
+ function checkTrigger(input) {
929
+ const hasTriggers = input.description.includes("use when") || input.description.includes("Use when") || input.description.includes("trigger") || input.description.includes("invoke");
930
+ return {
931
+ drifted: !hasTriggers,
932
+ category: "Trigger",
933
+ detail: hasTriggers ? "Description includes activation phrases" : 'No trigger phrases found \u2014 add "Use when..." to description'
934
+ };
935
+ }
936
+ function checkStructure(input) {
937
+ const hasSteps = /^\s*\d+\.\s/m.test(input.content) || /^\s*[-*]\s/m.test(input.content);
938
+ return {
939
+ drifted: !hasSteps,
940
+ category: "Structure",
941
+ detail: hasSteps ? "Has step-by-step instructions" : "No ordered steps or checklists \u2014 agent needs a clear sequence to follow"
942
+ };
943
+ }
944
+ function checkVoice(input) {
945
+ const hasImperative = /\b(Create|Add|Run|Install|Configure|Set|Build|Use|Check|Verify|Ensure)\b/.test(input.content);
946
+ return {
947
+ drifted: !hasImperative,
948
+ category: "Voice",
949
+ detail: hasImperative ? 'Uses imperative voice ("Do X" not "You might X")' : "Passive or suggestive phrasing \u2014 use direct imperatives"
950
+ };
951
+ }
952
+ function checkExample(input) {
953
+ const hasCode = input.content.includes("```");
954
+ return {
955
+ drifted: !hasCode,
956
+ category: "Example",
957
+ detail: hasCode ? "Has code examples" : "No code blocks found \u2014 add examples if the skill involves code"
958
+ };
959
+ }
960
+ function checkGuardrail(input) {
961
+ const hasConstraints = /\bMUST\b/.test(input.content) || /\bMUST NOT\b/.test(input.content);
962
+ return {
963
+ drifted: !hasConstraints,
964
+ category: "Guardrail",
965
+ detail: hasConstraints ? "Has MUST/MUST NOT constraints" : "No explicit constraints \u2014 add MUST / MUST NOT guardrails"
966
+ };
967
+ }
968
+ function checkClarity(input) {
969
+ const ambiguous = input.content.match(/\b(maybe|possibly|consider|you might want to|perhaps)\b/gi);
970
+ const drifted = !!ambiguous && ambiguous.length > 0;
971
+ return {
972
+ drifted,
973
+ category: "Clarity",
974
+ detail: drifted ? `Ambiguous phrasing detected: ${ambiguous.slice(0, 3).join(", ")}` : "No ambiguous language found"
975
+ };
976
+ }
977
+ function analyzeDrift(input) {
978
+ const drifts = checks2.map((check) => check(input));
979
+ return { drifts, driftCount: drifts.filter((d) => d.drifted).length, total: drifts.length };
980
+ }
981
+ var checks2;
982
+ var init_skill_drift = __esm(() => {
983
+ checks2 = [
984
+ checkTrigger,
985
+ checkStructure,
986
+ checkVoice,
987
+ checkExample,
988
+ checkGuardrail,
989
+ checkClarity
990
+ ];
991
+ });
992
+
993
+ // src/cli/commands/drift.ts
994
+ var exports_drift = {};
995
+ __export(exports_drift, {
996
+ default: () => drift_default
997
+ });
998
+ import { existsSync as existsSync2 } from "fs";
999
+ import { resolve as resolve2 } from "path";
1000
+ var import_picocolors3, drift_default;
1001
+ var init_drift = __esm(() => {
1002
+ init_dist();
1003
+ init_out();
1004
+ init_frontmatter();
1005
+ init_skill_drift();
1006
+ import_picocolors3 = __toESM(require_picocolors(), 1);
1007
+ drift_default = defineCommand({
1008
+ meta: {
1009
+ name: "drift",
1010
+ description: "Measure how far a skill has drifted from rubric standards"
1011
+ },
1012
+ args: {
1013
+ path: {
1014
+ type: "positional",
1015
+ description: "Path to skill directory or plugin root",
1016
+ required: true
1017
+ },
1018
+ for: {
1019
+ type: "string",
1020
+ description: 'Target a provider ("claude") or specific validator ("claude:skill")'
1021
+ },
1022
+ format: {
1023
+ type: "string",
1024
+ alias: "f",
1025
+ description: "Output format (json or table)",
1026
+ default: "table"
1027
+ },
1028
+ verbose: {
1029
+ type: "boolean",
1030
+ alias: "v",
1031
+ description: "Show detailed diagnostics",
1032
+ default: false
1033
+ },
1034
+ ci: {
1035
+ type: "boolean",
1036
+ description: "Machine-friendly output, non-zero exit on issues",
1037
+ default: false
1038
+ }
1039
+ },
1040
+ async run({ args }) {
1041
+ const targetPath = args.path;
1042
+ const fullPath = resolve2(targetPath);
1043
+ const skillMd = resolve2(fullPath, "SKILL.md");
1044
+ if (!existsSync2(skillMd)) {
1045
+ ui.fail(`No SKILL.md found at ${targetPath}
1046
+
1047
+ Check that the path points to a skill directory containing SKILL.md.`);
1048
+ process.exit(1);
1049
+ }
1050
+ const raw = await Bun.file(skillMd).text();
1051
+ let parsed;
1052
+ try {
1053
+ parsed = parseFrontmatter(raw);
1054
+ } catch {
1055
+ ui.fail("Failed to parse YAML frontmatter in SKILL.md");
1056
+ process.exit(1);
1057
+ }
1058
+ const desc = String(parsed.data.description || "");
1059
+ const when = String(parsed.data.when_to_use || "");
1060
+ const { drifts, driftCount, total } = analyzeDrift({
1061
+ description: (desc + " " + when).trim(),
1062
+ content: parsed.content
1063
+ });
1064
+ if (args.format === "json") {
1065
+ console.log(JSON.stringify({ path: targetPath, driftCount, total, drifts }, null, 2));
1066
+ } else {
1067
+ ui.heading("dora skill drift \u2014 Measuring rubric drift");
1068
+ ui.info(` Path: ${targetPath}
1069
+ `);
1070
+ for (const d of drifts) {
1071
+ const icon = d.drifted ? import_picocolors3.default.yellow("\u2197") : import_picocolors3.default.green("\xB7");
1072
+ const cat = d.drifted ? import_picocolors3.default.yellow(d.category.padEnd(10)) : import_picocolors3.default.dim(d.category.padEnd(10));
1073
+ ui.write(` ${icon} ${cat} ${import_picocolors3.default.white(d.detail)}`);
1074
+ }
1075
+ if (driftCount === 0) {
1076
+ ui.write(`
1077
+ ${import_picocolors3.default.green("No drift detected.")} ${import_picocolors3.default.white("Skill aligns with rubric standards.")}
1078
+ `);
1079
+ } else {
1080
+ ui.write(`
1081
+ ${import_picocolors3.default.yellow(`${driftCount}/${total}`)} ${import_picocolors3.default.white("rubric areas have drifted.")}
1082
+ `);
1083
+ }
1084
+ }
1085
+ if (args.ci && driftCount > 0) {
1086
+ process.exit(1);
1087
+ }
1088
+ process.exit(0);
1089
+ }
1090
+ });
1091
+ });
1092
+
1093
+ // src/cli/commands/judge.ts
1094
+ var exports_judge = {};
1095
+ __export(exports_judge, {
1096
+ default: () => judge_default
1097
+ });
1098
+ var judge_default;
1099
+ var init_judge = __esm(() => {
1100
+ init_dist();
1101
+ init_out();
1102
+ judge_default = defineCommand({
1103
+ meta: {
1104
+ name: "judge",
1105
+ description: "AI-driven qualitative assessment of a skill"
1106
+ },
1107
+ args: {
1108
+ path: {
1109
+ type: "positional",
1110
+ description: "Path to skill directory",
1111
+ required: true
1112
+ },
1113
+ for: {
1114
+ type: "string",
1115
+ description: 'Target a provider ("claude") or specific validator ("claude:skill")'
1116
+ },
1117
+ format: {
1118
+ type: "string",
1119
+ alias: "f",
1120
+ description: "Output format (json or table)",
1121
+ default: "table"
1122
+ },
1123
+ verbose: {
1124
+ type: "boolean",
1125
+ alias: "v",
1126
+ description: "Show detailed diagnostics",
1127
+ default: false
1128
+ }
1129
+ },
1130
+ async run({ args }) {
1131
+ ui.heading("doraval skill judge \u2014 AI-driven assessment");
1132
+ ui.info(` Path: ${args.path}
1133
+ `);
1134
+ ui.warn(`Not yet implemented. This command will send the skill to an LLM for qualitative review (clarity, completeness, effectiveness).
1135
+ `);
1136
+ process.exit(2);
1137
+ }
1138
+ });
1139
+ });
1140
+
1141
+ // src/core/journal-config.ts
1142
+ import { existsSync as existsSync3, mkdirSync } from "fs";
1143
+ import { homedir } from "os";
1144
+ import { join } from "path";
1145
+ var {YAML: YAML2 } = globalThis.Bun;
1146
+ function getDoravalDir() {
1147
+ return process.env.DORAVAL_HOME ?? join(homedir(), ".doraval");
1148
+ }
1149
+ function getConfigPath() {
1150
+ return join(getDoravalDir(), "config.yml");
1151
+ }
1152
+ function getJournalsDir() {
1153
+ return join(getDoravalDir(), "journals");
1154
+ }
1155
+ function getPendingDir() {
1156
+ return join(getDoravalDir(), "pending");
1157
+ }
1158
+ function getPendingProjectDir(project) {
1159
+ return join(getPendingDir(), project);
1160
+ }
1161
+ function ensureDoravalDirs() {
1162
+ const base = getDoravalDir();
1163
+ for (const dir of [base, getJournalsDir(), getPendingDir()]) {
1164
+ if (!existsSync3(dir)) {
1165
+ mkdirSync(dir, { recursive: true });
1166
+ }
1167
+ }
1168
+ }
1169
+ async function readConfig() {
1170
+ const path = getConfigPath();
1171
+ if (!existsSync3(path))
1172
+ return null;
1173
+ const raw = await Bun.file(path).text();
1174
+ return YAML2.parse(raw);
1175
+ }
1176
+ async function writeConfig(config) {
1177
+ ensureDoravalDirs();
1178
+ const raw = serializeConfig(config);
1179
+ await Bun.write(getConfigPath(), raw);
1180
+ }
1181
+ function serializeConfig(config) {
1182
+ return YAML2.stringify(config);
1183
+ }
1184
+ function resolveProjectName(config) {
1185
+ if (!config)
1186
+ return null;
1187
+ const cwd = process.cwd();
1188
+ const base = cwd.split("/").pop() ?? "";
1189
+ if (config.journal.projects[base]) {
1190
+ try {
1191
+ return sanitizeProjectName(base);
1192
+ } catch {
1193
+ return null;
1194
+ }
1195
+ }
1196
+ return null;
1197
+ }
1198
+ function sanitizeProjectName(name) {
1199
+ if (!name || typeof name !== "string") {
1200
+ throw new Error("Project name must be a non-empty string");
1201
+ }
1202
+ let sanitized = name.toLowerCase().replace(/[^a-z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^[-_]+|[-_]+$/g, "").slice(0, 64);
1203
+ if (!sanitized || sanitized.includes("..")) {
1204
+ throw new Error(`Invalid or unsafe project name: "${name}"`);
1205
+ }
1206
+ return sanitized;
1207
+ }
1208
+ var init_journal_config = () => {};
1209
+
1210
+ // src/core/journal-remote.ts
1211
+ var {spawnSync } = globalThis.Bun;
1212
+ function hasGhCli() {
1213
+ const result = spawnSync(["gh", "--version"], {
1214
+ stdout: "pipe",
1215
+ stderr: "pipe"
1216
+ });
1217
+ return result.exitCode === 0;
1218
+ }
1219
+ function ensureGhCliOrExit() {
1220
+ if (hasGhCli())
1221
+ return;
1222
+ ui.write(` ${import_picocolors4.default.red("\u2717")} ${import_picocolors4.default.white("The GitHub CLI (")}${import_picocolors4.default.bold("gh")}${import_picocolors4.default.white(") is not installed.")}
1223
+ `);
1224
+ ui.info(` doraval uses ${import_picocolors4.default.bold("gh")} to fetch and sync journal files with GitHub.
1225
+ `);
1226
+ ui.info(` Install it:
1227
+ `);
1228
+ ui.info(` macOS: ${import_picocolors4.default.dim("brew install gh")}`);
1229
+ ui.info(` Linux: ${import_picocolors4.default.dim("https://github.com/cli/cli/blob/trunk/docs/install_linux.md")}`);
1230
+ ui.info(` Windows: ${import_picocolors4.default.dim("winget install --id GitHub.cli")}
1231
+ `);
1232
+ ui.info(` Then authenticate: ${import_picocolors4.default.dim("gh auth login")}
1233
+ `);
1234
+ process.exit(1);
1235
+ }
1236
+ function fetchRemoteJournalFile(repo, path) {
1237
+ const result = spawnSync(["gh", "api", `repos/${repo}/contents/${path}`, "--jq", "{sha, content, encoding}"], { stdout: "pipe", stderr: "pipe" });
1238
+ if (result.exitCode !== 0) {
1239
+ const stderr = result.stderr.toString();
1240
+ if (stderr.includes("404") || stderr.includes("Not Found")) {
1241
+ return null;
1242
+ }
1243
+ ui.fail(`Failed to fetch ${path} from ${repo}:`);
1244
+ ui.info(stderr);
1245
+ process.exit(1);
1246
+ }
1247
+ try {
1248
+ const parsed = JSON.parse(result.stdout.toString());
1249
+ let decoded;
1250
+ if (!parsed.encoding || parsed.encoding === "base64") {
1251
+ decoded = Buffer.from(parsed.content, "base64").toString("utf-8");
1252
+ } else {
1253
+ decoded = parsed.content;
1254
+ }
1255
+ return {
1256
+ content: decoded,
1257
+ sha: parsed.sha
1258
+ };
1259
+ } catch {
1260
+ ui.fail(`Unexpected response when fetching ${path} from ${repo}`);
1261
+ process.exit(1);
1262
+ }
1263
+ }
1264
+ async function refreshLocalJournalFile(repo, remotePath, localPath) {
1265
+ const remote = fetchRemoteJournalFile(repo, remotePath);
1266
+ if (!remote) {
1267
+ return false;
1268
+ }
1269
+ await Bun.write(localPath, remote.content);
1270
+ return true;
1271
+ }
1272
+ function getRemoteJournalFileMeta(repo, path) {
1273
+ const result = spawnSync(["gh", "api", `repos/${repo}/contents/${path}`, "--jq", "{sha, content, encoding}"], { stdout: "pipe", stderr: "pipe" });
1274
+ if (result.exitCode !== 0) {
1275
+ const stderr = result.stderr.toString();
1276
+ if (stderr.includes("404") || stderr.includes("Not Found")) {
1277
+ return null;
1278
+ }
1279
+ ui.fail(`Failed to fetch ${path} from ${repo}:`);
1280
+ ui.info(stderr);
1281
+ process.exit(1);
1282
+ }
1283
+ try {
1284
+ return JSON.parse(result.stdout.toString());
1285
+ } catch {
1286
+ ui.fail(`Unexpected response when fetching ${path} from ${repo}`);
1287
+ process.exit(1);
1288
+ }
1289
+ }
1290
+ function getGitRemoteOwner() {
1291
+ const result = spawnSync(["git", "config", "--get", "remote.origin.url"], {
1292
+ stdout: "pipe",
1293
+ stderr: "pipe"
1294
+ });
1295
+ if (result.exitCode !== 0)
1296
+ return null;
1297
+ const url = result.stdout.toString().trim();
1298
+ if (!url)
1299
+ return null;
1300
+ const match = url.match(/[:/]([^/]+)\/([^/.]+)(\.git)?$/);
1301
+ return match ? match[1] : null;
1302
+ }
1303
+ function ghUser() {
1304
+ const result = spawnSync(["gh", "api", "user", "--jq", ".login"], {
1305
+ stdout: "pipe",
1306
+ stderr: "pipe"
1307
+ });
1308
+ if (result.exitCode !== 0)
1309
+ return null;
1310
+ return result.stdout.toString().trim() || null;
1311
+ }
1312
+ function repoExists(repo) {
1313
+ const result = spawnSync(["gh", "api", `repos/${repo}`, "--jq", ".full_name"], { stdout: "pipe", stderr: "pipe" });
1314
+ return result.exitCode === 0 && result.stdout.toString().trim().length > 0;
1315
+ }
1316
+ var import_picocolors4;
1317
+ var init_journal_remote = __esm(() => {
1318
+ init_out();
1319
+ import_picocolors4 = __toESM(require_picocolors(), 1);
1320
+ });
1321
+
1322
+ // src/cli/prompt.ts
1323
+ function prompt(label, fallback) {
1324
+ process.stderr.write(`${label} ${import_picocolors5.default.dim(`(${fallback})`)} `);
1325
+ const buf = new Uint8Array(1024);
1326
+ const n = __require("fs").readSync(0, buf);
1327
+ const input = new TextDecoder().decode(buf.subarray(0, n)).trim();
1328
+ return input || fallback;
1329
+ }
1330
+ var import_picocolors5;
1331
+ var init_prompt = __esm(() => {
1332
+ import_picocolors5 = __toESM(require_picocolors(), 1);
1333
+ });
1334
+
1335
+ // src/cli/commands/journal/init.ts
1336
+ var exports_init = {};
1337
+ __export(exports_init, {
1338
+ default: () => init_default
1339
+ });
1340
+ import { basename, join as join2 } from "path";
1341
+ var import_picocolors6, init_default;
1342
+ var init_init = __esm(() => {
1343
+ init_dist();
1344
+ init_out();
1345
+ init_journal_config();
1346
+ init_journal_remote();
1347
+ init_prompt();
1348
+ import_picocolors6 = __toESM(require_picocolors(), 1);
1349
+ init_default = defineCommand({
1350
+ meta: {
1351
+ name: "init",
1352
+ description: "Register a project and link it to your journal repo"
1353
+ },
1354
+ args: {
1355
+ repo: {
1356
+ type: "string",
1357
+ alias: "r",
1358
+ description: "Journal repo (owner/name). Smart default from git remote or gh account. Env: DORAVAL_JOURNAL_REPO"
1359
+ },
1360
+ project: {
1361
+ type: "string",
1362
+ alias: "p",
1363
+ description: "Project name (default: basename of current directory)"
1364
+ },
1365
+ refresh: {
1366
+ type: "boolean",
1367
+ description: "Re-fetch journal files even if the project is already registered",
1368
+ default: false
1369
+ }
1370
+ },
1371
+ async run({ args }) {
1372
+ ui.write(`
1373
+ ${import_picocolors6.default.bold(import_picocolors6.default.white("dora journal init"))} (or top-level ${import_picocolors6.default.dim(import_picocolors6.default.gray("dora init"))}) \u2014 Set up your journal
1374
+ `);
1375
+ ensureGhCliOrExit();
1376
+ let repo = args.repo || process.env.DORAVAL_JOURNAL_REPO;
1377
+ if (!repo) {
1378
+ const gitOwner = getGitRemoteOwner();
1379
+ const ghLogin = ghUser();
1380
+ let defaultRepo;
1381
+ let sourceNote = "";
1382
+ if (gitOwner) {
1383
+ defaultRepo = `${gitOwner}/${gitOwner}.md`;
1384
+ if (ghLogin && ghLogin !== gitOwner) {
1385
+ sourceNote = ` ${import_picocolors6.default.dim("(from git remote; your active gh account is " + ghLogin + ")")}
1386
+ `;
1387
+ } else {
1388
+ sourceNote = ` ${import_picocolors6.default.dim("(from git remote)")}
1389
+ `;
1390
+ }
1391
+ } else if (ghLogin) {
1392
+ defaultRepo = `${ghLogin}/${ghLogin}.md`;
1393
+ sourceNote = ` ${import_picocolors6.default.dim("(from your active gh account)")}
1394
+ `;
1395
+ } else {
1396
+ ui.write(` ${import_picocolors6.default.yellow("\u26A0")} Not logged in to GitHub. Run ${import_picocolors6.default.dim("gh auth login")} first.
1397
+ `);
1398
+ process.exit(1);
1399
+ }
1400
+ const existingConfig = await readConfig();
1401
+ if (existingConfig?.journal.repo) {
1402
+ defaultRepo = existingConfig.journal.repo;
1403
+ sourceNote = ` ${import_picocolors6.default.dim("(from your previous journal setup)")}
1404
+ `;
1405
+ }
1406
+ ui.write(` Journal repo ${import_picocolors6.default.dim(import_picocolors6.default.gray("(owner/name)"))}`);
1407
+ if (sourceNote)
1408
+ ui.write(sourceNote);
1409
+ repo = prompt(" >", defaultRepo);
1410
+ }
1411
+ let project = args.project || process.env.DORAVAL_PROJECT;
1412
+ if (!project) {
1413
+ const defaultProject = basename(process.cwd());
1414
+ project = prompt(" Project name", defaultProject);
1415
+ }
1416
+ project = sanitizeProjectName(project);
1417
+ if (!repoExists(repo)) {
1418
+ ui.write(` ${import_picocolors6.default.red("\u2717")} Repository ${import_picocolors6.default.bold(import_picocolors6.default.white(repo))} not found on GitHub.
1419
+ `);
1420
+ ui.write(` Create it first:
1421
+ `);
1422
+ ui.write(` ${import_picocolors6.default.dim(`gh repo create ${repo} --private --description "Personal journal for agent decisions"`)}
1423
+ `);
1424
+ ui.write(` The repo should be private. doraval will populate it on first ${import_picocolors6.default.dim("dora journal sync")}.
1425
+ `);
1426
+ process.exit(1);
1427
+ }
1428
+ const existing = await readConfig();
1429
+ const alreadyRegistered = existing?.journal.projects[project];
1430
+ const isRefresh = alreadyRegistered && args.refresh;
1431
+ if (alreadyRegistered && !isRefresh) {
1432
+ ui.write(` ${import_picocolors6.default.yellow("\u26A0")} Project ${import_picocolors6.default.bold(import_picocolors6.default.white(project))} is already registered.
1433
+ `);
1434
+ ui.write(` Repo: ${import_picocolors6.default.gray(existing.journal.repo)}`);
1435
+ ui.write(` Remote: ${existing.journal.projects[project].remote_path}
1436
+ `);
1437
+ ui.write(` To refresh local files, run: ${import_picocolors6.default.dim(import_picocolors6.default.gray(`dora journal update`))}
1438
+ ` + ` (init --refresh still works for compatibility.)
1439
+ ` + ` Or remove the project from ${import_picocolors6.default.dim(import_picocolors6.default.gray("~/.doraval/config.yml"))} to fully re-initialize.
1440
+ `);
1441
+ process.exit(0);
1442
+ }
1443
+ const journalsDir = getJournalsDir();
1444
+ const remotePath = `projects/${project}.md`;
1445
+ const localPath = join2(journalsDir, `${project}.md`);
1446
+ const effectiveRepo = isRefresh && !args.repo ? existing.journal.repo : repo;
1447
+ const config = existing ?? {
1448
+ journal: { repo: effectiveRepo, projects: {} }
1449
+ };
1450
+ config.journal.repo = effectiveRepo;
1451
+ config.journal.projects[project] = {
1452
+ remote_path: remotePath,
1453
+ local_path: localPath
1454
+ };
1455
+ ensureDoravalDirs();
1456
+ const actionLabel = isRefresh ? "Refreshing" : "Fetching";
1457
+ ui.write(` ${import_picocolors6.default.dim(import_picocolors6.default.gray(`${actionLabel} journal files from`))} ${import_picocolors6.default.gray(effectiveRepo)}${import_picocolors6.default.dim(import_picocolors6.default.gray("..."))}
1458
+ `);
1459
+ const globalDest = join2(journalsDir, "global.md");
1460
+ const wroteGlobal = await refreshLocalJournalFile(effectiveRepo, "global.md", globalDest);
1461
+ if (wroteGlobal) {
1462
+ ui.write(` ${import_picocolors6.default.green("\u2713")} global.md`);
1463
+ } else {
1464
+ ui.write(` ${import_picocolors6.default.dim("\xB7")} global.md ${import_picocolors6.default.dim("(not found \u2014 will be created on first sync)")}`);
1465
+ await Bun.write(globalDest, `# Global Journal
1466
+
1467
+ Cross-project principles.
1468
+ `);
1469
+ }
1470
+ const wroteProject = await refreshLocalJournalFile(effectiveRepo, remotePath, localPath);
1471
+ if (wroteProject) {
1472
+ ui.write(` ${import_picocolors6.default.green("\u2713")} ${remotePath}`);
1473
+ } else {
1474
+ ui.write(` ${import_picocolors6.default.dim("\xB7")} ${remotePath} ${import_picocolors6.default.dim("(not found \u2014 will be created on first sync)")}`);
1475
+ await Bun.write(localPath, `# ${project} Journal
1476
+
1477
+ Project-specific decisions.
1478
+ `);
1479
+ }
1480
+ await writeConfig(config);
1481
+ ui.write(`
1482
+ ${import_picocolors6.default.green("\u2713")} Project ${import_picocolors6.default.bold(import_picocolors6.default.white(project))} registered to ${import_picocolors6.default.bold(import_picocolors6.default.white(repo))}.
1483
+ `);
1484
+ ui.write(` Config: ${import_picocolors6.default.dim(import_picocolors6.default.gray("~/.doraval/config.yml"))}`);
1485
+ ui.write(` Journals: ${import_picocolors6.default.dim(import_picocolors6.default.gray("~/.doraval/journals/"))}`);
1486
+ ui.write(` Pending: ${import_picocolors6.default.dim(import_picocolors6.default.gray("~/.doraval/pending/"))}
1487
+ `);
1488
+ ui.write(` Use ${import_picocolors6.default.dim(import_picocolors6.default.gray("dora journal add"))} to propose decisions and ${import_picocolors6.default.dim(import_picocolors6.default.gray("dora journal list"))} to view them.
1489
+ `);
1490
+ process.exit(0);
1491
+ }
1492
+ });
1493
+ });
1494
+
1495
+ // src/core/journal-parse.ts
1496
+ var {YAML: YAML3 } = globalThis.Bun;
1497
+ function parseJournalEntries(raw) {
1498
+ const { entries } = parseJournalEntriesWithWarnings(raw);
1499
+ return entries;
1500
+ }
1501
+ function parseJournalEntriesWithWarnings(raw) {
1502
+ const entries = [];
1503
+ const warnings = [];
1504
+ if (!raw || !raw.trim()) {
1505
+ return { entries, warnings };
1506
+ }
1507
+ const sectionRegex = /^##\s+(.+)$/gm;
1508
+ const matches = Array.from(raw.matchAll(sectionRegex));
1509
+ if (matches.length === 0) {
1510
+ return { entries, warnings };
1511
+ }
1512
+ for (let i = 0;i < matches.length; i++) {
1513
+ const match = matches[i];
1514
+ const title = match[1].trim();
1515
+ const start = match.index + match[0].length;
1516
+ const end = i + 1 < matches.length ? matches[i + 1].index : raw.length;
1517
+ const sectionBody = raw.slice(start, end).trim();
1518
+ const yamlFenceMatch = sectionBody.match(/```(?:ya?ml)?\s*\n([\s\S]*?)\n```/);
1519
+ if (!yamlFenceMatch) {
1520
+ warnings.push(`Entry "${title}" has no YAML metadata block`);
1521
+ continue;
1522
+ }
1523
+ const yamlContent = yamlFenceMatch[1];
1524
+ let meta = {};
1525
+ try {
1526
+ const parsed = YAML3.parse(yamlContent);
1527
+ if (parsed && typeof parsed === "object") {
1528
+ meta = parsed;
1529
+ }
1530
+ } catch (err) {
1531
+ warnings.push(`Entry "${title}" has invalid YAML: ${err.message}`);
1532
+ continue;
1533
+ }
1534
+ const yamlBlockEnd = sectionBody.indexOf(yamlFenceMatch[0]) + yamlFenceMatch[0].length;
1535
+ const rationale = sectionBody.slice(yamlBlockEnd).trim();
1536
+ const pushback = Number(meta.pushback);
1537
+ const tags = Array.isArray(meta.tags) ? meta.tags : Array.isArray(meta.scope) ? meta.scope : [];
1538
+ const author = typeof meta.author === "string" ? meta.author : "human";
1539
+ const date = typeof meta.date === "string" ? meta.date : "";
1540
+ const status = meta.status || "active";
1541
+ const superseded_by = typeof meta.superseded_by === "string" ? meta.superseded_by : undefined;
1542
+ entries.push({
1543
+ title,
1544
+ pushback: isNaN(pushback) ? 0 : pushback,
1545
+ tags,
1546
+ author,
1547
+ date,
1548
+ status,
1549
+ superseded_by,
1550
+ rationale
1551
+ });
1552
+ }
1553
+ return { entries, warnings };
1554
+ }
1555
+ var init_journal_parse = () => {};
1556
+
1557
+ // src/cli/commands/journal/list.ts
1558
+ var exports_list = {};
1559
+ __export(exports_list, {
1560
+ default: () => list_default
1561
+ });
1562
+ import { existsSync as existsSync4, readdirSync } from "fs";
1563
+ import { join as join3 } from "path";
1564
+ var import_picocolors7, list_default;
1565
+ var init_list = __esm(() => {
1566
+ init_dist();
1567
+ init_out();
1568
+ init_journal_config();
1569
+ init_journal_parse();
1570
+ import_picocolors7 = __toESM(require_picocolors(), 1);
1571
+ list_default = defineCommand({
1572
+ meta: {
1573
+ name: "list",
1574
+ description: "List active journal entries for the current project"
1575
+ },
1576
+ args: {
1577
+ project: {
1578
+ type: "string",
1579
+ alias: "p",
1580
+ description: "Project name (defaults to directory-based mapping)"
1581
+ },
1582
+ all: {
1583
+ type: "boolean",
1584
+ description: "Include non-active entries (superseded/retired)",
1585
+ default: false
1586
+ },
1587
+ format: {
1588
+ type: "string",
1589
+ alias: "f",
1590
+ description: "Output format (table or json)",
1591
+ default: "table"
1592
+ }
1593
+ },
1594
+ async run({ args }) {
1595
+ const config = await readConfig();
1596
+ let project = args.project;
1597
+ if (!project) {
1598
+ project = resolveProjectName(config) ?? undefined;
1599
+ }
1600
+ if (project) {
1601
+ project = sanitizeProjectName(project);
1602
+ }
1603
+ if (!project) {
1604
+ ui.write(`${import_picocolors7.default.yellow("\u26A0")} ${import_picocolors7.default.yellow("No project mapping found.")}
1605
+
1606
+ ` + `Run ${import_picocolors7.default.dim(import_picocolors7.default.gray("dora init"))} (or ${import_picocolors7.default.dim(import_picocolors7.default.gray("doraval journal init"))}) first, or pass ${import_picocolors7.default.dim(import_picocolors7.default.gray("--project <name>"))}.`);
1607
+ process.exit(1);
1608
+ }
1609
+ const journalRepo = config?.journal.repo ?? "(unknown)";
1610
+ const journalsDir = getJournalsDir();
1611
+ const projectFile = join3(journalsDir, `${project}.md`);
1612
+ const globalFile = join3(journalsDir, "global.md");
1613
+ let raw = "";
1614
+ try {
1615
+ raw = await Bun.file(projectFile).text();
1616
+ } catch {
1617
+ raw = "";
1618
+ }
1619
+ let allEntries = parseJournalEntries(raw);
1620
+ if (!args.all) {
1621
+ allEntries = allEntries.filter((e) => e.status === "active");
1622
+ }
1623
+ const staged = [];
1624
+ try {
1625
+ const pdir = getPendingProjectDir(project);
1626
+ if (existsSync4(pdir)) {
1627
+ const files = readdirSync(pdir).filter((f) => f.endsWith(".md") && f !== ".gitkeep");
1628
+ for (const f of files) {
1629
+ const txt = await Bun.file(join3(pdir, f)).text();
1630
+ const parsed = parseJournalEntries(txt);
1631
+ for (const e of parsed) {
1632
+ e._staged = true;
1633
+ staged.push(e);
1634
+ }
1635
+ }
1636
+ }
1637
+ } catch {}
1638
+ if (args.format === "json") {
1639
+ console.log(JSON.stringify({ project, entries: [...staged, ...allEntries] }, null, 2));
1640
+ return;
1641
+ }
1642
+ ui.write(`
1643
+ ${import_picocolors7.default.bold(import_picocolors7.default.white("dora journal list"))} \u2014 ${import_picocolors7.default.white(project)} ${import_picocolors7.default.dim(import_picocolors7.default.gray(`(from ${journalRepo})`))}
1644
+ `);
1645
+ const hasStaged = staged.length > 0;
1646
+ const hasCommitted = allEntries.length > 0;
1647
+ const seen = new Set;
1648
+ const dups = [];
1649
+ for (const e of [...staged, ...allEntries]) {
1650
+ if (seen.has(e.title))
1651
+ dups.push(e.title);
1652
+ else
1653
+ seen.add(e.title);
1654
+ }
1655
+ if (dups.length > 0) {
1656
+ const uniqueDups = [...new Set(dups)];
1657
+ ui.write(` ${import_picocolors7.default.yellow("\u26A0")} ${import_picocolors7.default.yellow("Duplicate titles in this view (clean in your journal repo + update):")} ${uniqueDups.map((t) => import_picocolors7.default.yellow(`"${t}"`)).join(", ")}
1658
+ `);
1659
+ }
1660
+ if (!hasStaged && !hasCommitted) {
1661
+ ui.write(` ${import_picocolors7.default.dim(import_picocolors7.default.gray("No active entries found for"))} ${import_picocolors7.default.bold(import_picocolors7.default.white(project))}.
1662
+ `);
1663
+ ui.write(` Journal repo: ${import_picocolors7.default.dim(import_picocolors7.default.gray(journalRepo))}`);
1664
+ ui.write(` Local file: ${import_picocolors7.default.dim(import_picocolors7.default.gray(projectFile))}
1665
+ `);
1666
+ ui.write(` ${import_picocolors7.default.dim(import_picocolors7.default.gray("This is normal for a freshly initialized project."))}
1667
+ ` + ` Use ${import_picocolors7.default.dim(import_picocolors7.default.gray("dora journal add"))} to propose decisions.
1668
+ ` + ` They will be staged locally until you run ${import_picocolors7.default.dim(import_picocolors7.default.gray("dora journal sync"))}.
1669
+ `);
1670
+ ui.write(` If you expect content, try: ${import_picocolors7.default.dim(import_picocolors7.default.gray(`dora journal update`))}
1671
+ `);
1672
+ return;
1673
+ }
1674
+ function printEntry(entry) {
1675
+ const pb = entry.pushback ?? 0;
1676
+ let pbColor = import_picocolors7.default.green;
1677
+ if (pb >= 7)
1678
+ pbColor = import_picocolors7.default.red;
1679
+ else if (pb >= 4)
1680
+ pbColor = import_picocolors7.default.yellow;
1681
+ const tagsStr = (entry.tags || []).join(", ") || import_picocolors7.default.dim("(none)");
1682
+ const statusNote = entry.status !== "active" ? import_picocolors7.default.dim(` [${entry.status}]`) : "";
1683
+ const stagedNote = entry._staged ? import_picocolors7.default.dim(" (staged)") : "";
1684
+ ui.write(` ${pbColor(String(pb).padStart(2))} ${import_picocolors7.default.bold(import_picocolors7.default.white(entry.title))}${statusNote}${stagedNote}`);
1685
+ ui.write(` ${import_picocolors7.default.dim(import_picocolors7.default.gray("tags:"))} ${import_picocolors7.default.gray(tagsStr)}`);
1686
+ const by = entry.author?.startsWith("agent:") ? import_picocolors7.default.cyan(entry.author) : entry.author || "human";
1687
+ ui.write(` ${import_picocolors7.default.dim(import_picocolors7.default.gray("by:"))} ${import_picocolors7.default.gray(by)} ${import_picocolors7.default.dim(import_picocolors7.default.gray("on"))} ${import_picocolors7.default.gray(entry.date)}`);
1688
+ const rat = (entry.rationale || "").replace(/\s+/g, " ").trim();
1689
+ if (rat) {
1690
+ const preview = rat.length > 88 ? rat.slice(0, 85) + import_picocolors7.default.dim(import_picocolors7.default.gray("\u2026")) : rat;
1691
+ ui.write(` ${import_picocolors7.default.dim(import_picocolors7.default.gray(preview))}`);
1692
+ }
1693
+ ui.write("");
1694
+ }
1695
+ if (hasStaged) {
1696
+ ui.write(` ${import_picocolors7.default.yellow("\u25CF")} ${import_picocolors7.default.bold(import_picocolors7.default.white("Staged / pending"))} (not yet in remote; run ${import_picocolors7.default.dim(import_picocolors7.default.gray("dora journal sync"))} to publish):
1697
+ `);
1698
+ for (const entry of staged) {
1699
+ printEntry(entry);
1700
+ }
1701
+ if (hasCommitted)
1702
+ ui.write("");
1703
+ }
1704
+ if (hasCommitted) {
1705
+ if (hasStaged) {
1706
+ ui.write(` ${import_picocolors7.default.dim(import_picocolors7.default.gray("Committed (from local cache):"))}
1707
+ `);
1708
+ }
1709
+ for (const entry of allEntries) {
1710
+ printEntry(entry);
1711
+ }
1712
+ }
1713
+ const totalShown = staged.length + allEntries.length;
1714
+ ui.write(` ${import_picocolors7.default.dim(import_picocolors7.default.gray(`${totalShown} entries shown from ${journalRepo}.`))}
1715
+ `);
1716
+ process.exit(0);
1717
+ }
1718
+ });
1719
+ });
1720
+
1721
+ // src/cli/commands/journal/update.ts
1722
+ var exports_update = {};
1723
+ __export(exports_update, {
1724
+ default: () => update_default
1725
+ });
1726
+ import { existsSync as existsSync5 } from "fs";
1727
+ import { join as join4 } from "path";
1728
+ var import_picocolors8, update_default;
1729
+ var init_update = __esm(() => {
1730
+ init_dist();
1731
+ init_out();
1732
+ init_journal_config();
1733
+ init_journal_remote();
1734
+ import_picocolors8 = __toESM(require_picocolors(), 1);
1735
+ update_default = defineCommand({
1736
+ meta: {
1737
+ name: "update",
1738
+ description: "Refresh local journal cache from the remote GitHub repo"
1739
+ },
1740
+ args: {
1741
+ project: {
1742
+ type: "string",
1743
+ alias: "p",
1744
+ description: "Project name (defaults to directory-based mapping)"
1745
+ },
1746
+ all: {
1747
+ type: "boolean",
1748
+ description: "Refresh all registered projects (and global.md)",
1749
+ default: false
1750
+ }
1751
+ },
1752
+ async run({ args }) {
1753
+ ensureGhCliOrExit();
1754
+ const config = await readConfig();
1755
+ if (!config?.journal.repo) {
1756
+ ui.write(`${import_picocolors8.default.red("\u2717")} No journal repo configured. Run ${import_picocolors8.default.dim("dora init")} (or ${import_picocolors8.default.dim("doraval journal init")}) first.`);
1757
+ process.exit(1);
1758
+ }
1759
+ const journalRepo = config.journal.repo;
1760
+ ensureDoravalDirs();
1761
+ const journalsDir = getJournalsDir();
1762
+ ui.write(`
1763
+ ${import_picocolors8.default.bold(import_picocolors8.default.white("dora journal update"))} \u2014 ${import_picocolors8.default.dim(import_picocolors8.default.gray(journalRepo))}
1764
+ `);
1765
+ const projectsToUpdate = [];
1766
+ if (args.all) {
1767
+ for (const name of Object.keys(config.journal.projects)) {
1768
+ try {
1769
+ projectsToUpdate.push(sanitizeProjectName(name));
1770
+ } catch {}
1771
+ }
1772
+ } else {
1773
+ let project = args.project;
1774
+ if (!project) {
1775
+ project = resolveProjectName(config) ?? undefined;
1776
+ }
1777
+ if (project) {
1778
+ try {
1779
+ projectsToUpdate.push(sanitizeProjectName(project));
1780
+ } catch {
1781
+ ui.write(`${import_picocolors8.default.red("\u2717")} Invalid project name: ${project}`);
1782
+ process.exit(1);
1783
+ }
1784
+ }
1785
+ }
1786
+ const globalLocal = join4(journalsDir, "global.md");
1787
+ const gotGlobal = await refreshLocalJournalFile(journalRepo, "global.md", globalLocal);
1788
+ if (gotGlobal) {
1789
+ ui.write(` ${import_picocolors8.default.green("\u2713")} global.md`);
1790
+ } else {
1791
+ ui.write(` ${import_picocolors8.default.dim("\xB7")} global.md ${import_picocolors8.default.dim("(not present on remote)")}`);
1792
+ }
1793
+ if (projectsToUpdate.length === 0) {
1794
+ if (args.all) {
1795
+ ui.write(`
1796
+ ${import_picocolors8.default.dim(import_picocolors8.default.gray("No projects registered."))}
1797
+ `);
1798
+ } else {
1799
+ ui.write(`
1800
+ ${import_picocolors8.default.yellow("\u26A0")} No project mapping found.
1801
+ ` + ` Run ${import_picocolors8.default.dim("dora init")} or pass ${import_picocolors8.default.dim("--project <name>")} / ${import_picocolors8.default.dim("--all")}.
1802
+ `);
1803
+ }
1804
+ return;
1805
+ }
1806
+ for (const project of projectsToUpdate) {
1807
+ const remotePath = `projects/${project}.md`;
1808
+ const localPath = join4(journalsDir, `${project}.md`);
1809
+ const got = await refreshLocalJournalFile(journalRepo, remotePath, localPath);
1810
+ if (got) {
1811
+ ui.write(` ${import_picocolors8.default.green("\u2713")} ${remotePath}`);
1812
+ } else {
1813
+ ui.write(` ${import_picocolors8.default.dim("\xB7")} ${remotePath} ${import_picocolors8.default.dim("(not present on remote \u2014 will be created on first sync)")}`);
1814
+ if (!existsSync5(localPath)) {
1815
+ await Bun.write(localPath, `# ${project} Journal
1816
+
1817
+ Project-specific decisions.
1818
+ `);
1819
+ }
1820
+ }
1821
+ }
1822
+ const summary = args.all && projectsToUpdate.length > 1 ? `${projectsToUpdate.length} projects + global` : projectsToUpdate.length === 1 ? projectsToUpdate[0] : "journals";
1823
+ ui.write(`
1824
+ ${import_picocolors8.default.dim(import_picocolors8.default.gray("Local cache refreshed for"))} ${import_picocolors8.default.bold(import_picocolors8.default.white(summary))}.
1825
+ `);
1826
+ }
1827
+ });
1828
+ });
1829
+
1830
+ // src/core/journal-validate.ts
1831
+ function validateEntry(entry) {
1832
+ const errors = [];
1833
+ const warnings = [];
1834
+ if (entry.pushback === undefined || entry.pushback === null) {
1835
+ warnings.push("pushback not supplied (will use default 5 when staging via journal add)");
1836
+ } else {
1837
+ const pb = Number(entry.pushback);
1838
+ if (!Number.isInteger(pb) || pb < 1 || pb > 10) {
1839
+ errors.push("pushback must be an integer between 1 and 10");
1840
+ }
1841
+ }
1842
+ if (!entry.tags || !Array.isArray(entry.tags) || entry.tags.length === 0) {
1843
+ warnings.push("tags not supplied or empty (will use [] when staging via journal add; consider canonical tags)");
1844
+ } else {
1845
+ const invalidTags = entry.tags.filter((s) => !CANONICAL_TAGS.includes(s));
1846
+ if (invalidTags.length > 0) {
1847
+ warnings.push(`tags contains non-canonical values: ${invalidTags.join(", ")} (valid: ${CANONICAL_TAGS.join(", ")})`);
1848
+ }
1849
+ }
1850
+ if (!entry.author || typeof entry.author !== "string") {
1851
+ errors.push("author is required");
1852
+ } else if (!entry.author.startsWith("human") && !entry.author.startsWith("agent:")) {
1853
+ warnings.push(`author "${entry.author}" does not follow the recommended pattern (human or agent:<name>)`);
1854
+ }
1855
+ if (!entry.date || typeof entry.date !== "string") {
1856
+ errors.push("date is required");
1857
+ }
1858
+ if (!entry.status || !VALID_STATUSES.includes(entry.status)) {
1859
+ errors.push(`status must be one of: ${VALID_STATUSES.join(", ")}`);
1860
+ }
1861
+ if (!entry.title || typeof entry.title !== "string" || entry.title.trim() === "") {
1862
+ errors.push("title is required");
1863
+ }
1864
+ return {
1865
+ valid: errors.length === 0,
1866
+ errors,
1867
+ warnings
1868
+ };
1869
+ }
1870
+ var CANONICAL_TAGS, VALID_STATUSES;
1871
+ var init_journal_validate = __esm(() => {
1872
+ CANONICAL_TAGS = [
1873
+ "naming",
1874
+ "cli",
1875
+ "architecture",
1876
+ "testing",
1877
+ "ux",
1878
+ "api",
1879
+ "docs",
1880
+ "notes"
1881
+ ];
1882
+ VALID_STATUSES = ["active", "superseded", "retired"];
1883
+ });
1884
+
1885
+ // src/cli/commands/journal/add.ts
1886
+ var exports_add = {};
1887
+ __export(exports_add, {
1888
+ default: () => add_default,
1889
+ buildAgentArgv: () => buildAgentArgv
1890
+ });
1891
+ import { existsSync as existsSync6 } from "fs";
1892
+ import { join as join5 } from "path";
1893
+ var {spawnSync: spawnSync2 } = globalThis.Bun;
1894
+ function buildAgentArgv(template, promptText) {
1895
+ const marker = "__DORA_PROMPT__";
1896
+ const substituted = template.replace("{{prompt}}", marker);
1897
+ const rawParts = substituted.split(/\s+/).filter(Boolean);
1898
+ return rawParts.map((part) => {
1899
+ let cleaned = part;
1900
+ if (cleaned.startsWith('"') && cleaned.endsWith('"'))
1901
+ cleaned = cleaned.slice(1, -1);
1902
+ if (cleaned.startsWith("'") && cleaned.endsWith("'"))
1903
+ cleaned = cleaned.slice(1, -1);
1904
+ if (cleaned === marker) {
1905
+ return promptText;
1906
+ }
1907
+ return cleaned;
1908
+ });
1909
+ }
1910
+ async function invokeConfiguredAgentForEntry(decisionText, agentCfg) {
1911
+ if (!agentCfg || !agentCfg.command)
1912
+ return null;
1913
+ const scaffold = `Raw user capture (a decision, observation, or useful note that just happened): "${decisionText}"
1914
+
1915
+ Turn this into a clean journal entry. Infer the core decision or note even if the input is phrased as a todo or reminder. Be professional and concise.
1916
+
1917
+ **CRITICAL INSTRUCTIONS (follow exactly):**
1918
+ - Output *ONLY* a single valid JSON object. Nothing before it, nothing after it, no markdown fences, no explanations, no extra text.
1919
+ - The JSON must have exactly these keys (use the suggested values as starting point but improve them):
1920
+ {
1921
+ "title": "Short, scannable, professional title (past tense or present perfect, max ~80 chars)",
1922
+ "pushback": 4,
1923
+ "tags": ["cli", "ux"],
1924
+ "rationale": "2-5 sentences explaining context and implications (or the note content).",
1925
+ "author": "agent:claude-code"
1926
+ }
1927
+
1928
+ If you cannot produce exactly this, output the JSON with the best you can and set "author" to "agent:claude-code" anyway.`;
1929
+ const template = agentCfg.prompt_template || '-p "{{prompt}}" --output-format json';
1930
+ const extraArgs = buildAgentArgv(template, scaffold);
1931
+ const shortTemplate = (agentCfg.prompt_template || '-p "{{prompt}}" --output-format json').slice(0, 80);
1932
+ ui.write(` ${import_picocolors9.default.dim(`\u2192 ${agentCfg.command} ${shortTemplate}...`)}`);
1933
+ try {
1934
+ const result = spawnSync2([agentCfg.command, ...extraArgs], {
1935
+ stdout: "pipe",
1936
+ stderr: "pipe"
1937
+ });
1938
+ const stdout = result.stdout.toString().trim();
1939
+ const stderr = result.stderr.toString().trim();
1940
+ if (result.exitCode !== 0) {
1941
+ ui.write(` ${import_picocolors9.default.yellow("\u26A0")} Configured agent (${agentCfg.command}) exited with code ${result.exitCode}. Falling back to defaults.`);
1942
+ if (stderr)
1943
+ ui.write(` ${import_picocolors9.default.dim("stderr:")}
1944
+ ${stderr.slice(0, 800)}`);
1945
+ if (stdout)
1946
+ ui.write(` ${import_picocolors9.default.dim("stdout:")}
1947
+ ${stdout.slice(0, 400)}`);
1948
+ return null;
1949
+ }
1950
+ let candidates = [];
1951
+ let jsonMatch = stdout.match(/\{[\s\S]*\}/);
1952
+ if (jsonMatch) {
1953
+ try {
1954
+ candidates.push(JSON.parse(jsonMatch[0]));
1955
+ } catch {}
1956
+ }
1957
+ const allMatches = stdout.match(/\{[\s\S]*?\}(?=\s*(?:\{|$))/g) || [];
1958
+ for (const m of allMatches) {
1959
+ try {
1960
+ const p = JSON.parse(m);
1961
+ candidates.push(p);
1962
+ } catch {}
1963
+ }
1964
+ let parsed = null;
1965
+ for (const c of candidates) {
1966
+ if (c && typeof c === "object" && (c.title || c.rationale)) {
1967
+ parsed = c;
1968
+ break;
1969
+ }
1970
+ }
1971
+ if (!parsed) {
1972
+ for (const c of candidates) {
1973
+ if (c && typeof c === "object" && c.result) {
1974
+ let inner = c.result;
1975
+ if (typeof inner === "string") {
1976
+ try {
1977
+ inner = JSON.parse(inner);
1978
+ } catch {}
1979
+ }
1980
+ if (inner && typeof inner === "object" && (inner.title || inner.rationale)) {
1981
+ parsed = inner;
1982
+ break;
1983
+ }
1984
+ }
1985
+ }
1986
+ }
1987
+ if (!parsed) {
1988
+ parsed = candidates[0] || null;
1989
+ }
1990
+ if (!parsed || typeof parsed !== "object") {
1991
+ ui.write(` ${import_picocolors9.default.yellow("\u26A0")} Agent produced output but no usable JSON was found. Falling back.`);
1992
+ ui.write(` ${import_picocolors9.default.dim("stdout (first 700 chars):")}
1993
+ ${stdout.slice(0, 700)}`);
1994
+ if (stderr)
1995
+ ui.write(` ${import_picocolors9.default.dim("stderr:")}
1996
+ ${stderr.slice(0, 500)}`);
1997
+ return null;
1998
+ }
1999
+ if (!parsed.title && !parsed.rationale) {
2000
+ ui.write(` ${import_picocolors9.default.yellow("\u26A0")} Agent returned JSON without expected fields (title/rationale). Using defaults.`);
2001
+ ui.write(` ${import_picocolors9.default.dim("parsed keys:")} ${Object.keys(parsed).join(", ")}`);
2002
+ ui.write(` ${import_picocolors9.default.dim("stdout (truncated):")}
2003
+ ${stdout.slice(0, 600)}`);
2004
+ return null;
2005
+ }
2006
+ return parsed;
2007
+ } catch (e) {
2008
+ ui.write(` ${import_picocolors9.default.yellow("\u26A0")} Failed to invoke configured agent (${agentCfg.command}): ${e.message}. Using defaults.`);
2009
+ return null;
2010
+ }
2011
+ }
2012
+ function slugify(title) {
2013
+ return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "untitled";
2014
+ }
2015
+ var import_picocolors9, add_default;
2016
+ var init_add = __esm(() => {
2017
+ init_dist();
2018
+ init_out();
2019
+ init_journal_config();
2020
+ init_journal_validate();
2021
+ import_picocolors9 = __toESM(require_picocolors(), 1);
2022
+ add_default = defineCommand({
2023
+ meta: {
2024
+ name: "add",
2025
+ description: "Propose a new decision, note or principle (pushback & tags optional; agent can enrich on the fly)"
2026
+ },
2027
+ args: {
2028
+ title: {
2029
+ type: "positional",
2030
+ description: "Title of the decision or principle (the only argument needed for the low-friction path; other fields use defaults or the configured agent)",
2031
+ required: false
2032
+ },
2033
+ pushback: {
2034
+ type: "number",
2035
+ alias: "b",
2036
+ description: "Pushback intensity (1-10). Optional \u2014 defaults are applied (or supplied by --json / on-the-fly agent).",
2037
+ required: false
2038
+ },
2039
+ tags: {
2040
+ type: "string",
2041
+ alias: "t",
2042
+ description: "Comma-separated tags (e.g. naming,cli,architecture). Optional \u2014 defaults are applied (or supplied by --json / on-the-fly agent). Renamed from --scope for broader use with notes too.",
2043
+ required: false
2044
+ },
2045
+ scope: {
2046
+ type: "string",
2047
+ description: "(deprecated) Use --tags instead",
2048
+ required: false
2049
+ },
2050
+ author: {
2051
+ type: "string",
2052
+ alias: "a",
2053
+ description: 'Author (default: "human", or "agent:grok", etc.)',
2054
+ default: "human"
2055
+ },
2056
+ status: {
2057
+ type: "string",
2058
+ description: "Status (active | superseded | retired)",
2059
+ default: "active"
2060
+ },
2061
+ rationale: {
2062
+ type: "string",
2063
+ alias: "r",
2064
+ description: "Rationale / explanation (one line). For rich/multi-line or long markdown content use --raw-markdown <file-or-> (or --json for full structured entries)."
2065
+ },
2066
+ rawMarkdown: {
2067
+ type: "string",
2068
+ description: 'Path to a raw markdown file (or "-" for stdin) to use as the entry body after the YAML block. Accepts --raw-markdown or --rawMarkdown. Title can be positional or extracted from the first "# Heading". Bypasses agent. Great for long notes and rich docs.'
2069
+ },
2070
+ project: {
2071
+ type: "string",
2072
+ alias: "p",
2073
+ description: "Project name (defaults to directory mapping)"
2074
+ },
2075
+ json: {
2076
+ type: "string",
2077
+ alias: "j",
2078
+ description: 'Full entry as JSON (title, pushback, tags, rationale, ...). Use "-" to read from stdin. Highest precedence; bypasses other input methods. (JSON may still use "scope" for legacy compat.)'
2079
+ },
2080
+ verbose: {
2081
+ type: "boolean",
2082
+ description: "Show full entry details (pushback, tags, author, file) in the success output",
2083
+ required: false
2084
+ }
2085
+ },
2086
+ async run({ args }) {
2087
+ const config = await readConfig();
2088
+ let project = args.project;
2089
+ if (!project) {
2090
+ project = resolveProjectName(config) ?? undefined;
2091
+ }
2092
+ if (project) {
2093
+ project = sanitizeProjectName(project);
2094
+ }
2095
+ if (!project) {
2096
+ ui.write(`${import_picocolors9.default.yellow("\u26A0")} No project mapping found.
2097
+
2098
+ ` + `Run ${import_picocolors9.default.dim("dora init")} (or ${import_picocolors9.default.dim("doraval journal init")}) first, or pass ${import_picocolors9.default.dim("--project <name>")}.`);
2099
+ process.exit(1);
2100
+ }
2101
+ let title;
2102
+ let pushback;
2103
+ let tags = [];
2104
+ let author = args.author || "human";
2105
+ let status = args.status || "active";
2106
+ let rationale;
2107
+ let date = new Date().toISOString().split("T")[0];
2108
+ const jsonInput = args.json;
2109
+ if (jsonInput) {
2110
+ let rawJson = jsonInput;
2111
+ if (jsonInput === "-" || jsonInput === "") {
2112
+ const stdinText = await new Response(Bun.stdin.stream()).text();
2113
+ rawJson = stdinText.trim();
2114
+ }
2115
+ try {
2116
+ const parsed = JSON.parse(rawJson);
2117
+ title = parsed.title ? String(parsed.title).trim() : undefined;
2118
+ pushback = typeof parsed.pushback === "number" ? parsed.pushback : parsed.pushback ? Number(parsed.pushback) : undefined;
2119
+ if (Array.isArray(parsed.tags)) {
2120
+ tags = parsed.tags.map((s) => String(s).trim()).filter(Boolean);
2121
+ } else if (typeof parsed.tags === "string") {
2122
+ tags = parsed.tags.split(",").map((s) => s.trim()).filter(Boolean);
2123
+ } else if (Array.isArray(parsed.scope)) {
2124
+ tags = parsed.scope.map((s) => String(s).trim()).filter(Boolean);
2125
+ } else if (typeof parsed.scope === "string") {
2126
+ tags = parsed.scope.split(",").map((s) => s.trim()).filter(Boolean);
2127
+ }
2128
+ rationale = parsed.rationale ? String(parsed.rationale).trim() : undefined;
2129
+ if (parsed.author)
2130
+ author = String(parsed.author);
2131
+ if (parsed.status)
2132
+ status = parsed.status;
2133
+ if (parsed.date)
2134
+ date = String(parsed.date);
2135
+ } catch (e) {
2136
+ ui.write(`${import_picocolors9.default.red("\u2717")} Failed to parse --json input: ${e.message}`);
2137
+ process.exit(1);
2138
+ }
2139
+ }
2140
+ let rawBody;
2141
+ const rawMdArg = args.rawMarkdown;
2142
+ if (rawMdArg && !jsonInput) {
2143
+ if (rawMdArg === "-" || rawMdArg === "") {
2144
+ rawBody = (await new Response(Bun.stdin.stream()).text()).trim();
2145
+ } else if (existsSync6(rawMdArg)) {
2146
+ rawBody = (await Bun.file(rawMdArg).text()).trim();
2147
+ } else {
2148
+ rawBody = rawMdArg.trim();
2149
+ }
2150
+ }
2151
+ if (!title) {
2152
+ title = args.title?.trim() || "";
2153
+ }
2154
+ if (!title && rawBody) {
2155
+ const headingMatch = rawBody.match(/^#+\s+(.+?)(?:\r?\n|$)/m);
2156
+ if (headingMatch) {
2157
+ title = headingMatch[1].trim();
2158
+ rawBody = rawBody.replace(/^#+\s+(.+?)(?:\r?\n|$)/m, "").trimStart();
2159
+ } else {
2160
+ ui.write(`${import_picocolors9.default.red("\u2717")} --raw-markdown provided without a TITLE and without a leading '# Heading' in the markdown.`);
2161
+ process.exit(1);
2162
+ }
2163
+ }
2164
+ if (!title) {
2165
+ title = "Untitled decision";
2166
+ }
2167
+ if (pushback === undefined) {
2168
+ const cliPb = args.pushback;
2169
+ pushback = cliPb !== undefined ? Number(cliPb) : 5;
2170
+ }
2171
+ if (tags.length === 0) {
2172
+ let cliTagsStr = args.tags || args.scope;
2173
+ if (cliTagsStr != null) {
2174
+ if (typeof cliTagsStr !== "string")
2175
+ cliTagsStr = String(cliTagsStr);
2176
+ tags = cliTagsStr.split(",").map((s) => s.trim()).filter(Boolean);
2177
+ }
2178
+ }
2179
+ if (rawBody !== undefined && !args.pushback && !args.tags && !args.scope) {
2180
+ if (tags.length === 0)
2181
+ tags = ["notes"];
2182
+ if (pushback === 5)
2183
+ pushback = 1;
2184
+ }
2185
+ if (rawBody !== undefined) {
2186
+ rationale = rawBody;
2187
+ } else if (!rationale) {
2188
+ const cliRat = args.rationale?.trim();
2189
+ rationale = cliRat || title;
2190
+ }
2191
+ const cameFromExplicitJson = !!jsonInput;
2192
+ const isThinInput = !args.pushback && !args.tags && !args.scope && !args.rationale && !rawMdArg;
2193
+ let agentCfg = null;
2194
+ let attemptedAgent = false;
2195
+ if (!cameFromExplicitJson && isThinInput) {
2196
+ const fullConfigForAgent = await readConfig();
2197
+ agentCfg = fullConfigForAgent?.agent;
2198
+ if (agentCfg) {
2199
+ attemptedAgent = true;
2200
+ ui.write(` ${import_picocolors9.default.dim(import_picocolors9.default.gray("(querying your configured coding agent...)"))}`);
2201
+ const agentResult = await invokeConfiguredAgentForEntry(title, agentCfg);
2202
+ if (agentResult) {
2203
+ if (agentResult.title)
2204
+ title = String(agentResult.title).trim();
2205
+ if (typeof agentResult.pushback === "number")
2206
+ pushback = agentResult.pushback;
2207
+ if (Array.isArray(agentResult.tags)) {
2208
+ tags = agentResult.tags.map((s) => String(s).trim()).filter(Boolean);
2209
+ } else if (Array.isArray(agentResult.scope)) {
2210
+ tags = agentResult.scope.map((s) => String(s).trim()).filter(Boolean);
2211
+ }
2212
+ if (agentResult.rationale)
2213
+ rationale = String(agentResult.rationale).trim();
2214
+ if (agentResult.author)
2215
+ author = String(agentResult.author);
2216
+ if (agentResult.status)
2217
+ status = agentResult.status;
2218
+ if (agentResult.date)
2219
+ date = String(agentResult.date);
2220
+ }
2221
+ }
2222
+ }
2223
+ const entry = {
2224
+ title,
2225
+ pushback,
2226
+ tags,
2227
+ author,
2228
+ date,
2229
+ status
2230
+ };
2231
+ const validation = validateEntry(entry);
2232
+ if (!validation.valid) {
2233
+ ui.write(`${import_picocolors9.default.red("\u2717")} Invalid entry:
2234
+ `);
2235
+ for (const err of validation.errors) {
2236
+ ui.write(` ${import_picocolors9.default.red("\u2022")} ${err}`);
2237
+ }
2238
+ process.exit(1);
2239
+ }
2240
+ for (const warn of validation.warnings) {
2241
+ if ((warn.includes("not supplied") || warn.includes("empty")) && attemptedAgent) {} else if (warn.includes("not supplied") || warn.includes("empty")) {
2242
+ ui.write(`${import_picocolors9.default.dim("\xB7")} ${warn}`);
2243
+ } else {
2244
+ ui.write(`${import_picocolors9.default.yellow("\u26A0")} ${warn}`);
2245
+ }
2246
+ }
2247
+ if (!rationale) {
2248
+ rationale = title;
2249
+ }
2250
+ const content = `## ${title}
2251
+
2252
+ \`\`\`yaml
2253
+ pushback: ${pushback}
2254
+ tags: [${tags.join(", ")}]
2255
+ author: ${author}
2256
+ date: ${date}
2257
+ status: ${status}
2258
+ \`\`\`
2259
+
2260
+ ${rationale}
2261
+ `;
2262
+ ensureDoravalDirs();
2263
+ const pendingDir = getPendingProjectDir(project);
2264
+ if (!existsSync6(pendingDir)) {
2265
+ await Bun.write(join5(pendingDir, ".gitkeep"), "");
2266
+ }
2267
+ const slug = slugify(title);
2268
+ const filename = `${date}-${slug}.md`;
2269
+ const filePath = join5(pendingDir, filename);
2270
+ await Bun.write(filePath, content);
2271
+ ui.write(`
2272
+ ${import_picocolors9.default.green("\u2713")} ${import_picocolors9.default.bold(import_picocolors9.default.white(title))}`);
2273
+ ui.write(` Project: ${import_picocolors9.default.white(project)} \xB7 run ${import_picocolors9.default.dim(import_picocolors9.default.gray("dora journal sync"))} to publish
2274
+ `);
2275
+ if (args.verbose) {
2276
+ const authorDisplay = author.startsWith("agent:") ? import_picocolors9.default.cyan(author) : author;
2277
+ ui.write(` Pushback: ${import_picocolors9.default.white(String(pushback))}`);
2278
+ ui.write(` Tags: ${import_picocolors9.default.gray(tags.join(", ") || import_picocolors9.default.dim("(none)"))}`);
2279
+ ui.write(` Author: ${authorDisplay}`);
2280
+ ui.write(` File: ${import_picocolors9.default.dim(import_picocolors9.default.gray(filePath))}
2281
+ `);
2282
+ }
2283
+ if (isThinInput && !author.startsWith("agent:")) {
2284
+ if (attemptedAgent) {
2285
+ ui.write(` ${import_picocolors9.default.dim(import_picocolors9.default.gray("Note: agent was called but returned no usable enrichment. Edit the pending file or re-run dora init."))}
2286
+ `);
2287
+ } else {
2288
+ ui.write(` ${import_picocolors9.default.dim(import_picocolors9.default.gray("Tip: run dora init to configure an agent for auto-enrichment."))}
2289
+ `);
2290
+ }
2291
+ }
2292
+ process.exit(0);
2293
+ }
2294
+ });
2295
+ });
2296
+
2297
+ // src/cli/commands/journal/sync.ts
2298
+ var exports_sync = {};
2299
+ __export(exports_sync, {
2300
+ default: () => sync_default
2301
+ });
2302
+ import { readdirSync as readdirSync2, existsSync as existsSync7 } from "fs";
2303
+ import { join as join6 } from "path";
2304
+ var {spawnSync: spawnSync3 } = globalThis.Bun;
2305
+ function updateGitHubFile(repo, path, content, message, sha) {
2306
+ const payload = {
2307
+ message,
2308
+ content: Buffer.from(content, "utf8").toString("base64"),
2309
+ ...sha ? { sha } : {}
2310
+ };
2311
+ const args = [
2312
+ "gh",
2313
+ "api",
2314
+ "--method",
2315
+ "PUT",
2316
+ "-H",
2317
+ "Accept: application/vnd.github+json",
2318
+ `repos/${repo}/contents/${path}`,
2319
+ "-f",
2320
+ `message=${payload.message}`,
2321
+ "-f",
2322
+ `content=${payload.content}`
2323
+ ];
2324
+ if (sha) {
2325
+ args.push("-f", `sha=${sha}`);
2326
+ }
2327
+ const result = spawnSync3(args, {
2328
+ stdout: "pipe",
2329
+ stderr: "pipe"
2330
+ });
2331
+ if (result.exitCode !== 0) {
2332
+ ui.write(import_picocolors10.default.red(`Failed to update ${path} on ${repo}:`));
2333
+ ui.write(result.stderr.toString());
2334
+ process.exit(1);
2335
+ }
2336
+ }
2337
+ var import_picocolors10, sync_default;
2338
+ var init_sync = __esm(() => {
2339
+ init_dist();
2340
+ init_out();
2341
+ init_journal_config();
2342
+ init_journal_remote();
2343
+ import_picocolors10 = __toESM(require_picocolors(), 1);
2344
+ sync_default = defineCommand({
2345
+ meta: {
2346
+ name: "sync",
2347
+ description: "Push pending journal entries to your remote GitHub journal repo"
2348
+ },
2349
+ args: {
2350
+ project: {
2351
+ type: "string",
2352
+ alias: "p",
2353
+ description: "Project to sync (defaults to current directory mapping)"
2354
+ },
2355
+ message: {
2356
+ type: "string",
2357
+ alias: "m",
2358
+ description: "Custom commit message for the sync"
2359
+ },
2360
+ verbose: {
2361
+ type: "boolean",
2362
+ alias: "v",
2363
+ description: "Show detailed diagnostics",
2364
+ default: false
2365
+ }
2366
+ },
2367
+ async run({ args }) {
2368
+ const config = await readConfig();
2369
+ let project = args.project;
2370
+ if (!project) {
2371
+ project = resolveProjectName(config) ?? undefined;
2372
+ }
2373
+ if (project) {
2374
+ project = sanitizeProjectName(project);
2375
+ }
2376
+ if (!project) {
2377
+ ui.write(`${import_picocolors10.default.yellow("\u26A0")} No project mapping found.
2378
+
2379
+ ` + `Run ${import_picocolors10.default.dim("dora init")} (or ${import_picocolors10.default.dim("doraval journal init")}) first, or pass ${import_picocolors10.default.dim("--project <name>")}.`);
2380
+ process.exit(1);
2381
+ }
2382
+ if (!config?.journal.repo) {
2383
+ ui.write(`${import_picocolors10.default.red("\u2717")} No journal repo configured. Run ${import_picocolors10.default.dim("dora init")} (or ${import_picocolors10.default.dim("doraval journal init")}) first.`);
2384
+ process.exit(1);
2385
+ }
2386
+ ensureGhCliOrExit();
2387
+ const journalRepo = config.journal.repo;
2388
+ const pendingDir = getPendingProjectDir(project);
2389
+ ui.write(`
2390
+ ${import_picocolors10.default.bold(import_picocolors10.default.white("dora journal sync"))} \u2014 ${import_picocolors10.default.white(project)}
2391
+ `);
2392
+ ui.write(` Journal repo: ${import_picocolors10.default.dim(import_picocolors10.default.gray(journalRepo))}`);
2393
+ ensureDoravalDirs();
2394
+ const journalsDir = getJournalsDir();
2395
+ const remoteProjectPath = `projects/${project}.md`;
2396
+ const localProjectPath = join6(journalsDir, `${project}.md`);
2397
+ ui.write(` ${import_picocolors10.default.dim(import_picocolors10.default.gray("Refreshing local cache from remote..."))}`);
2398
+ const gotGlobal = await refreshLocalJournalFile(journalRepo, "global.md", join6(journalsDir, "global.md"));
2399
+ if (gotGlobal) {
2400
+ ui.write(` ${import_picocolors10.default.dim(import_picocolors10.default.gray("\u2713 global.md"))}`);
2401
+ }
2402
+ const gotProjectCache = await refreshLocalJournalFile(journalRepo, remoteProjectPath, localProjectPath);
2403
+ if (gotProjectCache) {
2404
+ ui.write(` ${import_picocolors10.default.dim(import_picocolors10.default.gray(`\u2713 ${remoteProjectPath}`))}`);
2405
+ }
2406
+ const pendingFiles = existsSync7(pendingDir) ? readdirSync2(pendingDir).filter((f) => f.endsWith(".md") && f !== ".gitkeep").sort() : [];
2407
+ if (pendingFiles.length === 0) {
2408
+ ui.write(`
2409
+ ${import_picocolors10.default.yellow("\u26A0")} No pending entries. Local cache is now up to date.
2410
+ `);
2411
+ process.exit(0);
2412
+ }
2413
+ ui.write(` Found ${pendingFiles.length} pending entr${pendingFiles.length === 1 ? "y" : "ies"}
2414
+ `);
2415
+ const remotePath = `projects/${project}.md`;
2416
+ const currentFile = getRemoteJournalFileMeta(journalRepo, remotePath);
2417
+ let existingContent = "";
2418
+ let currentSha;
2419
+ if (currentFile) {
2420
+ existingContent = Buffer.from(currentFile.content, "base64").toString("utf8");
2421
+ currentSha = currentFile.sha;
2422
+ if (args.verbose)
2423
+ ui.write(` ${import_picocolors10.default.dim(import_picocolors10.default.gray("Found existing remote file (sha: " + currentSha.slice(0, 7) + "...)"))}`);
2424
+ } else {
2425
+ if (args.verbose)
2426
+ ui.write(` ${import_picocolors10.default.dim(import_picocolors10.default.gray("No existing file on remote \u2014 will create it"))}`);
2427
+ }
2428
+ let newEntries = "";
2429
+ for (const file of pendingFiles) {
2430
+ const fullPath = join6(pendingDir, file);
2431
+ const entryContent = await Bun.file(fullPath).text();
2432
+ newEntries += `
2433
+ ` + entryContent.trim() + `
2434
+ `;
2435
+ }
2436
+ let newContent;
2437
+ if (existingContent.trim().length === 0) {
2438
+ newContent = `# ${project} Journal
2439
+
2440
+ ` + `Project-specific decisions for **${project}**.
2441
+
2442
+ ` + newEntries.trim();
2443
+ } else {
2444
+ newContent = existingContent.trimEnd() + `
2445
+ ` + newEntries;
2446
+ }
2447
+ const commitMessage = args.message || `journal: add ${pendingFiles.length} entr${pendingFiles.length === 1 ? "y" : "ies"} for ${project}`;
2448
+ if (args.verbose)
2449
+ ui.write(`
2450
+ ${import_picocolors10.default.dim(import_picocolors10.default.gray("Pushing to remote..."))}`);
2451
+ try {
2452
+ updateGitHubFile(journalRepo, remotePath, newContent, commitMessage, currentSha);
2453
+ ui.write(` ${import_picocolors10.default.green("\u2713")} ${import_picocolors10.default.white("Successfully pushed to")} ${import_picocolors10.default.white(remotePath)}`);
2454
+ } catch (err) {
2455
+ ui.write(`${import_picocolors10.default.red("\u2717")} ${import_picocolors10.default.white("Failed to push to GitHub.")}`);
2456
+ process.exit(1);
2457
+ }
2458
+ for (const file of pendingFiles) {
2459
+ const fullPath = join6(pendingDir, file);
2460
+ try {
2461
+ await Bun.file(fullPath).unlink();
2462
+ } catch {}
2463
+ }
2464
+ ui.write(` ${import_picocolors10.default.green("\u2713")} ${import_picocolors10.default.white("Cleared local pending entries")}`);
2465
+ try {
2466
+ const wrote = await refreshLocalJournalFile(journalRepo, remotePath, localProjectPath);
2467
+ if (wrote) {
2468
+ if (args.verbose)
2469
+ ui.write(` ${import_picocolors10.default.green("\u2713")} ${import_picocolors10.default.white("Re-fetched")} ${import_picocolors10.default.white(project)}.md ${import_picocolors10.default.white("into local cache")}`);
2470
+ }
2471
+ } catch {
2472
+ ui.write(` ${import_picocolors10.default.yellow("\u26A0")} Could not re-fetch updated file (you can run sync again later)`);
2473
+ }
2474
+ ui.write(`
2475
+ ${import_picocolors10.default.green("Done!")} ${import_picocolors10.default.white(pendingFiles.length + " entr" + (pendingFiles.length === 1 ? "y" : "ies") + " published.")}
2476
+ `);
2477
+ process.exit(0);
2478
+ }
2479
+ });
2480
+ });
2481
+
2482
+ // src/validators/claude/skill.ts
2483
+ import { existsSync as existsSync8 } from "fs";
2484
+ import { resolve as resolve3 } from "path";
2485
+ var OPTIONAL_DIRS2, claudeSkillValidator;
2486
+ var init_skill = __esm(() => {
2487
+ init_frontmatter();
2488
+ init_skill_validate();
2489
+ OPTIONAL_DIRS2 = ["references", "scripts", "assets"];
2490
+ claudeSkillValidator = {
2491
+ id: "claude:skill",
2492
+ provider: "claude",
2493
+ name: "Claude Skill",
2494
+ description: "Validates SKILL.md per current Claude Code spec: frontmatter (name/description relaxed to recommended; directory name usually provides the /command), body, supporting files, dynamic injection (!`cmd`), substitutions ($ARGUMENTS, ${CLAUDE_*}), and advanced fields (allowed-tools, context, disable-model-invocation, when_to_use, etc.)",
2495
+ detect(dir) {
2496
+ return existsSync8(resolve3(dir, "SKILL.md"));
2497
+ },
2498
+ async validate(dir, _opts) {
2499
+ const skillMd = resolve3(dir, "SKILL.md");
2500
+ const raw = await Bun.file(skillMd).text();
2501
+ let parsed;
2502
+ try {
2503
+ parsed = parseFrontmatter(raw);
2504
+ } catch {
2505
+ return {
2506
+ errors: ["Failed to parse YAML frontmatter in SKILL.md"],
2507
+ warnings: [],
2508
+ passes: []
2509
+ };
2510
+ }
2511
+ const existingDirs = OPTIONAL_DIRS2.filter((d) => existsSync8(resolve3(dir, d)));
2512
+ return validateSkillModel(parsed, { existingDirs: [...existingDirs] });
2513
+ }
2514
+ };
2515
+ });
2516
+
2517
+ // src/validators/claude/plugin.ts
2518
+ import { existsSync as existsSync9, readdirSync as readdirSync3 } from "fs";
2519
+ import { resolve as resolve4, join as join7 } from "path";
2520
+ var NAME_REGEX2, RELATIVE_PATH_REGEX, claudePluginValidator;
2521
+ var init_plugin = __esm(() => {
2522
+ NAME_REGEX2 = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
2523
+ RELATIVE_PATH_REGEX = /^\.\//;
2524
+ claudePluginValidator = {
2525
+ id: "claude:plugin",
2526
+ provider: "claude",
2527
+ name: "Claude Plugin",
2528
+ description: "Validates .claude-plugin/plugin.json manifest, component directories, and structure",
2529
+ detect(dir) {
2530
+ return existsSync9(resolve4(dir, ".claude-plugin", "plugin.json"));
2531
+ },
2532
+ async validate(dir, _opts) {
2533
+ const errors = [];
2534
+ const warnings = [];
2535
+ const passes = [];
2536
+ const manifestPath = resolve4(dir, ".claude-plugin", "plugin.json");
2537
+ let manifest;
2538
+ try {
2539
+ const raw = await Bun.file(manifestPath).text();
2540
+ manifest = JSON.parse(raw);
2541
+ passes.push(".claude-plugin/plugin.json is valid JSON");
2542
+ } catch {
2543
+ errors.push(".claude-plugin/plugin.json is missing or invalid JSON");
2544
+ return { errors, warnings, passes };
2545
+ }
2546
+ if (!manifest.name) {
2547
+ errors.push('Missing required field: "name"');
2548
+ } else {
2549
+ const name = String(manifest.name);
2550
+ if (!NAME_REGEX2.test(name)) {
2551
+ errors.push(`Invalid name format: "${name}" \u2014 must be kebab-case (a-z, 0-9, hyphens)`);
2552
+ } else {
2553
+ passes.push(`name: "${name}"`);
2554
+ }
2555
+ }
2556
+ if (manifest.version !== undefined) {
2557
+ const v = String(manifest.version);
2558
+ if (!/^\d+\.\d+\.\d+/.test(v)) {
2559
+ errors.push(`Invalid version format: "${v}" \u2014 must be semver (MAJOR.MINOR.PATCH)`);
2560
+ } else {
2561
+ passes.push(`version: "${v}"`);
2562
+ }
2563
+ }
2564
+ if (manifest.description !== undefined) {
2565
+ const desc = String(manifest.description);
2566
+ if (desc.length < 10) {
2567
+ warnings.push(`Description is very short (${desc.length} chars) \u2014 50-200 chars recommended`);
2568
+ } else {
2569
+ passes.push("description field present");
2570
+ }
2571
+ }
2572
+ const checkPaths = (field, value) => {
2573
+ const paths = Array.isArray(value) ? value : [value];
2574
+ for (const p of paths) {
2575
+ const s = String(p);
2576
+ if (!RELATIVE_PATH_REGEX.test(s)) {
2577
+ errors.push(`${field}: path "${s}" must start with "./" (relative)`);
2578
+ } else if (s.includes("..")) {
2579
+ errors.push(`${field}: path "${s}" must not use ".." (no parent traversal)`);
2580
+ } else if (existsSync9(resolve4(dir, s))) {
2581
+ passes.push(`${field}: path "${s}" exists`);
2582
+ } else {
2583
+ warnings.push(`${field}: path "${s}" does not exist on disk`);
2584
+ }
2585
+ }
2586
+ };
2587
+ for (const field of ["commands", "agents", "hooks", "mcpServers"]) {
2588
+ if (manifest[field] !== undefined) {
2589
+ checkPaths(field, manifest[field]);
2590
+ }
2591
+ }
2592
+ const skillsDir = resolve4(dir, "skills");
2593
+ if (existsSync9(skillsDir)) {
2594
+ const skillEntries = readdirSync3(skillsDir, { withFileTypes: true }).filter((e) => e.isDirectory());
2595
+ for (const skill of skillEntries) {
2596
+ const skillMd = join7(skillsDir, skill.name, "SKILL.md");
2597
+ if (existsSync9(skillMd)) {
2598
+ passes.push(`skills/${skill.name}/SKILL.md exists`);
2599
+ } else {
2600
+ errors.push(`skills/${skill.name}/ missing SKILL.md`);
2601
+ }
2602
+ }
2603
+ }
2604
+ const commandsDir = resolve4(dir, "commands");
2605
+ if (existsSync9(commandsDir)) {
2606
+ const mdFiles = readdirSync3(commandsDir).filter((f) => f.endsWith(".md"));
2607
+ if (mdFiles.length > 0) {
2608
+ passes.push(`commands/ has ${mdFiles.length} .md file(s)`);
2609
+ } else {
2610
+ warnings.push("commands/ directory exists but has no .md files");
2611
+ }
2612
+ }
2613
+ const agentsDir = resolve4(dir, "agents");
2614
+ if (existsSync9(agentsDir)) {
2615
+ const mdFiles = readdirSync3(agentsDir).filter((f) => f.endsWith(".md"));
2616
+ if (mdFiles.length > 0) {
2617
+ passes.push(`agents/ has ${mdFiles.length} .md file(s)`);
2618
+ } else {
2619
+ warnings.push("agents/ directory exists but has no .md files");
2620
+ }
2621
+ }
2622
+ return { errors, warnings, passes };
2623
+ }
2624
+ };
2625
+ });
2626
+
2627
+ // src/validators/claude/marketplace.ts
2628
+ import { existsSync as existsSync10, readdirSync as readdirSync4 } from "fs";
2629
+ import { resolve as resolve5, join as join8 } from "path";
2630
+ var claudeMarketplaceValidator;
2631
+ var init_marketplace = __esm(() => {
2632
+ claudeMarketplaceValidator = {
2633
+ id: "claude:marketplace",
2634
+ provider: "claude",
2635
+ name: "Claude Plugin Marketplace",
2636
+ description: "Validates marketplace structure: plugins/ directory with valid plugin subdirectories",
2637
+ detect(dir) {
2638
+ const pluginsDir = resolve5(dir, "plugins");
2639
+ if (!existsSync10(pluginsDir))
2640
+ return false;
2641
+ try {
2642
+ const entries = readdirSync4(pluginsDir, { withFileTypes: true });
2643
+ for (const entry of entries) {
2644
+ if (!entry.isDirectory())
2645
+ continue;
2646
+ const hasSkills = existsSync10(join8(pluginsDir, entry.name, "skills"));
2647
+ const hasManifest = existsSync10(join8(pluginsDir, entry.name, ".claude-plugin", "plugin.json"));
2648
+ if (hasSkills || hasManifest)
2649
+ return true;
2650
+ }
2651
+ } catch {}
2652
+ return false;
2653
+ },
2654
+ async validate(dir, _opts) {
2655
+ const errors = [];
2656
+ const warnings = [];
2657
+ const passes = [];
2658
+ const pluginsDir = resolve5(dir, "plugins");
2659
+ if (!existsSync10(pluginsDir)) {
2660
+ errors.push("Missing plugins/ directory");
2661
+ return { errors, warnings, passes };
2662
+ }
2663
+ passes.push("plugins/ directory exists");
2664
+ const pluginEntries = readdirSync4(pluginsDir, { withFileTypes: true }).filter((e) => e.isDirectory());
2665
+ if (pluginEntries.length === 0) {
2666
+ errors.push("plugins/ directory is empty \u2014 expected at least one plugin");
2667
+ return { errors, warnings, passes };
2668
+ }
2669
+ passes.push(`${pluginEntries.length} plugin(s) found`);
2670
+ if (existsSync10(resolve5(dir, "README.md"))) {
2671
+ passes.push("README.md exists at marketplace root");
2672
+ } else {
2673
+ warnings.push("No README.md at marketplace root \u2014 recommended for discoverability");
2674
+ }
2675
+ if (existsSync10(resolve5(dir, "LICENSE"))) {
2676
+ passes.push("LICENSE exists at marketplace root");
2677
+ } else {
2678
+ warnings.push("No LICENSE at marketplace root \u2014 recommended");
2679
+ }
2680
+ for (const plugin of pluginEntries) {
2681
+ const pluginPath = join8(pluginsDir, plugin.name);
2682
+ const hasSkills = existsSync10(join8(pluginPath, "skills"));
2683
+ const hasManifest = existsSync10(join8(pluginPath, ".claude-plugin", "plugin.json"));
2684
+ const hasReadme = existsSync10(join8(pluginPath, "README.md"));
2685
+ if (hasManifest || hasSkills) {
2686
+ passes.push(`Plugin "${plugin.name}" has ${hasManifest ? "manifest" : "skills/"}`);
2687
+ } else {
2688
+ warnings.push(`Plugin "${plugin.name}" has neither .claude-plugin/plugin.json nor skills/`);
2689
+ }
2690
+ if (!hasReadme) {
2691
+ warnings.push(`Plugin "${plugin.name}" has no README.md`);
2692
+ }
2693
+ }
2694
+ return { errors, warnings, passes };
2695
+ }
2696
+ };
2697
+ });
2698
+
2699
+ // src/validators/claude/hooks.ts
2700
+ import { existsSync as existsSync11 } from "fs";
2701
+ import { resolve as resolve6 } from "path";
2702
+ var KNOWN_EVENTS, claudeHooksValidator;
2703
+ var init_hooks = __esm(() => {
2704
+ KNOWN_EVENTS = [
2705
+ "PreToolUse",
2706
+ "PostToolUse",
2707
+ "Stop",
2708
+ "SubagentStop",
2709
+ "SessionStart",
2710
+ "SessionEnd",
2711
+ "UserPromptSubmit",
2712
+ "PreCompact",
2713
+ "Notification",
2714
+ "PermissionRequest"
2715
+ ];
2716
+ claudeHooksValidator = {
2717
+ id: "claude:hooks",
2718
+ provider: "claude",
2719
+ name: "Claude Hooks",
2720
+ description: "Validates hooks/hooks.json: event names, matcher structure, hook types",
2721
+ detect(dir) {
2722
+ return existsSync11(resolve6(dir, "hooks", "hooks.json")) || existsSync11(resolve6(dir, "hooks.json"));
2723
+ },
2724
+ async validate(dir, _opts) {
2725
+ const errors = [];
2726
+ const warnings = [];
2727
+ const passes = [];
2728
+ const hooksPath = existsSync11(resolve6(dir, "hooks", "hooks.json")) ? resolve6(dir, "hooks", "hooks.json") : resolve6(dir, "hooks.json");
2729
+ let config;
2730
+ try {
2731
+ const raw = await Bun.file(hooksPath).text();
2732
+ config = JSON.parse(raw);
2733
+ passes.push("hooks.json is valid JSON");
2734
+ } catch {
2735
+ errors.push("hooks.json is missing or invalid JSON");
2736
+ return { errors, warnings, passes };
2737
+ }
2738
+ const eventNames = Object.keys(config);
2739
+ for (const name of eventNames) {
2740
+ if (KNOWN_EVENTS.includes(name)) {
2741
+ passes.push(`Event "${name}" is a known lifecycle event`);
2742
+ } else {
2743
+ warnings.push(`Unknown event name: "${name}" \u2014 expected one of: ${KNOWN_EVENTS.join(", ")}`);
2744
+ }
2745
+ }
2746
+ return { errors, warnings, passes };
2747
+ }
2748
+ };
2749
+ });
2750
+
2751
+ // src/validators/claude/mcp.ts
2752
+ import { existsSync as existsSync12 } from "fs";
2753
+ import { resolve as resolve7 } from "path";
2754
+ var claudeMcpValidator;
2755
+ var init_mcp = __esm(() => {
2756
+ claudeMcpValidator = {
2757
+ id: "claude:mcp",
2758
+ provider: "claude",
2759
+ name: "Claude MCP Config",
2760
+ description: "Validates .mcp.json: server definitions, required fields, path portability",
2761
+ detect(dir) {
2762
+ return existsSync12(resolve7(dir, ".mcp.json"));
2763
+ },
2764
+ async validate(dir, _opts) {
2765
+ const errors = [];
2766
+ const warnings = [];
2767
+ const passes = [];
2768
+ const mcpPath = resolve7(dir, ".mcp.json");
2769
+ let config;
2770
+ try {
2771
+ const raw = await Bun.file(mcpPath).text();
2772
+ config = JSON.parse(raw);
2773
+ passes.push(".mcp.json is valid JSON");
2774
+ } catch {
2775
+ errors.push(".mcp.json is missing or invalid JSON");
2776
+ return { errors, warnings, passes };
2777
+ }
2778
+ if (typeof config !== "object" || Array.isArray(config)) {
2779
+ errors.push(".mcp.json must be a JSON object with server name keys");
2780
+ return { errors, warnings, passes };
2781
+ }
2782
+ const serverNames = Object.keys(config);
2783
+ if (serverNames.length === 0) {
2784
+ warnings.push(".mcp.json is empty \u2014 no servers defined");
2785
+ return { errors, warnings, passes };
2786
+ }
2787
+ passes.push(`${serverNames.length} server(s) defined`);
2788
+ return { errors, warnings, passes };
2789
+ }
2790
+ };
2791
+ });
2792
+
2793
+ // src/validators/claude/subagent.ts
2794
+ import { existsSync as existsSync13, readdirSync as readdirSync5 } from "fs";
2795
+ import { resolve as resolve8, join as join9 } from "path";
2796
+ var claudeSubagentValidator;
2797
+ var init_subagent = __esm(() => {
2798
+ init_frontmatter();
2799
+ claudeSubagentValidator = {
2800
+ id: "claude:subagent",
2801
+ provider: "claude",
2802
+ name: "Claude Subagents",
2803
+ description: "Validates agents/ directory: .md files with frontmatter and description",
2804
+ detect(dir) {
2805
+ const agentsDir = resolve8(dir, "agents");
2806
+ if (!existsSync13(agentsDir))
2807
+ return false;
2808
+ try {
2809
+ return readdirSync5(agentsDir).some((f) => f.endsWith(".md"));
2810
+ } catch {
2811
+ return false;
2812
+ }
2813
+ },
2814
+ async validate(dir, _opts) {
2815
+ const errors = [];
2816
+ const warnings = [];
2817
+ const passes = [];
2818
+ const agentsDir = resolve8(dir, "agents");
2819
+ const mdFiles = readdirSync5(agentsDir).filter((f) => f.endsWith(".md"));
2820
+ if (mdFiles.length === 0) {
2821
+ errors.push("agents/ directory has no .md files");
2822
+ return { errors, warnings, passes };
2823
+ }
2824
+ passes.push(`${mdFiles.length} agent definition(s) found`);
2825
+ for (const file of mdFiles) {
2826
+ const filePath = join9(agentsDir, file);
2827
+ const raw = await Bun.file(filePath).text();
2828
+ try {
2829
+ const parsed = parseFrontmatter(raw);
2830
+ if (Object.keys(parsed.data).length === 0) {
2831
+ warnings.push(`${file}: no YAML frontmatter`);
2832
+ } else if (!parsed.data.description) {
2833
+ warnings.push(`${file}: missing "description" in frontmatter`);
2834
+ } else {
2835
+ passes.push(`${file}: has frontmatter with description`);
2836
+ }
2837
+ if (!parsed.content.trim()) {
2838
+ errors.push(`${file}: body is empty`);
2839
+ }
2840
+ } catch {
2841
+ errors.push(`${file}: failed to parse`);
2842
+ }
2843
+ }
2844
+ return { errors, warnings, passes };
2845
+ }
2846
+ };
2847
+ });
2848
+
2849
+ // src/validators/claude/command.ts
2850
+ import { existsSync as existsSync14, readdirSync as readdirSync6 } from "fs";
2851
+ import { resolve as resolve9, join as join10 } from "path";
2852
+ var claudeCommandValidator;
2853
+ var init_command = __esm(() => {
2854
+ init_frontmatter();
2855
+ claudeCommandValidator = {
2856
+ id: "claude:command",
2857
+ provider: "claude",
2858
+ name: "Claude Commands",
2859
+ description: "Validates commands/ (or legacy .claude/commands/) .md files: frontmatter (including rich skill fields), description, body",
2860
+ detect(dir) {
2861
+ const commandsDir = resolve9(dir, "commands");
2862
+ if (!existsSync14(commandsDir))
2863
+ return false;
2864
+ try {
2865
+ return readdirSync6(commandsDir).some((f) => f.endsWith(".md"));
2866
+ } catch {
2867
+ return false;
2868
+ }
2869
+ },
2870
+ async validate(dir, _opts) {
2871
+ const errors = [];
2872
+ const warnings = [];
2873
+ const passes = [];
2874
+ const commandsDir = resolve9(dir, "commands");
2875
+ const mdFiles = readdirSync6(commandsDir).filter((f) => f.endsWith(".md"));
2876
+ if (mdFiles.length === 0) {
2877
+ errors.push("commands/ directory has no .md files");
2878
+ return { errors, warnings, passes };
2879
+ }
2880
+ passes.push(`${mdFiles.length} command definition(s) found`);
2881
+ for (const file of mdFiles) {
2882
+ const filePath = join10(commandsDir, file);
2883
+ const raw = await Bun.file(filePath).text();
2884
+ try {
2885
+ const parsed = parseFrontmatter(raw);
2886
+ if (Object.keys(parsed.data).length === 0) {
2887
+ warnings.push(`${file}: no YAML frontmatter`);
2888
+ } else if (!parsed.data.description) {
2889
+ warnings.push(`${file}: missing "description" in frontmatter`);
2890
+ } else {
2891
+ passes.push(`${file}: has frontmatter with description`);
2892
+ }
2893
+ if (!parsed.content.trim()) {
2894
+ errors.push(`${file}: body is empty`);
2895
+ }
2896
+ const advancedKeys = ["allowed-tools", "disallowed-tools", "context", "when_to_use", "disable-model-invocation", "user-invocable", "arguments", "argument-hint", "shell", "paths", "hooks"];
2897
+ const foundAdvanced = advancedKeys.filter((k) => parsed.data[k] !== undefined);
2898
+ if (foundAdvanced.length > 0) {
2899
+ passes.push(`${file}: advanced frontmatter: ${foundAdvanced.join(", ")}`);
2900
+ }
2901
+ } catch {
2902
+ errors.push(`${file}: failed to parse`);
2903
+ }
2904
+ }
2905
+ return { errors, warnings, passes };
2906
+ }
2907
+ };
2908
+ });
2909
+
2910
+ // src/validators/claude/memory.ts
2911
+ import { existsSync as existsSync15 } from "fs";
2912
+ import { resolve as resolve10 } from "path";
2913
+ var claudeMemoryValidator;
2914
+ var init_memory = __esm(() => {
2915
+ claudeMemoryValidator = {
2916
+ id: "claude:memory",
2917
+ provider: "claude",
2918
+ name: "Claude CLAUDE.md",
2919
+ description: "Validates CLAUDE.md: non-empty, length recommendations, @path imports",
2920
+ detect(dir) {
2921
+ return existsSync15(resolve10(dir, "CLAUDE.md"));
2922
+ },
2923
+ async validate(dir, _opts) {
2924
+ const errors = [];
2925
+ const warnings = [];
2926
+ const passes = [];
2927
+ const filePath = resolve10(dir, "CLAUDE.md");
2928
+ const raw = await Bun.file(filePath).text();
2929
+ if (!raw.trim()) {
2930
+ errors.push("CLAUDE.md is empty");
2931
+ return { errors, warnings, passes };
2932
+ }
2933
+ passes.push("CLAUDE.md is non-empty");
2934
+ const lines = raw.split(`
2935
+ `);
2936
+ if (lines.length > 200) {
2937
+ warnings.push(`CLAUDE.md is ${lines.length} lines \u2014 official recommendation is under 200. Move reference content to skills.`);
2938
+ } else {
2939
+ passes.push(`CLAUDE.md is ${lines.length} lines (under 200 recommended limit)`);
2940
+ }
2941
+ const importRegex = /^@([^\s]+)\s*$/gm;
2942
+ let match;
2943
+ while ((match = importRegex.exec(raw)) !== null) {
2944
+ const importPath = match[1];
2945
+ const resolvedImport = resolve10(dir, importPath);
2946
+ if (existsSync15(resolvedImport)) {
2947
+ passes.push(`@import "${importPath}" exists`);
2948
+ } else {
2949
+ warnings.push(`@import "${importPath}" \u2014 file not found at ${resolvedImport}`);
2950
+ }
2951
+ }
2952
+ return { errors, warnings, passes };
2953
+ }
2954
+ };
2955
+ });
2956
+
2957
+ // src/validators/index.ts
2958
+ function resolveFor(forFlag, allValidators = validators) {
2959
+ if (!forFlag) {
2960
+ return { matched: allValidators };
2961
+ }
2962
+ if (forFlag.includes(":")) {
2963
+ const exact = allValidators.filter((v) => v.id === forFlag);
2964
+ if (exact.length === 0) {
2965
+ const available = allValidators.map((v) => v.id).join(", ");
2966
+ return { matched: [], error: `Unknown validator: "${forFlag}"
2967
+
2968
+ Available: ${available}` };
2969
+ }
2970
+ return { matched: exact };
2971
+ }
2972
+ const byProvider = allValidators.filter((v) => v.provider === forFlag);
2973
+ if (byProvider.length === 0) {
2974
+ const providers = [...new Set(allValidators.map((v) => v.provider))];
2975
+ return { matched: [], error: `Unknown provider: "${forFlag}"
2976
+
2977
+ Available providers: ${providers.join(", ")}` };
2978
+ }
2979
+ return { matched: byProvider };
2980
+ }
2981
+ var validators;
2982
+ var init_validators = __esm(() => {
2983
+ init_skill();
2984
+ init_plugin();
2985
+ init_marketplace();
2986
+ init_hooks();
2987
+ init_mcp();
2988
+ init_subagent();
2989
+ init_command();
2990
+ init_memory();
2991
+ validators = [
2992
+ claudeSkillValidator,
2993
+ claudePluginValidator,
2994
+ claudeMarketplaceValidator,
2995
+ claudeHooksValidator,
2996
+ claudeMcpValidator,
2997
+ claudeSubagentValidator,
2998
+ claudeCommandValidator,
2999
+ claudeMemoryValidator
3000
+ ];
3001
+ });
3002
+
3003
+ // src/core/remote.ts
3004
+ import { spawnSync as spawnSync4 } from "child_process";
3005
+ import { mkdtempSync, rmSync } from "fs";
3006
+ import { join as join11 } from "path";
3007
+ import { tmpdir } from "os";
3008
+ function parseRemoteUrl(input) {
3009
+ if (input.startsWith(".") || input.startsWith("/") || input.startsWith("~")) {
3010
+ return null;
3011
+ }
3012
+ const ghMatch = input.match(GITHUB_RE);
3013
+ if (ghMatch) {
3014
+ const [, ownerRepo, ref, subpath] = ghMatch;
3015
+ return {
3016
+ original: input,
3017
+ ghRepo: ownerRepo,
3018
+ gitUrl: `https://github.com/${ownerRepo}.git`,
3019
+ ref,
3020
+ subpath
3021
+ };
3022
+ }
3023
+ if (GENERIC_GIT_RE.test(input)) {
3024
+ const gitUrl = input.endsWith(".git") ? input : `${input}.git`;
3025
+ return {
3026
+ original: input,
3027
+ gitUrl
3028
+ };
3029
+ }
3030
+ return null;
3031
+ }
3032
+ function isGhAvailable() {
3033
+ if (ghAvailable !== null)
3034
+ return ghAvailable;
3035
+ const result = spawnSync4("gh", ["auth", "status"], {
3036
+ stdio: "pipe",
3037
+ timeout: 5000
3038
+ });
3039
+ ghAvailable = result.status === 0;
3040
+ return ghAvailable;
3041
+ }
3042
+ async function cloneToTemp(parsed) {
3043
+ const tmpDir = mkdtempSync(join11(tmpdir(), "dora-"));
3044
+ const cleanup = () => {
3045
+ try {
3046
+ rmSync(tmpDir, { recursive: true, force: true });
3047
+ } catch {}
3048
+ };
3049
+ const exitHandler = () => cleanup();
3050
+ process.on("exit", exitHandler);
3051
+ const removeExitHandler = () => {
3052
+ process.removeListener("exit", exitHandler);
3053
+ };
3054
+ const wrappedCleanup = () => {
3055
+ removeExitHandler();
3056
+ cleanup();
3057
+ };
3058
+ if (parsed.ghRepo && isGhAvailable()) {
3059
+ const ghArgs = ["repo", "clone", parsed.ghRepo, tmpDir, "--"];
3060
+ ghArgs.push("--depth", "1");
3061
+ if (parsed.ref)
3062
+ ghArgs.push("--branch", parsed.ref);
3063
+ const gh = spawnSync4("gh", ghArgs, { stdio: "pipe", timeout: 60000 });
3064
+ if (gh.status === 0) {
3065
+ return { dir: tmpDir, cleanup: wrappedCleanup };
3066
+ }
3067
+ }
3068
+ const gitArgs = ["clone", "--depth", "1"];
3069
+ if (parsed.ref)
3070
+ gitArgs.push("--branch", parsed.ref);
3071
+ gitArgs.push(parsed.gitUrl, tmpDir);
3072
+ const git = spawnSync4("git", gitArgs, { stdio: "pipe", timeout: 60000 });
3073
+ if (git.status !== 0) {
3074
+ wrappedCleanup();
3075
+ const stderr = git.stderr?.toString().trim() || "unknown error";
3076
+ throw new Error(`Failed to clone ${parsed.original}: ${stderr}`);
3077
+ }
3078
+ return { dir: tmpDir, cleanup: wrappedCleanup };
3079
+ }
3080
+ var GITHUB_RE, GENERIC_GIT_RE, ghAvailable = null;
3081
+ var init_remote = __esm(() => {
3082
+ GITHUB_RE = /^(?:https?:\/\/)?github\.com\/([^/]+\/[^/]+?)(?:\.git)?(?:\/(?:tree|blob)\/([^/]+)(?:\/(.+))?)?$/;
3083
+ GENERIC_GIT_RE = /^https?:\/\/[^/]+\/[^/]+\/[^/]+/;
3084
+ });
3085
+
3086
+ // src/cli/commands/validate-top.ts
3087
+ var exports_validate_top = {};
3088
+ __export(exports_validate_top, {
3089
+ default: () => validate_top_default
3090
+ });
3091
+ import { existsSync as existsSync17 } from "fs";
3092
+ import { resolve as resolve11 } from "path";
3093
+ var import_picocolors11, validate_top_default;
3094
+ var init_validate_top = __esm(() => {
3095
+ init_dist();
3096
+ init_out();
3097
+ init_validators();
3098
+ init_remote();
3099
+ import_picocolors11 = __toESM(require_picocolors(), 1);
3100
+ validate_top_default = defineCommand({
3101
+ meta: {
3102
+ name: "validate",
3103
+ description: "Auto-detect project type and run matching validators. Accepts a local path or a Git URL (e.g. https://github.com/owner/repo)"
3104
+ },
3105
+ args: {
3106
+ path: {
3107
+ type: "positional",
3108
+ description: "Path or Git URL to validate",
3109
+ required: true
3110
+ },
3111
+ for: {
3112
+ type: "string",
3113
+ description: 'Target a provider ("claude") or specific validator ("claude:plugin")'
3114
+ },
3115
+ format: {
3116
+ type: "string",
3117
+ alias: "f",
3118
+ description: "Output format (json or table)",
3119
+ default: "table"
3120
+ },
3121
+ verbose: {
3122
+ type: "boolean",
3123
+ alias: "v",
3124
+ description: "Show detailed diagnostics",
3125
+ default: false
3126
+ },
3127
+ ci: {
3128
+ type: "boolean",
3129
+ description: "Machine-friendly output, non-zero exit on issues",
3130
+ default: false
3131
+ }
3132
+ },
3133
+ async run({ args }) {
3134
+ const remote = parseRemoteUrl(args.path);
3135
+ let fullPath;
3136
+ let cleanup;
3137
+ if (remote) {
3138
+ ui.info(`
3139
+ Cloning ${import_picocolors11.default.dim(args.path)}...`);
3140
+ try {
3141
+ const result = await cloneToTemp(remote);
3142
+ fullPath = remote.subpath ? resolve11(result.dir, remote.subpath) : result.dir;
3143
+ cleanup = result.cleanup;
3144
+ } catch (err) {
3145
+ const msg = err instanceof Error ? err.message : String(err);
3146
+ ui.fail(msg);
3147
+ process.exit(1);
3148
+ }
3149
+ if (!existsSync17(fullPath)) {
3150
+ cleanup();
3151
+ ui.fail(`Subdirectory not found in repo: ${remote.subpath}`);
3152
+ process.exit(1);
3153
+ }
3154
+ } else {
3155
+ fullPath = resolve11(args.path);
3156
+ if (!existsSync17(fullPath)) {
3157
+ ui.fail(`Path not found: ${args.path}
3158
+
3159
+ Check that the path is correct and the directory exists.`);
3160
+ process.exit(1);
3161
+ }
3162
+ }
3163
+ try {
3164
+ const opts = {
3165
+ format: args.format ?? "table",
3166
+ verbose: !!args.verbose,
3167
+ ci: !!args.ci
3168
+ };
3169
+ const { matched: candidates, error } = resolveFor(args.for);
3170
+ if (error) {
3171
+ ui.fail(error);
3172
+ process.exit(1);
3173
+ }
3174
+ let matched;
3175
+ if (args.for && args.for.includes(":")) {
3176
+ matched = candidates;
3177
+ } else {
3178
+ matched = candidates.filter((v) => v.detect(fullPath));
3179
+ }
3180
+ if (matched.length === 0) {
3181
+ const providers = [...new Set(validators.map((v) => v.provider))];
3182
+ ui.fail(`No validator matched this directory: ${args.path}
3183
+
3184
+ ` + `Available providers:
3185
+ ` + providers.map((p) => {
3186
+ const pvs = validators.filter((v) => v.provider === p);
3187
+ return ` ${import_picocolors11.default.bold(p)}
3188
+ ` + pvs.map((v) => ` \u2022 ${import_picocolors11.default.dim(v.id)} \u2014 ${v.description}`).join(`
3189
+ `);
3190
+ }).join(`
3191
+ `) + `
3192
+
3193
+ Use ${import_picocolors11.default.dim("--for <provider>")} or ${import_picocolors11.default.dim("--for <provider:type>")} to target explicitly.`);
3194
+ process.exit(1);
3195
+ }
3196
+ const allResults = [];
3197
+ let totalErrors = 0;
3198
+ for (const v of matched) {
3199
+ const result = await v.validate(fullPath, opts);
3200
+ allResults.push({ id: v.id, name: v.name, result });
3201
+ totalErrors += result.errors.length;
3202
+ }
3203
+ if (opts.format === "json") {
3204
+ const output = allResults.map((r) => ({
3205
+ validator: r.id,
3206
+ name: r.name,
3207
+ path: args.path,
3208
+ ...r.result
3209
+ }));
3210
+ console.log(JSON.stringify(output, null, 2));
3211
+ } else {
3212
+ for (const { id, name, result } of allResults) {
3213
+ ui.write(`
3214
+ ${import_picocolors11.default.bold("dora validate")} \u2014 ${import_picocolors11.default.white(name)} ${import_picocolors11.default.dim(`(${id})`)}
3215
+ `);
3216
+ ui.info(` Path: ${args.path}
3217
+ `);
3218
+ for (const p of result.passes) {
3219
+ ui.pass(p);
3220
+ }
3221
+ for (const w of result.warnings) {
3222
+ ui.warnItem(w);
3223
+ }
3224
+ for (const e of result.errors) {
3225
+ ui.failItem(e);
3226
+ }
3227
+ if (result.errors.length === 0 && result.warnings.length === 0) {
3228
+ ui.write(`
3229
+ ${import_picocolors11.default.green("\u2713")} ${import_picocolors11.default.white("All checks passed.")}
3230
+ `);
3231
+ } else {
3232
+ ui.info(`
3233
+ Result: ${result.errors.length} error(s), ${result.warnings.length} warning(s)
3234
+ `);
3235
+ }
3236
+ }
3237
+ }
3238
+ process.exit(totalErrors > 0 ? 1 : 0);
3239
+ } finally {
3240
+ cleanup?.();
3241
+ }
3242
+ }
3243
+ });
3244
+ });
3245
+
3246
+ // src/cli/commands/init.ts
3247
+ var exports_init2 = {};
3248
+ __export(exports_init2, {
3249
+ default: () => init_default2
3250
+ });
3251
+ import { basename as basename2, join as join12 } from "path";
3252
+ var {spawnSync: spawnSync5 } = globalThis.Bun;
3253
+ var import_picocolors12, init_default2;
3254
+ var init_init2 = __esm(() => {
3255
+ init_dist();
3256
+ init_out();
3257
+ init_journal_config();
3258
+ init_journal_remote();
3259
+ init_prompt();
3260
+ import_picocolors12 = __toESM(require_picocolors(), 1);
3261
+ init_default2 = defineCommand({
3262
+ meta: {
3263
+ name: "init",
3264
+ description: "One-time setup for doraval + journal (decisions + notes) + your coding agent (recommended starting point)"
3265
+ },
3266
+ args: {
3267
+ repo: {
3268
+ type: "string",
3269
+ alias: "r",
3270
+ description: "Journal repo (owner/name). Smart default from git remote or gh account. Env: DORAVAL_JOURNAL_REPO"
3271
+ },
3272
+ project: {
3273
+ type: "string",
3274
+ alias: "p",
3275
+ description: "Project name (default: basename of current directory)"
3276
+ },
3277
+ refresh: {
3278
+ type: "boolean",
3279
+ description: "Re-fetch journal files even if the project is already registered",
3280
+ default: false
3281
+ }
3282
+ },
3283
+ async run({ args }) {
3284
+ ui.heading("dora init \u2014 Set up doraval, your journal, and the coding agent dora should use on the fly");
3285
+ ensureGhCliOrExit();
3286
+ let repo = args.repo || process.env.DORAVAL_JOURNAL_REPO;
3287
+ if (!repo) {
3288
+ const gitOwner = getGitRemoteOwner();
3289
+ const ghLogin = ghUser();
3290
+ let defaultRepo;
3291
+ let sourceNote = "";
3292
+ if (gitOwner) {
3293
+ defaultRepo = `${gitOwner}/${gitOwner}.md`;
3294
+ if (ghLogin && ghLogin !== gitOwner) {
3295
+ sourceNote = ` ${import_picocolors12.default.dim("(from git remote; your active gh account is " + ghLogin + ")")}
3296
+ `;
3297
+ } else {
3298
+ sourceNote = ` ${import_picocolors12.default.dim("(from git remote)")}
3299
+ `;
3300
+ }
3301
+ } else if (ghLogin) {
3302
+ defaultRepo = `${ghLogin}/${ghLogin}.md`;
3303
+ sourceNote = ` ${import_picocolors12.default.dim("(from your active gh account)")}
3304
+ `;
3305
+ } else {
3306
+ ui.warn(`Not logged in to GitHub. Run ${import_picocolors12.default.dim("gh auth login")} first.
3307
+ `);
3308
+ process.exit(1);
3309
+ }
3310
+ const existingConfig = await readConfig();
3311
+ if (existingConfig?.journal.repo) {
3312
+ defaultRepo = existingConfig.journal.repo;
3313
+ sourceNote = ` ${import_picocolors12.default.dim("(from your previous journal setup)")}
3314
+ `;
3315
+ }
3316
+ ui.info(` Journal repo ${import_picocolors12.default.dim("(owner/name)")}`);
3317
+ if (sourceNote)
3318
+ ui.write(sourceNote);
3319
+ repo = prompt(" >", defaultRepo);
3320
+ }
3321
+ let project = args.project || process.env.DORAVAL_PROJECT;
3322
+ if (!project) {
3323
+ const defaultProject = basename2(process.cwd());
3324
+ project = prompt(" Project name", defaultProject);
3325
+ }
3326
+ project = sanitizeProjectName(project);
3327
+ if (!repoExists(repo)) {
3328
+ ui.write(` ${import_picocolors12.default.red("\u2717")} ${import_picocolors12.default.white("Repository")} ${import_picocolors12.default.bold(repo)} ${import_picocolors12.default.white("not found on GitHub.")}
3329
+ `);
3330
+ ui.info(` Create it first:
3331
+ `);
3332
+ ui.info(` ${import_picocolors12.default.dim(`gh repo create ${repo} --private --description "Personal journal for agent decisions"`)}
3333
+ `);
3334
+ process.exit(1);
3335
+ }
3336
+ const existing = await readConfig();
3337
+ const alreadyRegistered = existing?.journal.projects[project];
3338
+ const isRefresh = alreadyRegistered && args.refresh;
3339
+ if (alreadyRegistered && !isRefresh) {
3340
+ ui.write(` ${import_picocolors12.default.yellow("\u26A0")} ${import_picocolors12.default.white("Project")} ${import_picocolors12.default.bold(project)} ${import_picocolors12.default.white("is already registered.")}
3341
+ `);
3342
+ ui.info(` Repo: ${existing.journal.repo}
3343
+ `);
3344
+ ui.info(` To refresh journal files, use ${import_picocolors12.default.dim("dora journal update")} (or ${import_picocolors12.default.dim("dora init --refresh")}).
3345
+ `);
3346
+ }
3347
+ const journalsDir = getJournalsDir();
3348
+ const remotePath = `projects/${project}.md`;
3349
+ const localPath = join12(journalsDir, `${project}.md`);
3350
+ const effectiveRepo = isRefresh && !args.repo ? existing.journal.repo : repo;
3351
+ const config = existing ?? {
3352
+ journal: { repo: effectiveRepo, projects: {} }
3353
+ };
3354
+ config.journal.repo = effectiveRepo;
3355
+ config.journal.projects[project] = {
3356
+ remote_path: remotePath,
3357
+ local_path: localPath
3358
+ };
3359
+ ensureDoravalDirs();
3360
+ ui.write(` ${import_picocolors12.default.dim(import_picocolors12.default.gray("Fetching journal files from"))} ${import_picocolors12.default.gray(effectiveRepo)}${import_picocolors12.default.dim(import_picocolors12.default.gray("..."))}
3361
+ `);
3362
+ const globalDest = join12(journalsDir, "global.md");
3363
+ const wroteGlobal = await refreshLocalJournalFile(effectiveRepo, "global.md", globalDest);
3364
+ if (wroteGlobal) {
3365
+ ui.success("global.md");
3366
+ } else {
3367
+ ui.write(` ${import_picocolors12.default.dim("\xB7")} global.md ${import_picocolors12.default.dim("(not found \u2014 will be created on first sync)")}`);
3368
+ await Bun.write(globalDest, `# Global Journal
3369
+
3370
+ Cross-project principles.
3371
+ `);
3372
+ }
3373
+ const wroteProject = await refreshLocalJournalFile(effectiveRepo, remotePath, localPath);
3374
+ if (wroteProject) {
3375
+ ui.success(remotePath);
3376
+ } else {
3377
+ ui.write(` ${import_picocolors12.default.dim("\xB7")} ${remotePath} ${import_picocolors12.default.dim("(not found \u2014 will be created on first sync)")}`);
3378
+ await Bun.write(localPath, `# ${project} Journal
3379
+
3380
+ Project-specific decisions.
3381
+ `);
3382
+ }
3383
+ await writeConfig(config);
3384
+ ui.write(`
3385
+ ${import_picocolors12.default.green("\u2713")} ${import_picocolors12.default.white("Journal ready for project")} ${import_picocolors12.default.bold(import_picocolors12.default.white(project))}.
3386
+ `);
3387
+ const existingAgent = (await readConfig())?.agent;
3388
+ if (existingAgent?.command) {
3389
+ ui.write(` ${import_picocolors12.default.bold(import_picocolors12.default.white("Coding agent (already configured)"))}
3390
+ `);
3391
+ ui.write(` Current: ${import_picocolors12.default.dim(import_picocolors12.default.gray(existingAgent.command))} template: ${import_picocolors12.default.dim(import_picocolors12.default.gray(existingAgent.prompt_template || "(default)"))}
3392
+ `);
3393
+ const change = prompt(" Reconfigure / change the coding agent for on-the-fly enrichment? (y/N)", "n");
3394
+ if (!/^y/i.test(String(change))) {
3395
+ ui.dim(` Keeping existing agent config. You can re-run dora init later to change it.
3396
+ `);
3397
+ const cfg = await readConfig() || { journal: { repo: effectiveRepo, projects: {} } };
3398
+ if (existingAgent)
3399
+ cfg.agent = existingAgent;
3400
+ await writeConfig(cfg);
3401
+ ui.write(` ${import_picocolors12.default.green("\u2713")} ${import_picocolors12.default.white("Try:")} ${import_picocolors12.default.dim(import_picocolors12.default.gray('dora journal add "short decision"'))}
3402
+ `);
3403
+ process.exit(0);
3404
+ return;
3405
+ }
3406
+ ui.blank();
3407
+ } else {
3408
+ ui.write(` ${import_picocolors12.default.bold(import_picocolors12.default.white("Coding agent for journal add"))}
3409
+ `);
3410
+ ui.info(` When configured, ${import_picocolors12.default.dim(import_picocolors12.default.gray('dora journal add ".."'))} will use your agent to enrich entries with tags and rationale automatically.
3411
+ `);
3412
+ }
3413
+ const common = [
3414
+ { name: "claude", template: '-p "{{prompt}}" --output-format json' },
3415
+ { name: "cursor", template: "" }
3416
+ ];
3417
+ let detected = "";
3418
+ for (const c of common) {
3419
+ let probe = spawnSync5(["command", "-v", c.name], { stdout: "pipe", stderr: "pipe" });
3420
+ if (probe.exitCode !== 0) {
3421
+ probe = spawnSync5(["which", c.name], { stdout: "pipe", stderr: "pipe" });
3422
+ }
3423
+ if (probe.exitCode === 0) {
3424
+ detected = c.name;
3425
+ break;
3426
+ }
3427
+ }
3428
+ let agentCmd = detected || "claude";
3429
+ ui.write(` Detected / default agent command: ${import_picocolors12.default.dim(import_picocolors12.default.gray(agentCmd))}`);
3430
+ agentCmd = prompt(" Agent command (the binary you run for prompts)", agentCmd);
3431
+ let template = detected ? common.find((c) => c.name === detected)?.template || '-p "{{prompt}}" --output-format json' : '-p "{{prompt}}" --output-format json';
3432
+ ui.info(` Prompt template (use {{prompt}} placeholder):`);
3433
+ template = prompt(" ", template);
3434
+ const finalConfig = await readConfig() || { journal: { repo: effectiveRepo, projects: {} } };
3435
+ finalConfig.agent = {
3436
+ command: agentCmd,
3437
+ prompt_template: template
3438
+ };
3439
+ await writeConfig(finalConfig);
3440
+ ui.write(`
3441
+ ${import_picocolors12.default.green("\u2713")} ${import_picocolors12.default.white("Agent configured.")}
3442
+ `);
3443
+ ui.info(` Re-run ${import_picocolors12.default.dim(import_picocolors12.default.gray("dora init"))} anytime to change it.
3444
+ `);
3445
+ ui.info(` Next: ${import_picocolors12.default.dim(import_picocolors12.default.gray('dora journal add ".."'))}, ${import_picocolors12.default.dim(import_picocolors12.default.gray("dora journal list"))}, or ${import_picocolors12.default.dim(import_picocolors12.default.gray("dora journal update"))}.
3446
+ `);
3447
+ process.exit(0);
3448
+ }
3449
+ });
3450
+ });
3451
+
3452
+ // src/cli/index.ts
3453
+ init_dist();
3454
+ // package.json
3455
+ var package_default = {
3456
+ name: "@hacksmith/doraval",
3457
+ version: "0.2.11",
3458
+ author: "Saif",
3459
+ repository: {
3460
+ type: "git",
3461
+ url: "git+https://github.com/saif-shines/doraval.git"
3462
+ },
3463
+ devDependencies: {
3464
+ "@types/bun": "latest"
3465
+ },
3466
+ bin: {
3467
+ doraval: "bin/doraval-wrapper.js",
3468
+ dora: "bin/doraval-wrapper.js"
3469
+ },
3470
+ description: "The context engineering toolkit for coding agents",
3471
+ engines: {
3472
+ bun: ">=1.2.0",
3473
+ node: ">=14.18.0"
3474
+ },
3475
+ files: [
3476
+ "bin/",
3477
+ "dist/",
3478
+ "README.md"
3479
+ ],
3480
+ keywords: [
3481
+ "cli",
3482
+ "skills",
3483
+ "plugins",
3484
+ "agent",
3485
+ "validation",
3486
+ "lint",
3487
+ "claude-code",
3488
+ "grok",
3489
+ "cursor",
3490
+ "windsurf",
3491
+ "mcp"
3492
+ ],
3493
+ license: "MIT",
3494
+ workspaces: [
3495
+ "apps/*"
3496
+ ],
3497
+ scripts: {
3498
+ build: "bun build ./src/cli/index.ts --outfile ./bin/doraval.js --target bun",
3499
+ dev: "bun run ./src/cli/index.ts",
3500
+ test: "bun test",
3501
+ prepublishOnly: `bun run build && node -e "const p=require('./package.json'),j=require('./jsr.json');if(p.version!==j.version){console.error('Version mismatch: package.json='+p.version+' jsr.json='+j.version);process.exit(1)}"`,
3502
+ bump: "bun run scripts/bump.ts",
3503
+ publish: "bunx jsr publish",
3504
+ "site:dev": "cd apps/website && bun run dev",
3505
+ "site:build": "cd apps/website && bun run build",
3506
+ "site:preview": "cd apps/website && bun run preview"
3507
+ },
3508
+ type: "module",
3509
+ dependencies: {
3510
+ citty: "^0.2.2",
3511
+ picocolors: "^1.1.1"
3512
+ }
3513
+ };
3514
+
3515
+ // src/cli/index.ts
3516
+ var import_picocolors13 = __toESM(require_picocolors(), 1);
3517
+ var skill = defineCommand({
3518
+ meta: {
3519
+ name: "skill",
3520
+ description: "Validate, measure drift, and judge AI agent skills"
3521
+ },
3522
+ subCommands: {
3523
+ validate: () => Promise.resolve().then(() => (init_validate(), exports_validate)).then((m) => m.default),
3524
+ drift: () => Promise.resolve().then(() => (init_drift(), exports_drift)).then((m) => m.default),
3525
+ judge: () => Promise.resolve().then(() => (init_judge(), exports_judge)).then((m) => m.default)
3526
+ },
3527
+ run() {
3528
+ showUsage(skill);
3529
+ }
3530
+ });
3531
+ var journal = defineCommand({
3532
+ meta: {
3533
+ name: "journal",
3534
+ description: "Decision & note memory (with optional pushback/tags) \u2014 record, view, and sync project principles and useful notes"
3535
+ },
3536
+ subCommands: {
3537
+ init: () => Promise.resolve().then(() => (init_init(), exports_init)).then((m) => m.default),
3538
+ list: () => Promise.resolve().then(() => (init_list(), exports_list)).then((m) => m.default),
3539
+ update: () => Promise.resolve().then(() => (init_update(), exports_update)).then((m) => m.default),
3540
+ add: () => Promise.resolve().then(() => (init_add(), exports_add)).then((m) => m.default),
3541
+ sync: () => Promise.resolve().then(() => (init_sync(), exports_sync)).then((m) => m.default)
3542
+ },
3543
+ run() {
3544
+ showUsage(journal);
3545
+ }
3546
+ });
3547
+ var doraemonArt = `
3548
+ \u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2880\u28E0\u28E4\u28F4\u28F6\u28F6\u28F6\u28F6\u28F6\u2836\u28F6\u28E4\u28E4\u28C0\u2800\u2800\u2800\u2800\u2800\u2800
3549
+ \u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2880\u28E4\u28FE\u28FF\u28FF\u28FF\u2801\u2800\u2880\u2808\u28BF\u2880\u28C0\u2800\u2839\u28FF\u28FF\u28FF\u28E6\u28C4\u2800\u2800\u2800
3550
+ \u2800\u2800\u2800\u2800\u2800\u2800\u28F4\u28FF\u28FF\u28FF\u28FF\u28FF\u283F\u2800\u2800\u28DF\u2847\u2898\u28FE\u28FD\u2800\u2800\u284F\u2809\u2819\u289B\u28FF\u28F7\u2856\u2800
3551
+ \u2800\u2800\u2800\u2800\u2800\u28FE\u28FF\u28FF\u287F\u283F\u2837\u2836\u2824\u2819\u2812\u2800\u2812\u28BB\u28FF\u28FF\u2877\u280B\u2800\u2834\u281E\u280B\u2801\u2899\u28FF\u28C4
3552
+ \u2800\u2800\u2800\u2800\u28B8\u28FF\u28FF\u28EF\u28E4\u28E4\u28E4\u28E4\u28E4\u2844\u2800\u2800\u2800\u2800\u2809\u28B9\u2844\u2800\u2800\u2800\u281B\u281B\u280B\u2809\u2839\u2847
3553
+ \u2800\u2800\u2800\u2800\u28B8\u28FF\u28FF\u2800\u2800\u2800\u28C0\u28E0\u28E4\u28E4\u28E4\u28E4\u28E4\u28E4\u28E4\u28FC\u28C7\u28C0\u28C0\u28C0\u28DB\u28DB\u28D2\u28F2\u28BE\u2877
3554
+ \u2880\u2824\u2812\u2812\u28BC\u28FF\u28FF\u2836\u281E\u28BB\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u287F\u2801\u2800\u28FC\u2803
3555
+ \u28AE\u2800\u2800\u2800\u2800\u28FF\u28FF\u28C6\u2800\u2800\u283B\u28FF\u287F\u281B\u2809\u2809\u2801\u2800\u2809\u2809\u281B\u283F\u28FF\u28FF\u281F\u2801\u2800\u28FC\u2803\u2800
3556
+ \u2808\u2813\u2836\u28F6\u28FE\u28FF\u28FF\u28FF\u28E7\u2840\u2800\u2808\u2812\u28A4\u28C0\u28C0\u2840\u2800\u2800\u28C0\u28C0\u2860\u281A\u2801\u2800\u2880\u287C\u2803\u2800\u2800
3557
+ \u2800\u2800\u2800\u2808\u28BF\u28FF\u28FF\u28FF\u28FF\u28FF\u28F7\u28E4\u28E4\u28E4\u28E4\u28ED\u28ED\u28ED\u28ED\u28ED\u28E5\u28E4\u28E4\u28E4\u28F4\u28DF\u2801
3558
+ `.trim();
3559
+ var main = defineCommand({
3560
+ meta: {
3561
+ name: "doraval",
3562
+ version: package_default.version,
3563
+ description: "The context engineering toolkit for coding agents"
3564
+ },
3565
+ subCommands: {
3566
+ validate: () => Promise.resolve().then(() => (init_validate_top(), exports_validate_top)).then((m) => m.default),
3567
+ init: () => Promise.resolve().then(() => (init_init2(), exports_init2)).then((m) => m.default),
3568
+ skill: () => Promise.resolve(skill),
3569
+ journal: () => Promise.resolve(journal)
3570
+ },
3571
+ run() {
3572
+ console.log(`
3573
+ ` + import_picocolors13.default.blue(doraemonArt) + `
3574
+ `);
3575
+ showUsage(main);
3576
+ }
3577
+ });
3578
+ runMain(main);