@agentrules/cli 0.0.10 → 0.0.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.
Files changed (3) hide show
  1. package/README.md +56 -15
  2. package/dist/index.js +281 -248
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -63,7 +63,7 @@ agentrules add agentic-dev-starter --platform opencode --dry-run
63
63
 
64
64
  ### `agentrules init [directory]`
65
65
 
66
- Initialize a new preset.
66
+ Initialize a preset config in a platform directory. The command guides you through the required fields for publishing.
67
67
 
68
68
  ```bash
69
69
  agentrules init [directory] [options]
@@ -73,7 +73,7 @@ agentrules init [directory] [options]
73
73
 
74
74
  | Option | Description |
75
75
  |--------|-------------|
76
- | `-n, --name <name>` | Preset name (default: directory name, or `my-preset`) |
76
+ | `-n, --name <name>` | Preset name (default: `my-preset`) |
77
77
  | `-t, --title <title>` | Display title |
78
78
  | `--description <text>` | Preset description |
79
79
  | `-p, --platform <platform>` | Target platform |
@@ -84,30 +84,69 @@ agentrules init [directory] [options]
84
84
  **Examples:**
85
85
 
86
86
  ```bash
87
- # Create a new preset directory and initialize (interactive prompts)
88
- agentrules init my-preset
89
- cd my-preset
90
-
91
- # Initialize in current directory
87
+ # Initialize in your existing platform directory
88
+ cd .opencode
92
89
  agentrules init
93
90
 
94
- # Set defaults for prompts
95
- agentrules init my-preset --name awesome-rules --platform opencode
91
+ # Initialize in a specific platform directory
92
+ agentrules init .claude
96
93
 
97
94
  # Accept all defaults, skip prompts
98
- agentrules init my-preset --yes
95
+ agentrules init .opencode --yes
96
+ ```
97
+
98
+ After running `init`, your preset structure is:
99
+
100
+ ```
101
+ .opencode/
102
+ ├── agentrules.json # Preset config (created by init)
103
+ ├── AGENTS.md # Your config files (included in bundle)
104
+ ├── commands/
105
+ │ └── review.md
106
+ └── .agentrules/ # Optional metadata folder
107
+ ├── README.md # Shown on registry page
108
+ ├── LICENSE.md # Full license text
109
+ └── INSTALL.txt # Shown after install
110
+ ```
111
+
112
+ ### Preset Config Fields
113
+
114
+ | Field | Required | Description |
115
+ |-------|----------|-------------|
116
+ | `name` | Yes | URL-safe identifier (lowercase, hyphens) |
117
+ | `title` | Yes | Display name |
118
+ | `description` | Yes | Short description (max 500 chars) |
119
+ | `license` | Yes | SPDX license identifier (e.g., `MIT`) |
120
+ | `platform` | Yes | Target platform: `opencode`, `claude`, `cursor`, `codex` |
121
+ | `version` | No | Major version (default: 1) |
122
+ | `tags` | No | Up to 10 tags for discoverability |
123
+ | `features` | No | Up to 5 key features to highlight |
124
+ | `ignore` | No | Additional patterns to exclude from bundle |
125
+
126
+ ### Auto-Excluded Files
127
+
128
+ These files are automatically excluded from bundles:
129
+ - `node_modules/`, `.git/`, `.DS_Store`
130
+ - Lock files: `package-lock.json`, `bun.lockb`, `pnpm-lock.yaml`, `*.lock`
131
+
132
+ Use the `ignore` field for additional exclusions:
133
+
134
+ ```json
135
+ {
136
+ "ignore": ["*.log", "test-fixtures", "*.tmp"]
137
+ }
99
138
  ```
100
139
 
101
140
  ### `agentrules validate [path]`
102
141
 
103
- Validate a preset configuration.
142
+ Validate a preset configuration before publishing.
104
143
 
105
144
  ```bash
106
145
  # Validate current directory
107
146
  agentrules validate
108
147
 
109
148
  # Validate a specific path
110
- agentrules validate ./my-preset
149
+ agentrules validate .opencode
111
150
  ```
112
151
 
113
152
  ---
@@ -161,14 +200,16 @@ agentrules publish --dry-run
161
200
 
162
201
  **Versioning:** Presets use `MAJOR.MINOR` versioning. You set the major version, and the registry auto-increments the minor version on each publish.
163
202
 
164
- ### `agentrules unpublish <name>`
203
+ ### `agentrules unpublish <slug> <platform> <version>`
165
204
 
166
- Remove a preset from the registry. Requires authentication.
205
+ Remove a specific version of a preset from the registry. Requires authentication.
167
206
 
168
207
  ```bash
