dryai 2.1.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +140 -122
  2. package/dest/cli.d.ts +68 -0
  3. package/dest/cli.js +147 -0
  4. package/dest/commands/skills/add.d.ts +4 -3
  5. package/dest/commands/skills/add.js +44 -12
  6. package/dest/commands/skills/index.d.ts +3 -3
  7. package/dest/commands/skills/index.js +19 -12
  8. package/dest/commands/skills/list.d.ts +2 -2
  9. package/dest/commands/skills/list.js +4 -3
  10. package/dest/commands/skills/rehash-all.d.ts +2 -2
  11. package/dest/commands/skills/rehash-all.js +6 -5
  12. package/dest/commands/skills/rehash.d.ts +2 -2
  13. package/dest/commands/skills/rehash.js +3 -2
  14. package/dest/commands/skills/remove.d.ts +2 -2
  15. package/dest/commands/skills/remove.js +3 -2
  16. package/dest/commands/skills/update-all.d.ts +2 -2
  17. package/dest/commands/skills/update-all.js +8 -7
  18. package/dest/commands/skills/update.d.ts +2 -2
  19. package/dest/commands/skills/update.js +6 -5
  20. package/dest/commands/sync.d.ts +6 -0
  21. package/dest/commands/sync.js +8 -0
  22. package/dest/lib/agent-definition-helpers.d.ts +74 -0
  23. package/dest/lib/agent-definition-helpers.js +68 -0
  24. package/dest/lib/agent-definitions.d.ts +333 -0
  25. package/dest/lib/agent-definitions.js +301 -0
  26. package/dest/lib/agent-types.d.ts +46 -0
  27. package/dest/lib/agent-types.js +1 -0
  28. package/dest/lib/agents.d.ts +81 -0
  29. package/dest/lib/agents.js +301 -0
  30. package/dest/lib/command-options.d.ts +1 -1
  31. package/dest/lib/command-options.js +1 -1
  32. package/dest/lib/context.d.ts +8 -25
  33. package/dest/lib/context.js +8 -26
  34. package/dest/lib/frontmatter.d.ts +27 -70
  35. package/dest/lib/frontmatter.js +23 -42
  36. package/dest/lib/object-helpers.d.ts +5 -0
  37. package/dest/lib/object-helpers.js +6 -0
  38. package/dest/lib/skills.d.ts +35 -93
  39. package/dest/lib/skills.js +66 -8
  40. package/dest/lib/sync.d.ts +7 -0
  41. package/dest/lib/sync.js +503 -0
  42. package/dest/main.js +6 -86
  43. package/package.json +3 -3
  44. package/dest/commands/install.d.ts +0 -3
  45. package/dest/commands/install.js +0 -4
  46. package/dest/lib/install.d.ts +0 -8
  47. package/dest/lib/install.js +0 -380
