@agentrules/cli 0.0.13 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -6
- package/dist/index.js +1063 -639
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
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,
|
|
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, getInstallPath, getLatestPresetVersion, getLatestRuleVersion, getPlatformFromDir, getPresetVariant, getPresetVersion, getRuleVariant, getRuleVersion, getValidRuleTypes, hasBundle, isLikelyText, isPlatformDir, isSupportedPlatform, licenseSchema, nameSchema, normalizeBundlePath, normalizePlatformEntry, normalizePlatformInput, resolveSlug, tagsSchema, titleSchema, toUtf8String, validatePresetConfig, verifyBundledFileChecksum } from "@agentrules/core";
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import { basename, dirname, join, relative, resolve, sep } from "path";
|
|
6
|
-
import { exec } from "child_process";
|
|
7
|
-
import { promisify } from "util";
|
|
8
|
-
import * as client from "openid-client";
|
|
9
6
|
import chalk from "chalk";
|
|
10
|
-
import {
|
|
11
|
-
import { access, constants as constants$1, copyFile, mkdir, readFile, readdir, rm, stat, writeFile } from "fs/promises";
|
|
7
|
+
import { access, constants, copyFile, mkdir, readFile, readdir, rm, stat, writeFile } from "fs/promises";
|
|
12
8
|
import { homedir } from "os";
|
|
9
|
+
import { chmod, constants as constants$1 } from "fs";
|
|
10
|
+
import * as client from "openid-client";
|
|
11
|
+
import { promisify } from "util";
|
|
12
|
+
import { exec } from "child_process";
|
|
13
13
|
import * as p from "@clack/prompts";
|
|
14
14
|
|
|
15
15
|
//#region src/lib/ui.ts
|
|
@@ -317,10 +317,16 @@ function relativeTime(date) {
|
|
|
317
317
|
}
|
|
318
318
|
/**
|
|
319
319
|
* Formats an array of files as a tree structure
|
|
320
|
+
*
|
|
321
|
+
* By default shows folder-level sizes only. Pass `showFileSizes: true` to also show
|
|
322
|
+
* individual file sizes.
|
|
320
323
|
*/
|
|
321
|
-
function fileTree(files) {
|
|
324
|
+
function fileTree(files, options) {
|
|
325
|
+
const { header: headerTitle, showFileSizes = false, showFolderSizes = false } = options;
|
|
322
326
|
const root = {
|
|
323
327
|
name: "",
|
|
328
|
+
totalSize: 0,
|
|
329
|
+
isFile: false,
|
|
324
330
|
children: new Map()
|
|
325
331
|
};
|
|
326
332
|
for (const file of files) {
|
|
@@ -333,7 +339,9 @@ function fileTree(files) {
|
|
|
333
339
|
if (!child) {
|
|
334
340
|
child = {
|
|
335
341
|
name: part,
|
|
336
|
-
|
|
342
|
+
fileSize: isFile ? file.size : void 0,
|
|
343
|
+
totalSize: 0,
|
|
344
|
+
isFile,
|
|
337
345
|
children: new Map()
|
|
338
346
|
};
|
|
339
347
|
current.children.set(part, child);
|
|
@@ -341,11 +349,22 @@ function fileTree(files) {
|
|
|
341
349
|
current = child;
|
|
342
350
|
}
|
|
343
351
|
}
|
|
352
|
+
function calculateSizes(node) {
|
|
353
|
+
if (node.isFile) node.totalSize = node.fileSize ?? 0;
|
|
354
|
+
else node.totalSize = Array.from(node.children.values()).reduce((sum, child) => sum + calculateSizes(child), 0);
|
|
355
|
+
return node.totalSize;
|
|
356
|
+
}
|
|
357
|
+
calculateSizes(root);
|
|
344
358
|
const lines = [];
|
|
359
|
+
const countStr = theme.muted(`(${files.length})`);
|
|
360
|
+
const sizeStr = showFolderSizes ? ` ${theme.info(`(${formatBytes$1(root.totalSize)} total)`)}` : "";
|
|
361
|
+
lines.push(`${theme.title(headerTitle)} ${countStr}${sizeStr}`);
|
|
345
362
|
function renderNode(node, prefix, isLast) {
|
|
346
363
|
const connector = isLast ? "└── " : "├── ";
|
|
347
|
-
|
|
348
|
-
|
|
364
|
+
let nodeSizeStr = "";
|
|
365
|
+
if (node.isFile && showFileSizes) nodeSizeStr = theme.info(` (${formatBytes$1(node.totalSize)})`);
|
|
366
|
+
else if (!node.isFile && showFolderSizes) nodeSizeStr = theme.info(` (${formatBytes$1(node.totalSize)})`);
|
|
367
|
+
lines.push(`${prefix}${connector}${node.name}${nodeSizeStr}`);
|
|
349
368
|
const children = Array.from(node.children.values());
|
|
350
369
|
const newPrefix = prefix + (isLast ? " " : "│ ");
|
|
351
370
|
children.forEach((child, index) => {
|
|
@@ -516,6 +535,164 @@ const log = {
|
|
|
516
535
|
spinner
|
|
517
536
|
};
|
|
518
537
|
|
|
538
|
+
//#endregion
|
|
539
|
+
//#region src/lib/config.ts
|
|
540
|
+
/** Directory for CLI configuration and credentials (e.g., ~/.agentrules/) */
|
|
541
|
+
const CONFIG_DIRNAME = ".agentrules";
|
|
542
|
+
const CONFIG_FILENAME = "config.json";
|
|
543
|
+
const CONFIG_HOME_ENV = "AGENT_RULES_HOME";
|
|
544
|
+
const DEFAULT_REGISTRY_ALIAS = "main";
|
|
545
|
+
const DEFAULT_REGISTRY_URL = "https://agentrules.directory/";
|
|
546
|
+
const DEFAULT_CONFIG = {
|
|
547
|
+
defaultRegistry: DEFAULT_REGISTRY_ALIAS,
|
|
548
|
+
registries: { [DEFAULT_REGISTRY_ALIAS]: { url: DEFAULT_REGISTRY_URL } }
|
|
549
|
+
};
|
|
550
|
+
async function loadConfig$1() {
|
|
551
|
+
await ensureConfigDir();
|
|
552
|
+
const configPath = getConfigPath();
|
|
553
|
+
log.debug(`Loading config from ${configPath}`);
|
|
554
|
+
try {
|
|
555
|
+
await access(configPath, constants$1.F_OK);
|
|
556
|
+
} catch (error$2) {
|
|
557
|
+
if (isNodeError(error$2) && error$2.code === "ENOENT") {
|
|
558
|
+
log.debug("Config file not found, creating default config");
|
|
559
|
+
await writeDefaultConfig();
|
|
560
|
+
return structuredClone(DEFAULT_CONFIG);
|
|
561
|
+
}
|
|
562
|
+
throw error$2 instanceof Error ? error$2 : new Error(String(error$2));
|
|
563
|
+
}
|
|
564
|
+
const raw = await readFile(configPath, "utf8");
|
|
565
|
+
let parsed = {};
|
|
566
|
+
try {
|
|
567
|
+
parsed = JSON.parse(raw);
|
|
568
|
+
} catch (error$2) {
|
|
569
|
+
throw new Error(`Failed to parse ${configPath}: ${error$2.message}`);
|
|
570
|
+
}
|
|
571
|
+
return mergeWithDefaults(parsed);
|
|
572
|
+
}
|
|
573
|
+
async function saveConfig(config) {
|
|
574
|
+
await ensureConfigDir();
|
|
575
|
+
const configPath = getConfigPath();
|
|
576
|
+
log.debug(`Saving config to ${configPath}`);
|
|
577
|
+
const serialized = JSON.stringify(config, null, 2);
|
|
578
|
+
await writeFile(configPath, serialized, "utf8");
|
|
579
|
+
}
|
|
580
|
+
function getConfigPath() {
|
|
581
|
+
return join(getConfigDir(), CONFIG_FILENAME);
|
|
582
|
+
}
|
|
583
|
+
function getConfigDir() {
|
|
584
|
+
const customDir = process.env[CONFIG_HOME_ENV];
|
|
585
|
+
if (customDir && customDir.trim().length > 0) return customDir;
|
|
586
|
+
return join(homedir(), CONFIG_DIRNAME);
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Normalizes a registry URL to a base URL with trailing slash.
|
|
590
|
+
*
|
|
591
|
+
* Examples:
|
|
592
|
+
* - "https://example.com" → "https://example.com/"
|
|
593
|
+
* - "https://example.com/custom/" → "https://example.com/custom/"
|
|
594
|
+
* - "https://example.com/custom" → "https://example.com/custom/"
|
|
595
|
+
*/
|
|
596
|
+
function normalizeRegistryUrl(input) {
|
|
597
|
+
try {
|
|
598
|
+
const parsed = new URL(input);
|
|
599
|
+
if (!parsed.pathname.endsWith("/")) parsed.pathname = `${parsed.pathname}/`;
|
|
600
|
+
return parsed.toString();
|
|
601
|
+
} catch (error$2) {
|
|
602
|
+
throw new Error(`Invalid registry URL "${input}": ${error$2.message}`);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
async function ensureConfigDir() {
|
|
606
|
+
const dir = getConfigDir();
|
|
607
|
+
await mkdir(dir, { recursive: true });
|
|
608
|
+
}
|
|
609
|
+
async function writeDefaultConfig() {
|
|
610
|
+
const configPath = getConfigPath();
|
|
611
|
+
await mkdir(dirname(configPath), { recursive: true });
|
|
612
|
+
await writeFile(configPath, JSON.stringify(DEFAULT_CONFIG, null, 2), "utf8");
|
|
613
|
+
}
|
|
614
|
+
function isNodeError(error$2) {
|
|
615
|
+
return error$2 instanceof Error && typeof error$2.code === "string";
|
|
616
|
+
}
|
|
617
|
+
function mergeWithDefaults(partial) {
|
|
618
|
+
const registries = {
|
|
619
|
+
...DEFAULT_CONFIG.registries,
|
|
620
|
+
...partial.registries ?? {}
|
|
621
|
+
};
|
|
622
|
+
if (!registries[DEFAULT_REGISTRY_ALIAS]) registries[DEFAULT_REGISTRY_ALIAS] = structuredClone(DEFAULT_CONFIG.registries[DEFAULT_REGISTRY_ALIAS]);
|
|
623
|
+
return {
|
|
624
|
+
defaultRegistry: partial.defaultRegistry ?? DEFAULT_CONFIG.defaultRegistry,
|
|
625
|
+
registries
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
//#endregion
|
|
630
|
+
//#region src/commands/registry/manage.ts
|
|
631
|
+
const REGISTRY_ALIAS_PATTERN = /^[a-z0-9][a-z0-9-_]{0,63}$/i;
|
|
632
|
+
async function listRegistries() {
|
|
633
|
+
const config = await loadConfig$1();
|
|
634
|
+
const items = Object.entries(config.registries).map(([alias, settings]) => ({
|
|
635
|
+
alias,
|
|
636
|
+
...settings,
|
|
637
|
+
isDefault: alias === config.defaultRegistry
|
|
638
|
+
})).sort((a, b) => a.alias.localeCompare(b.alias));
|
|
639
|
+
log.debug(`Loaded ${items.length} registries`);
|
|
640
|
+
return items;
|
|
641
|
+
}
|
|
642
|
+
async function addRegistry(alias, url, options = {}) {
|
|
643
|
+
const normalizedAlias = normalizeAlias(alias);
|
|
644
|
+
const normalizedUrl = normalizeRegistryUrl(url);
|
|
645
|
+
const config = await loadConfig$1();
|
|
646
|
+
if (config.registries[normalizedAlias] && !options.overwrite) throw new Error(`Registry "${normalizedAlias}" already exists. Re-run with --force to overwrite.`);
|
|
647
|
+
const isUpdate = !!config.registries[normalizedAlias];
|
|
648
|
+
config.registries[normalizedAlias] = { url: normalizedUrl };
|
|
649
|
+
if (!config.defaultRegistry || options.makeDefault) {
|
|
650
|
+
config.defaultRegistry = normalizedAlias;
|
|
651
|
+
log.debug(`Set "${normalizedAlias}" as default registry`);
|
|
652
|
+
}
|
|
653
|
+
await saveConfig(config);
|
|
654
|
+
log.debug(`${isUpdate ? "Updated" : "Added"} registry "${normalizedAlias}" -> ${normalizedUrl}`);
|
|
655
|
+
return config.registries[normalizedAlias];
|
|
656
|
+
}
|
|
657
|
+
async function removeRegistry(alias, options = {}) {
|
|
658
|
+
const normalizedAlias = normalizeAlias(alias);
|
|
659
|
+
const config = await loadConfig$1();
|
|
660
|
+
if (!config.registries[normalizedAlias]) throw new Error(`Registry "${normalizedAlias}" was not found.`);
|
|
661
|
+
if (normalizedAlias === DEFAULT_REGISTRY_ALIAS) throw new Error("The built-in main registry cannot be removed. Point it somewhere else instead.");
|
|
662
|
+
const isDefault = normalizedAlias === config.defaultRegistry;
|
|
663
|
+
if (isDefault && !options.allowDefaultRemoval) throw new Error(`Registry "${normalizedAlias}" is currently the default. Re-run with --force to remove it.`);
|
|
664
|
+
delete config.registries[normalizedAlias];
|
|
665
|
+
log.debug(`Removed registry "${normalizedAlias}"`);
|
|
666
|
+
if (isDefault) {
|
|
667
|
+
config.defaultRegistry = pickFallbackDefault(config, normalizedAlias);
|
|
668
|
+
log.debug(`Default registry changed to "${config.defaultRegistry}"`);
|
|
669
|
+
}
|
|
670
|
+
await saveConfig(config);
|
|
671
|
+
return {
|
|
672
|
+
removedDefault: isDefault,
|
|
673
|
+
nextDefault: config.defaultRegistry
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
async function useRegistry(alias) {
|
|
677
|
+
const normalizedAlias = normalizeAlias(alias);
|
|
678
|
+
const config = await loadConfig$1();
|
|
679
|
+
if (!config.registries[normalizedAlias]) throw new Error(`Registry "${normalizedAlias}" is not defined.`);
|
|
680
|
+
config.defaultRegistry = normalizedAlias;
|
|
681
|
+
await saveConfig(config);
|
|
682
|
+
log.debug(`Switched default registry to "${normalizedAlias}"`);
|
|
683
|
+
}
|
|
684
|
+
function normalizeAlias(alias) {
|
|
685
|
+
const trimmed = alias.trim();
|
|
686
|
+
if (!trimmed) throw new Error("Alias is required.");
|
|
687
|
+
const normalized = trimmed.toLowerCase();
|
|
688
|
+
if (!REGISTRY_ALIAS_PATTERN.test(normalized)) throw new Error("Aliases may only contain letters, numbers, dashes, and underscores.");
|
|
689
|
+
return normalized;
|
|
690
|
+
}
|
|
691
|
+
function pickFallbackDefault(config, removedAlias) {
|
|
692
|
+
const fallback = Object.keys(config.registries).find((alias) => alias !== removedAlias);
|
|
693
|
+
return fallback ?? DEFAULT_REGISTRY_ALIAS;
|
|
694
|
+
}
|
|
695
|
+
|
|
519
696
|
//#endregion
|
|
520
697
|
//#region src/lib/url.ts
|
|
521
698
|
/**
|
|
@@ -682,9 +859,78 @@ async function publishPreset(baseUrl, token, input) {
|
|
|
682
859
|
}
|
|
683
860
|
/**
|
|
684
861
|
* Unpublishes a preset version from the registry.
|
|
862
|
+
* This unpublishes all platform variants for the specified version.
|
|
685
863
|
*/
|
|
686
|
-
async function unpublishPreset(baseUrl, token, slug,
|
|
687
|
-
const url = `${baseUrl}${API_ENDPOINTS.presets.unpublish(slug,
|
|
864
|
+
async function unpublishPreset(baseUrl, token, slug, version$1) {
|
|
865
|
+
const url = `${baseUrl}${API_ENDPOINTS.presets.unpublish(slug, version$1)}`;
|
|
866
|
+
log.debug(`DELETE ${url}`);
|
|
867
|
+
try {
|
|
868
|
+
const response = await fetch(url, {
|
|
869
|
+
method: "DELETE",
|
|
870
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
871
|
+
});
|
|
872
|
+
log.debug(`Response status: ${response.status}`);
|
|
873
|
+
if (!response.ok) {
|
|
874
|
+
const errorData = await response.json();
|
|
875
|
+
return {
|
|
876
|
+
success: false,
|
|
877
|
+
error: errorData.error || `HTTP ${response.status}`
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
const data = await response.json();
|
|
881
|
+
return {
|
|
882
|
+
success: true,
|
|
883
|
+
data
|
|
884
|
+
};
|
|
885
|
+
} catch (error$2) {
|
|
886
|
+
return {
|
|
887
|
+
success: false,
|
|
888
|
+
error: `Failed to connect to registry: ${getErrorMessage(error$2)}`
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
//#endregion
|
|
894
|
+
//#region src/lib/api/rules.ts
|
|
895
|
+
/**
|
|
896
|
+
* Publish a rule (create or update).
|
|
897
|
+
* The registry handles create vs update automatically.
|
|
898
|
+
*/
|
|
899
|
+
async function publishRule(baseUrl, token, input) {
|
|
900
|
+
const url = `${baseUrl}${API_ENDPOINTS.rules.base}`;
|
|
901
|
+
log.debug(`POST ${url}`);
|
|
902
|
+
try {
|
|
903
|
+
const response = await fetch(url, {
|
|
904
|
+
method: "POST",
|
|
905
|
+
headers: {
|
|
906
|
+
"Content-Type": "application/json",
|
|
907
|
+
Authorization: `Bearer ${token}`
|
|
908
|
+
},
|
|
909
|
+
body: JSON.stringify(input)
|
|
910
|
+
});
|
|
911
|
+
log.debug(`Response status: ${response.status}`);
|
|
912
|
+
if (!response.ok) {
|
|
913
|
+
const errorData = await response.json();
|
|
914
|
+
return {
|
|
915
|
+
success: false,
|
|
916
|
+
error: errorData.error || `HTTP ${response.status}`,
|
|
917
|
+
issues: errorData.issues
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
const data = await response.json();
|
|
921
|
+
return {
|
|
922
|
+
success: true,
|
|
923
|
+
data
|
|
924
|
+
};
|
|
925
|
+
} catch (error$2) {
|
|
926
|
+
return {
|
|
927
|
+
success: false,
|
|
928
|
+
error: `Failed to connect to registry: ${getErrorMessage(error$2)}`
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
async function deleteRule(baseUrl, token, slug) {
|
|
933
|
+
const url = `${baseUrl}${API_ENDPOINTS.rules.bySlug(slug)}`;
|
|
688
934
|
log.debug(`DELETE ${url}`);
|
|
689
935
|
try {
|
|
690
936
|
const response = await fetch(url, {
|
|
@@ -735,164 +981,6 @@ async function fetchSession(baseUrl, token) {
|
|
|
735
981
|
}
|
|
736
982
|
}
|
|
737
983
|
|
|
738
|
-
//#endregion
|
|
739
|
-
//#region src/lib/config.ts
|
|
740
|
-
/** Directory for CLI configuration and credentials (e.g., ~/.agentrules/) */
|
|
741
|
-
const CONFIG_DIRNAME = ".agentrules";
|
|
742
|
-
const CONFIG_FILENAME = "config.json";
|
|
743
|
-
const CONFIG_HOME_ENV = "AGENT_RULES_HOME";
|
|
744
|
-
const DEFAULT_REGISTRY_ALIAS = "main";
|
|
745
|
-
const DEFAULT_REGISTRY_URL = "https://agentrules.directory/";
|
|
746
|
-
const DEFAULT_CONFIG = {
|
|
747
|
-
defaultRegistry: DEFAULT_REGISTRY_ALIAS,
|
|
748
|
-
registries: { [DEFAULT_REGISTRY_ALIAS]: { url: DEFAULT_REGISTRY_URL } }
|
|
749
|
-
};
|
|
750
|
-
async function loadConfig() {
|
|
751
|
-
await ensureConfigDir();
|
|
752
|
-
const configPath = getConfigPath();
|
|
753
|
-
log.debug(`Loading config from ${configPath}`);
|
|
754
|
-
try {
|
|
755
|
-
await access(configPath, constants.F_OK);
|
|
756
|
-
} catch (error$2) {
|
|
757
|
-
if (isNodeError(error$2) && error$2.code === "ENOENT") {
|
|
758
|
-
log.debug("Config file not found, creating default config");
|
|
759
|
-
await writeDefaultConfig();
|
|
760
|
-
return structuredClone(DEFAULT_CONFIG);
|
|
761
|
-
}
|
|
762
|
-
throw error$2 instanceof Error ? error$2 : new Error(String(error$2));
|
|
763
|
-
}
|
|
764
|
-
const raw = await readFile(configPath, "utf8");
|
|
765
|
-
let parsed = {};
|
|
766
|
-
try {
|
|
767
|
-
parsed = JSON.parse(raw);
|
|
768
|
-
} catch (error$2) {
|
|
769
|
-
throw new Error(`Failed to parse ${configPath}: ${error$2.message}`);
|
|
770
|
-
}
|
|
771
|
-
return mergeWithDefaults(parsed);
|
|
772
|
-
}
|
|
773
|
-
async function saveConfig(config) {
|
|
774
|
-
await ensureConfigDir();
|
|
775
|
-
const configPath = getConfigPath();
|
|
776
|
-
log.debug(`Saving config to ${configPath}`);
|
|
777
|
-
const serialized = JSON.stringify(config, null, 2);
|
|
778
|
-
await writeFile(configPath, serialized, "utf8");
|
|
779
|
-
}
|
|
780
|
-
function getConfigPath() {
|
|
781
|
-
return join(getConfigDir(), CONFIG_FILENAME);
|
|
782
|
-
}
|
|
783
|
-
function getConfigDir() {
|
|
784
|
-
const customDir = process.env[CONFIG_HOME_ENV];
|
|
785
|
-
if (customDir && customDir.trim().length > 0) return customDir;
|
|
786
|
-
return join(homedir(), CONFIG_DIRNAME);
|
|
787
|
-
}
|
|
788
|
-
/**
|
|
789
|
-
* Normalizes a registry URL to a base URL with trailing slash.
|
|
790
|
-
*
|
|
791
|
-
* Examples:
|
|
792
|
-
* - "https://example.com" → "https://example.com/"
|
|
793
|
-
* - "https://example.com/custom/" → "https://example.com/custom/"
|
|
794
|
-
* - "https://example.com/custom" → "https://example.com/custom/"
|
|
795
|
-
*/
|
|
796
|
-
function normalizeRegistryUrl(input) {
|
|
797
|
-
try {
|
|
798
|
-
const parsed = new URL(input);
|
|
799
|
-
if (!parsed.pathname.endsWith("/")) parsed.pathname = `${parsed.pathname}/`;
|
|
800
|
-
return parsed.toString();
|
|
801
|
-
} catch (error$2) {
|
|
802
|
-
throw new Error(`Invalid registry URL "${input}": ${error$2.message}`);
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
async function ensureConfigDir() {
|
|
806
|
-
const dir = getConfigDir();
|
|
807
|
-
await mkdir(dir, { recursive: true });
|
|
808
|
-
}
|
|
809
|
-
async function writeDefaultConfig() {
|
|
810
|
-
const configPath = getConfigPath();
|
|
811
|
-
await mkdir(dirname(configPath), { recursive: true });
|
|
812
|
-
await writeFile(configPath, JSON.stringify(DEFAULT_CONFIG, null, 2), "utf8");
|
|
813
|
-
}
|
|
814
|
-
function isNodeError(error$2) {
|
|
815
|
-
return error$2 instanceof Error && typeof error$2.code === "string";
|
|
816
|
-
}
|
|
817
|
-
function mergeWithDefaults(partial) {
|
|
818
|
-
const registries = {
|
|
819
|
-
...DEFAULT_CONFIG.registries,
|
|
820
|
-
...partial.registries ?? {}
|
|
821
|
-
};
|
|
822
|
-
if (!registries[DEFAULT_REGISTRY_ALIAS]) registries[DEFAULT_REGISTRY_ALIAS] = structuredClone(DEFAULT_CONFIG.registries[DEFAULT_REGISTRY_ALIAS]);
|
|
823
|
-
return {
|
|
824
|
-
defaultRegistry: partial.defaultRegistry ?? DEFAULT_CONFIG.defaultRegistry,
|
|
825
|
-
registries
|
|
826
|
-
};
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
//#endregion
|
|
830
|
-
//#region src/commands/registry/manage.ts
|
|
831
|
-
const REGISTRY_ALIAS_PATTERN = /^[a-z0-9][a-z0-9-_]{0,63}$/i;
|
|
832
|
-
async function listRegistries() {
|
|
833
|
-
const config = await loadConfig();
|
|
834
|
-
const items = Object.entries(config.registries).map(([alias, settings]) => ({
|
|
835
|
-
alias,
|
|
836
|
-
...settings,
|
|
837
|
-
isDefault: alias === config.defaultRegistry
|
|
838
|
-
})).sort((a, b) => a.alias.localeCompare(b.alias));
|
|
839
|
-
log.debug(`Loaded ${items.length} registries`);
|
|
840
|
-
return items;
|
|
841
|
-
}
|
|
842
|
-
async function addRegistry(alias, url, options = {}) {
|
|
843
|
-
const normalizedAlias = normalizeAlias(alias);
|
|
844
|
-
const normalizedUrl = normalizeRegistryUrl(url);
|
|
845
|
-
const config = await loadConfig();
|
|
846
|
-
if (config.registries[normalizedAlias] && !options.overwrite) throw new Error(`Registry "${normalizedAlias}" already exists. Re-run with --force to overwrite.`);
|
|
847
|
-
const isUpdate = !!config.registries[normalizedAlias];
|
|
848
|
-
config.registries[normalizedAlias] = { url: normalizedUrl };
|
|
849
|
-
if (!config.defaultRegistry || options.makeDefault) {
|
|
850
|
-
config.defaultRegistry = normalizedAlias;
|
|
851
|
-
log.debug(`Set "${normalizedAlias}" as default registry`);
|
|
852
|
-
}
|
|
853
|
-
await saveConfig(config);
|
|
854
|
-
log.debug(`${isUpdate ? "Updated" : "Added"} registry "${normalizedAlias}" -> ${normalizedUrl}`);
|
|
855
|
-
return config.registries[normalizedAlias];
|
|
856
|
-
}
|
|
857
|
-
async function removeRegistry(alias, options = {}) {
|
|
858
|
-
const normalizedAlias = normalizeAlias(alias);
|
|
859
|
-
const config = await loadConfig();
|
|
860
|
-
if (!config.registries[normalizedAlias]) throw new Error(`Registry "${normalizedAlias}" was not found.`);
|
|
861
|
-
if (normalizedAlias === DEFAULT_REGISTRY_ALIAS) throw new Error("The built-in main registry cannot be removed. Point it somewhere else instead.");
|
|
862
|
-
const isDefault = normalizedAlias === config.defaultRegistry;
|
|
863
|
-
if (isDefault && !options.allowDefaultRemoval) throw new Error(`Registry "${normalizedAlias}" is currently the default. Re-run with --force to remove it.`);
|
|
864
|
-
delete config.registries[normalizedAlias];
|
|
865
|
-
log.debug(`Removed registry "${normalizedAlias}"`);
|
|
866
|
-
if (isDefault) {
|
|
867
|
-
config.defaultRegistry = pickFallbackDefault(config, normalizedAlias);
|
|
868
|
-
log.debug(`Default registry changed to "${config.defaultRegistry}"`);
|
|
869
|
-
}
|
|
870
|
-
await saveConfig(config);
|
|
871
|
-
return {
|
|
872
|
-
removedDefault: isDefault,
|
|
873
|
-
nextDefault: config.defaultRegistry
|
|
874
|
-
};
|
|
875
|
-
}
|
|
876
|
-
async function useRegistry(alias) {
|
|
877
|
-
const normalizedAlias = normalizeAlias(alias);
|
|
878
|
-
const config = await loadConfig();
|
|
879
|
-
if (!config.registries[normalizedAlias]) throw new Error(`Registry "${normalizedAlias}" is not defined.`);
|
|
880
|
-
config.defaultRegistry = normalizedAlias;
|
|
881
|
-
await saveConfig(config);
|
|
882
|
-
log.debug(`Switched default registry to "${normalizedAlias}"`);
|
|
883
|
-
}
|
|
884
|
-
function normalizeAlias(alias) {
|
|
885
|
-
const trimmed = alias.trim();
|
|
886
|
-
if (!trimmed) throw new Error("Alias is required.");
|
|
887
|
-
const normalized = trimmed.toLowerCase();
|
|
888
|
-
if (!REGISTRY_ALIAS_PATTERN.test(normalized)) throw new Error("Aliases may only contain letters, numbers, dashes, and underscores.");
|
|
889
|
-
return normalized;
|
|
890
|
-
}
|
|
891
|
-
function pickFallbackDefault(config, removedAlias) {
|
|
892
|
-
const fallback = Object.keys(config.registries).find((alias) => alias !== removedAlias);
|
|
893
|
-
return fallback ?? DEFAULT_REGISTRY_ALIAS;
|
|
894
|
-
}
|
|
895
|
-
|
|
896
984
|
//#endregion
|
|
897
985
|
//#region src/lib/credentials.ts
|
|
898
986
|
const chmodAsync = promisify(chmod);
|
|
@@ -922,7 +1010,7 @@ function normalizeUrl(url) {
|
|
|
922
1010
|
async function loadStore() {
|
|
923
1011
|
const credentialsPath = getCredentialsPath();
|
|
924
1012
|
try {
|
|
925
|
-
await access(credentialsPath, constants
|
|
1013
|
+
await access(credentialsPath, constants.F_OK);
|
|
926
1014
|
} catch {
|
|
927
1015
|
log.debug(`Credentials file not found at ${credentialsPath}`);
|
|
928
1016
|
return {};
|
|
@@ -1014,7 +1102,7 @@ async function clearAllCredentials() {
|
|
|
1014
1102
|
async function createAppContext(options = {}) {
|
|
1015
1103
|
const { url: explicitUrl, registryAlias } = options;
|
|
1016
1104
|
log.debug("Loading app context");
|
|
1017
|
-
const config = await loadConfig();
|
|
1105
|
+
const config = await loadConfig$1();
|
|
1018
1106
|
const registry$1 = explicitUrl ? resolveExplicitUrl(explicitUrl) : resolveRegistry(config, registryAlias);
|
|
1019
1107
|
log.debug(`Active registry: ${registry$1.alias} → ${registry$1.url}`);
|
|
1020
1108
|
const credentials = await getCredentials(registry$1.url);
|
|
@@ -1099,273 +1187,150 @@ async function initAppContext(options = {}) {
|
|
|
1099
1187
|
}
|
|
1100
1188
|
|
|
1101
1189
|
//#endregion
|
|
1102
|
-
//#region src/commands/
|
|
1103
|
-
|
|
1104
|
-
const CLIENT_ID = "agentrules-cli";
|
|
1105
|
-
/**
|
|
1106
|
-
* Performs device code flow login to an agentrules registry.
|
|
1107
|
-
*/
|
|
1108
|
-
async function login(options = {}) {
|
|
1109
|
-
const { noBrowser = false, onDeviceCode, onBrowserOpen, onPollingStart, onAuthorized } = options;
|
|
1190
|
+
//#region src/commands/add.ts
|
|
1191
|
+
async function add(options) {
|
|
1110
1192
|
const ctx = useAppContext();
|
|
1111
|
-
const { url: registryUrl } = ctx.registry;
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
});
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
const { data: deviceAuthResponse, config } = codeResult;
|
|
1124
|
-
const formattedCode = formatUserCode(deviceAuthResponse.user_code);
|
|
1125
|
-
onDeviceCode?.({
|
|
1126
|
-
userCode: formattedCode,
|
|
1127
|
-
verificationUri: deviceAuthResponse.verification_uri,
|
|
1128
|
-
verificationUriComplete: deviceAuthResponse.verification_uri_complete
|
|
1129
|
-
});
|
|
1130
|
-
let browserOpened = false;
|
|
1131
|
-
if (!noBrowser) try {
|
|
1132
|
-
const urlToOpen = deviceAuthResponse.verification_uri_complete ?? deviceAuthResponse.verification_uri;
|
|
1133
|
-
log.debug(`Opening browser: ${urlToOpen}`);
|
|
1134
|
-
await openBrowser(urlToOpen);
|
|
1135
|
-
browserOpened = true;
|
|
1136
|
-
} catch {
|
|
1137
|
-
log.debug("Failed to open browser");
|
|
1138
|
-
}
|
|
1139
|
-
onBrowserOpen?.(browserOpened);
|
|
1140
|
-
log.debug("Waiting for user authorization");
|
|
1141
|
-
onPollingStart?.();
|
|
1142
|
-
const pollResult = await pollForToken({
|
|
1143
|
-
config,
|
|
1144
|
-
deviceAuthorizationResponse: deviceAuthResponse
|
|
1145
|
-
});
|
|
1146
|
-
if (pollResult.success === false) return {
|
|
1147
|
-
success: false,
|
|
1148
|
-
error: pollResult.error
|
|
1149
|
-
};
|
|
1150
|
-
onAuthorized?.();
|
|
1151
|
-
const token = pollResult.token.access_token;
|
|
1152
|
-
log.debug("Fetching user info");
|
|
1153
|
-
const session = await fetchSession(registryUrl, token);
|
|
1154
|
-
const user = session?.user;
|
|
1155
|
-
const sessionExpiresAt = session?.session?.expiresAt;
|
|
1156
|
-
const expiresAt = sessionExpiresAt ?? (pollResult.token.expires_in ? new Date(Date.now() + pollResult.token.expires_in * 1e3).toISOString() : void 0);
|
|
1157
|
-
log.debug("Saving credentials");
|
|
1158
|
-
await saveCredentials(registryUrl, {
|
|
1159
|
-
token,
|
|
1160
|
-
expiresAt,
|
|
1161
|
-
userId: user?.id,
|
|
1162
|
-
userName: user?.name,
|
|
1163
|
-
userEmail: user?.email
|
|
1193
|
+
const { alias: registryAlias, url: registryUrl } = ctx.registry;
|
|
1194
|
+
const dryRun = Boolean(options.dryRun);
|
|
1195
|
+
const { slug, platform, version: version$1 } = parseInput(options.slug, options.platform, options.version);
|
|
1196
|
+
log.debug(`Resolving ${slug}${version$1 ? ` (version ${version$1})` : ""}`);
|
|
1197
|
+
const resolved = await resolveSlug(registryUrl, slug, version$1);
|
|
1198
|
+
if (!resolved) throw new Error(`"${slug}" was not found in the registry.`);
|
|
1199
|
+
if (resolved.kind === "preset") {
|
|
1200
|
+
const { selectedVersion: presetVersion, selectedVariant: presetVariant } = selectPresetVariant(resolved, version$1, platform);
|
|
1201
|
+
return addPreset(resolved, presetVersion, presetVariant, {
|
|
1202
|
+
...options,
|
|
1203
|
+
registryAlias,
|
|
1204
|
+
dryRun
|
|
1164
1205
|
});
|
|
1165
|
-
return {
|
|
1166
|
-
success: true,
|
|
1167
|
-
user: user ? {
|
|
1168
|
-
id: user.id,
|
|
1169
|
-
name: user.name,
|
|
1170
|
-
email: user.email
|
|
1171
|
-
} : void 0
|
|
1172
|
-
};
|
|
1173
|
-
} catch (error$2) {
|
|
1174
|
-
return {
|
|
1175
|
-
success: false,
|
|
1176
|
-
error: getErrorMessage(error$2)
|
|
1177
|
-
};
|
|
1178
1206
|
}
|
|
1207
|
+
const { selectedVersion: ruleVersion, selectedVariant: ruleVariant } = selectRuleVariant(resolved, version$1, platform);
|
|
1208
|
+
return addRule(resolved, ruleVersion, ruleVariant, {
|
|
1209
|
+
...options,
|
|
1210
|
+
registryAlias,
|
|
1211
|
+
dryRun
|
|
1212
|
+
});
|
|
1179
1213
|
}
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
if (cleaned.length <= 4) return cleaned;
|
|
1186
|
-
return `${cleaned.slice(0, 4)}-${cleaned.slice(4, 8)}`;
|
|
1187
|
-
}
|
|
1188
|
-
/**
|
|
1189
|
-
* Opens a URL in the default browser.
|
|
1190
|
-
*/
|
|
1191
|
-
async function openBrowser(url) {
|
|
1192
|
-
const platform = process.platform;
|
|
1193
|
-
const commands = {
|
|
1194
|
-
darwin: `open "${url}"`,
|
|
1195
|
-
win32: `start "" "${url}"`,
|
|
1196
|
-
linux: `xdg-open "${url}"`
|
|
1197
|
-
};
|
|
1198
|
-
const command$1 = commands[platform];
|
|
1199
|
-
if (!command$1) throw new Error(`Unsupported platform: ${platform}`);
|
|
1200
|
-
await execAsync(command$1);
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
//#endregion
|
|
1204
|
-
//#region src/commands/auth/logout.ts
|
|
1205
|
-
/**
|
|
1206
|
-
* Logs out by clearing stored credentials
|
|
1207
|
-
*/
|
|
1208
|
-
async function logout(options = {}) {
|
|
1209
|
-
const { all = false } = options;
|
|
1210
|
-
if (all) {
|
|
1211
|
-
log.debug("Clearing all stored credentials");
|
|
1212
|
-
await clearAllCredentials();
|
|
1213
|
-
return {
|
|
1214
|
-
success: true,
|
|
1215
|
-
hadCredentials: true
|
|
1216
|
-
};
|
|
1214
|
+
function selectPresetVariant(resolved, requestedVersion, platform) {
|
|
1215
|
+
const selectedVersion = requestedVersion ? getPresetVersion(resolved, requestedVersion) : getLatestPresetVersion(resolved);
|
|
1216
|
+
if (!selectedVersion) {
|
|
1217
|
+
const versionLabel = requestedVersion ?? "latest";
|
|
1218
|
+
throw new Error(`Version "${versionLabel}" not found for "${resolved.slug}".`);
|
|
1217
1219
|
}
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1220
|
+
if (selectedVersion.variants.length === 0) throw new Error(`No platform variants found for "${resolved.slug}".`);
|
|
1221
|
+
if (selectedVersion.variants.length > 1 && !platform) {
|
|
1222
|
+
const platforms = selectedVersion.variants.map((v) => v.platform).join(", ");
|
|
1223
|
+
throw new Error(`"${resolved.slug}" is available for multiple platforms: ${platforms}. Use --platform <platform> to specify which one.`);
|
|
1224
|
+
}
|
|
1225
|
+
const selectedVariant = platform ? getPresetVariant(selectedVersion, platform) : selectedVersion.variants[0];
|
|
1226
|
+
if (!selectedVariant) throw new Error(`Platform "${platform}" not found for "${resolved.slug}" v${selectedVersion.version}.`);
|
|
1225
1227
|
return {
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
+
selectedVersion,
|
|
1229
|
+
selectedVariant
|
|
1228
1230
|
};
|
|
1229
1231
|
}
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1232
|
+
function selectRuleVariant(resolved, requestedVersion, platform) {
|
|
1233
|
+
const selectedVersion = requestedVersion ? getRuleVersion(resolved, requestedVersion) : getLatestRuleVersion(resolved);
|
|
1234
|
+
if (!selectedVersion) {
|
|
1235
|
+
const versionLabel = requestedVersion ?? "latest";
|
|
1236
|
+
throw new Error(`Version "${versionLabel}" not found for "${resolved.slug}".`);
|
|
1237
|
+
}
|
|
1238
|
+
if (selectedVersion.variants.length === 0) throw new Error(`No platform variants found for "${resolved.slug}".`);
|
|
1239
|
+
if (selectedVersion.variants.length > 1 && !platform) {
|
|
1240
|
+
const platforms = selectedVersion.variants.map((v) => v.platform).join(", ");
|
|
1241
|
+
throw new Error(`"${resolved.slug}" is available for multiple platforms: ${platforms}. Use --platform <platform> to specify which one.`);
|
|
1242
|
+
}
|
|
1243
|
+
const selectedVariant = platform ? getRuleVariant(selectedVersion, platform) : selectedVersion.variants[0];
|
|
1244
|
+
if (!selectedVariant) throw new Error(`Platform "${platform}" not found for "${resolved.slug}" v${selectedVersion.version}.`);
|
|
1239
1245
|
return {
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
user: ctx.user ?? void 0,
|
|
1243
|
-
registryUrl,
|
|
1244
|
-
expiresAt: ctx.credentials?.expiresAt
|
|
1246
|
+
selectedVersion,
|
|
1247
|
+
selectedVariant
|
|
1245
1248
|
};
|
|
1246
1249
|
}
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
const
|
|
1259
|
-
if (bundle.slug !== preset.slug || bundle.platform !== preset.platform) throw new Error(`Preset bundle metadata mismatch for "${preset.name}". Expected slug "${preset.slug}" (${preset.platform}).`);
|
|
1260
|
-
const target = resolveInstallTarget(bundle.platform, options);
|
|
1250
|
+
async function addPreset(resolved, version$1, variant, options) {
|
|
1251
|
+
log.debug(`Installing preset: ${variant.platform} v${version$1.version}`);
|
|
1252
|
+
let bundle;
|
|
1253
|
+
if (hasBundle(variant)) {
|
|
1254
|
+
log.debug(`Downloading bundle from ${variant.bundleUrl}`);
|
|
1255
|
+
bundle = await fetchBundle(variant.bundleUrl);
|
|
1256
|
+
} else {
|
|
1257
|
+
log.debug("Using inline bundle content");
|
|
1258
|
+
bundle = JSON.parse(variant.content);
|
|
1259
|
+
}
|
|
1260
|
+
if (bundle.slug !== resolved.slug || bundle.platform !== variant.platform) throw new Error(`Bundle metadata mismatch for "${resolved.slug}". Expected slug "${resolved.slug}" (${variant.platform}).`);
|
|
1261
|
+
const target = resolveInstallTarget(variant.platform, options);
|
|
1261
1262
|
log.debug(`Writing ${bundle.files.length} files to ${target.root}`);
|
|
1262
|
-
const
|
|
1263
|
+
const filesToWrite = [];
|
|
1264
|
+
for (const file of bundle.files) {
|
|
1265
|
+
const decoded = decodeBundledFile(file);
|
|
1266
|
+
const data = Buffer.from(decoded);
|
|
1267
|
+
await verifyBundledFileChecksum(file, data);
|
|
1268
|
+
const destPath = computePresetDestinationPath(file.path, target);
|
|
1269
|
+
filesToWrite.push({
|
|
1270
|
+
path: destPath,
|
|
1271
|
+
content: data
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
const writeStats = await writeFiles(filesToWrite, target.root, {
|
|
1263
1275
|
force: Boolean(options.force),
|
|
1264
1276
|
skipConflicts: Boolean(options.skipConflicts),
|
|
1265
1277
|
noBackup: Boolean(options.noBackup),
|
|
1266
|
-
dryRun
|
|
1278
|
+
dryRun: options.dryRun
|
|
1267
1279
|
});
|
|
1268
1280
|
return {
|
|
1269
|
-
preset,
|
|
1281
|
+
kind: "preset",
|
|
1282
|
+
resolved,
|
|
1283
|
+
version: version$1,
|
|
1284
|
+
variant,
|
|
1270
1285
|
bundle,
|
|
1271
1286
|
files: writeStats.files,
|
|
1272
|
-
conflicts: writeStats.conflicts,
|
|
1273
1287
|
backups: writeStats.backups,
|
|
1274
1288
|
targetRoot: target.root,
|
|
1275
1289
|
targetLabel: target.label,
|
|
1276
|
-
registryAlias,
|
|
1277
|
-
dryRun
|
|
1278
|
-
};
|
|
1279
|
-
}
|
|
1280
|
-
/**
|
|
1281
|
-
* Parses preset input to extract slug, platform, and version.
|
|
1282
|
-
* Supports formats:
|
|
1283
|
-
* - "my-preset" (requires explicit platform)
|
|
1284
|
-
* - "my-preset.claude" (platform inferred from suffix)
|
|
1285
|
-
* - "my-preset@1.0" (with version)
|
|
1286
|
-
* - "my-preset.claude@1.0" (platform and version)
|
|
1287
|
-
*
|
|
1288
|
-
* Version can also be provided via --version flag (takes precedence).
|
|
1289
|
-
*/
|
|
1290
|
-
function parsePresetInput(input, explicitPlatform, explicitVersion) {
|
|
1291
|
-
let normalized = input.toLowerCase().trim();
|
|
1292
|
-
let parsedVersion;
|
|
1293
|
-
const atIndex = normalized.lastIndexOf("@");
|
|
1294
|
-
if (atIndex > 0) {
|
|
1295
|
-
parsedVersion = normalized.slice(atIndex + 1);
|
|
1296
|
-
normalized = normalized.slice(0, atIndex);
|
|
1297
|
-
}
|
|
1298
|
-
const version$1 = explicitVersion ?? parsedVersion;
|
|
1299
|
-
if (explicitPlatform) {
|
|
1300
|
-
const parts$1 = normalized.split(".");
|
|
1301
|
-
const maybePlatform$1 = parts$1.at(-1);
|
|
1302
|
-
if (maybePlatform$1 && isSupportedPlatform(maybePlatform$1)) return {
|
|
1303
|
-
slug: parts$1.slice(0, -1).join("."),
|
|
1304
|
-
platform: explicitPlatform,
|
|
1305
|
-
version: version$1
|
|
1306
|
-
};
|
|
1307
|
-
return {
|
|
1308
|
-
slug: normalized,
|
|
1309
|
-
platform: explicitPlatform,
|
|
1310
|
-
version: version$1
|
|
1311
|
-
};
|
|
1312
|
-
}
|
|
1313
|
-
const parts = normalized.split(".");
|
|
1314
|
-
const maybePlatform = parts.at(-1);
|
|
1315
|
-
if (maybePlatform && isSupportedPlatform(maybePlatform)) return {
|
|
1316
|
-
slug: parts.slice(0, -1).join("."),
|
|
1317
|
-
platform: maybePlatform,
|
|
1318
|
-
version: version$1
|
|
1290
|
+
registryAlias: options.registryAlias,
|
|
1291
|
+
dryRun: options.dryRun
|
|
1319
1292
|
};
|
|
1320
|
-
throw new Error(`Platform not specified. Use --platform <platform> or specify as <slug>.<platform> (e.g., "${input}.claude").`);
|
|
1321
1293
|
}
|
|
1322
|
-
function
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
const globalRoot = resolve(expandHome(globalDir));
|
|
1336
|
-
return {
|
|
1337
|
-
root: globalRoot,
|
|
1338
|
-
mode: "global",
|
|
1339
|
-
platform,
|
|
1340
|
-
projectDir,
|
|
1341
|
-
label: `global path ${globalRoot}`
|
|
1342
|
-
};
|
|
1343
|
-
}
|
|
1344
|
-
const projectRoot = process.cwd();
|
|
1294
|
+
async function addRule(resolved, version$1, variant, options) {
|
|
1295
|
+
log.debug(`Installing rule: ${variant.platform} v${version$1.version}`);
|
|
1296
|
+
const { targetPath, targetRoot, targetLabel } = resolveRuleTarget(variant.platform, variant.type, resolved.name, options);
|
|
1297
|
+
log.debug(`Target path: ${targetPath}`);
|
|
1298
|
+
const writeStats = await writeFiles([{
|
|
1299
|
+
path: targetPath,
|
|
1300
|
+
content: Buffer.from(variant.content, "utf-8")
|
|
1301
|
+
}], targetRoot, {
|
|
1302
|
+
force: Boolean(options.force),
|
|
1303
|
+
skipConflicts: false,
|
|
1304
|
+
noBackup: Boolean(options.noBackup),
|
|
1305
|
+
dryRun: options.dryRun
|
|
1306
|
+
});
|
|
1345
1307
|
return {
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1308
|
+
kind: "rule",
|
|
1309
|
+
resolved,
|
|
1310
|
+
version: version$1,
|
|
1311
|
+
variant,
|
|
1312
|
+
files: writeStats.files,
|
|
1313
|
+
backups: writeStats.backups,
|
|
1314
|
+
targetRoot,
|
|
1315
|
+
targetLabel,
|
|
1316
|
+
registryAlias: options.registryAlias,
|
|
1317
|
+
dryRun: options.dryRun
|
|
1351
1318
|
};
|
|
1352
1319
|
}
|
|
1353
|
-
|
|
1320
|
+
/**
|
|
1321
|
+
* Write files with conflict detection, backup support, and diff preview.
|
|
1322
|
+
* Used by both preset and rule installation.
|
|
1323
|
+
*/
|
|
1324
|
+
async function writeFiles(filesToWrite, root, options) {
|
|
1354
1325
|
const files = [];
|
|
1355
|
-
const conflicts = [];
|
|
1356
1326
|
const backups = [];
|
|
1357
|
-
if (!
|
|
1358
|
-
for (const
|
|
1359
|
-
|
|
1360
|
-
const data = Buffer.from(decoded);
|
|
1361
|
-
await verifyBundledFileChecksum(file, data);
|
|
1362
|
-
const destResult = computeDestinationPath(file.path, target);
|
|
1363
|
-
const destination = destResult.path;
|
|
1364
|
-
if (!behavior.dryRun) await mkdir(dirname(destination), { recursive: true });
|
|
1327
|
+
if (!options.dryRun) await mkdir(root, { recursive: true });
|
|
1328
|
+
for (const { path: destination, content } of filesToWrite) {
|
|
1329
|
+
if (!options.dryRun) await mkdir(dirname(destination), { recursive: true });
|
|
1365
1330
|
const existing = await readExistingFile(destination);
|
|
1366
|
-
const relativePath = relativize(destination,
|
|
1331
|
+
const relativePath = relativize(destination, root);
|
|
1367
1332
|
if (!existing) {
|
|
1368
|
-
if (!
|
|
1333
|
+
if (!options.dryRun) await writeFile(destination, content);
|
|
1369
1334
|
files.push({
|
|
1370
1335
|
path: relativePath,
|
|
1371
1336
|
status: "created"
|
|
@@ -1373,7 +1338,7 @@ async function writeBundleFiles(bundle, target, behavior) {
|
|
|
1373
1338
|
log.debug(`Created: ${relativePath}`);
|
|
1374
1339
|
continue;
|
|
1375
1340
|
}
|
|
1376
|
-
if (existing.equals(
|
|
1341
|
+
if (existing.equals(content)) {
|
|
1377
1342
|
files.push({
|
|
1378
1343
|
path: relativePath,
|
|
1379
1344
|
status: "unchanged"
|
|
@@ -1381,18 +1346,19 @@ async function writeBundleFiles(bundle, target, behavior) {
|
|
|
1381
1346
|
log.debug(`Unchanged: ${relativePath}`);
|
|
1382
1347
|
continue;
|
|
1383
1348
|
}
|
|
1384
|
-
|
|
1385
|
-
|
|
1349
|
+
const diff = renderDiffPreview(relativePath, existing, content);
|
|
1350
|
+
if (options.force) {
|
|
1351
|
+
if (!options.noBackup) {
|
|
1386
1352
|
const backupPath = `${destination}.bak`;
|
|
1387
1353
|
const relativeBackupPath = `${relativePath}.bak`;
|
|
1388
|
-
if (!
|
|
1354
|
+
if (!options.dryRun) await copyFile(destination, backupPath);
|
|
1389
1355
|
backups.push({
|
|
1390
1356
|
originalPath: relativePath,
|
|
1391
1357
|
backupPath: relativeBackupPath
|
|
1392
1358
|
});
|
|
1393
1359
|
log.debug(`Backed up: ${relativePath} → ${relativeBackupPath}`);
|
|
1394
1360
|
}
|
|
1395
|
-
if (!
|
|
1361
|
+
if (!options.dryRun) await writeFile(destination, content);
|
|
1396
1362
|
files.push({
|
|
1397
1363
|
path: relativePath,
|
|
1398
1364
|
status: "overwritten"
|
|
@@ -1400,31 +1366,78 @@ async function writeBundleFiles(bundle, target, behavior) {
|
|
|
1400
1366
|
log.debug(`Overwritten: ${relativePath}`);
|
|
1401
1367
|
continue;
|
|
1402
1368
|
}
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
}
|
|
1411
|
-
|
|
1369
|
+
if (options.skipConflicts) {
|
|
1370
|
+
files.push({
|
|
1371
|
+
path: relativePath,
|
|
1372
|
+
status: "skipped",
|
|
1373
|
+
diff
|
|
1374
|
+
});
|
|
1375
|
+
log.debug(`Skipped: ${relativePath}`);
|
|
1376
|
+
} else {
|
|
1377
|
+
files.push({
|
|
1378
|
+
path: relativePath,
|
|
1379
|
+
status: "conflict",
|
|
1380
|
+
diff
|
|
1381
|
+
});
|
|
1382
|
+
log.debug(`Conflict: ${relativePath}`);
|
|
1383
|
+
}
|
|
1412
1384
|
}
|
|
1413
1385
|
return {
|
|
1414
1386
|
files,
|
|
1415
|
-
conflicts,
|
|
1416
1387
|
backups
|
|
1417
1388
|
};
|
|
1418
1389
|
}
|
|
1419
1390
|
/**
|
|
1420
|
-
*
|
|
1421
|
-
*
|
|
1422
|
-
* Bundle files are stored with paths relative to the platform directory
|
|
1423
|
-
* (e.g., "AGENTS.md", "commands/test.md") and installed to:
|
|
1424
|
-
* - Project/custom: <root>/<projectDir>/<path> (e.g., .opencode/AGENTS.md)
|
|
1425
|
-
* - Global: <root>/<path> (e.g., ~/.config/opencode/AGENTS.md)
|
|
1391
|
+
* Parse input to extract slug and version.
|
|
1392
|
+
* Platform must be specified via --platform flag.
|
|
1426
1393
|
*/
|
|
1427
|
-
function
|
|
1394
|
+
function parseInput(input, explicitPlatform, explicitVersion) {
|
|
1395
|
+
let normalized = input.toLowerCase().trim();
|
|
1396
|
+
let parsedVersion;
|
|
1397
|
+
const atIndex = normalized.lastIndexOf("@");
|
|
1398
|
+
if (atIndex > 0) {
|
|
1399
|
+
parsedVersion = normalized.slice(atIndex + 1);
|
|
1400
|
+
normalized = normalized.slice(0, atIndex);
|
|
1401
|
+
}
|
|
1402
|
+
return {
|
|
1403
|
+
slug: normalized,
|
|
1404
|
+
platform: explicitPlatform,
|
|
1405
|
+
version: explicitVersion ?? parsedVersion
|
|
1406
|
+
};
|
|
1407
|
+
}
|
|
1408
|
+
function resolveInstallTarget(platform, options) {
|
|
1409
|
+
const { projectDir, globalDir } = PLATFORMS[platform];
|
|
1410
|
+
if (options.directory) {
|
|
1411
|
+
const customRoot = resolve(expandHome(options.directory));
|
|
1412
|
+
return {
|
|
1413
|
+
root: customRoot,
|
|
1414
|
+
mode: "custom",
|
|
1415
|
+
platform,
|
|
1416
|
+
projectDir,
|
|
1417
|
+
label: `custom directory ${customRoot}`
|
|
1418
|
+
};
|
|
1419
|
+
}
|
|
1420
|
+
if (options.global) {
|
|
1421
|
+
if (!globalDir) throw new Error(`Platform "${platform}" does not support global installation`);
|
|
1422
|
+
const globalRoot = resolve(expandHome(globalDir));
|
|
1423
|
+
return {
|
|
1424
|
+
root: globalRoot,
|
|
1425
|
+
mode: "global",
|
|
1426
|
+
platform,
|
|
1427
|
+
projectDir,
|
|
1428
|
+
label: `global path ${globalRoot}`
|
|
1429
|
+
};
|
|
1430
|
+
}
|
|
1431
|
+
const projectRoot = process.cwd();
|
|
1432
|
+
return {
|
|
1433
|
+
root: projectRoot,
|
|
1434
|
+
mode: "project",
|
|
1435
|
+
platform,
|
|
1436
|
+
projectDir,
|
|
1437
|
+
label: `project root ${projectRoot}`
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1440
|
+
function computePresetDestinationPath(pathInput, target) {
|
|
1428
1441
|
const normalized = normalizeBundlePath(pathInput);
|
|
1429
1442
|
if (!normalized) throw new Error(`Unable to derive destination for ${pathInput}. The computed relative path is empty.`);
|
|
1430
1443
|
let relativePath;
|
|
@@ -1432,7 +1445,36 @@ function computeDestinationPath(pathInput, target) {
|
|
|
1432
1445
|
else relativePath = `${target.projectDir}/${normalized}`;
|
|
1433
1446
|
const destination = resolve(target.root, relativePath);
|
|
1434
1447
|
ensureWithinRoot(destination, target.root);
|
|
1435
|
-
return
|
|
1448
|
+
return destination;
|
|
1449
|
+
}
|
|
1450
|
+
function resolveRuleTarget(platform, type, name, options) {
|
|
1451
|
+
const location = options.global ? "global" : "project";
|
|
1452
|
+
const pathTemplate = getInstallPath(platform, type, name, location);
|
|
1453
|
+
if (!pathTemplate) {
|
|
1454
|
+
const locationLabel = options.global ? "globally" : "to a project";
|
|
1455
|
+
throw new Error(`Rule type "${type}" cannot be installed ${locationLabel} for platform "${platform}"`);
|
|
1456
|
+
}
|
|
1457
|
+
if (options.directory) {
|
|
1458
|
+
const customRoot = resolve(expandHome(options.directory));
|
|
1459
|
+
const filename = pathTemplate.split("/").pop() ?? `${name}.md`;
|
|
1460
|
+
const targetPath$1 = resolve(customRoot, filename);
|
|
1461
|
+
ensureWithinRoot(targetPath$1, customRoot);
|
|
1462
|
+
return {
|
|
1463
|
+
targetPath: targetPath$1,
|
|
1464
|
+
targetRoot: customRoot,
|
|
1465
|
+
targetLabel: `custom directory ${customRoot}`
|
|
1466
|
+
};
|
|
1467
|
+
}
|
|
1468
|
+
const expanded = expandHome(pathTemplate);
|
|
1469
|
+
const targetPath = expanded.startsWith("/") ? expanded : resolve(process.cwd(), expanded);
|
|
1470
|
+
const targetRoot = options.global ? dirname(targetPath) : process.cwd();
|
|
1471
|
+
ensureWithinRoot(targetPath, targetRoot);
|
|
1472
|
+
const targetLabel = options.global ? `global path ${targetRoot}` : `project root ${targetRoot}`;
|
|
1473
|
+
return {
|
|
1474
|
+
targetPath,
|
|
1475
|
+
targetRoot,
|
|
1476
|
+
targetLabel
|
|
1477
|
+
};
|
|
1436
1478
|
}
|
|
1437
1479
|
async function readExistingFile(pathname) {
|
|
1438
1480
|
try {
|
|
@@ -1463,14 +1505,169 @@ function ensureWithinRoot(candidate, root) {
|
|
|
1463
1505
|
if (candidate === root) return;
|
|
1464
1506
|
if (!candidate.startsWith(normalizedRoot)) throw new Error(`Refusing to write outside of ${root}. Derived path: ${candidate}`);
|
|
1465
1507
|
}
|
|
1508
|
+
/**
|
|
1509
|
+
* Expands ~ to the user's home directory.
|
|
1510
|
+
*
|
|
1511
|
+
* Uses process.env.HOME (Unix) or process.env.USERPROFILE (Windows) first,
|
|
1512
|
+
* falling back to os.homedir(). This matches shell behavior and allows
|
|
1513
|
+
* tests to override the home directory via environment variables.
|
|
1514
|
+
*/
|
|
1466
1515
|
function expandHome(value) {
|
|
1467
1516
|
if (value.startsWith("~")) {
|
|
1468
1517
|
const remainder = value.slice(1);
|
|
1469
|
-
|
|
1470
|
-
if (remainder
|
|
1471
|
-
return `${
|
|
1518
|
+
const home = process.env.HOME || process.env.USERPROFILE || homedir();
|
|
1519
|
+
if (!remainder) return home;
|
|
1520
|
+
if (remainder.startsWith("/") || remainder.startsWith("\\")) return `${home}${remainder}`;
|
|
1521
|
+
return `${home}/${remainder}`;
|
|
1522
|
+
}
|
|
1523
|
+
return value;
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
//#endregion
|
|
1527
|
+
//#region src/commands/auth/login.ts
|
|
1528
|
+
const execAsync = promisify(exec);
|
|
1529
|
+
const CLIENT_ID = "agentrules-cli";
|
|
1530
|
+
/**
|
|
1531
|
+
* Performs device code flow login to an agentrules registry.
|
|
1532
|
+
*/
|
|
1533
|
+
async function login(options = {}) {
|
|
1534
|
+
const { noBrowser = false, onDeviceCode, onBrowserOpen, onPollingStart, onAuthorized } = options;
|
|
1535
|
+
const ctx = useAppContext();
|
|
1536
|
+
const { url: registryUrl } = ctx.registry;
|
|
1537
|
+
log.debug(`Authenticating with ${registryUrl}`);
|
|
1538
|
+
try {
|
|
1539
|
+
log.debug("Requesting device code");
|
|
1540
|
+
const codeResult = await requestDeviceCode({
|
|
1541
|
+
issuer: registryUrl,
|
|
1542
|
+
clientId: CLIENT_ID
|
|
1543
|
+
});
|
|
1544
|
+
if (codeResult.success === false) return {
|
|
1545
|
+
success: false,
|
|
1546
|
+
error: codeResult.error
|
|
1547
|
+
};
|
|
1548
|
+
const { data: deviceAuthResponse, config } = codeResult;
|
|
1549
|
+
const formattedCode = formatUserCode(deviceAuthResponse.user_code);
|
|
1550
|
+
onDeviceCode?.({
|
|
1551
|
+
userCode: formattedCode,
|
|
1552
|
+
verificationUri: deviceAuthResponse.verification_uri,
|
|
1553
|
+
verificationUriComplete: deviceAuthResponse.verification_uri_complete
|
|
1554
|
+
});
|
|
1555
|
+
let browserOpened = false;
|
|
1556
|
+
if (!noBrowser) try {
|
|
1557
|
+
const urlToOpen = deviceAuthResponse.verification_uri_complete ?? deviceAuthResponse.verification_uri;
|
|
1558
|
+
log.debug(`Opening browser: ${urlToOpen}`);
|
|
1559
|
+
await openBrowser(urlToOpen);
|
|
1560
|
+
browserOpened = true;
|
|
1561
|
+
} catch {
|
|
1562
|
+
log.debug("Failed to open browser");
|
|
1563
|
+
}
|
|
1564
|
+
onBrowserOpen?.(browserOpened);
|
|
1565
|
+
log.debug("Waiting for user authorization");
|
|
1566
|
+
onPollingStart?.();
|
|
1567
|
+
const pollResult = await pollForToken({
|
|
1568
|
+
config,
|
|
1569
|
+
deviceAuthorizationResponse: deviceAuthResponse
|
|
1570
|
+
});
|
|
1571
|
+
if (pollResult.success === false) return {
|
|
1572
|
+
success: false,
|
|
1573
|
+
error: pollResult.error
|
|
1574
|
+
};
|
|
1575
|
+
onAuthorized?.();
|
|
1576
|
+
const token = pollResult.token.access_token;
|
|
1577
|
+
log.debug("Fetching user info");
|
|
1578
|
+
const session = await fetchSession(registryUrl, token);
|
|
1579
|
+
const user = session?.user;
|
|
1580
|
+
const sessionExpiresAt = session?.session?.expiresAt;
|
|
1581
|
+
const expiresAt = sessionExpiresAt ?? (pollResult.token.expires_in ? new Date(Date.now() + pollResult.token.expires_in * 1e3).toISOString() : void 0);
|
|
1582
|
+
log.debug("Saving credentials");
|
|
1583
|
+
await saveCredentials(registryUrl, {
|
|
1584
|
+
token,
|
|
1585
|
+
expiresAt,
|
|
1586
|
+
userId: user?.id,
|
|
1587
|
+
userName: user?.name,
|
|
1588
|
+
userEmail: user?.email
|
|
1589
|
+
});
|
|
1590
|
+
return {
|
|
1591
|
+
success: true,
|
|
1592
|
+
user: user ? {
|
|
1593
|
+
id: user.id,
|
|
1594
|
+
name: user.name,
|
|
1595
|
+
email: user.email
|
|
1596
|
+
} : void 0
|
|
1597
|
+
};
|
|
1598
|
+
} catch (error$2) {
|
|
1599
|
+
return {
|
|
1600
|
+
success: false,
|
|
1601
|
+
error: getErrorMessage(error$2)
|
|
1602
|
+
};
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
/**
|
|
1606
|
+
* Format user code as XXXX-XXXX for display.
|
|
1607
|
+
*/
|
|
1608
|
+
function formatUserCode(code$1) {
|
|
1609
|
+
const cleaned = code$1.toUpperCase().replace(/[^A-Z0-9]/g, "");
|
|
1610
|
+
if (cleaned.length <= 4) return cleaned;
|
|
1611
|
+
return `${cleaned.slice(0, 4)}-${cleaned.slice(4, 8)}`;
|
|
1612
|
+
}
|
|
1613
|
+
/**
|
|
1614
|
+
* Opens a URL in the default browser.
|
|
1615
|
+
*/
|
|
1616
|
+
async function openBrowser(url) {
|
|
1617
|
+
const platform = process.platform;
|
|
1618
|
+
const commands = {
|
|
1619
|
+
darwin: `open "${url}"`,
|
|
1620
|
+
win32: `start "" "${url}"`,
|
|
1621
|
+
linux: `xdg-open "${url}"`
|
|
1622
|
+
};
|
|
1623
|
+
const command$1 = commands[platform];
|
|
1624
|
+
if (!command$1) throw new Error(`Unsupported platform: ${platform}`);
|
|
1625
|
+
await execAsync(command$1);
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
//#endregion
|
|
1629
|
+
//#region src/commands/auth/logout.ts
|
|
1630
|
+
/**
|
|
1631
|
+
* Logs out by clearing stored credentials
|
|
1632
|
+
*/
|
|
1633
|
+
async function logout(options = {}) {
|
|
1634
|
+
const { all = false } = options;
|
|
1635
|
+
if (all) {
|
|
1636
|
+
log.debug("Clearing all stored credentials");
|
|
1637
|
+
await clearAllCredentials();
|
|
1638
|
+
return {
|
|
1639
|
+
success: true,
|
|
1640
|
+
hadCredentials: true
|
|
1641
|
+
};
|
|
1472
1642
|
}
|
|
1473
|
-
|
|
1643
|
+
const ctx = useAppContext();
|
|
1644
|
+
const { url: registryUrl } = ctx.registry;
|
|
1645
|
+
const hadCredentials = ctx.credentials !== null;
|
|
1646
|
+
if (hadCredentials) {
|
|
1647
|
+
log.debug(`Clearing credentials for ${registryUrl}`);
|
|
1648
|
+
await clearCredentials(registryUrl);
|
|
1649
|
+
} else log.debug(`No credentials found for ${registryUrl}`);
|
|
1650
|
+
return {
|
|
1651
|
+
success: true,
|
|
1652
|
+
hadCredentials
|
|
1653
|
+
};
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
//#endregion
|
|
1657
|
+
//#region src/commands/auth/whoami.ts
|
|
1658
|
+
/**
|
|
1659
|
+
* Returns information about the currently authenticated user
|
|
1660
|
+
*/
|
|
1661
|
+
async function whoami() {
|
|
1662
|
+
const ctx = useAppContext();
|
|
1663
|
+
const { url: registryUrl } = ctx.registry;
|
|
1664
|
+
return {
|
|
1665
|
+
success: true,
|
|
1666
|
+
loggedIn: ctx.isLoggedIn,
|
|
1667
|
+
user: ctx.user ?? void 0,
|
|
1668
|
+
registryUrl,
|
|
1669
|
+
expiresAt: ctx.credentials?.expiresAt
|
|
1670
|
+
};
|
|
1474
1671
|
}
|
|
1475
1672
|
|
|
1476
1673
|
//#endregion
|
|
@@ -1534,61 +1731,116 @@ async function resolveConfigPath(inputPath) {
|
|
|
1534
1731
|
return inputPath;
|
|
1535
1732
|
}
|
|
1536
1733
|
/**
|
|
1537
|
-
* Load
|
|
1734
|
+
* Load and normalize a preset config file.
|
|
1538
1735
|
*
|
|
1539
|
-
*
|
|
1540
|
-
*
|
|
1541
|
-
* - Metadata: .agentrules/ subfolder
|
|
1736
|
+
* This is the single source of truth for reading preset configs.
|
|
1737
|
+
* Always returns a normalized shape with `platforms` as PlatformConfig[].
|
|
1542
1738
|
*
|
|
1543
|
-
*
|
|
1544
|
-
* - Preset files: in .claude/ (or `path` from config)
|
|
1545
|
-
* - Metadata: .agentrules/ subfolder
|
|
1739
|
+
* @throws Error if config file is missing, invalid JSON, or fails validation
|
|
1546
1740
|
*/
|
|
1547
|
-
async function
|
|
1548
|
-
const configPath =
|
|
1549
|
-
|
|
1550
|
-
const configRaw = await readFile(configPath, "utf8");
|
|
1741
|
+
async function loadConfig(inputPath) {
|
|
1742
|
+
const configPath = await resolveConfigPath(inputPath);
|
|
1743
|
+
log.debug(`Resolved config path: ${configPath}`);
|
|
1744
|
+
const configRaw = await readFile(configPath, "utf8").catch(() => null);
|
|
1745
|
+
if (configRaw === null) throw new Error(`Config file not found: ${configPath}`);
|
|
1551
1746
|
let configJson;
|
|
1552
1747
|
try {
|
|
1553
1748
|
configJson = JSON.parse(configRaw);
|
|
1554
|
-
} catch {
|
|
1555
|
-
throw new Error(`Invalid JSON in ${configPath}`);
|
|
1749
|
+
} catch (e) {
|
|
1750
|
+
throw new Error(`Invalid JSON in ${configPath}: ${e instanceof Error ? e.message : String(e)}`);
|
|
1556
1751
|
}
|
|
1557
1752
|
const configObj = configJson;
|
|
1558
1753
|
const identifier = typeof configObj?.name === "string" ? configObj.name : configPath;
|
|
1559
|
-
const
|
|
1560
|
-
const
|
|
1561
|
-
|
|
1562
|
-
const
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1754
|
+
const rawConfig = validatePresetConfig(configJson, identifier);
|
|
1755
|
+
const platforms = rawConfig.platforms.map(normalizePlatformEntry);
|
|
1756
|
+
if (platforms.length === 0) throw new Error(`Config must have at least one platform in the "platforms" array.`);
|
|
1757
|
+
const config = {
|
|
1758
|
+
$schema: rawConfig.$schema,
|
|
1759
|
+
name: rawConfig.name,
|
|
1760
|
+
title: rawConfig.title,
|
|
1761
|
+
description: rawConfig.description,
|
|
1762
|
+
license: rawConfig.license,
|
|
1763
|
+
version: rawConfig.version,
|
|
1764
|
+
tags: rawConfig.tags,
|
|
1765
|
+
features: rawConfig.features,
|
|
1766
|
+
ignore: rawConfig.ignore,
|
|
1767
|
+
agentrulesDir: rawConfig.agentrulesDir,
|
|
1768
|
+
platforms
|
|
1769
|
+
};
|
|
1770
|
+
const configDir = dirname(configPath);
|
|
1771
|
+
const dirName = basename(configDir);
|
|
1772
|
+
const isInProjectMode = isPlatformDir(dirName);
|
|
1773
|
+
if (isInProjectMode && platforms.length > 1) throw new Error(`Multi-platform configs must be placed at project root, not inside a platform directory like "${dirName}".`);
|
|
1774
|
+
const platformNames = platforms.map((p$1) => p$1.platform).join(", ");
|
|
1775
|
+
log.debug(`Loaded config: ${config.name}, platforms: ${platformNames}, mode: ${isInProjectMode ? "in-project" : "standalone"}`);
|
|
1776
|
+
return {
|
|
1777
|
+
configPath,
|
|
1778
|
+
config,
|
|
1779
|
+
configDir,
|
|
1780
|
+
isInProjectMode
|
|
1781
|
+
};
|
|
1782
|
+
}
|
|
1783
|
+
/**
|
|
1784
|
+
* Load a preset from a directory containing agentrules.json.
|
|
1785
|
+
*
|
|
1786
|
+
* Always returns normalized PresetInput format with platformFiles array.
|
|
1787
|
+
*
|
|
1788
|
+
* Config in platform dir (e.g., .claude/agentrules.json):
|
|
1789
|
+
* - Preset files: siblings of config
|
|
1790
|
+
* - Extras (README, LICENSE, INSTALL): agentrulesDir subfolder (default: .agentrules/)
|
|
1791
|
+
*
|
|
1792
|
+
* Config at repo root:
|
|
1793
|
+
* - Files in each platform's directory (or custom path if specified)
|
|
1794
|
+
* - Extras (README, LICENSE, INSTALL): agentrulesDir subfolder (default: .agentrules/)
|
|
1795
|
+
*
|
|
1796
|
+
* Use agentrulesDir: "." to read extras from the config directory itself
|
|
1797
|
+
* (useful for dedicated preset repos where README.md should be at root).
|
|
1798
|
+
*/
|
|
1799
|
+
async function loadPreset(presetDir) {
|
|
1800
|
+
const { config, configDir, isInProjectMode } = await loadConfig(join(presetDir, PRESET_CONFIG_FILENAME));
|
|
1801
|
+
const { platforms } = config;
|
|
1802
|
+
const agentrulesDir = config.agentrulesDir ?? AGENT_RULES_DIR;
|
|
1803
|
+
const isAgentrulesAtRoot = agentrulesDir === ".";
|
|
1804
|
+
const agentrulesPath = isAgentrulesAtRoot ? configDir : join(configDir, agentrulesDir);
|
|
1576
1805
|
let installMessage;
|
|
1577
1806
|
let readmeContent;
|
|
1578
1807
|
let licenseContent;
|
|
1579
|
-
if (await directoryExists(
|
|
1580
|
-
installMessage = await readFileIfExists(join(
|
|
1581
|
-
readmeContent = await readFileIfExists(join(
|
|
1582
|
-
licenseContent = await readFileIfExists(join(
|
|
1808
|
+
if (isAgentrulesAtRoot || await directoryExists(agentrulesPath)) {
|
|
1809
|
+
installMessage = await readFileIfExists(join(agentrulesPath, INSTALL_FILENAME));
|
|
1810
|
+
readmeContent = await readFileIfExists(join(agentrulesPath, README_FILENAME));
|
|
1811
|
+
licenseContent = await readFileIfExists(join(agentrulesPath, LICENSE_FILENAME));
|
|
1583
1812
|
}
|
|
1584
1813
|
const ignorePatterns = [...DEFAULT_IGNORE_PATTERNS, ...config.ignore ?? []];
|
|
1585
|
-
const
|
|
1586
|
-
const
|
|
1587
|
-
|
|
1814
|
+
const platformFiles = [];
|
|
1815
|
+
for (const entry of platforms) {
|
|
1816
|
+
const { platform, path: customPath } = entry;
|
|
1817
|
+
let filesDir;
|
|
1818
|
+
if (isInProjectMode) {
|
|
1819
|
+
filesDir = configDir;
|
|
1820
|
+
log.debug(`Config in platform dir: files for ${platform} in ${filesDir}`);
|
|
1821
|
+
} else {
|
|
1822
|
+
const platformDir = customPath ?? PLATFORMS[platform].projectDir;
|
|
1823
|
+
filesDir = join(configDir, platformDir);
|
|
1824
|
+
log.debug(`Config at repo root: files for ${platform} in ${filesDir}`);
|
|
1825
|
+
if (!await directoryExists(filesDir)) throw new Error(`Files directory not found: ${filesDir}. Create the directory or set "path" in the platform entry.`);
|
|
1826
|
+
}
|
|
1827
|
+
const rootExclude = isAgentrulesAtRoot ? [
|
|
1828
|
+
PRESET_CONFIG_FILENAME,
|
|
1829
|
+
README_FILENAME,
|
|
1830
|
+
LICENSE_FILENAME,
|
|
1831
|
+
INSTALL_FILENAME
|
|
1832
|
+
] : [PRESET_CONFIG_FILENAME, agentrulesDir];
|
|
1833
|
+
const files = await collectFiles(filesDir, rootExclude, ignorePatterns);
|
|
1834
|
+
if (files.length === 0) throw new Error(`No files found in ${filesDir}. Presets must include at least one file.`);
|
|
1835
|
+
platformFiles.push({
|
|
1836
|
+
platform,
|
|
1837
|
+
files
|
|
1838
|
+
});
|
|
1839
|
+
}
|
|
1588
1840
|
return {
|
|
1589
|
-
|
|
1841
|
+
name: config.name,
|
|
1590
1842
|
config,
|
|
1591
|
-
|
|
1843
|
+
platformFiles,
|
|
1592
1844
|
installMessage,
|
|
1593
1845
|
readmeContent,
|
|
1594
1846
|
licenseContent
|
|
@@ -1638,11 +1890,11 @@ async function collectFiles(dir, rootExclude, ignorePatterns, root) {
|
|
|
1638
1890
|
const nested = await collectFiles(fullPath, rootExclude, ignorePatterns, configRoot);
|
|
1639
1891
|
files.push(...nested);
|
|
1640
1892
|
} else if (entry.isFile()) {
|
|
1641
|
-
const
|
|
1893
|
+
const content = await readFile(fullPath, "utf8");
|
|
1642
1894
|
const relativePath = relative(configRoot, fullPath);
|
|
1643
1895
|
files.push({
|
|
1644
1896
|
path: relativePath,
|
|
1645
|
-
|
|
1897
|
+
content
|
|
1646
1898
|
});
|
|
1647
1899
|
}
|
|
1648
1900
|
}
|
|
@@ -1775,7 +2027,7 @@ async function initPreset(options) {
|
|
|
1775
2027
|
description,
|
|
1776
2028
|
tags: options.tags ?? [],
|
|
1777
2029
|
license,
|
|
1778
|
-
platform
|
|
2030
|
+
platforms: [platform]
|
|
1779
2031
|
};
|
|
1780
2032
|
let createdDir;
|
|
1781
2033
|
if (await directoryExists(platformDir)) log.debug(`Platform directory exists: ${platformDir}`);
|
|
@@ -1891,10 +2143,10 @@ async function initInteractive(options) {
|
|
|
1891
2143
|
}
|
|
1892
2144
|
const result = await p.group({
|
|
1893
2145
|
name: () => p.text({
|
|
1894
|
-
message: "Preset name
|
|
2146
|
+
message: "Preset name",
|
|
1895
2147
|
placeholder: normalizeName(defaultName),
|
|
1896
2148
|
defaultValue: normalizeName(defaultName),
|
|
1897
|
-
validate: check(
|
|
2149
|
+
validate: check(nameSchema)
|
|
1898
2150
|
}),
|
|
1899
2151
|
title: ({ results }) => {
|
|
1900
2152
|
const defaultTitle = titleOption ?? toTitleCase(results.name ?? defaultName);
|
|
@@ -1972,78 +2224,56 @@ async function initInteractive(options) {
|
|
|
1972
2224
|
//#endregion
|
|
1973
2225
|
//#region src/commands/preset/validate.ts
|
|
1974
2226
|
async function validatePreset(options) {
|
|
1975
|
-
const configPath = await resolveConfigPath(options.path);
|
|
1976
|
-
log.debug(`Resolved config path: ${configPath}`);
|
|
1977
2227
|
const errors = [];
|
|
1978
2228
|
const warnings = [];
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
return {
|
|
1984
|
-
valid: false,
|
|
1985
|
-
configPath,
|
|
1986
|
-
preset: null,
|
|
1987
|
-
errors,
|
|
1988
|
-
warnings
|
|
1989
|
-
};
|
|
1990
|
-
}
|
|
1991
|
-
log.debug("Config file read successfully");
|
|
1992
|
-
let configJson;
|
|
1993
|
-
try {
|
|
1994
|
-
configJson = JSON.parse(configRaw);
|
|
1995
|
-
log.debug("JSON parsed successfully");
|
|
1996
|
-
} catch (e) {
|
|
1997
|
-
errors.push(`Invalid JSON: ${e instanceof Error ? e.message : String(e)}`);
|
|
1998
|
-
log.debug(`JSON parse error: ${e instanceof Error ? e.message : String(e)}`);
|
|
1999
|
-
return {
|
|
2000
|
-
valid: false,
|
|
2001
|
-
configPath,
|
|
2002
|
-
preset: null,
|
|
2003
|
-
errors,
|
|
2004
|
-
warnings
|
|
2005
|
-
};
|
|
2006
|
-
}
|
|
2007
|
-
let preset;
|
|
2229
|
+
let configPath;
|
|
2230
|
+
let config;
|
|
2231
|
+
let configDir;
|
|
2232
|
+
let isInProjectMode;
|
|
2008
2233
|
try {
|
|
2009
|
-
|
|
2010
|
-
|
|
2234
|
+
const result = await loadConfig(options.path);
|
|
2235
|
+
configPath = result.configPath;
|
|
2236
|
+
config = result.config;
|
|
2237
|
+
configDir = result.configDir;
|
|
2238
|
+
isInProjectMode = result.isInProjectMode;
|
|
2239
|
+
log.debug("Config loaded and normalized successfully");
|
|
2011
2240
|
} catch (e) {
|
|
2012
|
-
|
|
2013
|
-
|
|
2241
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
2242
|
+
errors.push(message);
|
|
2243
|
+
log.debug(`Config load failed: ${message}`);
|
|
2244
|
+
const fallbackPath = options.path ?? "agentrules.json";
|
|
2014
2245
|
return {
|
|
2015
2246
|
valid: false,
|
|
2016
|
-
configPath,
|
|
2247
|
+
configPath: fallbackPath,
|
|
2017
2248
|
preset: null,
|
|
2018
2249
|
errors,
|
|
2019
2250
|
warnings
|
|
2020
2251
|
};
|
|
2021
2252
|
}
|
|
2022
|
-
log.debug(`Preset
|
|
2023
|
-
const
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2253
|
+
log.debug(`Preset name: ${config.name}`);
|
|
2254
|
+
for (const entry of config.platforms) {
|
|
2255
|
+
const { platform, path: customPath } = entry;
|
|
2256
|
+
log.debug(`Checking platform: ${platform}`);
|
|
2257
|
+
if (!isSupportedPlatform(platform)) {
|
|
2258
|
+
errors.push(`Unknown platform "${platform}". Supported: ${PLATFORM_IDS.join(", ")}`);
|
|
2259
|
+
log.debug(`Platform "${platform}" is not supported`);
|
|
2260
|
+
continue;
|
|
2261
|
+
}
|
|
2262
|
+
if (isInProjectMode) log.debug(`In-project mode: files expected in ${configDir}`);
|
|
2030
2263
|
else {
|
|
2031
|
-
const filesPath =
|
|
2032
|
-
const filesDir = join(
|
|
2264
|
+
const filesPath = customPath ?? PLATFORMS[platform].projectDir;
|
|
2265
|
+
const filesDir = join(configDir, filesPath);
|
|
2033
2266
|
const filesExists = await directoryExists(filesDir);
|
|
2034
|
-
log.debug(`Standalone mode: files directory check: ${filesDir} - ${filesExists ? "exists" : "not found"}`);
|
|
2035
|
-
if (!filesExists) errors.push(`Files directory not found: ${filesPath}`);
|
|
2267
|
+
log.debug(`Standalone mode: files directory check for ${platform}: ${filesDir} - ${filesExists ? "exists" : "not found"}`);
|
|
2268
|
+
if (!filesExists) errors.push(`Files directory not found for ${platform}: ${filesPath}`);
|
|
2036
2269
|
}
|
|
2037
|
-
} else {
|
|
2038
|
-
errors.push(`Unknown platform "${platform}". Supported: ${PLATFORM_IDS.join(", ")}`);
|
|
2039
|
-
log.debug(`Platform "${platform}" is not supported`);
|
|
2040
2270
|
}
|
|
2041
|
-
const hasPlaceholderTags =
|
|
2042
|
-
const hasPlaceholderFeatures =
|
|
2271
|
+
const hasPlaceholderTags = config.tags?.some((t) => t.startsWith("//"));
|
|
2272
|
+
const hasPlaceholderFeatures = config.features?.some((f) => f.startsWith("//"));
|
|
2043
2273
|
if (hasPlaceholderTags) {
|
|
2044
2274
|
errors.push("Replace placeholder comments in tags before publishing.");
|
|
2045
2275
|
log.debug("Found placeholder comments in tags");
|
|
2046
|
-
} else if (!
|
|
2276
|
+
} else if (!config.tags || config.tags.length === 0) {
|
|
2047
2277
|
errors.push("At least one tag is required.");
|
|
2048
2278
|
log.debug("No tags specified");
|
|
2049
2279
|
}
|
|
@@ -2056,7 +2286,7 @@ async function validatePreset(options) {
|
|
|
2056
2286
|
return {
|
|
2057
2287
|
valid: isValid,
|
|
2058
2288
|
configPath,
|
|
2059
|
-
preset: isValid ?
|
|
2289
|
+
preset: isValid ? config : null,
|
|
2060
2290
|
errors,
|
|
2061
2291
|
warnings
|
|
2062
2292
|
};
|
|
@@ -2064,8 +2294,8 @@ async function validatePreset(options) {
|
|
|
2064
2294
|
|
|
2065
2295
|
//#endregion
|
|
2066
2296
|
//#region src/commands/publish.ts
|
|
2067
|
-
/** Maximum size per bundle in bytes (1MB) */
|
|
2068
|
-
const
|
|
2297
|
+
/** Maximum size per variant/platform bundle in bytes (1MB) */
|
|
2298
|
+
const MAX_VARIANT_SIZE_BYTES = 1 * 1024 * 1024;
|
|
2069
2299
|
/**
|
|
2070
2300
|
* Formats bytes as human-readable string
|
|
2071
2301
|
*/
|
|
@@ -2107,7 +2337,8 @@ async function publish(options = {}) {
|
|
|
2107
2337
|
let presetInput;
|
|
2108
2338
|
try {
|
|
2109
2339
|
presetInput = await loadPreset(presetDir);
|
|
2110
|
-
|
|
2340
|
+
const platforms$1 = presetInput.config.platforms.map((p$1) => p$1.platform).join(", ");
|
|
2341
|
+
log.debug(`Loaded preset "${presetInput.name}" for platforms: ${platforms$1}`);
|
|
2111
2342
|
} catch (error$2) {
|
|
2112
2343
|
const message = getErrorMessage(error$2);
|
|
2113
2344
|
spinner$1.fail("Failed to load preset");
|
|
@@ -2117,63 +2348,74 @@ async function publish(options = {}) {
|
|
|
2117
2348
|
error: message
|
|
2118
2349
|
};
|
|
2119
2350
|
}
|
|
2120
|
-
spinner$1.update("Building
|
|
2351
|
+
spinner$1.update("Building platform bundles...");
|
|
2121
2352
|
let publishInput;
|
|
2122
2353
|
try {
|
|
2123
2354
|
publishInput = await buildPresetPublishInput({
|
|
2124
2355
|
preset: presetInput,
|
|
2125
2356
|
version: version$1
|
|
2126
2357
|
});
|
|
2127
|
-
|
|
2358
|
+
const platforms$1 = publishInput.variants.map((v) => v.platform).join(", ");
|
|
2359
|
+
log.debug(`Built publish input for platforms: ${platforms$1}`);
|
|
2128
2360
|
} catch (error$2) {
|
|
2129
2361
|
const message = getErrorMessage(error$2);
|
|
2130
|
-
spinner$1.fail("Failed to build
|
|
2362
|
+
spinner$1.fail("Failed to build platform bundles");
|
|
2131
2363
|
log.error(message);
|
|
2132
2364
|
return {
|
|
2133
2365
|
success: false,
|
|
2134
2366
|
error: message
|
|
2135
2367
|
};
|
|
2136
2368
|
}
|
|
2137
|
-
const
|
|
2138
|
-
const
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
const
|
|
2143
|
-
|
|
2144
|
-
log.
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2369
|
+
const totalFileCount = publishInput.variants.reduce((sum, v) => sum + v.files.length, 0);
|
|
2370
|
+
const platformList = publishInput.variants.map((v) => v.platform).join(", ");
|
|
2371
|
+
let totalSize = 0;
|
|
2372
|
+
for (const variant of publishInput.variants) {
|
|
2373
|
+
const variantJson = JSON.stringify(variant);
|
|
2374
|
+
const variantSize = Buffer.byteLength(variantJson, "utf8");
|
|
2375
|
+
totalSize += variantSize;
|
|
2376
|
+
log.debug(`Variant ${variant.platform}: ${formatBytes(variantSize)}, ${variant.files.length} files`);
|
|
2377
|
+
if (variantSize > MAX_VARIANT_SIZE_BYTES) {
|
|
2378
|
+
const errorMessage = `Files for "${variant.platform}" exceed maximum size (${formatBytes(variantSize)} > ${formatBytes(MAX_VARIANT_SIZE_BYTES)})`;
|
|
2379
|
+
spinner$1.fail("Platform bundle too large");
|
|
2380
|
+
log.error(errorMessage);
|
|
2381
|
+
return {
|
|
2382
|
+
success: false,
|
|
2383
|
+
error: errorMessage
|
|
2384
|
+
};
|
|
2385
|
+
}
|
|
2149
2386
|
}
|
|
2387
|
+
log.debug(`Total publish size: ${formatBytes(totalSize)}, files: ${totalFileCount}, platforms: ${platformList}`);
|
|
2150
2388
|
if (dryRun) {
|
|
2151
2389
|
spinner$1.success("Dry run complete");
|
|
2152
2390
|
log.print("");
|
|
2153
2391
|
log.print(ui.header("Publish Preview"));
|
|
2154
2392
|
log.print(ui.keyValue("Preset", publishInput.title));
|
|
2155
|
-
log.print(ui.keyValue("
|
|
2156
|
-
log.print(ui.keyValue("
|
|
2393
|
+
log.print(ui.keyValue("Name", publishInput.name));
|
|
2394
|
+
log.print(ui.keyValue("Platforms", platformList));
|
|
2157
2395
|
log.print(ui.keyValue("Version", version$1 ? `${version$1}.x (auto-assigned minor)` : "1.x (auto-assigned)"));
|
|
2158
|
-
log.print(ui.keyValue("Files", `${
|
|
2159
|
-
log.print(ui.keyValue("Size", formatBytes(
|
|
2160
|
-
log.print("");
|
|
2161
|
-
log.print(ui.header("Files to publish", fileCount));
|
|
2162
|
-
log.print(ui.fileTree(publishInput.files));
|
|
2396
|
+
log.print(ui.keyValue("Files", `${totalFileCount} file${totalFileCount === 1 ? "" : "s"}`));
|
|
2397
|
+
log.print(ui.keyValue("Size", formatBytes(totalSize)));
|
|
2163
2398
|
log.print("");
|
|
2399
|
+
for (const variant of publishInput.variants) {
|
|
2400
|
+
log.print(ui.fileTree(variant.files, {
|
|
2401
|
+
showFolderSizes: true,
|
|
2402
|
+
header: `Files for ${variant.platform}`
|
|
2403
|
+
}));
|
|
2404
|
+
log.print("");
|
|
2405
|
+
}
|
|
2164
2406
|
log.print(ui.hint("Run without --dry-run to publish."));
|
|
2165
2407
|
return {
|
|
2166
2408
|
success: true,
|
|
2167
2409
|
preview: {
|
|
2168
|
-
slug: publishInput.
|
|
2169
|
-
|
|
2410
|
+
slug: publishInput.name,
|
|
2411
|
+
platforms: publishInput.variants.map((v) => v.platform),
|
|
2170
2412
|
title: publishInput.title,
|
|
2171
|
-
totalSize
|
|
2172
|
-
fileCount
|
|
2413
|
+
totalSize,
|
|
2414
|
+
fileCount: totalFileCount
|
|
2173
2415
|
}
|
|
2174
2416
|
};
|
|
2175
2417
|
}
|
|
2176
|
-
spinner$1.update(`Publishing ${publishInput.title} (${
|
|
2418
|
+
spinner$1.update(`Publishing ${publishInput.title} (${platformList})...`);
|
|
2177
2419
|
if (!ctx.credentials) throw new Error("Credentials should exist at this point");
|
|
2178
2420
|
const result = await publishPreset(ctx.registry.url, ctx.credentials.token, publishInput);
|
|
2179
2421
|
if (!result.success) {
|
|
@@ -2197,23 +2439,27 @@ async function publish(options = {}) {
|
|
|
2197
2439
|
}
|
|
2198
2440
|
const { data } = result;
|
|
2199
2441
|
const action = data.isNewPreset ? "Published new preset" : "Published";
|
|
2200
|
-
|
|
2442
|
+
const platforms = data.variants.map((v) => v.platform).join(", ");
|
|
2443
|
+
spinner$1.success(`${action} ${ui.code(data.slug)} ${ui.version(data.version)} (${platforms})`);
|
|
2201
2444
|
log.print("");
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2445
|
+
for (const variant of publishInput.variants) {
|
|
2446
|
+
log.print(ui.fileTree(variant.files, {
|
|
2447
|
+
showFolderSizes: true,
|
|
2448
|
+
header: `Published files for ${variant.platform}`
|
|
2449
|
+
}));
|
|
2450
|
+
log.print("");
|
|
2451
|
+
}
|
|
2206
2452
|
log.info("");
|
|
2207
|
-
log.info(ui.keyValue("Now live at", ui.link(
|
|
2453
|
+
log.info(ui.keyValue("Now live at", ui.link(data.url)));
|
|
2208
2454
|
return {
|
|
2209
2455
|
success: true,
|
|
2210
2456
|
preset: {
|
|
2211
2457
|
slug: data.slug,
|
|
2212
|
-
platform: data.platform,
|
|
2213
2458
|
title: data.title,
|
|
2214
2459
|
version: data.version,
|
|
2215
2460
|
isNewPreset: data.isNewPreset,
|
|
2216
|
-
|
|
2461
|
+
variants: data.variants,
|
|
2462
|
+
url: data.url
|
|
2217
2463
|
}
|
|
2218
2464
|
};
|
|
2219
2465
|
}
|
|
@@ -2242,7 +2488,7 @@ async function buildRegistry(options) {
|
|
|
2242
2488
|
});
|
|
2243
2489
|
if (validateOnly || !outputDir) return {
|
|
2244
2490
|
presets: presets.length,
|
|
2245
|
-
|
|
2491
|
+
items: result.items.length,
|
|
2246
2492
|
bundles: result.bundles.length,
|
|
2247
2493
|
outputDir: null,
|
|
2248
2494
|
validateOnly
|
|
@@ -2257,23 +2503,20 @@ async function buildRegistry(options) {
|
|
|
2257
2503
|
await writeFile(join(bundleDir, bundle.version), bundleJson);
|
|
2258
2504
|
await writeFile(join(bundleDir, LATEST_VERSION), bundleJson);
|
|
2259
2505
|
}
|
|
2260
|
-
for (const
|
|
2261
|
-
const
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
await writeFile(
|
|
2265
|
-
await writeFile(join(apiPresetDir, LATEST_VERSION), entryJson);
|
|
2506
|
+
for (const item of result.items) {
|
|
2507
|
+
const itemJson = JSON.stringify(item, null, indent$1);
|
|
2508
|
+
const itemPath = join(outputDir, API_ENDPOINTS.items.get(item.slug));
|
|
2509
|
+
await mkdir(join(itemPath, ".."), { recursive: true });
|
|
2510
|
+
await writeFile(itemPath, itemJson);
|
|
2266
2511
|
}
|
|
2267
2512
|
const registryJson = JSON.stringify({
|
|
2268
2513
|
$schema: "https://agentrules.directory/schema/registry.json",
|
|
2269
|
-
items: result.
|
|
2514
|
+
items: result.items
|
|
2270
2515
|
}, null, indent$1);
|
|
2271
2516
|
await writeFile(join(outputDir, "registry.json"), registryJson);
|
|
2272
|
-
const indexJson = JSON.stringify(result.index, null, indent$1);
|
|
2273
|
-
await writeFile(join(outputDir, "registry.index.json"), indexJson);
|
|
2274
2517
|
return {
|
|
2275
2518
|
presets: presets.length,
|
|
2276
|
-
|
|
2519
|
+
items: result.items.length,
|
|
2277
2520
|
bundles: result.bundles.length,
|
|
2278
2521
|
outputDir,
|
|
2279
2522
|
validateOnly: false
|
|
@@ -2296,19 +2539,147 @@ async function discoverPresetDirs(inputDir) {
|
|
|
2296
2539
|
return presetDirs.sort();
|
|
2297
2540
|
}
|
|
2298
2541
|
|
|
2542
|
+
//#endregion
|
|
2543
|
+
//#region src/commands/share.ts
|
|
2544
|
+
async function share(options = {}) {
|
|
2545
|
+
const ctx = useAppContext();
|
|
2546
|
+
if (!(ctx.isLoggedIn && ctx.credentials)) {
|
|
2547
|
+
const error$2 = "Not logged in. Run `agentrules login` to authenticate.";
|
|
2548
|
+
log.error(error$2);
|
|
2549
|
+
return {
|
|
2550
|
+
success: false,
|
|
2551
|
+
error: error$2
|
|
2552
|
+
};
|
|
2553
|
+
}
|
|
2554
|
+
let content = options.content;
|
|
2555
|
+
if (options.file) {
|
|
2556
|
+
const filePath = resolve(options.file);
|
|
2557
|
+
try {
|
|
2558
|
+
content = await readFile(filePath, "utf-8");
|
|
2559
|
+
} catch {
|
|
2560
|
+
const error$2 = `Failed to read file: ${options.file}`;
|
|
2561
|
+
log.error(error$2);
|
|
2562
|
+
return {
|
|
2563
|
+
success: false,
|
|
2564
|
+
error: error$2
|
|
2565
|
+
};
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
if (!options.name) {
|
|
2569
|
+
const error$2 = "Name is required. Use --name <name>";
|
|
2570
|
+
log.error(error$2);
|
|
2571
|
+
return {
|
|
2572
|
+
success: false,
|
|
2573
|
+
error: error$2
|
|
2574
|
+
};
|
|
2575
|
+
}
|
|
2576
|
+
if (!options.platform) {
|
|
2577
|
+
const error$2 = `Platform is required. Use --platform <${PLATFORM_IDS.join("|")}>`;
|
|
2578
|
+
log.error(error$2);
|
|
2579
|
+
return {
|
|
2580
|
+
success: false,
|
|
2581
|
+
error: error$2
|
|
2582
|
+
};
|
|
2583
|
+
}
|
|
2584
|
+
if (!PLATFORM_IDS.includes(options.platform)) {
|
|
2585
|
+
const error$2 = `Invalid platform "${options.platform}". Valid platforms: ${PLATFORM_IDS.join(", ")}`;
|
|
2586
|
+
log.error(error$2);
|
|
2587
|
+
return {
|
|
2588
|
+
success: false,
|
|
2589
|
+
error: error$2
|
|
2590
|
+
};
|
|
2591
|
+
}
|
|
2592
|
+
const validTypes = getValidRuleTypes(options.platform);
|
|
2593
|
+
if (!options.type) {
|
|
2594
|
+
const error$2 = `Type is required. Use --type <${validTypes.join("|")}>`;
|
|
2595
|
+
log.error(error$2);
|
|
2596
|
+
return {
|
|
2597
|
+
success: false,
|
|
2598
|
+
error: error$2
|
|
2599
|
+
};
|
|
2600
|
+
}
|
|
2601
|
+
if (!validTypes.includes(options.type)) {
|
|
2602
|
+
const error$2 = `Invalid type "${options.type}" for platform "${options.platform}". Valid types: ${validTypes.join(", ")}`;
|
|
2603
|
+
log.error(error$2);
|
|
2604
|
+
return {
|
|
2605
|
+
success: false,
|
|
2606
|
+
error: error$2
|
|
2607
|
+
};
|
|
2608
|
+
}
|
|
2609
|
+
if (!options.title) {
|
|
2610
|
+
const error$2 = "Title is required. Use --title <title>";
|
|
2611
|
+
log.error(error$2);
|
|
2612
|
+
return {
|
|
2613
|
+
success: false,
|
|
2614
|
+
error: error$2
|
|
2615
|
+
};
|
|
2616
|
+
}
|
|
2617
|
+
if (!content) {
|
|
2618
|
+
const error$2 = "Content is required. Provide a file path or use --content";
|
|
2619
|
+
log.error(error$2);
|
|
2620
|
+
return {
|
|
2621
|
+
success: false,
|
|
2622
|
+
error: error$2
|
|
2623
|
+
};
|
|
2624
|
+
}
|
|
2625
|
+
if (!options.tags || options.tags.length === 0) {
|
|
2626
|
+
const error$2 = "At least one tag is required. Use --tags <tag1,tag2,...>";
|
|
2627
|
+
log.error(error$2);
|
|
2628
|
+
return {
|
|
2629
|
+
success: false,
|
|
2630
|
+
error: error$2
|
|
2631
|
+
};
|
|
2632
|
+
}
|
|
2633
|
+
const spinner$1 = await log.spinner(`Publishing rule "${options.name}"...`);
|
|
2634
|
+
const result = await publishRule(ctx.registry.url, ctx.credentials.token, {
|
|
2635
|
+
name: options.name,
|
|
2636
|
+
platform: options.platform,
|
|
2637
|
+
type: options.type,
|
|
2638
|
+
title: options.title,
|
|
2639
|
+
description: options.description,
|
|
2640
|
+
content,
|
|
2641
|
+
tags: options.tags
|
|
2642
|
+
});
|
|
2643
|
+
if (!result.success) {
|
|
2644
|
+
spinner$1.fail("Publish failed");
|
|
2645
|
+
log.error(result.error);
|
|
2646
|
+
if (result.issues) for (const issue of result.issues) log.error(` ${issue.path}: ${issue.message}`);
|
|
2647
|
+
return {
|
|
2648
|
+
success: false,
|
|
2649
|
+
error: result.error
|
|
2650
|
+
};
|
|
2651
|
+
}
|
|
2652
|
+
const action = result.data.isNew ? "Created" : "Updated";
|
|
2653
|
+
spinner$1.success(`${action} rule ${ui.code(result.data.slug)}`);
|
|
2654
|
+
log.print("");
|
|
2655
|
+
log.print(ui.keyValue("Now live at", ui.link(result.data.url)));
|
|
2656
|
+
log.print("");
|
|
2657
|
+
log.print(ui.keyValue("Install command", ui.code(`npx @agentrules/cli add ${result.data.slug}`)));
|
|
2658
|
+
return {
|
|
2659
|
+
success: true,
|
|
2660
|
+
rule: {
|
|
2661
|
+
slug: result.data.slug,
|
|
2662
|
+
platform: result.data.platform,
|
|
2663
|
+
type: result.data.type,
|
|
2664
|
+
title: result.data.title,
|
|
2665
|
+
isNew: result.data.isNew
|
|
2666
|
+
}
|
|
2667
|
+
};
|
|
2668
|
+
}
|
|
2669
|
+
|
|
2299
2670
|
//#endregion
|
|
2300
2671
|
//#region src/commands/unpublish.ts
|
|
2301
2672
|
/**
|
|
2302
|
-
* Parses preset input to extract slug
|
|
2673
|
+
* Parses preset input to extract slug and version.
|
|
2303
2674
|
* Supports formats:
|
|
2304
|
-
* - "my-preset
|
|
2305
|
-
* - "my-preset@1.0" (
|
|
2306
|
-
* - "my-preset
|
|
2675
|
+
* - "my-preset@1.0" (slug and version)
|
|
2676
|
+
* - "username/my-preset@1.0" (namespaced slug and version)
|
|
2677
|
+
* - "my-preset" (requires explicit --version)
|
|
2307
2678
|
*
|
|
2308
|
-
* Explicit --
|
|
2679
|
+
* Explicit --version flag takes precedence.
|
|
2309
2680
|
*/
|
|
2310
|
-
function parseUnpublishInput(input,
|
|
2311
|
-
let normalized = input.
|
|
2681
|
+
function parseUnpublishInput(input, explicitVersion) {
|
|
2682
|
+
let normalized = input.trim();
|
|
2312
2683
|
let parsedVersion;
|
|
2313
2684
|
const atIndex = normalized.lastIndexOf("@");
|
|
2314
2685
|
if (atIndex > 0) {
|
|
@@ -2316,28 +2687,17 @@ function parseUnpublishInput(input, explicitPlatform, explicitVersion) {
|
|
|
2316
2687
|
normalized = normalized.slice(0, atIndex);
|
|
2317
2688
|
}
|
|
2318
2689
|
const version$1 = explicitVersion ?? parsedVersion;
|
|
2319
|
-
const parts = normalized.split(".");
|
|
2320
|
-
const maybePlatform = parts.at(-1);
|
|
2321
|
-
let slug;
|
|
2322
|
-
let platform;
|
|
2323
|
-
if (maybePlatform && isSupportedPlatform(maybePlatform)) {
|
|
2324
|
-
slug = parts.slice(0, -1).join(".");
|
|
2325
|
-
platform = explicitPlatform ?? maybePlatform;
|
|
2326
|
-
} else {
|
|
2327
|
-
slug = normalized;
|
|
2328
|
-
platform = explicitPlatform;
|
|
2329
|
-
}
|
|
2330
2690
|
return {
|
|
2331
|
-
slug,
|
|
2332
|
-
platform,
|
|
2691
|
+
slug: normalized,
|
|
2333
2692
|
version: version$1
|
|
2334
2693
|
};
|
|
2335
2694
|
}
|
|
2336
2695
|
/**
|
|
2337
|
-
* Unpublishes a preset version from the registry
|
|
2696
|
+
* Unpublishes a preset version from the registry.
|
|
2697
|
+
* This unpublishes all platform variants for the specified version.
|
|
2338
2698
|
*/
|
|
2339
2699
|
async function unpublish(options) {
|
|
2340
|
-
const { slug,
|
|
2700
|
+
const { slug, version: version$1 } = parseUnpublishInput(options.preset, options.version);
|
|
2341
2701
|
if (!slug) {
|
|
2342
2702
|
log.error("Preset slug is required");
|
|
2343
2703
|
return {
|
|
@@ -2345,21 +2705,14 @@ async function unpublish(options) {
|
|
|
2345
2705
|
error: "Preset slug is required"
|
|
2346
2706
|
};
|
|
2347
2707
|
}
|
|
2348
|
-
if (!platform) {
|
|
2349
|
-
log.error("Platform is required. Use --platform or specify as <slug>.<platform>@<version>");
|
|
2350
|
-
return {
|
|
2351
|
-
success: false,
|
|
2352
|
-
error: "Platform is required"
|
|
2353
|
-
};
|
|
2354
|
-
}
|
|
2355
2708
|
if (!version$1) {
|
|
2356
|
-
log.error("Version is required. Use --version or specify as <slug
|
|
2709
|
+
log.error("Version is required. Use --version or specify as <slug>@<version>");
|
|
2357
2710
|
return {
|
|
2358
2711
|
success: false,
|
|
2359
2712
|
error: "Version is required"
|
|
2360
2713
|
};
|
|
2361
2714
|
}
|
|
2362
|
-
log.debug(`Unpublishing preset: ${slug}
|
|
2715
|
+
log.debug(`Unpublishing preset: ${slug}@${version$1}`);
|
|
2363
2716
|
const ctx = useAppContext();
|
|
2364
2717
|
if (!(ctx.isLoggedIn && ctx.credentials)) {
|
|
2365
2718
|
const error$2 = "Not logged in. Run `agentrules login` to authenticate.";
|
|
@@ -2370,8 +2723,8 @@ async function unpublish(options) {
|
|
|
2370
2723
|
};
|
|
2371
2724
|
}
|
|
2372
2725
|
log.debug(`Authenticated, unpublishing from ${ctx.registry.url}`);
|
|
2373
|
-
const spinner$1 = await log.spinner(`Unpublishing ${ui.code(slug)}
|
|
2374
|
-
const result = await unpublishPreset(ctx.registry.url, ctx.credentials.token, slug,
|
|
2726
|
+
const spinner$1 = await log.spinner(`Unpublishing ${ui.code(slug)} ${ui.version(version$1)}...`);
|
|
2727
|
+
const result = await unpublishPreset(ctx.registry.url, ctx.credentials.token, slug, version$1);
|
|
2375
2728
|
if (!result.success) {
|
|
2376
2729
|
spinner$1.fail("Unpublish failed");
|
|
2377
2730
|
log.error(result.error);
|
|
@@ -2382,18 +2735,63 @@ async function unpublish(options) {
|
|
|
2382
2735
|
};
|
|
2383
2736
|
}
|
|
2384
2737
|
const { data } = result;
|
|
2385
|
-
spinner$1.success(`Unpublished ${ui.code(data.slug)}
|
|
2386
|
-
log.info(ui.hint("This version
|
|
2738
|
+
spinner$1.success(`Unpublished ${ui.code(data.slug)} ${ui.version(data.version)}`);
|
|
2739
|
+
log.info(ui.hint("This version and all its platform variants have been removed."));
|
|
2387
2740
|
return {
|
|
2388
2741
|
success: true,
|
|
2389
2742
|
preset: {
|
|
2390
2743
|
slug: data.slug,
|
|
2391
|
-
platform: data.platform,
|
|
2392
2744
|
version: data.version
|
|
2393
2745
|
}
|
|
2394
2746
|
};
|
|
2395
2747
|
}
|
|
2396
2748
|
|
|
2749
|
+
//#endregion
|
|
2750
|
+
//#region src/commands/unshare.ts
|
|
2751
|
+
/**
|
|
2752
|
+
* Unshares a rule from the registry (soft delete)
|
|
2753
|
+
*/
|
|
2754
|
+
async function unshare(options) {
|
|
2755
|
+
const slug = options.slug?.trim().toLowerCase();
|
|
2756
|
+
if (!slug) {
|
|
2757
|
+
const error$2 = "Rule slug is required. Usage: agentrules unshare <slug>";
|
|
2758
|
+
log.error(error$2);
|
|
2759
|
+
return {
|
|
2760
|
+
success: false,
|
|
2761
|
+
error: error$2
|
|
2762
|
+
};
|
|
2763
|
+
}
|
|
2764
|
+
log.debug(`Unsharing rule: ${slug}`);
|
|
2765
|
+
const ctx = useAppContext();
|
|
2766
|
+
if (!(ctx.isLoggedIn && ctx.credentials)) {
|
|
2767
|
+
const error$2 = "Not logged in. Run `agentrules login` to authenticate.";
|
|
2768
|
+
log.error(error$2);
|
|
2769
|
+
return {
|
|
2770
|
+
success: false,
|
|
2771
|
+
error: error$2
|
|
2772
|
+
};
|
|
2773
|
+
}
|
|
2774
|
+
log.debug(`Authenticated, unsharing from ${ctx.registry.url}`);
|
|
2775
|
+
const spinner$1 = await log.spinner(`Unsharing ${ui.code(slug)}...`);
|
|
2776
|
+
const result = await deleteRule(ctx.registry.url, ctx.credentials.token, slug);
|
|
2777
|
+
if (!result.success) {
|
|
2778
|
+
spinner$1.fail("Unshare failed");
|
|
2779
|
+
log.error(result.error);
|
|
2780
|
+
if (result.error.includes("connect")) log.info(ui.hint("Check your network connection and try again."));
|
|
2781
|
+
return {
|
|
2782
|
+
success: false,
|
|
2783
|
+
error: result.error
|
|
2784
|
+
};
|
|
2785
|
+
}
|
|
2786
|
+
const { data } = result;
|
|
2787
|
+
spinner$1.success(`Unshared ${ui.code(data.slug)}`);
|
|
2788
|
+
log.info(ui.hint("The rule has been removed from the registry."));
|
|
2789
|
+
return {
|
|
2790
|
+
success: true,
|
|
2791
|
+
rule: { slug: data.slug }
|
|
2792
|
+
};
|
|
2793
|
+
}
|
|
2794
|
+
|
|
2397
2795
|
//#endregion
|
|
2398
2796
|
//#region src/help-agent/publish.ts
|
|
2399
2797
|
/**
|
|
@@ -2680,14 +3078,14 @@ program.name("agentrules").description("The AI Agent Directory CLI").version(pac
|
|
|
2680
3078
|
log.debug(`Failed to init context: ${getErrorMessage(error$2)}`);
|
|
2681
3079
|
}
|
|
2682
3080
|
}).showHelpAfterError();
|
|
2683
|
-
program.command("add <
|
|
2684
|
-
const platform = options.platform ? normalizePlatformInput(options.platform) : void 0;
|
|
3081
|
+
program.command("add <item>").description("Download and install a preset or rule from the registry").option("-p, --platform <platform>", "Target platform (opencode, codex, claude, cursor)").option("--version <version>", "Install a specific version (or use slug@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 (backs up originals)").option("-y, --yes", "Alias for --force").option("--dry-run", "Preview changes without writing").option("--skip-conflicts", "Skip conflicting files (presets only)").option("--no-backup", "Don't backup files before overwriting (use with --force)").action(handle(async (item, options) => {
|
|
2685
3082
|
const dryRun = Boolean(options.dryRun);
|
|
2686
|
-
const
|
|
3083
|
+
const platform = options.platform ? normalizePlatformInput(options.platform) : void 0;
|
|
3084
|
+
const spinner$1 = await log.spinner("Resolving...");
|
|
2687
3085
|
let result;
|
|
2688
3086
|
try {
|
|
2689
|
-
result = await
|
|
2690
|
-
|
|
3087
|
+
result = await add({
|
|
3088
|
+
slug: item,
|
|
2691
3089
|
platform,
|
|
2692
3090
|
version: options.version,
|
|
2693
3091
|
global: Boolean(options.global),
|
|
@@ -2702,22 +3100,28 @@ program.command("add <preset>").description("Download and install a preset from
|
|
|
2702
3100
|
throw err;
|
|
2703
3101
|
}
|
|
2704
3102
|
spinner$1.stop();
|
|
2705
|
-
const
|
|
3103
|
+
const conflicts = result.files.filter((f) => f.status === "conflict");
|
|
3104
|
+
const hasBlockingConflicts = conflicts.length > 0 && !options.skipConflicts && !dryRun;
|
|
2706
3105
|
if (hasBlockingConflicts) {
|
|
2707
|
-
const count$1 =
|
|
3106
|
+
const count$1 = conflicts.length === 1 ? "1 file has" : `${conflicts.length} files have`;
|
|
2708
3107
|
const forceHint = `Use ${ui.command("--force")} to overwrite ${ui.muted("(--no-backup to skip backups)")}`;
|
|
2709
3108
|
log.error(`${count$1} conflicts. ${forceHint}`);
|
|
2710
3109
|
log.print("");
|
|
2711
|
-
for (const conflict of
|
|
3110
|
+
for (const conflict of conflicts.slice(0, 3)) {
|
|
2712
3111
|
log.print(` ${ui.muted("•")} ${conflict.path}`);
|
|
2713
3112
|
if (conflict.diff) log.print(conflict.diff.split("\n").map((l) => ` ${l}`).join("\n"));
|
|
2714
3113
|
}
|
|
2715
|
-
if (
|
|
3114
|
+
if (conflicts.length > 3) log.print(`\n ${ui.muted(`...and ${conflicts.length - 3} more`)}`);
|
|
2716
3115
|
log.print("");
|
|
2717
3116
|
log.print(forceHint);
|
|
2718
3117
|
process.exitCode = 1;
|
|
2719
3118
|
return;
|
|
2720
3119
|
}
|
|
3120
|
+
const allUnchanged = result.files.every((f) => f.status === "unchanged");
|
|
3121
|
+
if (allUnchanged) {
|
|
3122
|
+
log.info("Already up to date.");
|
|
3123
|
+
return;
|
|
3124
|
+
}
|
|
2721
3125
|
if (result.backups.length > 0) {
|
|
2722
3126
|
log.print("");
|
|
2723
3127
|
for (const backup of result.backups) log.print(ui.backupStatus(backup.originalPath, backup.backupPath, { dryRun }));
|
|
@@ -2730,10 +3134,12 @@ program.command("add <preset>").description("Download and install a preset from
|
|
|
2730
3134
|
}
|
|
2731
3135
|
log.print("");
|
|
2732
3136
|
const verb = dryRun ? "Would install" : "Installed";
|
|
2733
|
-
|
|
2734
|
-
|
|
3137
|
+
const kindLabel = result.kind === "rule" ? `(${result.variant.platform}/${result.variant.type})` : `for ${result.variant.platform}`;
|
|
3138
|
+
log.success(`${verb} ${ui.bold(result.resolved.title)} ${ui.muted(kindLabel)}`);
|
|
3139
|
+
const skippedConflicts = result.files.filter((f) => f.status === "skipped");
|
|
3140
|
+
if (skippedConflicts.length > 0) log.warn(`${skippedConflicts.length} conflicting file${skippedConflicts.length === 1 ? "" : "s"} skipped`);
|
|
2735
3141
|
if (dryRun) log.print(ui.hint("\nDry run complete. No files were written."));
|
|
2736
|
-
if (result.bundle.installMessage) log.print(`\n${result.bundle.installMessage}`);
|
|
3142
|
+
if (result.kind === "preset" && result.bundle.installMessage) log.print(`\n${result.bundle.installMessage}`);
|
|
2737
3143
|
}));
|
|
2738
3144
|
program.command("init").description("Initialize a new preset").argument("[directory]", "Directory to initialize (created if it doesn't exist)").option("-y, --yes", "Accept defaults without prompting").option("-n, --name <name>", "Preset name").option("-t, --title <title>", "Display title").option("--description <text>", "Preset description").option("-p, --platform <platform>", "Target platform (opencode, claude, cursor, codex)").option("-l, --license <license>", "License (e.g., MIT)").option("-f, --force", "Overwrite existing agentrules.json").action(handle(async (directory, options) => {
|
|
2739
3145
|
const targetDir = directory ?? process.cwd();
|
|
@@ -2799,10 +3205,11 @@ program.command("validate").description("Validate an agentrules.json configurati
|
|
|
2799
3205
|
const result = await validatePreset({ path: path$1 });
|
|
2800
3206
|
if (result.valid && result.preset) {
|
|
2801
3207
|
const p$1 = result.preset;
|
|
3208
|
+
const platforms = p$1.platforms.map((entry) => entry.platform).join(", ");
|
|
2802
3209
|
log.success(p$1.title);
|
|
2803
3210
|
log.print(ui.keyValue("Description", p$1.description));
|
|
2804
3211
|
log.print(ui.keyValue("License", p$1.license));
|
|
2805
|
-
log.print(ui.keyValue("
|
|
3212
|
+
log.print(ui.keyValue("Platforms", platforms));
|
|
2806
3213
|
if (p$1.tags?.length) log.print(ui.keyValue("Tags", p$1.tags.join(", ")));
|
|
2807
3214
|
} else if (!result.valid) log.error(`Invalid: ${ui.path(result.configPath)}`);
|
|
2808
3215
|
if (result.errors.length > 0) {
|
|
@@ -2839,15 +3246,15 @@ registry.command("build").description("Build registry from preset directories").
|
|
|
2839
3246
|
validateOnly: options.validateOnly
|
|
2840
3247
|
});
|
|
2841
3248
|
if (result.validateOnly) {
|
|
2842
|
-
log.success(`Validated ${result.presets} preset${result.presets === 1 ? "" : "s"} ${ui.muted(`→ ${result.
|
|
3249
|
+
log.success(`Validated ${result.presets} preset${result.presets === 1 ? "" : "s"} ${ui.muted(`→ ${result.items} items`)}`);
|
|
2843
3250
|
return;
|
|
2844
3251
|
}
|
|
2845
3252
|
if (!result.outputDir) {
|
|
2846
|
-
log.info(`Found ${result.presets} preset${result.presets === 1 ? "" : "s"} → ${result.
|
|
3253
|
+
log.info(`Found ${result.presets} preset${result.presets === 1 ? "" : "s"} → ${result.items} items`);
|
|
2847
3254
|
log.print(ui.hint(`Use ${ui.command("--out <path>")} to write files`));
|
|
2848
3255
|
return;
|
|
2849
3256
|
}
|
|
2850
|
-
log.success(`Built ${result.presets} preset${result.presets === 1 ? "" : "s"} ${ui.muted(`→ ${result.
|
|
3257
|
+
log.success(`Built ${result.presets} preset${result.presets === 1 ? "" : "s"} ${ui.muted(`→ ${result.items} items, ${result.bundles} bundles`)}`);
|
|
2851
3258
|
log.print(ui.keyValue("Output", ui.path(result.outputDir)));
|
|
2852
3259
|
}));
|
|
2853
3260
|
registry.command("add <alias> <url>").description("Add or update a registry endpoint").option("-f, --force", "Overwrite existing entry").option("-d, --default", "Set as default registry").action(handle(async (alias, url, options) => {
|
|
@@ -2925,7 +3332,7 @@ program.command("whoami").description("Show the currently authenticated user").a
|
|
|
2925
3332
|
log.print(ui.keyValue("Session", `expires in ${daysUntilExpiry} day${daysUntilExpiry === 1 ? "" : "s"}`));
|
|
2926
3333
|
}
|
|
2927
3334
|
}));
|
|
2928
|
-
program.command("publish").description("Publish a preset to the registry").argument("[path]", "Path to agentrules.json or directory containing it").option("
|
|
3335
|
+
program.command("publish").description("Publish a preset to the registry").argument("[path]", "Path to agentrules.json or directory containing it").option("--version <major>", "Major version (overrides config)", Number.parseInt).option("--dry-run", "Preview what would be published without publishing").action(handle(async (path$1, options) => {
|
|
2929
3336
|
const result = await publish({
|
|
2930
3337
|
path: path$1,
|
|
2931
3338
|
version: options.version,
|
|
@@ -2933,11 +3340,28 @@ program.command("publish").description("Publish a preset to the registry").argum
|
|
|
2933
3340
|
});
|
|
2934
3341
|
if (!result.success) process.exitCode = 1;
|
|
2935
3342
|
}));
|
|
2936
|
-
program.command("
|
|
2937
|
-
const platform =
|
|
3343
|
+
program.command("share").description("Share a rule to the registry").argument("[file]", "Path to file containing rule content").requiredOption("-n, --name <name>", "Rule name (URL identifier)").requiredOption("-p, --platform <platform>", "Target platform (opencode, codex, claude, cursor)").requiredOption("-t, --type <type>", "Rule type (instruction, agent, command, tool, skill, rule)").requiredOption("--title <title>", "Display title").requiredOption("--tags <tags>", "Comma-separated tags (e.g., typescript,react,testing)").option("--description <text>", "Optional description").option("-c, --content <content>", "Rule content (or provide file path)").action(handle(async (file, options) => {
|
|
3344
|
+
const platform = normalizePlatformInput(options.platform);
|
|
3345
|
+
const tags = options.tags.split(",").map((t) => t.trim()).filter(Boolean);
|
|
3346
|
+
const result = await share({
|
|
3347
|
+
name: options.name,
|
|
3348
|
+
platform,
|
|
3349
|
+
type: options.type,
|
|
3350
|
+
title: options.title,
|
|
3351
|
+
description: options.description,
|
|
3352
|
+
content: options.content,
|
|
3353
|
+
file,
|
|
3354
|
+
tags
|
|
3355
|
+
});
|
|
3356
|
+
if (!result.success) process.exitCode = 1;
|
|
3357
|
+
}));
|
|
3358
|
+
program.command("unshare").description("Remove a rule from the registry").argument("<slug>", "Rule slug to unshare").action(handle(async (slug) => {
|
|
3359
|
+
const result = await unshare({ slug });
|
|
3360
|
+
if (!result.success) process.exitCode = 1;
|
|
3361
|
+
}));
|
|
3362
|
+
program.command("unpublish").description("Remove a preset version from the registry (removes all platform variants)").argument("<preset>", "Preset to unpublish (e.g., my-preset@1.0.0)").option("--version <version>", "Version to unpublish (or use preset@version)").action(handle(async (preset, options) => {
|
|
2938
3363
|
const result = await unpublish({
|
|
2939
3364
|
preset,
|
|
2940
|
-
platform,
|
|
2941
3365
|
version: options.version
|
|
2942
3366
|
});
|
|
2943
3367
|
if (!result.success) process.exitCode = 1;
|