169
- agentrules unpublish my-preset
208
+ agentrules unpublish my-preset opencode 1.0
170
209
  ```
171
210
 
211
+ **Note:** Unpublished versions cannot be republished with the same version number.
212
+
172
213
  ---
173
214
 
174
215
  ## Registry Management
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from "module";
3
+ import { AGENT_RULES_DIR, API_ENDPOINTS, COMMON_LICENSES, LATEST_VERSION, PLATFORMS, PLATFORM_IDS, PRESET_CONFIG_FILENAME, PRESET_SCHEMA_URL, STATIC_BUNDLE_DIR, buildPresetPublishInput, buildPresetRegistry, createDiffPreview, decodeBundledFile, descriptionSchema, fetchBundle, getPlatformFromDir, isLikelyText, isPlatformDir, isSupportedPlatform, licenseSchema, normalizeBundlePath, normalizePlatformInput, resolvePreset, slugSchema, titleSchema, toUtf8String, validatePresetConfig, verifyBundledFileChecksum } from "@agentrules/core";
3
4
  import { Command } from "commander";
4
5
  import { basename, dirname, join, relative, resolve, sep } from "path";
5
6
  import { exec } from "child_process";
6
7
  import { promisify } from "util";
7
- import { API_ENDPOINTS, COMMON_LICENSES, CONFIG_DIR_NAME, LATEST_VERSION, PLATFORMS, PLATFORM_IDS, PRESET_CONFIG_FILENAME, PRESET_SCHEMA_URL, STATIC_BUNDLE_DIR, buildPresetPublishInput, buildPresetRegistry, createDiffPreview, decodeBundledFile, descriptionSchema, fetchBundle, isLikelyText, isSupportedPlatform, licenseSchema, normalizeBundlePath, normalizePlatformInput, resolvePreset, slugSchema, titleSchema, toUtf8String, validatePresetConfig, verifyBundledFileChecksum } from "@agentrules/core";
8
8
  import * as client from "openid-client";
9
9
  import chalk from "chalk";
10
10
  import { chmod, constants } from "fs";
@@ -575,6 +575,19 @@ function formatPollingError(err) {
575
575
  return "Lost connection to server. Please try again.";
576
576
  }
577
577
 
578
+ //#endregion
579
+ //#region src/lib/errors.ts
580
+ /**
581
+ * Error utilities
582
+ */
583
+ /**
584
+ * Safely extract an error message from any error type.
585
+ */
586
+ function getErrorMessage(error$2) {
587
+ if (error$2 instanceof Error) return error$2.message;
588
+ return String(error$2);
589
+ }
590
+
578
591
  //#endregion
579
592
  //#region src/lib/api/presets.ts
580
593
  /**
@@ -608,10 +621,9 @@ async function publishPreset(baseUrl, token, input) {
608
621
  data
609
622
  };
610
623
  } catch (error$2) {
611
- const message = error$2 instanceof Error ? error$2.message : String(error$2);
612
624
  return {
613
625
  success: false,
614
- error: `Failed to connect to registry: ${message}`
626
+ error: `Failed to connect to registry: ${getErrorMessage(error$2)}`
615
627
  };
616
628
  }
617
629
  }
@@ -640,10 +652,9 @@ async function unpublishPreset(baseUrl, token, slug, platform, version$1) {
640
652
  data
641
653
  };
642
654
  } catch (error$2) {
643
- const message = error$2 instanceof Error ? error$2.message : String(error$2);
644
655
  return {
645
656
  success: false,
646
- error: `Failed to connect to registry: ${message}`
657
+ error: `Failed to connect to registry: ${getErrorMessage(error$2)}`
647
658
  };
648
659
  }
649
660
  }
@@ -673,6 +684,7 @@ async function fetchSession(baseUrl, token) {
673
684
 
674
685
  //#endregion
675
686
  //#region src/lib/config.ts
687
+ /** Directory for CLI configuration and credentials (e.g., ~/.agentrules/) */
676
688
  const CONFIG_DIRNAME = ".agentrules";
677
689
  const CONFIG_FILENAME = "config.json";
678
690
  const CONFIG_HOME_ENV = "AGENT_RULES_HOME";
@@ -867,7 +879,7 @@ async function loadStore() {
867
879
  const store = JSON.parse(raw);
868
880
  return store;
869
881
  } catch (error$2) {
870
- log.debug(`Failed to load credentials: ${error$2 instanceof Error ? error$2.message : String(error$2)}`);
882
+ log.debug(`Failed to load credentials: ${getErrorMessage(error$2)}`);
871
883
  return {};
872
884
  }
873
885
  }
@@ -981,7 +993,7 @@ async function createAppContext(options = {}) {
981
993
  log.debug("Saved fetched user info to credentials");
982
994
  }
983
995
  } catch (error$2) {
984
- log.debug(`Failed to fetch user info: ${error$2 instanceof Error ? error$2.message : String(error$2)}`);
996
+ log.debug(`Failed to fetch user info: ${getErrorMessage(error$2)}`);
985
997
  }
986
998
  }
987
999
  log.debug(`App context loaded: isLoggedIn=${isLoggedIn}, user=${user?.name ?? "none"}`);
@@ -1018,10 +1030,11 @@ function resolveRegistry(config, alias) {
1018
1030
  }
1019
1031
  let globalContext = null;
1020
1032
  /**
1021
- * Gets the global app context, or null if not initialized.
1022
- * Use this in commands to access cached config, registry, and auth state.
1033
+ * Gets the global app context.
1034
+ * Throws if context has not been initialized via initAppContext().
1023
1035
  */
1024
1036
  function useAppContext() {
1037
+ if (!globalContext) throw new Error("App context not initialized");
1025
1038
  return globalContext;
1026
1039
  }
1027
1040
  /**
@@ -1042,7 +1055,6 @@ const CLIENT_ID = "agentrules-cli";
1042
1055
  async function login(options = {}) {
1043
1056
  const { noBrowser = false, onDeviceCode, onBrowserOpen, onPollingStart, onAuthorized } = options;
1044
1057
  const ctx = useAppContext();
1045
- if (!ctx) throw new Error("App context not initialized");
1046
1058
  const { url: registryUrl } = ctx.registry;
1047
1059
  log.debug(`Authenticating with ${registryUrl}`);
1048
1060
  try {
@@ -1106,10 +1118,9 @@ async function login(options = {}) {
1106
1118
  } : void 0
1107
1119
  };
1108
1120
  } catch (error$2) {
1109
- const message = error$2 instanceof Error ? error$2.message : String(error$2);
1110
1121
  return {
1111
1122
  success: false,
1112
- error: message
1123
+ error: getErrorMessage(error$2)
1113
1124
  };
1114
1125
  }
1115
1126
  }
@@ -1152,7 +1163,6 @@ async function logout(options = {}) {
1152
1163
  };
1153
1164
  }
1154
1165
  const ctx = useAppContext();
1155
- if (!ctx) throw new Error("App context not initialized");
1156
1166
  const { url: registryUrl } = ctx.registry;
1157
1167
  const hadCredentials = ctx.credentials !== null;
1158
1168
  if (hadCredentials) {
@@ -1172,7 +1182,6 @@ async function logout(options = {}) {
1172
1182
  */
1173
1183
  async function whoami() {
1174
1184
  const ctx = useAppContext();
1175
- if (!ctx) throw new Error("App context not initialized");
1176
1185
  const { url: registryUrl } = ctx.registry;
1177
1186
  return {
1178
1187
  success: true,
@@ -1187,7 +1196,6 @@ async function whoami() {
1187
1196
  //#region src/commands/preset/add.ts
1188
1197
  async function addPreset(options) {
1189
1198
  const ctx = useAppContext();
1190
- if (!ctx) throw new Error("App context not initialized");
1191
1199
  const { alias: registryAlias, url: registryUrl } = ctx.registry;
1192
1200
  const dryRun = Boolean(options.dryRun);
1193
1201
  const { slug, platform, version: version$1 } = parsePresetInput(options.preset, options.platform, options.version);
@@ -1296,14 +1304,6 @@ async function writeBundleFiles(bundle, target, behavior) {
1296
1304
  const data = Buffer.from(decoded);
1297
1305
  await verifyBundledFileChecksum(file, data);
1298
1306
  const destResult = computeDestinationPath(file.path, target);
1299
- if (destResult.skipped) {
1300
- files.push({
1301
- path: file.path,
1302
- status: "skipped"
1303
- });
1304
- log.debug(`Skipped (root file): ${file.path}`);
1305
- continue;
1306
- }
1307
1307
  const destination = destResult.path;
1308
1308
  if (!behavior.dryRun) await mkdir(dirname(destination), { recursive: true });
1309
1309
  const existing = await readExistingFile(destination);
@@ -1349,27 +1349,23 @@ async function writeBundleFiles(bundle, target, behavior) {
1349
1349
  conflicts
1350
1350
  };
1351
1351
  }
1352
+ /**
1353
+ * Compute destination path for a bundled file.
1354
+ *
1355
+ * Bundle files are stored with paths relative to the platform directory
1356
+ * (e.g., "AGENTS.md", "commands/test.md") and installed to:
1357
+ * - Project/custom: <root>/<projectDir>/<path> (e.g., .opencode/AGENTS.md)
1358
+ * - Global: <root>/<path> (e.g., ~/.config/opencode/AGENTS.md)
1359
+ */
1352
1360
  function computeDestinationPath(pathInput, target) {
1353
1361
  const normalized = normalizeBundlePath(pathInput);
1354
- const configPrefix = `${CONFIG_DIR_NAME}/`;
1355
- const isConfigFile = normalized.startsWith(configPrefix);
1356
- if (target.mode === "global" && !isConfigFile) return {
1357
- skipped: true,
1358
- path: null
1359
- };
1362
+ if (!normalized) throw new Error(`Unable to derive destination for ${pathInput}. The computed relative path is empty.`);
1360
1363
  let relativePath;
1361
- if (isConfigFile) {
1362
- const withoutConfigPrefix = normalized.slice(configPrefix.length);
1363
- if (target.mode === "global") relativePath = withoutConfigPrefix;
1364
- else relativePath = `${target.projectDir}/${withoutConfigPrefix}`;
1365
- } else relativePath = normalized;
1366
- if (!relativePath) throw new Error(`Unable to derive destination for ${pathInput}. The computed relative path is empty.`);
1364
+ if (target.mode === "global") relativePath = normalized;
1365
+ else relativePath = `${target.projectDir}/${normalized}`;
1367
1366
  const destination = resolve(target.root, relativePath);
1368
1367
  ensureWithinRoot(destination, target.root);
1369
- return {
1370
- skipped: false,
1371
- path: destination
1372
- };
1368
+ return { path: destination };
1373
1369
  }
1374
1370
  async function readExistingFile(pathname) {
1375
1371
  try {
@@ -1431,6 +1427,22 @@ async function directoryExists(path$1) {
1431
1427
 
1432
1428
  //#endregion
1433
1429
  //#region src/lib/preset-utils.ts
1430
+ const INSTALL_FILENAME = "INSTALL.txt";
1431
+ const README_FILENAME = "README.md";
1432
+ const LICENSE_FILENAME = "LICENSE.md";
1433
+ /**
1434
+ * Files/directories that are always excluded from presets.
1435
+ * These are never useful in a preset bundle.
1436
+ */
1437
+ const DEFAULT_IGNORE_PATTERNS = [
1438
+ "node_modules",
1439
+ ".git",
1440
+ ".DS_Store",
1441
+ "*.lock",
1442
+ "package-lock.json",
1443
+ "bun.lockb",
1444
+ "pnpm-lock.yaml"
1445
+ ];
1434
1446
  /**
1435
1447
  * Normalize a string to a valid preset slug (lowercase kebab-case)
1436
1448
  */
@@ -1454,6 +1466,128 @@ async function resolveConfigPath(inputPath) {
1454
1466
  if (stats?.isDirectory()) return join(inputPath, PRESET_CONFIG_FILENAME);
1455
1467
  return inputPath;
1456
1468
  }
1469
+ /**
1470
+ * Load a preset from a directory containing agentrules.json.
1471
+ *
1472
+ * Config in platform dir (e.g., .claude/agentrules.json):
1473
+ * - Preset files: siblings of config
1474
+ * - Metadata: .agentrules/ subfolder
1475
+ *
1476
+ * Config at repo root:
1477
+ * - Preset files: in .claude/ (or `path` from config)
1478
+ * - Metadata: .agentrules/ subfolder
1479
+ */
1480
+ async function loadPreset(presetDir) {
1481
+ const configPath = join(presetDir, PRESET_CONFIG_FILENAME);
1482
+ if (!await fileExists(configPath)) throw new Error(`Config file not found: ${configPath}`);
1483
+ const configRaw = await readFile(configPath, "utf8");
1484
+ let configJson;
1485
+ try {
1486
+ configJson = JSON.parse(configRaw);
1487
+ } catch {
1488
+ throw new Error(`Invalid JSON in ${configPath}`);
1489
+ }
1490
+ const configObj = configJson;
1491
+ const identifier = typeof configObj?.name === "string" ? configObj.name : configPath;
1492
+ const config = validatePresetConfig(configJson, identifier);
1493
+ const slug = config.name;
1494
+ const dirName = basename(presetDir);
1495
+ const isConfigInPlatformDir = isPlatformDir(dirName);
1496
+ let filesDir;
1497
+ let metadataDir;
1498
+ if (isConfigInPlatformDir) {
1499
+ filesDir = presetDir;
1500
+ metadataDir = join(presetDir, AGENT_RULES_DIR);
1501
+ log.debug(`Config in platform dir: files in ${filesDir}, metadata in ${metadataDir}`);
1502
+ } else {
1503
+ const platformDir = config.path ?? PLATFORMS[config.platform].projectDir;
1504
+ filesDir = join(presetDir, platformDir);
1505
+ metadataDir = join(presetDir, AGENT_RULES_DIR);
1506
+ log.debug(`Config at repo root: files in ${filesDir}, metadata in ${metadataDir}`);
1507
+ if (!await directoryExists(filesDir)) throw new Error(`Files directory not found: ${filesDir}. Create the directory or set "path" in ${PRESET_CONFIG_FILENAME}.`);
1508
+ }
1509
+ let installMessage;
1510
+ let readmeContent;
1511
+ let licenseContent;
1512
+ if (await directoryExists(metadataDir)) {
1513
+ installMessage = await readFileIfExists(join(metadataDir, INSTALL_FILENAME));
1514
+ readmeContent = await readFileIfExists(join(metadataDir, README_FILENAME));
1515
+ licenseContent = await readFileIfExists(join(metadataDir, LICENSE_FILENAME));
1516
+ }
1517
+ const ignorePatterns = [...DEFAULT_IGNORE_PATTERNS, ...config.ignore ?? []];
1518
+ const rootExclude = [PRESET_CONFIG_FILENAME, AGENT_RULES_DIR];
1519
+ const files = await collectFiles(filesDir, rootExclude, ignorePatterns);
1520
+ if (files.length === 0) throw new Error(`No files found in ${filesDir}. Presets must include at least one file.`);
1521
+ return {
1522
+ slug,
1523
+ config,
1524
+ files,
1525
+ installMessage,
1526
+ readmeContent,
1527
+ licenseContent
1528
+ };
1529
+ }
1530
+ /**
1531
+ * Check if a filename matches an ignore pattern.
1532
+ * Supports:
1533
+ * - Exact match: "node_modules"
1534
+ * - Extension match: "*.lock"
1535
+ * - Prefix match: ".git*" (not implemented yet, keeping simple)
1536
+ */
1537
+ function matchesPattern(name, pattern) {
1538
+ if (pattern.startsWith("*.")) {
1539
+ const ext = pattern.slice(1);
1540
+ return name.endsWith(ext);
1541
+ }
1542
+ return name === pattern;
1543
+ }
1544
+ /**
1545
+ * Check if a filename should be ignored based on patterns.
1546
+ */
1547
+ function shouldIgnore(name, patterns) {
1548
+ return patterns.some((pattern) => matchesPattern(name, pattern));
1549
+ }
1550
+ /**
1551
+ * Recursively collect all files from a directory.
1552
+ *
1553
+ * @param dir - Current directory being scanned
1554
+ * @param rootExclude - Entries to exclude at root level only (config, metadata dir)
1555
+ * @param ignorePatterns - Patterns to ignore at all levels
1556
+ * @param root - The root directory (for computing relative paths)
1557
+ */
1558
+ async function collectFiles(dir, rootExclude, ignorePatterns, root) {
1559
+ const configRoot = root ?? dir;
1560
+ const isRoot = configRoot === dir;
1561
+ const entries = await readdir(dir, { withFileTypes: true });
1562
+ const files = [];
1563
+ for (const entry of entries) {
1564
+ if (isRoot && rootExclude.includes(entry.name)) continue;
1565
+ if (shouldIgnore(entry.name, ignorePatterns)) {
1566
+ log.debug(`Ignoring: ${entry.name}`);
1567
+ continue;
1568
+ }
1569
+ const fullPath = join(dir, entry.name);
1570
+ if (entry.isDirectory()) {
1571
+ const nested = await collectFiles(fullPath, rootExclude, ignorePatterns, configRoot);
1572
+ files.push(...nested);
1573
+ } else if (entry.isFile()) {
1574
+ const contents = await readFile(fullPath, "utf8");
1575
+ const relativePath = relative(configRoot, fullPath);
1576
+ files.push({
1577
+ path: relativePath,
1578
+ contents
1579
+ });
1580
+ }
1581
+ }
1582
+ return files;
1583
+ }
1584
+ /**
1585
+ * Read a file if it exists, otherwise return undefined.
1586
+ */
1587
+ async function readFileIfExists(path$1) {
1588
+ if (await fileExists(path$1)) return await readFile(path$1, "utf8");
1589
+ return;
1590
+ }
1457
1591
 
1458
1592
  //#endregion
1459
1593
  //#region src/commands/preset/init.ts
@@ -1461,11 +1595,9 @@ async function resolveConfigPath(inputPath) {
1461
1595
  const PLATFORM_DETECTION_PATHS = {
1462
1596
  opencode: [".opencode"],
1463
1597
  claude: [".claude"],
1464
- cursor: [".cursor", ".cursorrules"],
1598
+ cursor: [".cursor"],
1465
1599
  codex: [".codex"]
1466
1600
  };
1467
- /** Default path for new preset authoring */
1468
- const DEFAULT_FILES_PATH = "files";
1469
1601
  /** Default preset name when none specified */
1470
1602
  const DEFAULT_PRESET_NAME$1 = "my-preset";
1471
1603
  /**
@@ -1488,49 +1620,45 @@ async function detectPlatforms(directory) {
1488
1620
  }
1489
1621
  return detected;
1490
1622
  }
1623
+ /**
1624
+ * Initialize a preset in a platform directory.
1625
+ *
1626
+ * Structure:
1627
+ * - platformDir/agentrules.json - preset config
1628
+ * - platformDir/* - platform files (added by user)
1629
+ * - platformDir/.agentrules/ - optional metadata folder (README, LICENSE, etc.)
1630
+ */
1491
1631
  async function initPreset(options) {
1492
- const directory = options.directory ?? process.cwd();
1493
- log.debug(`Initializing preset in: ${directory}`);
1632
+ const platformDir = options.directory ?? process.cwd();
1633
+ log.debug(`Initializing preset in: ${platformDir}`);
1634
+ const inferredPlatform = getPlatformFromDir(basename(platformDir));
1635
+ const platform = normalizePlatform(options.platform ?? inferredPlatform ?? "opencode");
1494
1636
  const name = normalizeName(options.name ?? DEFAULT_PRESET_NAME$1);
1495
1637
  const title = options.title ?? toTitleCase(name);
1496
1638
  const description = options.description ?? `${title} preset`;
1497
- const platform = normalizePlatform(options.platform ?? "opencode");
1498
- const detectedPath = options.detectedPath;
1499
1639
  const license = options.license ?? "MIT";
1500
1640
  log.debug(`Preset name: ${name}, platform: ${platform}`);
1501
- const configPath = join(directory, PRESET_CONFIG_FILENAME);
1641
+ const configPath = join(platformDir, PRESET_CONFIG_FILENAME);
1502
1642
  if (!options.force && await fileExists(configPath)) throw new Error(`${PRESET_CONFIG_FILENAME} already exists. Use --force to overwrite.`);
1503
- const defaultPath = PLATFORMS[platform].projectDir;
1504
- const effectivePath = detectedPath ?? DEFAULT_FILES_PATH;
1505
1643
  const preset = {
1506
1644
  $schema: PRESET_SCHEMA_URL,
1507
1645
  name,
1508
1646
  title,
1509
1647
  version: 1,
1510
1648
  description,
1511
- tags: ["// TODO: Replace - Tags help users discover your preset (e.g., typescript, react)"],
1512
- features: ["// TODO: Replace - Features describe what your preset does (e.g., Built-in commands for common workflows)"],
1513
1649
  license,
1514
1650
  platform
1515
1651
  };
1516
- if (effectivePath !== defaultPath) preset.path = effectivePath;
1517
- await mkdir(directory, { recursive: true });
1518
- log.debug(`Created/verified directory: ${directory}`);
1519
- const content = `${JSON.stringify(preset, null, 2)}\n`;
1520
- await writeFile(configPath, content, "utf8");
1521
- log.debug(`Wrote config file: ${configPath}`);
1522
1652
  let createdDir;
1523
- if (detectedPath) log.debug(`Using detected platform directory: ${detectedPath}`);
1653
+ if (await directoryExists(platformDir)) log.debug(`Platform directory exists: ${platformDir}`);
1524
1654
  else {
1525
- const filesPath = effectivePath;
1526
- const fullPath = join(directory, filesPath);
1527
- if (await directoryExists(fullPath)) log.debug(`Files directory already exists: ${filesPath}`);
1528
- else {
1529
- await mkdir(fullPath, { recursive: true });
1530
- createdDir = filesPath;
1531
- log.debug(`Created files directory: ${filesPath}`);
1532
- }
1655
+ await mkdir(platformDir, { recursive: true });
1656
+ createdDir = platformDir;
1657
+ log.debug(`Created platform directory: ${platformDir}`);
1533
1658
  }
1659
+ const content = `${JSON.stringify(preset, null, 2)}\n`;
1660
+ await writeFile(configPath, content, "utf8");
1661
+ log.debug(`Wrote config file: ${configPath}`);
1534
1662
  log.debug("Preset initialization complete.");
1535
1663
  return {
1536
1664
  configPath,
@@ -1562,17 +1690,49 @@ function check(schema) {
1562
1690
  //#region src/commands/preset/init-interactive.ts
1563
1691
  const DEFAULT_PRESET_NAME = "my-preset";
1564
1692
  /**
1565
- * Run interactive init flow with clack prompts
1693
+ * Run interactive init flow with clack prompts.
1694
+ *
1695
+ * If platformDir is provided, init directly in that directory.
1696
+ * Otherwise, detect platform directories and prompt user to select one.
1566
1697
  */
1567
1698
  async function initInteractive(options) {
1568
- const { directory, name: nameOption, title: titleOption, description: descriptionOption, platform: platformOption, license: licenseOption } = options;
1699
+ const { baseDir, platformDir: explicitPlatformDir, name: nameOption, title: titleOption, description: descriptionOption, platform: platformOption, license: licenseOption } = options;
1569
1700
  let { force } = options;
1570
1701
  const defaultName = nameOption ?? DEFAULT_PRESET_NAME;
1571
1702
  p.intro("Create a new preset");
1572
- const configPath = join(directory, PRESET_CONFIG_FILENAME);
1703
+ let targetPlatformDir;
1704
+ let selectedPlatform;
1705
+ if (explicitPlatformDir) {
1706
+ targetPlatformDir = explicitPlatformDir;
1707
+ const dirName = explicitPlatformDir.split("/").pop() ?? explicitPlatformDir;
1708
+ selectedPlatform = platformOption ?? getPlatformFromDir(dirName) ?? "opencode";
1709
+ } else {
1710
+ const detected = await detectPlatforms(baseDir);
1711
+ const detectedMap = new Map(detected.map((d) => [d.id, d]));
1712
+ if (detected.length > 0) p.note(detected.map((d) => `${d.id} → ${d.path}`).join("\n"), "Detected platform directories");
1713
+ const defaultPlatform = platformOption ?? (detected.length > 0 ? detected[0].id : "opencode");
1714
+ const platformChoice = await p.select({
1715
+ message: "Platform",
1716
+ options: PLATFORM_IDS.map((id) => ({
1717
+ value: id,
1718
+ label: detectedMap.has(id) ? `${id} (detected)` : id,
1719
+ hint: detectedMap.get(id)?.path
1720
+ })),
1721
+ initialValue: defaultPlatform
1722
+ });
1723
+ if (p.isCancel(platformChoice)) {
1724
+ p.cancel("Cancelled");
1725
+ process.exit(0);
1726
+ }
1727
+ selectedPlatform = platformChoice;
1728
+ const detectedInfo = detectedMap.get(selectedPlatform);
1729
+ if (detectedInfo) targetPlatformDir = join(baseDir, detectedInfo.path);
1730
+ else targetPlatformDir = join(baseDir, PLATFORMS[selectedPlatform].projectDir);
1731
+ }
1732
+ const configPath = join(targetPlatformDir, PRESET_CONFIG_FILENAME);
1573
1733
  if (!force && await fileExists(configPath)) {
1574
1734
  const overwrite = await p.confirm({
1575
- message: `${PRESET_CONFIG_FILENAME} already exists. Overwrite?`,
1735
+ message: `${PRESET_CONFIG_FILENAME} already exists in ${targetPlatformDir}. Overwrite?`,
1576
1736
  initialValue: false
1577
1737
  });
1578
1738
  if (p.isCancel(overwrite) || !overwrite) {
@@ -1581,9 +1741,6 @@ async function initInteractive(options) {
1581
1741
  }
1582
1742
  force = true;
1583
1743
  }
1584
- const detected = await detectPlatforms(directory);
1585
- const detectedMap = new Map(detected.map((d) => [d.id, d]));
1586
- if (detected.length > 0) p.note(detected.map((d) => `${d.id} → ${d.path}`).join("\n"), "Detected platform directories");
1587
1744
  const result = await p.group({
1588
1745
  name: () => p.text({
1589
1746
  message: "Preset name (slug)",
@@ -1609,18 +1766,6 @@ async function initInteractive(options) {
1609
1766
  validate: check(descriptionSchema)
1610
1767
  });
1611
1768
  },
1612
- platform: () => {
1613
- const defaultPlatform = platformOption ?? (detected.length > 0 ? detected[0].id : "opencode");
1614
- return p.select({
1615
- message: "Platform",
1616
- options: PLATFORM_IDS.map((id) => ({
1617
- value: id,
1618
- label: detectedMap.has(id) ? `${id} (detected)` : id,
1619
- hint: detectedMap.get(id)?.path
1620
- })),
1621
- initialValue: defaultPlatform
1622
- });
1623
- },
1624
1769
  license: async () => {
1625
1770
  const defaultLicense = licenseOption ?? "MIT";
1626
1771
  const choice = await p.select({
@@ -1656,14 +1801,12 @@ async function initInteractive(options) {
1656
1801
  p.cancel("Cancelled");
1657
1802
  return process.exit(0);
1658
1803
  } });
1659
- const detectedPath = detectedMap.get(result.platform)?.path;
1660
1804
  const initOptions = {
1661
- directory,
1805
+ directory: targetPlatformDir,
1662
1806
  name: result.name,
1663
1807
  title: result.title,
1664
1808
  description: result.description,
1665
- platform: result.platform,
1666
- detectedPath,
1809
+ platform: selectedPlatform,
1667
1810
  license: result.license,
1668
1811
  force
1669
1812
  };
@@ -1727,11 +1870,16 @@ async function validatePreset(options) {
1727
1870
  const platform = preset.platform;
1728
1871
  log.debug(`Checking platform: ${platform}`);
1729
1872
  if (isSupportedPlatform(platform)) {
1730
- const filesPath = preset.path ?? PLATFORMS[platform].projectDir;
1731
- const filesDir = join(presetDir, filesPath);
1732
- const filesExists = await directoryExists(filesDir);
1733
- log.debug(`Files directory check: ${filesDir} - ${filesExists ? "exists" : "not found"}`);
1734
- if (!filesExists) errors.push(`Files directory not found: ${filesPath}`);
1873
+ const dirName = basename(presetDir);
1874
+ const isInProjectMode = isPlatformDir(dirName);
1875
+ if (isInProjectMode) log.debug(`In-project mode: files expected in ${presetDir}`);
1876
+ else {
1877
+ const filesPath = preset.path ?? PLATFORMS[platform].projectDir;
1878
+ const filesDir = join(presetDir, filesPath);
1879
+ const filesExists = await directoryExists(filesDir);
1880
+ log.debug(`Standalone mode: files directory check: ${filesDir} - ${filesExists ? "exists" : "not found"}`);
1881
+ if (!filesExists) errors.push(`Files directory not found: ${filesPath}`);
1882
+ }
1735
1883
  } else {
1736
1884
  errors.push(`Unknown platform "${platform}". Supported: ${PLATFORM_IDS.join(", ")}`);
1737
1885
  log.debug(`Platform "${platform}" is not supported`);
@@ -1762,9 +1910,6 @@ async function validatePreset(options) {
1762
1910
 
1763
1911
  //#endregion
1764
1912
  //#region src/commands/publish.ts
1765
- const INSTALL_FILENAME$1 = "INSTALL.txt";
1766
- const README_FILENAME$1 = "README.md";
1767
- const LICENSE_FILENAME$1 = "LICENSE.md";
1768
1913
  /** Maximum size per bundle in bytes (1MB) */
1769
1914
  const MAX_BUNDLE_SIZE_BYTES = 1 * 1024 * 1024;
1770
1915
  /**
@@ -1782,12 +1927,12 @@ async function publish(options = {}) {
1782
1927
  const { path: path$1, version: version$1, dryRun = false } = options;
1783
1928
  log.debug(`Publishing preset from path: ${path$1 ?? process.cwd()}${dryRun ? " (dry run)" : ""}`);
1784
1929
  const ctx = useAppContext();
1785
- if (!ctx) throw new Error("App context not initialized");
1786
1930
  if (!(dryRun || ctx.isLoggedIn && ctx.credentials)) {
1787
- log.error(`Not logged in. Run ${ui.command("agentrules login")} to authenticate.`);
1931
+ const error$2 = "Not logged in. Run `agentrules login` to authenticate.";
1932
+ log.error(error$2);
1788
1933
  return {
1789
1934
  success: false,
1790
- error: `Not logged in. Run ${ui.command("agentrules login")} to authenticate.`
1935
+ error: error$2
1791
1936
  };
1792
1937
  }
1793
1938
  if (!dryRun) log.debug(`Authenticated as user, publishing to ${ctx.registry.url}`);
@@ -1807,15 +1952,15 @@ async function publish(options = {}) {
1807
1952
  spinner$1.update("Loading preset...");
1808
1953
  let presetInput;
1809
1954
  try {
1810
- presetInput = await loadPreset$1(presetDir);
1955
+ presetInput = await loadPreset(presetDir);
1811
1956
  log.debug(`Loaded preset "${presetInput.slug}" for platform ${presetInput.config.platform}`);
1812
1957
  } catch (error$2) {
1813
- const errorMessage = error$2 instanceof Error ? error$2.message : String(error$2);
1958
+ const message = getErrorMessage(error$2);
1814
1959
  spinner$1.fail("Failed to load preset");
1815
- log.error(errorMessage);
1960
+ log.error(message);
1816
1961
  return {
1817
1962
  success: false,
1818
- error: errorMessage
1963
+ error: message
1819
1964
  };
1820
1965
  }
1821
1966
  spinner$1.update("Building bundle...");
@@ -1827,12 +1972,12 @@ async function publish(options = {}) {
1827
1972
  });
1828
1973
  log.debug(`Built publish input for ${publishInput.platform}`);
1829
1974
  } catch (error$2) {
1830
- const errorMessage = error$2 instanceof Error ? error$2.message : String(error$2);
1975
+ const message = getErrorMessage(error$2);
1831
1976
  spinner$1.fail("Failed to build bundle");
1832
- log.error(errorMessage);
1977
+ log.error(message);
1833
1978
  return {
1834
1979
  success: false,
1835
- error: errorMessage
1980
+ error: message
1836
1981
  };
1837
1982
  }
1838
1983
  const inputJson = JSON.stringify(publishInput);
@@ -1912,79 +2057,9 @@ async function publish(options = {}) {
1912
2057
  }
1913
2058
  };
1914
2059
  }
1915
- /**
1916
- * Load a preset from a directory
1917
- */
1918
- async function loadPreset$1(presetDir) {
1919
- const configPath = join(presetDir, PRESET_CONFIG_FILENAME);
1920
- if (!await fileExists(configPath)) throw new Error(`Config file not found: ${configPath}`);
1921
- const configRaw = await readFile(configPath, "utf8");
1922
- let configJson;
1923
- try {
1924
- configJson = JSON.parse(configRaw);
1925
- } catch {
1926
- throw new Error(`Invalid JSON in ${configPath}`);
1927
- }
1928
- const configObj = configJson;
1929
- const identifier = typeof configObj?.name === "string" ? configObj.name : configPath;
1930
- const config = validatePresetConfig(configJson, identifier);
1931
- const slug = config.name;
1932
- const installPath = join(presetDir, INSTALL_FILENAME$1);
1933
- const installMessage = await readFileIfExists$1(installPath);
1934
- const readmePath = join(presetDir, README_FILENAME$1);
1935
- const readmeContent = await readFileIfExists$1(readmePath);
1936
- const licensePath = join(presetDir, LICENSE_FILENAME$1);
1937
- const licenseContent = await readFileIfExists$1(licensePath);
1938
- const filesPath = config.path ?? PLATFORMS[config.platform].projectDir;
1939
- const filesDir = join(presetDir, filesPath);
1940
- if (!await directoryExists(filesDir)) throw new Error(`Files directory not found: ${filesDir} (referenced in ${configPath})`);
1941
- const files = await collectFiles$1(filesDir);
1942
- if (files.length === 0) throw new Error(`No files found in ${filesDir}. Presets must include at least one file.`);
1943
- return {
1944
- slug,
1945
- config,
1946
- files,
1947
- installMessage,
1948
- readmeContent,
1949
- licenseContent
1950
- };
1951
- }
1952
- /**
1953
- * Recursively collect all files from a directory
1954
- */
1955
- async function collectFiles$1(dir, baseDir) {
1956
- const root = baseDir ?? dir;
1957
- const entries = await readdir(dir, { withFileTypes: true });
1958
- const files = [];
1959
- for (const entry of entries) {
1960
- const fullPath = join(dir, entry.name);
1961
- if (entry.isDirectory()) {
1962
- const nested = await collectFiles$1(fullPath, root);
1963
- files.push(...nested);
1964
- } else if (entry.isFile()) {
1965
- const contents = await readFile(fullPath, "utf8");
1966
- const relativePath = relative(root, fullPath);
1967
- files.push({
1968
- path: relativePath,
1969
- contents
1970
- });
1971
- }
1972
- }
1973
- return files;
1974
- }
1975
- /**
1976
- * Read a file if it exists, otherwise return undefined
1977
- */
1978
- async function readFileIfExists$1(path$1) {
1979
- if (await fileExists(path$1)) return await readFile(path$1, "utf8");
1980
- return;
1981
- }
1982
2060
 
1983
2061
  //#endregion
1984
2062
  //#region src/commands/registry/build.ts
1985
- const INSTALL_FILENAME = "INSTALL.txt";
1986
- const README_FILENAME = "README.md";
1987
- const LICENSE_FILENAME = "LICENSE.md";
1988
2063
  async function buildRegistry(options) {
1989
2064
  const inputDir = options.input;
1990
2065
  const outputDir = options.out ?? null;
@@ -2060,61 +2135,6 @@ async function discoverPresetDirs(inputDir) {
2060
2135
  await searchDir(inputDir, 0);
2061
2136
  return presetDirs.sort();
2062
2137
  }
2063
- async function loadPreset(presetDir) {
2064
- const configPath = join(presetDir, PRESET_CONFIG_FILENAME);
2065
- const configRaw = await readFile(configPath, "utf8");
2066
- let configJson;
2067
- try {
2068
- configJson = JSON.parse(configRaw);
2069
- } catch {
2070
- throw new Error(`Invalid JSON in ${configPath}`);
2071
- }
2072
- const config = validatePresetConfig(configJson, basename(presetDir));
2073
- const slug = config.name;
2074
- const installPath = join(presetDir, INSTALL_FILENAME);
2075
- const installMessage = await readFileIfExists(installPath);
2076
- const readmePath = join(presetDir, README_FILENAME);
2077
- const readmeContent = await readFileIfExists(readmePath);
2078
- const licensePath = join(presetDir, LICENSE_FILENAME);
2079
- const licenseContent = await readFileIfExists(licensePath);
2080
- const filesPath = config.path ?? PLATFORMS[config.platform].projectDir;
2081
- const filesDir = join(presetDir, filesPath);
2082
- if (!await directoryExists(filesDir)) throw new Error(`Files directory not found: ${filesDir} (referenced in ${configPath})`);
2083
- const files = await collectFiles(filesDir);
2084
- if (files.length === 0) throw new Error(`No files found in ${filesDir}. Presets must include at least one file.`);
2085
- return {
2086
- slug,
2087
- config,
2088
- files,
2089
- installMessage,
2090
- readmeContent,
2091
- licenseContent
2092
- };
2093
- }
2094
- async function collectFiles(dir, baseDir) {
2095
- const root = baseDir ?? dir;
2096
- const entries = await readdir(dir, { withFileTypes: true });
2097
- const files = [];
2098
- for (const entry of entries) {
2099
- const fullPath = join(dir, entry.name);
2100
- if (entry.isDirectory()) {
2101
- const nested = await collectFiles(fullPath, root);
2102
- files.push(...nested);
2103
- } else if (entry.isFile()) {
2104
- const contents = await readFile(fullPath, "utf8");
2105
- const relativePath = relative(root, fullPath);
2106
- files.push({
2107
- path: relativePath,
2108
- contents
2109
- });
2110
- }
2111
- }
2112
- return files;
2113
- }
2114
- async function readFileIfExists(path$1) {
2115
- if (await fileExists(path$1)) return await readFile(path$1, "utf8");
2116
- return;
2117
- }
2118
2138
 
2119
2139
  //#endregion
2120
2140
  //#region src/commands/unpublish.ts
@@ -2146,12 +2166,12 @@ async function unpublish(options) {
2146
2166
  }
2147
2167
  log.debug(`Unpublishing preset: ${slug}.${platform}@${version$1}`);
2148
2168
  const ctx = useAppContext();
2149
- if (!ctx) throw new Error("App context not initialized");
2150
2169
  if (!(ctx.isLoggedIn && ctx.credentials)) {
2151
- log.error(`Not logged in. Run ${ui.command("agentrules login")} to authenticate.`);
2170
+ const error$2 = "Not logged in. Run `agentrules login` to authenticate.";
2171
+ log.error(error$2);
2152
2172
  return {
2153
2173
  success: false,
2154
- error: `Not logged in. Run ${ui.command("agentrules login")} to authenticate.`
2174
+ error: error$2
2155
2175
  };
2156
2176
  }
2157
2177
  log.debug(`Authenticated, unpublishing from ${ctx.registry.url}`);
@@ -2194,7 +2214,7 @@ program.name("agentrules").description("The AI Agent Directory CLI").version(pac
2194
2214
  url: actionOpts.url
2195
2215
  });
2196
2216
  } catch (error$2) {
2197
- log.debug(`Failed to init context: ${error$2 instanceof Error ? error$2.message : String(error$2)}`);
2217
+ log.debug(`Failed to init context: ${getErrorMessage(error$2)}`);
2198
2218
  }
2199
2219
  }).showHelpAfterError();
2200
2220
  program.command("add <preset>").description("Download and install a preset from the registry").option("-p, --platform <platform>", "Target platform (opencode, codex, claude, cursor)").option("-V, --version <version>", "Install a specific version").option("-r, --registry <alias>", "Use a specific registry alias").option("-g, --global", "Install to global directory").option("--dir <path>", "Install to a custom directory").option("-f, --force", "Overwrite existing files").option("-y, --yes", "Alias for --force").option("--dry-run", "Preview changes without writing").option("--skip-conflicts", "Skip conflicting files").action(handle(async (preset, options) => {
@@ -2250,7 +2270,7 @@ program.command("init").description("Initialize a new preset").argument("[direct
2250
2270
  const useInteractive = !options.yes && process.stdin.isTTY;
2251
2271
  if (useInteractive) {
2252
2272
  const result$1 = await initInteractive({
2253
- directory: targetDir,
2273
+ baseDir: targetDir,
2254
2274
  name: options.name ?? defaultName,
2255
2275
  title: options.title,
2256
2276
  description: options.description,
@@ -2262,23 +2282,34 @@ program.command("init").description("Initialize a new preset").argument("[direct
2262
2282
  log.print(`\n${ui.header("Directory created")}`);
2263
2283
  log.print(ui.list([ui.path(result$1.createdDir)]));
2264
2284
  }
2265
- const nextSteps$1 = [];
2266
- if (directory) nextSteps$1.push(`Run ${ui.command(`cd ${directory}`)}`);
2267
- nextSteps$1.push("Add your config files to the files directory", "Replace the placeholder comments in tags and features", `Run ${ui.command("agentrules validate")} to check your preset`);
2285
+ const nextSteps$1 = [
2286
+ "Add your config files to the platform directory",
2287
+ "Add tags (required) and features (recommended) to agentrules.json",
2288
+ `Run ${ui.command("agentrules publish")} to publish your preset`
2289
+ ];
2268
2290
  log.print(`\n${ui.header("Next steps")}`);
2269
2291
  log.print(ui.numberedList(nextSteps$1));
2270
2292
  return;
2271
2293
  }
2272
- const detected = await detectPlatforms(targetDir);
2273
- const platform = options.platform ?? detected[0]?.id ?? "opencode";
2274
- const detectedPath = detected.find((d) => d.id === platform)?.path;
2294
+ const targetDirName = basename(targetDir);
2295
+ const targetIsPlatformDir = getPlatformFromDir(targetDirName);
2296
+ let platformDir;
2297
+ let platform;
2298
+ if (targetIsPlatformDir) {
2299
+ platformDir = targetDir;
2300
+ platform = options.platform ?? targetIsPlatformDir;
2301
+ } else {
2302
+ const detected = await detectPlatforms(targetDir);
2303
+ platform = options.platform ?? detected[0]?.id ?? "opencode";
2304
+ const detectedPath = detected.find((d) => d.id === platform)?.path;
2305
+ platformDir = detectedPath ? join(targetDir, detectedPath) : join(targetDir, PLATFORMS[platform].projectDir);
2306
+ }
2275
2307
  const result = await initPreset({
2276
- directory: targetDir,
2308
+ directory: platformDir,
2277
2309
  name: options.name ?? defaultName,
2278
2310
  title: options.title,
2279
2311
  description: options.description,
2280
2312
  platform,
2281
- detectedPath,
2282
2313
  license: options.license,
2283
2314
  force: options.force
2284
2315
  });
@@ -2287,9 +2318,11 @@ program.command("init").description("Initialize a new preset").argument("[direct
2287
2318
  log.print(`\n${ui.header("Directory created")}`);
2288
2319
  log.print(ui.list([ui.path(result.createdDir)]));
2289
2320
  }
2290
- const nextSteps = [];
2291
- if (directory) nextSteps.push(`Run ${ui.command(`cd ${directory}`)}`);
2292
- nextSteps.push("Add your config files to the files directory", "Replace the placeholder comments in tags and features", `Run ${ui.command("agentrules validate")} to check your preset`);
2321
+ const nextSteps = [
2322
+ "Add your config files to the platform directory",
2323
+ "Add tags (required) and features (recommended) to agentrules.json",
2324
+ `Run ${ui.command("agentrules publish")} to publish your preset`
2325
+ ];
2293
2326
  log.print(`\n${ui.header("Next steps")}`);
2294
2327
  log.print(ui.numberedList(nextSteps));
2295
2328
  }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentrules/cli",
3
- "version": "0.0.10",
3
+ "version": "0.0.11",
4
4
  "author": "Brian Cheung <bcheung.dev@gmail.com> (https://github.com/bcheung)",
5
5
  "license": "MIT",
6
6
  "homepage": "https://agentrules.directory",
@@ -48,7 +48,7 @@
48
48
  "clean": "rm -rf node_modules dist .turbo"
49
49
  },
50
50
  "dependencies": {
51
- "@agentrules/core": "0.0.8",
51
+ "@agentrules/core": "0.0.9",
52
52
  "@clack/prompts": "^0.11.0",
53
53
  "chalk": "^5.4.1",
54
54
  "commander": "^12.1.0",