@@ -0,0 +1,503 @@
1
+ import { Chalk } from 'chalk';
2
+ import fs from 'fs-extra';
3
+ import { glob } from 'glob';
4
+ import path from 'node:path';
5
+ import { z } from 'zod';
6
+ import { buildSyncTargets, createAgentCmdSyncSpec, createOwnershipKey, createAgentRuleSyncSpec, describeOwnershipKey, getAgentLabel, isSyncAgent, listTargetRootPaths, SYNC_AGENTS, SYNC_ITEM_KINDS, } from './agents.js';
7
+ import { commandFrontmatterSchema, parseFrontmatter, renderMarkdown, ruleFrontmatterSchema, validateFrontmatter, } from './frontmatter.js';
8
+ const chalk = new Chalk({ level: 3 });
9
+ const syncAgentSchema = z.custom((value) => typeof value === 'string' && isSyncAgent(value), {
10
+ message: 'Expected one configured sync agent.',
11
+ });
12
+ const syncManifestEntrySchema = z.object({
13
+ agent: syncAgentSchema,
14
+ kind: z.enum(SYNC_ITEM_KINDS),
15
+ name: z.string().min(1),
16
+ outputPath: z.string().min(1),
17
+ });
18
+ const syncManifestSchema = z.object({
19
+ version: z.literal(2),
20
+ outputs: z.array(syncManifestEntrySchema),
21
+ });
22
+ /**
23
+ * Validates and returns the agent name from a sync target, throwing if it is unrecognized.
24
+ */
25
+ function parseSyncAgent(agent) {
26
+ if (isSyncAgent(agent)) {
27
+ return agent;
28
+ }
29
+ throw new Error(`Unsupported sync agent: ${agent}`);
30
+ }
31
+ /**
32
+ * Writes all command, rule, and skill outputs to their target directories, then prunes any stale dryai-managed files from prior runs.
33
+ */
34
+ export async function syncToTargets(context, runtime) {
35
+ const { targetRoots } = context;
36
+ await ensureTargetDirectories(targetRoots);
37
+ const previousManifest = await loadSyncManifest(context.syncManifestPath);
38
+ const syncItems = [
39
+ ...(await collectCommandSyncItems(context, runtime)),
40
+ ...(await collectRuleSyncItems(context, runtime)),
41
+ ...(await collectSkillSyncItems(context)),
42
+ ];
43
+ const { syncableItems, skippedItems } = collectConflictFilterResult(syncItems);
44
+ const skippedOwnershipKeys = collectSkippedOwnershipKeys(skippedItems);
45
+ const desiredManifestEntries = collectManifestEntries(syncableItems);
46
+ const desiredOutputPaths = new Set(desiredManifestEntries.map((entry) => entry.outputPath));
47
+ const removedEntries = collectRemovedManifestEntries(previousManifest.outputs, {
48
+ desiredOutputPaths,
49
+ skippedOwnershipKeys,
50
+ });
51
+ await removeStaleOutputs(removedEntries);
52
+ const appliedItems = [];
53
+ for (const syncItem of syncableItems) {
54
+ appliedItems.push(await applySyncItem(syncItem));
55
+ }
56
+ const preservedEntries = collectPreservedManifestEntries(previousManifest.outputs, {
57
+ desiredOutputPaths,
58
+ skippedOwnershipKeys,
59
+ });
60
+ await saveSyncManifest(context.syncManifestPath, createSyncManifest([...desiredManifestEntries, ...preservedEntries]));
61
+ runtime.logInfo(renderSyncReport(appliedItems, removedEntries, skippedItems));
62
+ }
63
+ /**
64
+ * Derives the ownership key claimed by one sync target for conflict detection.
65
+ */
66
+ function deriveOwnershipKeyForSyncTarget(syncItem, target) {
67
+ return createOwnershipKey(parseSyncAgent(target.agent), syncItem.kind, {
68
+ name: syncItem.name,
69
+ outputPath: target.outputPath,
70
+ });
71
+ }
72
+ /**
73
+ * Returns the ownership key for a saved manifest entry.
74
+ */
75
+ function deriveOwnershipKeyForManifestEntry(manifestEntry) {
76
+ return createOwnershipKey(manifestEntry.agent, manifestEntry.kind, {
77
+ name: manifestEntry.name,
78
+ outputPath: manifestEntry.outputPath,
79
+ });
80
+ }
81
+ /**
82
+ * Ensures that all target root directories exist before generated files are written.
83
+ */
84
+ async function ensureTargetDirectories(targetRoots) {
85
+ await Promise.all(listTargetRootPaths(targetRoots).map(fs.ensureDir));
86
+ }
87
+ /**
88
+ * Reads the sync manifest from disk, or returns an empty manifest if none exists yet.
89
+ */
90
+ async function loadSyncManifest(manifestPath) {
91
+ if (!(await fs.pathExists(manifestPath))) {
92
+ return createSyncManifest([]);
93
+ }
94
+ const rawManifest = await fs.readFile(manifestPath, 'utf8');
95
+ const parsedManifest = JSON.parse(rawManifest);
96
+ return syncManifestSchema.parse(parsedManifest);
97
+ }
98
+ /**
99
+ * Serializes and writes the sync manifest to manifestPath.
100
+ */
101
+ async function saveSyncManifest(manifestPath, manifest) {
102
+ await fs.ensureDir(path.dirname(manifestPath));
103
+ await fs.writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
104
+ }
105
+ /**
106
+ * Creates a normalized sync manifest with deterministic output ordering.
107
+ */
108
+ function createSyncManifest(entries) {
109
+ const entriesByOutputPath = new Map();
110
+ for (const entry of entries) {
111
+ entriesByOutputPath.set(entry.outputPath, entry);
112
+ }
113
+ return {
114
+ version: 2,
115
+ outputs: [...entriesByOutputPath.values()].sort(compareManifestEntries),
116
+ };
117
+ }
118
+ /**
119
+ * Returns the markdown source files found directly under a source root.
120
+ */
121
+ async function collectMarkdownFiles(rootDir) {
122
+ await fs.ensureDir(rootDir);
123
+ const matches = await glob([path.join(rootDir, '*.md')]);
124
+ return matches.sort();
125
+ }
126
+ /**
127
+ * Writes one markdown file after rendering its frontmatter and body content.
128
+ */
129
+ async function writeMarkdownFile(filePath, metadata, body) {
130
+ await fs.ensureDir(path.dirname(filePath));
131
+ await fs.writeFile(filePath, renderMarkdown({ metadata, body }), 'utf8');
132
+ }
133
+ /**
134
+ * Detects whether each agent target for one item will be installed or updated.
135
+ */
136
+ async function detectSyncChanges(syncItem) {
137
+ return Promise.all(syncItem.targets.map(async (target) => ({
138
+ agent: parseSyncAgent(target.agent),
139
+ changeType: (await fs.pathExists(target.outputPath))
140
+ ? 'updated'
141
+ : 'installed',
142
+ })));
143
+ }
144
+ /**
145
+ * Applies one sync item and records the change type for each agent target.
146
+ */
147
+ async function applySyncItem(syncItem) {
148
+ const changes = await detectSyncChanges(syncItem);
149
+ await writeSyncItem(syncItem);
150
+ return {
151
+ item: syncItem,
152
+ changes,
153
+ };
154
+ }
155
+ /**
156
+ * Writes one sync target to its output path, either as a markdown file or a directory copy.
157
+ */
158
+ async function writeSyncTarget(target) {
159
+ if (target.targetType === 'markdown') {
160
+ await writeMarkdownFile(target.writePath, target.metadata, target.body);
161
+ return;
162
+ }
163
+ await copyDirectoryContents(target.sourceDir, target.outputPath);
164
+ }
165
+ /**
166
+ * Writes one sync item to all of its target outputs.
167
+ */
168
+ async function writeSyncItem(syncItem) {
169
+ for (const target of syncItem.targets) {
170
+ await writeSyncTarget(target);
171
+ }
172
+ }
173
+ /**
174
+ * Collects sync operations for command sources after validating their frontmatter.
175
+ */
176
+ async function collectCommandSyncItems(context, runtime) {
177
+ const { targetRoots } = context;
178
+ const commandFiles = await collectMarkdownFiles(context.sourceRoots.commands);
179
+ const syncItems = [];
180
+ for (const filePath of commandFiles) {
181
+ const fileName = path.basename(filePath, '.md');
182
+ const rawContent = await fs.readFile(filePath, 'utf8');
183
+ const { metadata, body } = parseFrontmatter(rawContent);
184
+ const commandMetadata = validateFrontmatter(runtime, {
185
+ filePath,
186
+ metadata,
187
+ schema: commandFrontmatterSchema,
188
+ });
189
+ if (!commandMetadata) {
190
+ continue;
191
+ }
192
+ const commandName = commandMetadata.name;
193
+ const commandInput = createAgentCmdSyncSpec(runtime, {
194
+ filePath,
195
+ sourceFileStem: fileName,
196
+ body,
197
+ frontmatter: commandMetadata,
198
+ });
199
+ if (!commandInput) {
200
+ continue;
201
+ }
202
+ syncItems.push({
203
+ kind: 'command',
204
+ name: commandName,
205
+ sourcePath: filePath,
206
+ targets: buildSyncTargets({
207
+ kind: 'command',
208
+ input: commandInput,
209
+ targetRoots,
210
+ }),
211
+ });
212
+ }
213
+ return syncItems;
214
+ }
215
+ /**
216
+ * Collects sync operations for rule sources after validating their frontmatter.
217
+ */
218
+ async function collectRuleSyncItems(context, runtime) {
219
+ const { targetRoots } = context;
220
+ const ruleFiles = await collectMarkdownFiles(context.sourceRoots.rules);
221
+ const syncItems = [];
222
+ for (const filePath of ruleFiles) {
223
+ const fileName = path.basename(filePath, '.md');
224
+ const rawContent = await fs.readFile(filePath, 'utf8');
225
+ const { metadata, body } = parseFrontmatter(rawContent);
226
+ const ruleMetadata = validateFrontmatter(runtime, {
227
+ filePath,
228
+ metadata,
229
+ schema: ruleFrontmatterSchema,
230
+ });
231
+ if (!ruleMetadata) {
232
+ continue;
233
+ }
234
+ const ruleInput = createAgentRuleSyncSpec(runtime, {
235
+ filePath,
236
+ sourceFileStem: fileName,
237
+ body,
238
+ frontmatter: ruleMetadata,
239
+ });
240
+ if (!ruleInput) {
241
+ continue;
242
+ }
243
+ syncItems.push({
244
+ kind: 'rule',
245
+ name: fileName,
246
+ sourcePath: filePath,
247
+ targets: buildSyncTargets({
248
+ kind: 'rule',
249
+ input: ruleInput,
250
+ targetRoots,
251
+ }),
252
+ });
253
+ }
254
+ return syncItems;
255
+ }
256
+ /**
257
+ * Collects sync operations for local skill directories.
258
+ */
259
+ async function collectSkillSyncItems(context) {
260
+ const { targetRoots } = context;
261
+ await fs.ensureDir(context.sourceRoots.skills);
262
+ const entries = await fs.readdir(context.sourceRoots.skills, {
263
+ withFileTypes: true,
264
+ });
265
+ const syncItems = [];
266
+ for (const entry of entries) {
267
+ if (!entry.isDirectory()) {
268
+ continue;
269
+ }
270
+ const sourceDir = path.join(context.sourceRoots.skills, entry.name);
271
+ syncItems.push({
272
+ kind: 'skill',
273
+ name: entry.name,
274
+ sourcePath: sourceDir,
275
+ targets: buildSyncTargets({
276
+ kind: 'skill',
277
+ input: {
278
+ name: entry.name,
279
+ sourceDir,
280
+ },
281
+ targetRoots,
282
+ }),
283
+ });
284
+ }
285
+ return syncItems;
286
+ }
287
+ /**
288
+ * Clears targetDir and copies all direct entries from sourceDir into it.
289
+ */
290
+ async function copyDirectoryContents(sourceDir, targetDir) {
291
+ await fs.emptyDir(targetDir);
292
+ const entryNames = await fs.readdir(sourceDir);
293
+ for (const entryName of entryNames) {
294
+ await fs.copy(path.join(sourceDir, entryName), path.join(targetDir, entryName));
295
+ }
296
+ }
297
+ /**
298
+ * Collects the syncable and skipped items after analyzing output namespace conflicts.
299
+ */
300
+ function collectConflictFilterResult(items) {
301
+ const ownershipMap = new Map();
302
+ for (const item of items) {
303
+ for (const ownershipKey of collectOwnershipKeys(item)) {
304
+ const existingOwners = ownershipMap.get(ownershipKey);
305
+ if (existingOwners) {
306
+ existingOwners.push(item);
307
+ }
308
+ else {
309
+ ownershipMap.set(ownershipKey, [item]);
310
+ }
311
+ }
312
+ }
313
+ const skippedItemsBySourcePath = new Map();
314
+ for (const [ownershipKey, owners] of ownershipMap) {
315
+ if (owners.length < 2) {
316
+ continue;
317
+ }
318
+ const conflictDescription = describeOwnershipKey(ownershipKey);
319
+ for (const owner of owners) {
320
+ const existingSkippedItem = skippedItemsBySourcePath.get(owner.sourcePath);
321
+ if (existingSkippedItem) {
322
+ existingSkippedItem.conflictDescriptions.push(conflictDescription);
323
+ }
324
+ else {
325
+ skippedItemsBySourcePath.set(owner.sourcePath, {
326
+ item: owner,
327
+ conflictDescriptions: [conflictDescription],
328
+ });
329
+ }
330
+ }
331
+ }
332
+ const skippedItems = [...skippedItemsBySourcePath.values()].map((skippedItem) => ({
333
+ item: skippedItem.item,
334
+ conflictDescriptions: [
335
+ ...new Set(skippedItem.conflictDescriptions),
336
+ ].sort(),
337
+ }));
338
+ const skippedSourcePaths = new Set(skippedItems.map((skippedItem) => skippedItem.item.sourcePath));
339
+ return {
340
+ syncableItems: items.filter((item) => !skippedSourcePaths.has(item.sourcePath)),
341
+ skippedItems,
342
+ };
343
+ }
344
+ /**
345
+ * Returns the ownership keys used to detect namespace conflicts for one sync item.
346
+ */
347
+ function collectOwnershipKeys(syncItem) {
348
+ return syncItem.targets.map((target) => deriveOwnershipKeyForSyncTarget(syncItem, target));
349
+ }
350
+ /**
351
+ * Returns the set of ownership keys whose items were skipped due to conflicts.
352
+ */
353
+ function collectSkippedOwnershipKeys(skippedItems) {
354
+ const ownershipKeys = skippedItems.flatMap((skippedItem) => collectOwnershipKeys(skippedItem.item));
355
+ return new Set(ownershipKeys);
356
+ }
357
+ /**
358
+ * Converts sync items into manifest entries for the current desired outputs.
359
+ */
360
+ function collectManifestEntries(syncItems) {
361
+ return syncItems.flatMap((syncItem) => syncItem.targets.map((target) => ({
362
+ agent: parseSyncAgent(target.agent),
363
+ kind: syncItem.kind,
364
+ name: syncItem.name,
365
+ outputPath: target.outputPath,
366
+ })));
367
+ }
368
+ /**
369
+ * Returns the manifest entries that should be removed because they are no longer desired.
370
+ */
371
+ function collectRemovedManifestEntries(manifestEntries, input) {
372
+ return manifestEntries.filter((entry) => !input.desiredOutputPaths.has(entry.outputPath) &&
373
+ !input.skippedOwnershipKeys.has(deriveOwnershipKeyForManifestEntry(entry)));
374
+ }
375
+ /**
376
+ * Returns manifest entries for skipped source items due to a conflict.
377
+ */
378
+ function collectPreservedManifestEntries(manifestEntries, input) {
379
+ return manifestEntries.filter((entry) => !input.desiredOutputPaths.has(entry.outputPath) &&
380
+ input.skippedOwnershipKeys.has(deriveOwnershipKeyForManifestEntry(entry)));
381
+ }
382
+ /**
383
+ * Removes stale dryai-managed outputs that are no longer part of the desired sync state.
384
+ */
385
+ async function removeStaleOutputs(removedEntries) {
386
+ for (const entry of removedEntries) {
387
+ await fs.remove(entry.outputPath);
388
+ }
389
+ }
390
+ /**
391
+ * Renders a sync summary grouped by agent, item kind, and skipped conflicts.
392
+ */
393
+ function renderSyncReport(appliedItems, removedEntries, skippedItems) {
394
+ const sections = [chalk.bold.cyan('Applied changes:')];
395
+ for (const agent of SYNC_AGENTS) {
396
+ sections.push(renderAgentSyncSection(getAgentLabel(agent), collectAgentReportedSyncChanges(appliedItems, removedEntries, agent)));
397
+ }
398
+ if (skippedItems.length === 0) {
399
+ sections.push(`${chalk.bold.green('Skipped conflicts:')} ${chalk.green('None')}`);
400
+ }
401
+ else {
402
+ const skippedLines = skippedItems
403
+ .slice()
404
+ .sort((left, right) => formatSyncItemLabel(left.item).localeCompare(formatSyncItemLabel(right.item)))
405
+ .map((skippedItem) => [
406
+ `- ${chalk.red(formatSyncItemLabel(skippedItem.item))}`,
407
+ ` * ${chalk.bold.red('due to:')} ${chalk.yellow(skippedItem.conflictDescriptions.join(', '))}`,
408
+ ].join('\n'));
409
+ sections.push(`${chalk.bold.red('Skipped conflicts:')}\n${skippedLines.join('\n')}`);
410
+ }
411
+ return sections.join('\n\n');
412
+ }
413
+ /**
414
+ * Collects the reported sync changes relevant to one agent.
415
+ */
416
+ function collectAgentReportedSyncChanges(appliedItems, removedEntries, agent) {
417
+ const appliedChanges = appliedItems.flatMap((appliedItem) => appliedItem.changes
418
+ .filter((change) => change.agent === agent)
419
+ .map((change) => ({
420
+ kind: appliedItem.item.kind,
421
+ name: appliedItem.item.name,
422
+ changeType: change.changeType,
423
+ })));
424
+ const removedChanges = removedEntries
425
+ .filter((entry) => entry.agent === agent)
426
+ .map((entry) => ({
427
+ kind: entry.kind,
428
+ name: entry.name,
429
+ changeType: 'removed',
430
+ }));
431
+ return [...appliedChanges, ...removedChanges];
432
+ }
433
+ /**
434
+ * Renders the synced items for one agent grouped by item kind.
435
+ */
436
+ function renderAgentSyncSection(agentLabel, reportedChanges) {
437
+ const kindSections = [
438
+ renderKindSyncLine('commands', 'command', reportedChanges),
439
+ renderKindSyncLine('rules', 'rule', reportedChanges),
440
+ renderKindSyncLine('skills', 'skill', reportedChanges),
441
+ ].filter((section) => section !== undefined);
442
+ return [`- ${colorAgentLabel(agentLabel)}`, ...kindSections].join('\n');
443
+ }
444
+ /**
445
+ * Renders one sync summary section for a specific item kind.
446
+ */
447
+ function renderKindSyncLine(label, kind, reportedChanges) {
448
+ const matchingChanges = reportedChanges
449
+ .filter((item) => item.kind === kind)
450
+ .slice()
451
+ .sort((left, right) => left.name.localeCompare(right.name));
452
+ if (matchingChanges.length === 0) {
453
+ return undefined;
454
+ }
455
+ return [
456
+ ` * ${colorKindLabel(label)}`,
457
+ ...matchingChanges.map(renderReportedSyncChangeLine),
458
+ ].join('\n');
459
+ }
460
+ /**
461
+ * Returns the styled agent label used in the sync summary.
462
+ */
463
+ function colorAgentLabel(agentLabel) {
464
+ return chalk.bold.blue(agentLabel);
465
+ }
466
+ /**
467
+ * Returns the styled item-kind label used in the sync summary.
468
+ */
469
+ function colorKindLabel(label) {
470
+ return chalk.bold.yellow(label);
471
+ }
472
+ /**
473
+ * Returns the styled change-type label used in the sync summary.
474
+ */
475
+ function colorChangeType(changeType) {
476
+ if (changeType === 'installed') {
477
+ return chalk.green(changeType);
478
+ }
479
+ if (changeType === 'removed') {
480
+ return chalk.red(changeType);
481
+ }
482
+ return chalk.yellow(changeType);
483
+ }
484
+ /**
485
+ * Renders one styled applied-item line in the sync summary.
486
+ */
487
+ function renderReportedSyncChangeLine(reportedChange) {
488
+ return ` - ${chalk.whiteBright(reportedChange.name)} (${colorChangeType(reportedChange.changeType)})`;
489
+ }
490
+ /**
491
+ * Returns a readable label for one sync item in conflict warnings.
492
+ */
493
+ function formatSyncItemLabel(item) {
494
+ return `${item.kind} "${item.name}" from ${item.sourcePath}`;
495
+ }
496
+ /**
497
+ * Orders manifest entries deterministically for stable on-disk state.
498
+ */
499
+ function compareManifestEntries(left, right) {
500
+ return [left.agent, left.kind, left.name, left.outputPath]
501
+ .join('\0')
502
+ .localeCompare([right.agent, right.kind, right.name, right.outputPath].join('\0'));
503
+ }
package/dest/main.js CHANGED
@@ -1,18 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import { Command } from 'commander';
3
2
  import fs from 'node:fs/promises';
4
3
  import path from 'node:path';
5
4
  import { fileURLToPath } from 'node:url';
6
5
  import { z } from 'zod';
7
- import { runInstallCommand } from './commands/install.js';
8
- import { addSkillsCommand } from './commands/skills/index.js';
9
- import { nonEmptyOptionStringSchema, parseOptionValue, parseOptionsObject, } from './lib/command-options.js';
10
- import { createAgentsContext, resolveRequestedConfigRoot, resolveRequestedOutputRoot, } from './lib/context.js';
11
- const rootOptionsSchema = z.object({
12
- test: z.boolean().optional().default(false),
13
- configRoot: nonEmptyOptionStringSchema.optional(),
14
- outputRoot: nonEmptyOptionStringSchema.optional(),
15
- });
6
+ import { runCLI } from './cli.js';
16
7
  const EXECUTABLE_NAME = 'dryai';
17
8
  /**
18
9
  * Reads the CLI version from the package manifest at the repository root.
@@ -28,85 +19,14 @@ async function readCliVersion() {
28
19
  return packageJsonSchema.parse(parsedPackageJson).version;
29
20
  }
30
21
  /**
31
- * Parses the top-level CLI options into a validated shape.
32
- */
33
- function getRootOptions(program) {
34
- return parseOptionsObject({
35
- schema: rootOptionsSchema,
36
- options: program.opts(),
37
- optionsLabel: 'root options',
38
- });
39
- }
40
- /**
41
- * Returns the active context after applying root-level input and output
42
- * overrides.
43
- */
44
- function resolveActiveContext(program) {
45
- const rootOptions = getRootOptions(program);
46
- const requestedConfigRoot = resolveRequestedConfigRoot({
47
- ...(rootOptions.configRoot ? { configRoot: rootOptions.configRoot } : {}),
48
- });
49
- const requestedOutputRoot = resolveRequestedOutputRoot({
50
- test: rootOptions.test,
51
- ...(rootOptions.outputRoot ? { outputRoot: rootOptions.outputRoot } : {}),
52
- });
53
- const context = createAgentsContext({
54
- ...(requestedConfigRoot ? { inputRoot: requestedConfigRoot } : {}),
55
- ...(requestedOutputRoot ? { outputRoot: requestedOutputRoot } : {}),
56
- });
57
- return context;
58
- }
59
- /**
60
- * Configures and runs the agents CLI entrypoint.
22
+ * Configures and runs the executable CLI entrypoint with the production CLI options.
61
23
  */
62
24
  async function main() {
63
- const program = new Command();
64
- const cliVersion = await readCliVersion();
65
- program
66
- .name(EXECUTABLE_NAME)
67
- .usage('[options] <command> [args]')
68
- .helpOption('-h, --help', 'Display this message')
69
- .version(cliVersion, '-v, --version', 'Display the current version')
70
- .option('--test', 'Shortcut for writing generated output into ./output-test unless --output-root is also provided')
71
- .option('--config-root <path>', 'Read configs from a different root instead of ~/.config/dryai', parseOptionValue({
72
- schema: nonEmptyOptionStringSchema,
73
- optionLabel: '--config-root',
74
- }))
75
- .option('--output-root <path>', 'Write generated output under a different root instead of the default home directory', parseOptionValue({
76
- schema: nonEmptyOptionStringSchema,
77
- optionLabel: '--output-root',
78
- }))
79
- .helpCommand(false)
80
- .action(() => {
81
- program.outputHelp();
82
- });
83
- program
84
- .command('install')
85
- .description('Install generated output into Copilot and Cursor targets')
86
- .usage('install')
87
- .action(async () => {
88
- const activeContext = resolveActiveContext(program);
89
- await runInstallCommand(activeContext);
90
- if (requestedOutputRootWasUsed(program)) {
91
- console.log(`Generated output written to ${activeContext.outputRoot}`);
92
- }
93
- });
94
- addSkillsCommand({
95
- parent: program,
96
- commandName: `${EXECUTABLE_NAME} skills`,
97
- resolveContext: () => resolveActiveContext(program),
25
+ await runCLI({
26
+ argv: process.argv.slice(2),
27
+ executableName: EXECUTABLE_NAME,
28
+ version: await readCliVersion(),
98
29
  });
99
- await program.parseAsync(process.argv);
100
- }
101
- /**
102
- * Returns whether the current invocation requested a non-default output root.
103
- */
104
- function requestedOutputRootWasUsed(program) {
105
- const rootOptions = getRootOptions(program);
106
- return (resolveRequestedOutputRoot({
107
- test: rootOptions.test,
108
- ...(rootOptions.outputRoot ? { outputRoot: rootOptions.outputRoot } : {}),
109
- }) !== undefined);
110
30
  }
111
31
  try {
112
32
  await main();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dryai",
3
- "version": "2.1.0",
4
- "description": "CLI for installing shared AI commands, rules, and skills into Copilot and Cursor.",
3
+ "version": "3.0.0",
4
+ "description": "CLI for syncing shared AI commands, rules, and skills into supported agent targets.",
5
5
  "type": "module",
6
6
  "repository": {
7
7
  "type": "git",
@@ -35,7 +35,7 @@
35
35
  "glob": "^13.0.6",
36
36
  "gray-matter": "^4.0.3",
37
37
  "simple-git": "^3.35.2",
38
- "zod": "^3.24.1"
38
+ "zod": "^4.3.6"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@commitlint/cli": "^20.5.0",
@@ -1,3 +0,0 @@
1
- import type { AgentsContext } from '../lib/context.js';
2
- export declare function runInstallCommand(context: AgentsContext): Promise<void>;
3
- //# sourceMappingURL=install.d.ts.map
@@ -1,4 +0,0 @@
1
- import { installToTargets } from '../lib/install.js';
2
- export async function runInstallCommand(context) {
3
- await installToTargets(context, { targetRoots: context.targetRoots });
4
- }
@@ -1,8 +0,0 @@
1
- import type { AgentsContext, TargetRoots } from './context.js';
2
- /**
3
- * Installs all generated command, rule, and skill outputs into the requested targets.
4
- */
5
- export declare function installToTargets(context: AgentsContext, { targetRoots }: {
6
- targetRoots: TargetRoots;
7
- }): Promise<void>;
8
- //# sourceMappingURL=install.d.ts.map