@agentrules/cli 0.0.14 → 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 +1060 -808
- 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, getInstallPath, 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,10 @@ 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)}`;
|
|
688
866
|
log.debug(`DELETE ${url}`);
|
|
689
867
|
try {
|
|
690
868
|
const response = await fetch(url, {
|
|
@@ -713,12 +891,52 @@ async function unpublishPreset(baseUrl, token, slug, platform, version$1) {
|
|
|
713
891
|
}
|
|
714
892
|
|
|
715
893
|
//#endregion
|
|
716
|
-
//#region src/lib/api/
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
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)}`;
|
|
934
|
+
log.debug(`DELETE ${url}`);
|
|
720
935
|
try {
|
|
721
|
-
const response = await fetch(url
|
|
936
|
+
const response = await fetch(url, {
|
|
937
|
+
method: "DELETE",
|
|
938
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
939
|
+
});
|
|
722
940
|
log.debug(`Response status: ${response.status}`);
|
|
723
941
|
if (!response.ok) {
|
|
724
942
|
const errorData = await response.json();
|
|
@@ -764,201 +982,43 @@ async function fetchSession(baseUrl, token) {
|
|
|
764
982
|
}
|
|
765
983
|
|
|
766
984
|
//#endregion
|
|
767
|
-
//#region src/lib/
|
|
768
|
-
|
|
769
|
-
const
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
985
|
+
//#region src/lib/credentials.ts
|
|
986
|
+
const chmodAsync = promisify(chmod);
|
|
987
|
+
const CREDENTIALS_FILENAME = "credentials.json";
|
|
988
|
+
/**
|
|
989
|
+
* Gets the path to the credentials file
|
|
990
|
+
*/
|
|
991
|
+
function getCredentialsPath() {
|
|
992
|
+
return join(getConfigDir(), CREDENTIALS_FILENAME);
|
|
993
|
+
}
|
|
994
|
+
/**
|
|
995
|
+
* Normalizes a registry URL for use as a credentials key.
|
|
996
|
+
* - Lowercases hostname (case-insensitive per spec)
|
|
997
|
+
* - Normalizes default ports (443 for https, 80 for http)
|
|
998
|
+
* - Preserves path (case-sensitive)
|
|
999
|
+
* - Strips trailing slash
|
|
1000
|
+
* - Strips query string and fragment (not relevant for auth)
|
|
1001
|
+
*/
|
|
1002
|
+
function normalizeUrl(url) {
|
|
1003
|
+
const parsed = new URL(url);
|
|
1004
|
+
const path$1 = parsed.pathname.replace(/\/$/, "");
|
|
1005
|
+
return parsed.origin + path$1;
|
|
1006
|
+
}
|
|
1007
|
+
/**
|
|
1008
|
+
* Loads the entire credentials store
|
|
1009
|
+
*/
|
|
1010
|
+
async function loadStore() {
|
|
1011
|
+
const credentialsPath = getCredentialsPath();
|
|
782
1012
|
try {
|
|
783
|
-
await access(
|
|
784
|
-
} catch
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
await writeDefaultConfig();
|
|
788
|
-
return structuredClone(DEFAULT_CONFIG);
|
|
789
|
-
}
|
|
790
|
-
throw error$2 instanceof Error ? error$2 : new Error(String(error$2));
|
|
1013
|
+
await access(credentialsPath, constants.F_OK);
|
|
1014
|
+
} catch {
|
|
1015
|
+
log.debug(`Credentials file not found at ${credentialsPath}`);
|
|
1016
|
+
return {};
|
|
791
1017
|
}
|
|
792
|
-
const raw = await readFile(configPath, "utf8");
|
|
793
|
-
let parsed = {};
|
|
794
1018
|
try {
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
}
|
|
799
|
-
return mergeWithDefaults(parsed);
|
|
800
|
-
}
|
|
801
|
-
async function saveConfig(config) {
|
|
802
|
-
await ensureConfigDir();
|
|
803
|
-
const configPath = getConfigPath();
|
|
804
|
-
log.debug(`Saving config to ${configPath}`);
|
|
805
|
-
const serialized = JSON.stringify(config, null, 2);
|
|
806
|
-
await writeFile(configPath, serialized, "utf8");
|
|
807
|
-
}
|
|
808
|
-
function getConfigPath() {
|
|
809
|
-
return join(getConfigDir(), CONFIG_FILENAME);
|
|
810
|
-
}
|
|
811
|
-
function getConfigDir() {
|
|
812
|
-
const customDir = process.env[CONFIG_HOME_ENV];
|
|
813
|
-
if (customDir && customDir.trim().length > 0) return customDir;
|
|
814
|
-
return join(homedir(), CONFIG_DIRNAME);
|
|
815
|
-
}
|
|
816
|
-
/**
|
|
817
|
-
* Normalizes a registry URL to a base URL with trailing slash.
|
|
818
|
-
*
|
|
819
|
-
* Examples:
|
|
820
|
-
* - "https://example.com" → "https://example.com/"
|
|
821
|
-
* - "https://example.com/custom/" → "https://example.com/custom/"
|
|
822
|
-
* - "https://example.com/custom" → "https://example.com/custom/"
|
|
823
|
-
*/
|
|
824
|
-
function normalizeRegistryUrl(input) {
|
|
825
|
-
try {
|
|
826
|
-
const parsed = new URL(input);
|
|
827
|
-
if (!parsed.pathname.endsWith("/")) parsed.pathname = `${parsed.pathname}/`;
|
|
828
|
-
return parsed.toString();
|
|
829
|
-
} catch (error$2) {
|
|
830
|
-
throw new Error(`Invalid registry URL "${input}": ${error$2.message}`);
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
async function ensureConfigDir() {
|
|
834
|
-
const dir = getConfigDir();
|
|
835
|
-
await mkdir(dir, { recursive: true });
|
|
836
|
-
}
|
|
837
|
-
async function writeDefaultConfig() {
|
|
838
|
-
const configPath = getConfigPath();
|
|
839
|
-
await mkdir(dirname(configPath), { recursive: true });
|
|
840
|
-
await writeFile(configPath, JSON.stringify(DEFAULT_CONFIG, null, 2), "utf8");
|
|
841
|
-
}
|
|
842
|
-
function isNodeError(error$2) {
|
|
843
|
-
return error$2 instanceof Error && typeof error$2.code === "string";
|
|
844
|
-
}
|
|
845
|
-
function mergeWithDefaults(partial) {
|
|
846
|
-
const registries = {
|
|
847
|
-
...DEFAULT_CONFIG.registries,
|
|
848
|
-
...partial.registries ?? {}
|
|
849
|
-
};
|
|
850
|
-
if (!registries[DEFAULT_REGISTRY_ALIAS]) registries[DEFAULT_REGISTRY_ALIAS] = structuredClone(DEFAULT_CONFIG.registries[DEFAULT_REGISTRY_ALIAS]);
|
|
851
|
-
return {
|
|
852
|
-
defaultRegistry: partial.defaultRegistry ?? DEFAULT_CONFIG.defaultRegistry,
|
|
853
|
-
registries
|
|
854
|
-
};
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
//#endregion
|
|
858
|
-
//#region src/commands/registry/manage.ts
|
|
859
|
-
const REGISTRY_ALIAS_PATTERN = /^[a-z0-9][a-z0-9-_]{0,63}$/i;
|
|
860
|
-
async function listRegistries() {
|
|
861
|
-
const config = await loadConfig();
|
|
862
|
-
const items = Object.entries(config.registries).map(([alias, settings]) => ({
|
|
863
|
-
alias,
|
|
864
|
-
...settings,
|
|
865
|
-
isDefault: alias === config.defaultRegistry
|
|
866
|
-
})).sort((a, b) => a.alias.localeCompare(b.alias));
|
|
867
|
-
log.debug(`Loaded ${items.length} registries`);
|
|
868
|
-
return items;
|
|
869
|
-
}
|
|
870
|
-
async function addRegistry(alias, url, options = {}) {
|
|
871
|
-
const normalizedAlias = normalizeAlias(alias);
|
|
872
|
-
const normalizedUrl = normalizeRegistryUrl(url);
|
|
873
|
-
const config = await loadConfig();
|
|
874
|
-
if (config.registries[normalizedAlias] && !options.overwrite) throw new Error(`Registry "${normalizedAlias}" already exists. Re-run with --force to overwrite.`);
|
|
875
|
-
const isUpdate = !!config.registries[normalizedAlias];
|
|
876
|
-
config.registries[normalizedAlias] = { url: normalizedUrl };
|
|
877
|
-
if (!config.defaultRegistry || options.makeDefault) {
|
|
878
|
-
config.defaultRegistry = normalizedAlias;
|
|
879
|
-
log.debug(`Set "${normalizedAlias}" as default registry`);
|
|
880
|
-
}
|
|
881
|
-
await saveConfig(config);
|
|
882
|
-
log.debug(`${isUpdate ? "Updated" : "Added"} registry "${normalizedAlias}" -> ${normalizedUrl}`);
|
|
883
|
-
return config.registries[normalizedAlias];
|
|
884
|
-
}
|
|
885
|
-
async function removeRegistry(alias, options = {}) {
|
|
886
|
-
const normalizedAlias = normalizeAlias(alias);
|
|
887
|
-
const config = await loadConfig();
|
|
888
|
-
if (!config.registries[normalizedAlias]) throw new Error(`Registry "${normalizedAlias}" was not found.`);
|
|
889
|
-
if (normalizedAlias === DEFAULT_REGISTRY_ALIAS) throw new Error("The built-in main registry cannot be removed. Point it somewhere else instead.");
|
|
890
|
-
const isDefault = normalizedAlias === config.defaultRegistry;
|
|
891
|
-
if (isDefault && !options.allowDefaultRemoval) throw new Error(`Registry "${normalizedAlias}" is currently the default. Re-run with --force to remove it.`);
|
|
892
|
-
delete config.registries[normalizedAlias];
|
|
893
|
-
log.debug(`Removed registry "${normalizedAlias}"`);
|
|
894
|
-
if (isDefault) {
|
|
895
|
-
config.defaultRegistry = pickFallbackDefault(config, normalizedAlias);
|
|
896
|
-
log.debug(`Default registry changed to "${config.defaultRegistry}"`);
|
|
897
|
-
}
|
|
898
|
-
await saveConfig(config);
|
|
899
|
-
return {
|
|
900
|
-
removedDefault: isDefault,
|
|
901
|
-
nextDefault: config.defaultRegistry
|
|
902
|
-
};
|
|
903
|
-
}
|
|
904
|
-
async function useRegistry(alias) {
|
|
905
|
-
const normalizedAlias = normalizeAlias(alias);
|
|
906
|
-
const config = await loadConfig();
|
|
907
|
-
if (!config.registries[normalizedAlias]) throw new Error(`Registry "${normalizedAlias}" is not defined.`);
|
|
908
|
-
config.defaultRegistry = normalizedAlias;
|
|
909
|
-
await saveConfig(config);
|
|
910
|
-
log.debug(`Switched default registry to "${normalizedAlias}"`);
|
|
911
|
-
}
|
|
912
|
-
function normalizeAlias(alias) {
|
|
913
|
-
const trimmed = alias.trim();
|
|
914
|
-
if (!trimmed) throw new Error("Alias is required.");
|
|
915
|
-
const normalized = trimmed.toLowerCase();
|
|
916
|
-
if (!REGISTRY_ALIAS_PATTERN.test(normalized)) throw new Error("Aliases may only contain letters, numbers, dashes, and underscores.");
|
|
917
|
-
return normalized;
|
|
918
|
-
}
|
|
919
|
-
function pickFallbackDefault(config, removedAlias) {
|
|
920
|
-
const fallback = Object.keys(config.registries).find((alias) => alias !== removedAlias);
|
|
921
|
-
return fallback ?? DEFAULT_REGISTRY_ALIAS;
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
//#endregion
|
|
925
|
-
//#region src/lib/credentials.ts
|
|
926
|
-
const chmodAsync = promisify(chmod);
|
|
927
|
-
const CREDENTIALS_FILENAME = "credentials.json";
|
|
928
|
-
/**
|
|
929
|
-
* Gets the path to the credentials file
|
|
930
|
-
*/
|
|
931
|
-
function getCredentialsPath() {
|
|
932
|
-
return join(getConfigDir(), CREDENTIALS_FILENAME);
|
|
933
|
-
}
|
|
934
|
-
/**
|
|
935
|
-
* Normalizes a registry URL for use as a credentials key.
|
|
936
|
-
* - Lowercases hostname (case-insensitive per spec)
|
|
937
|
-
* - Normalizes default ports (443 for https, 80 for http)
|
|
938
|
-
* - Preserves path (case-sensitive)
|
|
939
|
-
* - Strips trailing slash
|
|
940
|
-
* - Strips query string and fragment (not relevant for auth)
|
|
941
|
-
*/
|
|
942
|
-
function normalizeUrl(url) {
|
|
943
|
-
const parsed = new URL(url);
|
|
944
|
-
const path$1 = parsed.pathname.replace(/\/$/, "");
|
|
945
|
-
return parsed.origin + path$1;
|
|
946
|
-
}
|
|
947
|
-
/**
|
|
948
|
-
* Loads the entire credentials store
|
|
949
|
-
*/
|
|
950
|
-
async function loadStore() {
|
|
951
|
-
const credentialsPath = getCredentialsPath();
|
|
952
|
-
try {
|
|
953
|
-
await access(credentialsPath, constants$1.F_OK);
|
|
954
|
-
} catch {
|
|
955
|
-
log.debug(`Credentials file not found at ${credentialsPath}`);
|
|
956
|
-
return {};
|
|
957
|
-
}
|
|
958
|
-
try {
|
|
959
|
-
const raw = await readFile(credentialsPath, "utf8");
|
|
960
|
-
const store = JSON.parse(raw);
|
|
961
|
-
return store;
|
|
1019
|
+
const raw = await readFile(credentialsPath, "utf8");
|
|
1020
|
+
const store = JSON.parse(raw);
|
|
1021
|
+
return store;
|
|
962
1022
|
} catch (error$2) {
|
|
963
1023
|
log.debug(`Failed to load credentials: ${getErrorMessage(error$2)}`);
|
|
964
1024
|
return {};
|
|
@@ -1042,7 +1102,7 @@ async function clearAllCredentials() {
|
|
|
1042
1102
|
async function createAppContext(options = {}) {
|
|
1043
1103
|
const { url: explicitUrl, registryAlias } = options;
|
|
1044
1104
|
log.debug("Loading app context");
|
|
1045
|
-
const config = await loadConfig();
|
|
1105
|
+
const config = await loadConfig$1();
|
|
1046
1106
|
const registry$1 = explicitUrl ? resolveExplicitUrl(explicitUrl) : resolveRegistry(config, registryAlias);
|
|
1047
1107
|
log.debug(`Active registry: ${registry$1.alias} → ${registry$1.url}`);
|
|
1048
1108
|
const credentials = await getCredentials(registry$1.url);
|
|
@@ -1127,274 +1187,150 @@ async function initAppContext(options = {}) {
|
|
|
1127
1187
|
}
|
|
1128
1188
|
|
|
1129
1189
|
//#endregion
|
|
1130
|
-
//#region src/commands/
|
|
1131
|
-
|
|
1132
|
-
const CLIENT_ID = "agentrules-cli";
|
|
1133
|
-
/**
|
|
1134
|
-
* Performs device code flow login to an agentrules registry.
|
|
1135
|
-
*/
|
|
1136
|
-
async function login(options = {}) {
|
|
1137
|
-
const { noBrowser = false, onDeviceCode, onBrowserOpen, onPollingStart, onAuthorized } = options;
|
|
1190
|
+
//#region src/commands/add.ts
|
|
1191
|
+
async function add(options) {
|
|
1138
1192
|
const ctx = useAppContext();
|
|
1139
|
-
const { url: registryUrl } = ctx.registry;
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
});
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
const { data: deviceAuthResponse, config } = codeResult;
|
|
1152
|
-
const formattedCode = formatUserCode(deviceAuthResponse.user_code);
|
|
1153
|
-
onDeviceCode?.({
|
|
1154
|
-
userCode: formattedCode,
|
|
1155
|
-
verificationUri: deviceAuthResponse.verification_uri,
|
|
1156
|
-
verificationUriComplete: deviceAuthResponse.verification_uri_complete
|
|
1157
|
-
});
|
|
1158
|
-
let browserOpened = false;
|
|
1159
|
-
if (!noBrowser) try {
|
|
1160
|
-
const urlToOpen = deviceAuthResponse.verification_uri_complete ?? deviceAuthResponse.verification_uri;
|
|
1161
|
-
log.debug(`Opening browser: ${urlToOpen}`);
|
|
1162
|
-
await openBrowser(urlToOpen);
|
|
1163
|
-
browserOpened = true;
|
|
1164
|
-
} catch {
|
|
1165
|
-
log.debug("Failed to open browser");
|
|
1166
|
-
}
|
|
1167
|
-
onBrowserOpen?.(browserOpened);
|
|
1168
|
-
log.debug("Waiting for user authorization");
|
|
1169
|
-
onPollingStart?.();
|
|
1170
|
-
const pollResult = await pollForToken({
|
|
1171
|
-
config,
|
|
1172
|
-
deviceAuthorizationResponse: deviceAuthResponse
|
|
1173
|
-
});
|
|
1174
|
-
if (pollResult.success === false) return {
|
|
1175
|
-
success: false,
|
|
1176
|
-
error: pollResult.error
|
|
1177
|
-
};
|
|
1178
|
-
onAuthorized?.();
|
|
1179
|
-
const token = pollResult.token.access_token;
|
|
1180
|
-
log.debug("Fetching user info");
|
|
1181
|
-
const session = await fetchSession(registryUrl, token);
|
|
1182
|
-
const user = session?.user;
|
|
1183
|
-
const sessionExpiresAt = session?.session?.expiresAt;
|
|
1184
|
-
const expiresAt = sessionExpiresAt ?? (pollResult.token.expires_in ? new Date(Date.now() + pollResult.token.expires_in * 1e3).toISOString() : void 0);
|
|
1185
|
-
log.debug("Saving credentials");
|
|
1186
|
-
await saveCredentials(registryUrl, {
|
|
1187
|
-
token,
|
|
1188
|
-
expiresAt,
|
|
1189
|
-
userId: user?.id,
|
|
1190
|
-
userName: user?.name,
|
|
1191
|
-
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
|
|
1192
1205
|
});
|
|
1193
|
-
return {
|
|
1194
|
-
success: true,
|
|
1195
|
-
user: user ? {
|
|
1196
|
-
id: user.id,
|
|
1197
|
-
name: user.name,
|
|
1198
|
-
email: user.email
|
|
1199
|
-
} : void 0
|
|
1200
|
-
};
|
|
1201
|
-
} catch (error$2) {
|
|
1202
|
-
return {
|
|
1203
|
-
success: false,
|
|
1204
|
-
error: getErrorMessage(error$2)
|
|
1205
|
-
};
|
|
1206
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
|
+
});
|
|
1207
1213
|
}
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
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}".`);
|
|
1219
|
+
}
|
|
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}.`);
|
|
1227
|
+
return {
|
|
1228
|
+
selectedVersion,
|
|
1229
|
+
selectedVariant
|
|
1230
|
+
};
|
|
1215
1231
|
}
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
const commands = {
|
|
1222
|
-
darwin: `open "${url}"`,
|
|
1223
|
-
win32: `start "" "${url}"`,
|
|
1224
|
-
linux: `xdg-open "${url}"`
|
|
1225
|
-
};
|
|
1226
|
-
const command$1 = commands[platform];
|
|
1227
|
-
if (!command$1) throw new Error(`Unsupported platform: ${platform}`);
|
|
1228
|
-
await execAsync(command$1);
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
|
-
//#endregion
|
|
1232
|
-
//#region src/commands/auth/logout.ts
|
|
1233
|
-
/**
|
|
1234
|
-
* Logs out by clearing stored credentials
|
|
1235
|
-
*/
|
|
1236
|
-
async function logout(options = {}) {
|
|
1237
|
-
const { all = false } = options;
|
|
1238
|
-
if (all) {
|
|
1239
|
-
log.debug("Clearing all stored credentials");
|
|
1240
|
-
await clearAllCredentials();
|
|
1241
|
-
return {
|
|
1242
|
-
success: true,
|
|
1243
|
-
hadCredentials: true
|
|
1244
|
-
};
|
|
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}".`);
|
|
1245
1237
|
}
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
return {
|
|
1254
|
-
success: true,
|
|
1255
|
-
hadCredentials
|
|
1256
|
-
};
|
|
1257
|
-
}
|
|
1258
|
-
|
|
1259
|
-
//#endregion
|
|
1260
|
-
//#region src/commands/auth/whoami.ts
|
|
1261
|
-
/**
|
|
1262
|
-
* Returns information about the currently authenticated user
|
|
1263
|
-
*/
|
|
1264
|
-
async function whoami() {
|
|
1265
|
-
const ctx = useAppContext();
|
|
1266
|
-
const { url: registryUrl } = ctx.registry;
|
|
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}.`);
|
|
1267
1245
|
return {
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
user: ctx.user ?? void 0,
|
|
1271
|
-
registryUrl,
|
|
1272
|
-
expiresAt: ctx.credentials?.expiresAt
|
|
1246
|
+
selectedVersion,
|
|
1247
|
+
selectedVariant
|
|
1273
1248
|
};
|
|
1274
1249
|
}
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
const
|
|
1287
|
-
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}).`);
|
|
1288
|
-
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);
|
|
1289
1262
|
log.debug(`Writing ${bundle.files.length} files to ${target.root}`);
|
|
1290
|
-
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, {
|
|
1291
1275
|
force: Boolean(options.force),
|
|
1292
1276
|
skipConflicts: Boolean(options.skipConflicts),
|
|
1293
1277
|
noBackup: Boolean(options.noBackup),
|
|
1294
|
-
dryRun
|
|
1278
|
+
dryRun: options.dryRun
|
|
1295
1279
|
});
|
|
1296
1280
|
return {
|
|
1297
|
-
preset,
|
|
1281
|
+
kind: "preset",
|
|
1282
|
+
resolved,
|
|
1283
|
+
version: version$1,
|
|
1284
|
+
variant,
|
|
1298
1285
|
bundle,
|
|
1299
1286
|
files: writeStats.files,
|
|
1300
|
-
conflicts: writeStats.conflicts,
|
|
1301
1287
|
backups: writeStats.backups,
|
|
1302
1288
|
targetRoot: target.root,
|
|
1303
1289
|
targetLabel: target.label,
|
|
1304
|
-
registryAlias,
|
|
1305
|
-
dryRun
|
|
1306
|
-
};
|
|
1307
|
-
}
|
|
1308
|
-
/**
|
|
1309
|
-
* Parses preset input to extract slug, platform, and version.
|
|
1310
|
-
* Supports formats:
|
|
1311
|
-
* - "my-preset" (requires explicit platform)
|
|
1312
|
-
* - "my-preset.claude" (platform inferred from suffix)
|
|
1313
|
-
* - "my-preset@1.0" (with version)
|
|
1314
|
-
* - "my-preset.claude@1.0" (platform and version)
|
|
1315
|
-
*
|
|
1316
|
-
* Version can also be provided via --version flag (takes precedence).
|
|
1317
|
-
*/
|
|
1318
|
-
function parsePresetInput(input, explicitPlatform, explicitVersion) {
|
|
1319
|
-
let normalized = input.toLowerCase().trim();
|
|
1320
|
-
let parsedVersion;
|
|
1321
|
-
const atIndex = normalized.lastIndexOf("@");
|
|
1322
|
-
if (atIndex > 0) {
|
|
1323
|
-
parsedVersion = normalized.slice(atIndex + 1);
|
|
1324
|
-
normalized = normalized.slice(0, atIndex);
|
|
1325
|
-
}
|
|
1326
|
-
const version$1 = explicitVersion ?? parsedVersion;
|
|
1327
|
-
if (explicitPlatform) {
|
|
1328
|
-
const parts$1 = normalized.split(".");
|
|
1329
|
-
const maybePlatform$1 = parts$1.at(-1);
|
|
1330
|
-
if (maybePlatform$1 && isSupportedPlatform(maybePlatform$1)) return {
|
|
1331
|
-
slug: parts$1.slice(0, -1).join("."),
|
|
1332
|
-
platform: explicitPlatform,
|
|
1333
|
-
version: version$1
|
|
1334
|
-
};
|
|
1335
|
-
return {
|
|
1336
|
-
slug: normalized,
|
|
1337
|
-
platform: explicitPlatform,
|
|
1338
|
-
version: version$1
|
|
1339
|
-
};
|
|
1340
|
-
}
|
|
1341
|
-
const parts = normalized.split(".");
|
|
1342
|
-
const maybePlatform = parts.at(-1);
|
|
1343
|
-
if (maybePlatform && isSupportedPlatform(maybePlatform)) return {
|
|
1344
|
-
slug: parts.slice(0, -1).join("."),
|
|
1345
|
-
platform: maybePlatform,
|
|
1346
|
-
version: version$1
|
|
1290
|
+
registryAlias: options.registryAlias,
|
|
1291
|
+
dryRun: options.dryRun
|
|
1347
1292
|
};
|
|
1348
|
-
throw new Error(`Platform not specified. Use --platform <platform> or specify as <slug>.<platform> (e.g., "${input}.claude").`);
|
|
1349
1293
|
}
|
|
1350
|
-
function
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
if (!globalDir) throw new Error(`Platform "${platform}" does not support global installation`);
|
|
1364
|
-
const globalRoot = resolve(expandHome$1(globalDir));
|
|
1365
|
-
return {
|
|
1366
|
-
root: globalRoot,
|
|
1367
|
-
mode: "global",
|
|
1368
|
-
platform,
|
|
1369
|
-
projectDir,
|
|
1370
|
-
label: `global path ${globalRoot}`
|
|
1371
|
-
};
|
|
1372
|
-
}
|
|
1373
|
-
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
|
+
});
|
|
1374
1307
|
return {
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
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
|
|
1380
1318
|
};
|
|
1381
1319
|
}
|
|
1382
|
-
|
|
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) {
|
|
1383
1325
|
const files = [];
|
|
1384
|
-
const conflicts = [];
|
|
1385
1326
|
const backups = [];
|
|
1386
|
-
if (!
|
|
1387
|
-
for (const
|
|
1388
|
-
|
|
1389
|
-
const
|
|
1390
|
-
|
|
1391
|
-
const destResult = computeDestinationPath(file.path, target);
|
|
1392
|
-
const destination = destResult.path;
|
|
1393
|
-
if (!behavior.dryRun) await mkdir(dirname(destination), { recursive: true });
|
|
1394
|
-
const existing = await readExistingFile$1(destination);
|
|
1395
|
-
const relativePath = relativize(destination, target.root);
|
|
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 });
|
|
1330
|
+
const existing = await readExistingFile(destination);
|
|
1331
|
+
const relativePath = relativize(destination, root);
|
|
1396
1332
|
if (!existing) {
|
|
1397
|
-
if (!
|
|
1333
|
+
if (!options.dryRun) await writeFile(destination, content);
|
|
1398
1334
|
files.push({
|
|
1399
1335
|
path: relativePath,
|
|
1400
1336
|
status: "created"
|
|
@@ -1402,7 +1338,7 @@ async function writeBundleFiles(bundle, target, behavior) {
|
|
|
1402
1338
|
log.debug(`Created: ${relativePath}`);
|
|
1403
1339
|
continue;
|
|
1404
1340
|
}
|
|
1405
|
-
if (existing.equals(
|
|
1341
|
+
if (existing.equals(content)) {
|
|
1406
1342
|
files.push({
|
|
1407
1343
|
path: relativePath,
|
|
1408
1344
|
status: "unchanged"
|
|
@@ -1410,18 +1346,19 @@ async function writeBundleFiles(bundle, target, behavior) {
|
|
|
1410
1346
|
log.debug(`Unchanged: ${relativePath}`);
|
|
1411
1347
|
continue;
|
|
1412
1348
|
}
|
|
1413
|
-
|
|
1414
|
-
|
|
1349
|
+
const diff = renderDiffPreview(relativePath, existing, content);
|
|
1350
|
+
if (options.force) {
|
|
1351
|
+
if (!options.noBackup) {
|
|
1415
1352
|
const backupPath = `${destination}.bak`;
|
|
1416
1353
|
const relativeBackupPath = `${relativePath}.bak`;
|
|
1417
|
-
if (!
|
|
1354
|
+
if (!options.dryRun) await copyFile(destination, backupPath);
|
|
1418
1355
|
backups.push({
|
|
1419
1356
|
originalPath: relativePath,
|
|
1420
1357
|
backupPath: relativeBackupPath
|
|
1421
1358
|
});
|
|
1422
1359
|
log.debug(`Backed up: ${relativePath} → ${relativeBackupPath}`);
|
|
1423
1360
|
}
|
|
1424
|
-
if (!
|
|
1361
|
+
if (!options.dryRun) await writeFile(destination, content);
|
|
1425
1362
|
files.push({
|
|
1426
1363
|
path: relativePath,
|
|
1427
1364
|
status: "overwritten"
|
|
@@ -1429,31 +1366,78 @@ async function writeBundleFiles(bundle, target, behavior) {
|
|
|
1429
1366
|
log.debug(`Overwritten: ${relativePath}`);
|
|
1430
1367
|
continue;
|
|
1431
1368
|
}
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
}
|
|
1440
|
-
|
|
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
|
+
}
|
|
1441
1384
|
}
|
|
1442
1385
|
return {
|
|
1443
1386
|
files,
|
|
1444
|
-
conflicts,
|
|
1445
1387
|
backups
|
|
1446
1388
|
};
|
|
1447
1389
|
}
|
|
1448
1390
|
/**
|
|
1449
|
-
*
|
|
1450
|
-
*
|
|
1451
|
-
* Bundle files are stored with paths relative to the platform directory
|
|
1452
|
-
* (e.g., "AGENTS.md", "commands/test.md") and installed to:
|
|
1453
|
-
* - Project/custom: <root>/<projectDir>/<path> (e.g., .opencode/AGENTS.md)
|
|
1454
|
-
* - 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.
|
|
1455
1393
|
*/
|
|
1456
|
-
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) {
|
|
1457
1441
|
const normalized = normalizeBundlePath(pathInput);
|
|
1458
1442
|
if (!normalized) throw new Error(`Unable to derive destination for ${pathInput}. The computed relative path is empty.`);
|
|
1459
1443
|
let relativePath;
|
|
@@ -1461,9 +1445,38 @@ function computeDestinationPath(pathInput, target) {
|
|
|
1461
1445
|
else relativePath = `${target.projectDir}/${normalized}`;
|
|
1462
1446
|
const destination = resolve(target.root, relativePath);
|
|
1463
1447
|
ensureWithinRoot(destination, target.root);
|
|
1464
|
-
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
|
+
};
|
|
1465
1478
|
}
|
|
1466
|
-
async function readExistingFile
|
|
1479
|
+
async function readExistingFile(pathname) {
|
|
1467
1480
|
try {
|
|
1468
1481
|
return await readFile(pathname);
|
|
1469
1482
|
} catch (error$2) {
|
|
@@ -1492,16 +1505,171 @@ function ensureWithinRoot(candidate, root) {
|
|
|
1492
1505
|
if (candidate === root) return;
|
|
1493
1506
|
if (!candidate.startsWith(normalizedRoot)) throw new Error(`Refusing to write outside of ${root}. Derived path: ${candidate}`);
|
|
1494
1507
|
}
|
|
1495
|
-
|
|
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
|
+
*/
|
|
1515
|
+
function expandHome(value) {
|
|
1496
1516
|
if (value.startsWith("~")) {
|
|
1497
1517
|
const remainder = value.slice(1);
|
|
1498
|
-
|
|
1499
|
-
if (remainder
|
|
1500
|
-
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}`;
|
|
1501
1522
|
}
|
|
1502
1523
|
return value;
|
|
1503
1524
|
}
|
|
1504
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
|
+
};
|
|
1642
|
+
}
|
|
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
|
+
};
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1505
1673
|
//#endregion
|
|
1506
1674
|
//#region src/lib/fs.ts
|
|
1507
1675
|
async function fileExists(path$1) {
|
|
@@ -1563,61 +1731,116 @@ async function resolveConfigPath(inputPath) {
|
|
|
1563
1731
|
return inputPath;
|
|
1564
1732
|
}
|
|
1565
1733
|
/**
|
|
1566
|
-
* Load
|
|
1734
|
+
* Load and normalize a preset config file.
|
|
1567
1735
|
*
|
|
1568
|
-
*
|
|
1569
|
-
*
|
|
1570
|
-
* - 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[].
|
|
1571
1738
|
*
|
|
1572
|
-
*
|
|
1573
|
-
* - Preset files: in .claude/ (or `path` from config)
|
|
1574
|
-
* - Metadata: .agentrules/ subfolder
|
|
1739
|
+
* @throws Error if config file is missing, invalid JSON, or fails validation
|
|
1575
1740
|
*/
|
|
1576
|
-
async function
|
|
1577
|
-
const configPath =
|
|
1578
|
-
|
|
1579
|
-
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}`);
|
|
1580
1746
|
let configJson;
|
|
1581
1747
|
try {
|
|
1582
1748
|
configJson = JSON.parse(configRaw);
|
|
1583
|
-
} catch {
|
|
1584
|
-
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)}`);
|
|
1585
1751
|
}
|
|
1586
1752
|
const configObj = configJson;
|
|
1587
1753
|
const identifier = typeof configObj?.name === "string" ? configObj.name : configPath;
|
|
1588
|
-
const
|
|
1589
|
-
const
|
|
1590
|
-
|
|
1591
|
-
const
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
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);
|
|
1605
1805
|
let installMessage;
|
|
1606
1806
|
let readmeContent;
|
|
1607
1807
|
let licenseContent;
|
|
1608
|
-
if (await directoryExists(
|
|
1609
|
-
installMessage = await readFileIfExists(join(
|
|
1610
|
-
readmeContent = await readFileIfExists(join(
|
|
1611
|
-
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));
|
|
1612
1812
|
}
|
|
1613
1813
|
const ignorePatterns = [...DEFAULT_IGNORE_PATTERNS, ...config.ignore ?? []];
|
|
1614
|
-
const
|
|
1615
|
-
const
|
|
1616
|
-
|
|
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
|
+
}
|
|
1617
1840
|
return {
|
|
1618
|
-
name,
|
|
1841
|
+
name: config.name,
|
|
1619
1842
|
config,
|
|
1620
|
-
|
|
1843
|
+
platformFiles,
|
|
1621
1844
|
installMessage,
|
|
1622
1845
|
readmeContent,
|
|
1623
1846
|
licenseContent
|
|
@@ -1667,11 +1890,11 @@ async function collectFiles(dir, rootExclude, ignorePatterns, root) {
|
|
|
1667
1890
|
const nested = await collectFiles(fullPath, rootExclude, ignorePatterns, configRoot);
|
|
1668
1891
|
files.push(...nested);
|
|
1669
1892
|
} else if (entry.isFile()) {
|
|
1670
|
-
const
|
|
1893
|
+
const content = await readFile(fullPath, "utf8");
|
|
1671
1894
|
const relativePath = relative(configRoot, fullPath);
|
|
1672
1895
|
files.push({
|
|
1673
1896
|
path: relativePath,
|
|
1674
|
-
|
|
1897
|
+
content
|
|
1675
1898
|
});
|
|
1676
1899
|
}
|
|
1677
1900
|
}
|
|
@@ -1804,7 +2027,7 @@ async function initPreset(options) {
|
|
|
1804
2027
|
description,
|
|
1805
2028
|
tags: options.tags ?? [],
|
|
1806
2029
|
license,
|
|
1807
|
-
platform
|
|
2030
|
+
platforms: [platform]
|
|
1808
2031
|
};
|
|
1809
2032
|
let createdDir;
|
|
1810
2033
|
if (await directoryExists(platformDir)) log.debug(`Platform directory exists: ${platformDir}`);
|
|
@@ -1920,10 +2143,10 @@ async function initInteractive(options) {
|
|
|
1920
2143
|
}
|
|
1921
2144
|
const result = await p.group({
|
|
1922
2145
|
name: () => p.text({
|
|
1923
|
-
message: "Preset name
|
|
2146
|
+
message: "Preset name",
|
|
1924
2147
|
placeholder: normalizeName(defaultName),
|
|
1925
2148
|
defaultValue: normalizeName(defaultName),
|
|
1926
|
-
validate: check(
|
|
2149
|
+
validate: check(nameSchema)
|
|
1927
2150
|
}),
|
|
1928
2151
|
title: ({ results }) => {
|
|
1929
2152
|
const defaultTitle = titleOption ?? toTitleCase(results.name ?? defaultName);
|
|
@@ -2001,78 +2224,56 @@ async function initInteractive(options) {
|
|
|
2001
2224
|
//#endregion
|
|
2002
2225
|
//#region src/commands/preset/validate.ts
|
|
2003
2226
|
async function validatePreset(options) {
|
|
2004
|
-
const configPath = await resolveConfigPath(options.path);
|
|
2005
|
-
log.debug(`Resolved config path: ${configPath}`);
|
|
2006
2227
|
const errors = [];
|
|
2007
2228
|
const warnings = [];
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
return {
|
|
2013
|
-
valid: false,
|
|
2014
|
-
configPath,
|
|
2015
|
-
preset: null,
|
|
2016
|
-
errors,
|
|
2017
|
-
warnings
|
|
2018
|
-
};
|
|
2019
|
-
}
|
|
2020
|
-
log.debug("Config file read successfully");
|
|
2021
|
-
let configJson;
|
|
2022
|
-
try {
|
|
2023
|
-
configJson = JSON.parse(configRaw);
|
|
2024
|
-
log.debug("JSON parsed successfully");
|
|
2025
|
-
} catch (e) {
|
|
2026
|
-
errors.push(`Invalid JSON: ${e instanceof Error ? e.message : String(e)}`);
|
|
2027
|
-
log.debug(`JSON parse error: ${e instanceof Error ? e.message : String(e)}`);
|
|
2028
|
-
return {
|
|
2029
|
-
valid: false,
|
|
2030
|
-
configPath,
|
|
2031
|
-
preset: null,
|
|
2032
|
-
errors,
|
|
2033
|
-
warnings
|
|
2034
|
-
};
|
|
2035
|
-
}
|
|
2036
|
-
let preset;
|
|
2229
|
+
let configPath;
|
|
2230
|
+
let config;
|
|
2231
|
+
let configDir;
|
|
2232
|
+
let isInProjectMode;
|
|
2037
2233
|
try {
|
|
2038
|
-
|
|
2039
|
-
|
|
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");
|
|
2040
2240
|
} catch (e) {
|
|
2041
|
-
|
|
2042
|
-
|
|
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";
|
|
2043
2245
|
return {
|
|
2044
2246
|
valid: false,
|
|
2045
|
-
configPath,
|
|
2247
|
+
configPath: fallbackPath,
|
|
2046
2248
|
preset: null,
|
|
2047
2249
|
errors,
|
|
2048
2250
|
warnings
|
|
2049
2251
|
};
|
|
2050
2252
|
}
|
|
2051
|
-
log.debug(`Preset name: ${
|
|
2052
|
-
const
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
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}`);
|
|
2059
2263
|
else {
|
|
2060
|
-
const filesPath =
|
|
2061
|
-
const filesDir = join(
|
|
2264
|
+
const filesPath = customPath ?? PLATFORMS[platform].projectDir;
|
|
2265
|
+
const filesDir = join(configDir, filesPath);
|
|
2062
2266
|
const filesExists = await directoryExists(filesDir);
|
|
2063
|
-
log.debug(`Standalone mode: files directory check: ${filesDir} - ${filesExists ? "exists" : "not found"}`);
|
|
2064
|
-
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}`);
|
|
2065
2269
|
}
|
|
2066
|
-
} else {
|
|
2067
|
-
errors.push(`Unknown platform "${platform}". Supported: ${PLATFORM_IDS.join(", ")}`);
|
|
2068
|
-
log.debug(`Platform "${platform}" is not supported`);
|
|
2069
2270
|
}
|
|
2070
|
-
const hasPlaceholderTags =
|
|
2071
|
-
const hasPlaceholderFeatures =
|
|
2271
|
+
const hasPlaceholderTags = config.tags?.some((t) => t.startsWith("//"));
|
|
2272
|
+
const hasPlaceholderFeatures = config.features?.some((f) => f.startsWith("//"));
|
|
2072
2273
|
if (hasPlaceholderTags) {
|
|
2073
2274
|
errors.push("Replace placeholder comments in tags before publishing.");
|
|
2074
2275
|
log.debug("Found placeholder comments in tags");
|
|
2075
|
-
} else if (!
|
|
2276
|
+
} else if (!config.tags || config.tags.length === 0) {
|
|
2076
2277
|
errors.push("At least one tag is required.");
|
|
2077
2278
|
log.debug("No tags specified");
|
|
2078
2279
|
}
|
|
@@ -2085,7 +2286,7 @@ async function validatePreset(options) {
|
|
|
2085
2286
|
return {
|
|
2086
2287
|
valid: isValid,
|
|
2087
2288
|
configPath,
|
|
2088
|
-
preset: isValid ?
|
|
2289
|
+
preset: isValid ? config : null,
|
|
2089
2290
|
errors,
|
|
2090
2291
|
warnings
|
|
2091
2292
|
};
|
|
@@ -2093,8 +2294,8 @@ async function validatePreset(options) {
|
|
|
2093
2294
|
|
|
2094
2295
|
//#endregion
|
|
2095
2296
|
//#region src/commands/publish.ts
|
|
2096
|
-
/** Maximum size per bundle in bytes (1MB) */
|
|
2097
|
-
const
|
|
2297
|
+
/** Maximum size per variant/platform bundle in bytes (1MB) */
|
|
2298
|
+
const MAX_VARIANT_SIZE_BYTES = 1 * 1024 * 1024;
|
|
2098
2299
|
/**
|
|
2099
2300
|
* Formats bytes as human-readable string
|
|
2100
2301
|
*/
|
|
@@ -2136,7 +2337,8 @@ async function publish(options = {}) {
|
|
|
2136
2337
|
let presetInput;
|
|
2137
2338
|
try {
|
|
2138
2339
|
presetInput = await loadPreset(presetDir);
|
|
2139
|
-
|
|
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}`);
|
|
2140
2342
|
} catch (error$2) {
|
|
2141
2343
|
const message = getErrorMessage(error$2);
|
|
2142
2344
|
spinner$1.fail("Failed to load preset");
|
|
@@ -2146,63 +2348,74 @@ async function publish(options = {}) {
|
|
|
2146
2348
|
error: message
|
|
2147
2349
|
};
|
|
2148
2350
|
}
|
|
2149
|
-
spinner$1.update("Building
|
|
2351
|
+
spinner$1.update("Building platform bundles...");
|
|
2150
2352
|
let publishInput;
|
|
2151
2353
|
try {
|
|
2152
2354
|
publishInput = await buildPresetPublishInput({
|
|
2153
2355
|
preset: presetInput,
|
|
2154
2356
|
version: version$1
|
|
2155
2357
|
});
|
|
2156
|
-
|
|
2358
|
+
const platforms$1 = publishInput.variants.map((v) => v.platform).join(", ");
|
|
2359
|
+
log.debug(`Built publish input for platforms: ${platforms$1}`);
|
|
2157
2360
|
} catch (error$2) {
|
|
2158
2361
|
const message = getErrorMessage(error$2);
|
|
2159
|
-
spinner$1.fail("Failed to build
|
|
2362
|
+
spinner$1.fail("Failed to build platform bundles");
|
|
2160
2363
|
log.error(message);
|
|
2161
2364
|
return {
|
|
2162
2365
|
success: false,
|
|
2163
2366
|
error: message
|
|
2164
2367
|
};
|
|
2165
2368
|
}
|
|
2166
|
-
const
|
|
2167
|
-
const
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
const
|
|
2172
|
-
|
|
2173
|
-
log.
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
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
|
+
}
|
|
2178
2386
|
}
|
|
2387
|
+
log.debug(`Total publish size: ${formatBytes(totalSize)}, files: ${totalFileCount}, platforms: ${platformList}`);
|
|
2179
2388
|
if (dryRun) {
|
|
2180
2389
|
spinner$1.success("Dry run complete");
|
|
2181
2390
|
log.print("");
|
|
2182
2391
|
log.print(ui.header("Publish Preview"));
|
|
2183
2392
|
log.print(ui.keyValue("Preset", publishInput.title));
|
|
2184
2393
|
log.print(ui.keyValue("Name", publishInput.name));
|
|
2185
|
-
log.print(ui.keyValue("
|
|
2394
|
+
log.print(ui.keyValue("Platforms", platformList));
|
|
2186
2395
|
log.print(ui.keyValue("Version", version$1 ? `${version$1}.x (auto-assigned minor)` : "1.x (auto-assigned)"));
|
|
2187
|
-
log.print(ui.keyValue("Files", `${
|
|
2188
|
-
log.print(ui.keyValue("Size", formatBytes(
|
|
2189
|
-
log.print("");
|
|
2190
|
-
log.print(ui.header("Files to publish", fileCount));
|
|
2191
|
-
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)));
|
|
2192
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
|
+
}
|
|
2193
2406
|
log.print(ui.hint("Run without --dry-run to publish."));
|
|
2194
2407
|
return {
|
|
2195
2408
|
success: true,
|
|
2196
2409
|
preview: {
|
|
2197
2410
|
slug: publishInput.name,
|
|
2198
|
-
|
|
2411
|
+
platforms: publishInput.variants.map((v) => v.platform),
|
|
2199
2412
|
title: publishInput.title,
|
|
2200
|
-
totalSize
|
|
2201
|
-
fileCount
|
|
2413
|
+
totalSize,
|
|
2414
|
+
fileCount: totalFileCount
|
|
2202
2415
|
}
|
|
2203
2416
|
};
|
|
2204
2417
|
}
|
|
2205
|
-
spinner$1.update(`Publishing ${publishInput.title} (${
|
|
2418
|
+
spinner$1.update(`Publishing ${publishInput.title} (${platformList})...`);
|
|
2206
2419
|
if (!ctx.credentials) throw new Error("Credentials should exist at this point");
|
|
2207
2420
|
const result = await publishPreset(ctx.registry.url, ctx.credentials.token, publishInput);
|
|
2208
2421
|
if (!result.success) {
|
|
@@ -2226,23 +2439,27 @@ async function publish(options = {}) {
|
|
|
2226
2439
|
}
|
|
2227
2440
|
const { data } = result;
|
|
2228
2441
|
const action = data.isNewPreset ? "Published new preset" : "Published";
|
|
2229
|
-
|
|
2442
|
+
const platforms = data.variants.map((v) => v.platform).join(", ");
|
|
2443
|
+
spinner$1.success(`${action} ${ui.code(data.slug)} ${ui.version(data.version)} (${platforms})`);
|
|
2230
2444
|
log.print("");
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
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
|
+
}
|
|
2235
2452
|
log.info("");
|
|
2236
|
-
log.info(ui.keyValue("Now live at", ui.link(
|
|
2453
|
+
log.info(ui.keyValue("Now live at", ui.link(data.url)));
|
|
2237
2454
|
return {
|
|
2238
2455
|
success: true,
|
|
2239
2456
|
preset: {
|
|
2240
2457
|
slug: data.slug,
|
|
2241
|
-
platform: data.platform,
|
|
2242
2458
|
title: data.title,
|
|
2243
2459
|
version: data.version,
|
|
2244
2460
|
isNewPreset: data.isNewPreset,
|
|
2245
|
-
|
|
2461
|
+
variants: data.variants,
|
|
2462
|
+
url: data.url
|
|
2246
2463
|
}
|
|
2247
2464
|
};
|
|
2248
2465
|
}
|
|
@@ -2271,7 +2488,7 @@ async function buildRegistry(options) {
|
|
|
2271
2488
|
});
|
|
2272
2489
|
if (validateOnly || !outputDir) return {
|
|
2273
2490
|
presets: presets.length,
|
|
2274
|
-
|
|
2491
|
+
items: result.items.length,
|
|
2275
2492
|
bundles: result.bundles.length,
|
|
2276
2493
|
outputDir: null,
|
|
2277
2494
|
validateOnly
|
|
@@ -2286,23 +2503,20 @@ async function buildRegistry(options) {
|
|
|
2286
2503
|
await writeFile(join(bundleDir, bundle.version), bundleJson);
|
|
2287
2504
|
await writeFile(join(bundleDir, LATEST_VERSION), bundleJson);
|
|
2288
2505
|
}
|
|
2289
|
-
for (const
|
|
2290
|
-
const
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
await writeFile(
|
|
2294
|
-
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);
|
|
2295
2511
|
}
|
|
2296
2512
|
const registryJson = JSON.stringify({
|
|
2297
2513
|
$schema: "https://agentrules.directory/schema/registry.json",
|
|
2298
|
-
items: result.
|
|
2514
|
+
items: result.items
|
|
2299
2515
|
}, null, indent$1);
|
|
2300
2516
|
await writeFile(join(outputDir, "registry.json"), registryJson);
|
|
2301
|
-
const indexJson = JSON.stringify(result.index, null, indent$1);
|
|
2302
|
-
await writeFile(join(outputDir, "registry.index.json"), indexJson);
|
|
2303
2517
|
return {
|
|
2304
2518
|
presets: presets.length,
|
|
2305
|
-
|
|
2519
|
+
items: result.items.length,
|
|
2306
2520
|
bundles: result.bundles.length,
|
|
2307
2521
|
outputDir,
|
|
2308
2522
|
validateOnly: false
|
|
@@ -2326,128 +2540,146 @@ async function discoverPresetDirs(inputDir) {
|
|
|
2326
2540
|
}
|
|
2327
2541
|
|
|
2328
2542
|
//#endregion
|
|
2329
|
-
//#region src/commands/
|
|
2330
|
-
async function
|
|
2543
|
+
//#region src/commands/share.ts
|
|
2544
|
+
async function share(options = {}) {
|
|
2331
2545
|
const ctx = useAppContext();
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
const targetPath = resolveTargetPath(platform, rule.type, rule.slug, {
|
|
2339
|
-
global: options.global,
|
|
2340
|
-
directory: options.directory
|
|
2341
|
-
});
|
|
2342
|
-
log.debug(`Target path: ${targetPath}`);
|
|
2343
|
-
const existing = await readExistingFile(targetPath);
|
|
2344
|
-
if (existing !== null) {
|
|
2345
|
-
if (existing === rule.content) return {
|
|
2346
|
-
slug: rule.slug,
|
|
2347
|
-
platform: rule.platform,
|
|
2348
|
-
type: rule.type,
|
|
2349
|
-
title: rule.title,
|
|
2350
|
-
targetPath,
|
|
2351
|
-
status: "unchanged",
|
|
2352
|
-
dryRun
|
|
2353
|
-
};
|
|
2354
|
-
if (!options.force) return {
|
|
2355
|
-
slug: rule.slug,
|
|
2356
|
-
platform: rule.platform,
|
|
2357
|
-
type: rule.type,
|
|
2358
|
-
title: rule.title,
|
|
2359
|
-
targetPath,
|
|
2360
|
-
status: "conflict",
|
|
2361
|
-
dryRun
|
|
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
|
|
2362
2552
|
};
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
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
|
+
};
|
|
2366
2566
|
}
|
|
2567
|
+
}
|
|
2568
|
+
if (!options.name) {
|
|
2569
|
+
const error$2 = "Name is required. Use --name <name>";
|
|
2570
|
+
log.error(error$2);
|
|
2367
2571
|
return {
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
type: rule.type,
|
|
2371
|
-
title: rule.title,
|
|
2372
|
-
targetPath,
|
|
2373
|
-
status: "overwritten",
|
|
2374
|
-
dryRun
|
|
2572
|
+
success: false,
|
|
2573
|
+
error: error$2
|
|
2375
2574
|
};
|
|
2376
2575
|
}
|
|
2377
|
-
if (!
|
|
2378
|
-
|
|
2379
|
-
|
|
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
|
+
};
|
|
2380
2583
|
}
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
dryRun
|
|
2389
|
-
};
|
|
2390
|
-
}
|
|
2391
|
-
function resolveTargetPath(platform, type, slug, options) {
|
|
2392
|
-
const location = options.global ? "global" : "project";
|
|
2393
|
-
const pathTemplate = getInstallPath(platform, type, slug, location);
|
|
2394
|
-
if (!pathTemplate) {
|
|
2395
|
-
const locationLabel = options.global ? "globally" : "to a project";
|
|
2396
|
-
throw new Error(`Rule type "${type}" cannot be installed ${locationLabel} for platform "${platform}"`);
|
|
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
|
+
};
|
|
2397
2591
|
}
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
const
|
|
2401
|
-
|
|
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
|
+
};
|
|
2402
2600
|
}
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
} catch (error$2) {
|
|
2411
|
-
if (error$2.code === "ENOENT") return null;
|
|
2412
|
-
throw error$2;
|
|
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
|
+
};
|
|
2413
2608
|
}
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
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
|
+
};
|
|
2421
2616
|
}
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
}
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
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
|
+
};
|
|
2436
2668
|
}
|
|
2437
2669
|
|
|
2438
2670
|
//#endregion
|
|
2439
2671
|
//#region src/commands/unpublish.ts
|
|
2440
2672
|
/**
|
|
2441
|
-
* Parses preset input to extract slug
|
|
2673
|
+
* Parses preset input to extract slug and version.
|
|
2442
2674
|
* Supports formats:
|
|
2443
|
-
* - "my-preset
|
|
2444
|
-
* - "my-preset@1.0" (
|
|
2445
|
-
* - "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)
|
|
2446
2678
|
*
|
|
2447
|
-
* Explicit --
|
|
2679
|
+
* Explicit --version flag takes precedence.
|
|
2448
2680
|
*/
|
|
2449
|
-
function parseUnpublishInput(input,
|
|
2450
|
-
let normalized = input.
|
|
2681
|
+
function parseUnpublishInput(input, explicitVersion) {
|
|
2682
|
+
let normalized = input.trim();
|
|
2451
2683
|
let parsedVersion;
|
|
2452
2684
|
const atIndex = normalized.lastIndexOf("@");
|
|
2453
2685
|
if (atIndex > 0) {
|
|
@@ -2455,28 +2687,17 @@ function parseUnpublishInput(input, explicitPlatform, explicitVersion) {
|
|
|
2455
2687
|
normalized = normalized.slice(0, atIndex);
|
|
2456
2688
|
}
|
|
2457
2689
|
const version$1 = explicitVersion ?? parsedVersion;
|
|
2458
|
-
const parts = normalized.split(".");
|
|
2459
|
-
const maybePlatform = parts.at(-1);
|
|
2460
|
-
let slug;
|
|
2461
|
-
let platform;
|
|
2462
|
-
if (maybePlatform && isSupportedPlatform(maybePlatform)) {
|
|
2463
|
-
slug = parts.slice(0, -1).join(".");
|
|
2464
|
-
platform = explicitPlatform ?? maybePlatform;
|
|
2465
|
-
} else {
|
|
2466
|
-
slug = normalized;
|
|
2467
|
-
platform = explicitPlatform;
|
|
2468
|
-
}
|
|
2469
2690
|
return {
|
|
2470
|
-
slug,
|
|
2471
|
-
platform,
|
|
2691
|
+
slug: normalized,
|
|
2472
2692
|
version: version$1
|
|
2473
2693
|
};
|
|
2474
2694
|
}
|
|
2475
2695
|
/**
|
|
2476
|
-
* 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.
|
|
2477
2698
|
*/
|
|
2478
2699
|
async function unpublish(options) {
|
|
2479
|
-
const { slug,
|
|
2700
|
+
const { slug, version: version$1 } = parseUnpublishInput(options.preset, options.version);
|
|
2480
2701
|
if (!slug) {
|
|
2481
2702
|
log.error("Preset slug is required");
|
|
2482
2703
|
return {
|
|
@@ -2484,21 +2705,14 @@ async function unpublish(options) {
|
|
|
2484
2705
|
error: "Preset slug is required"
|
|
2485
2706
|
};
|
|
2486
2707
|
}
|
|
2487
|
-
if (!platform) {
|
|
2488
|
-
log.error("Platform is required. Use --platform or specify as <slug>.<platform>@<version>");
|
|
2489
|
-
return {
|
|
2490
|
-
success: false,
|
|
2491
|
-
error: "Platform is required"
|
|
2492
|
-
};
|
|
2493
|
-
}
|
|
2494
2708
|
if (!version$1) {
|
|
2495
|
-
log.error("Version is required. Use --version or specify as <slug
|
|
2709
|
+
log.error("Version is required. Use --version or specify as <slug>@<version>");
|
|
2496
2710
|
return {
|
|
2497
2711
|
success: false,
|
|
2498
2712
|
error: "Version is required"
|
|
2499
2713
|
};
|
|
2500
2714
|
}
|
|
2501
|
-
log.debug(`Unpublishing preset: ${slug}
|
|
2715
|
+
log.debug(`Unpublishing preset: ${slug}@${version$1}`);
|
|
2502
2716
|
const ctx = useAppContext();
|
|
2503
2717
|
if (!(ctx.isLoggedIn && ctx.credentials)) {
|
|
2504
2718
|
const error$2 = "Not logged in. Run `agentrules login` to authenticate.";
|
|
@@ -2509,8 +2723,8 @@ async function unpublish(options) {
|
|
|
2509
2723
|
};
|
|
2510
2724
|
}
|
|
2511
2725
|
log.debug(`Authenticated, unpublishing from ${ctx.registry.url}`);
|
|
2512
|
-
const spinner$1 = await log.spinner(`Unpublishing ${ui.code(slug)}
|
|
2513
|
-
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);
|
|
2514
2728
|
if (!result.success) {
|
|
2515
2729
|
spinner$1.fail("Unpublish failed");
|
|
2516
2730
|
log.error(result.error);
|
|
@@ -2521,18 +2735,63 @@ async function unpublish(options) {
|
|
|
2521
2735
|
};
|
|
2522
2736
|
}
|
|
2523
2737
|
const { data } = result;
|
|
2524
|
-
spinner$1.success(`Unpublished ${ui.code(data.slug)}
|
|
2525
|
-
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."));
|
|
2526
2740
|
return {
|
|
2527
2741
|
success: true,
|
|
2528
2742
|
preset: {
|
|
2529
2743
|
slug: data.slug,
|
|
2530
|
-
platform: data.platform,
|
|
2531
2744
|
version: data.version
|
|
2532
2745
|
}
|
|
2533
2746
|
};
|
|
2534
2747
|
}
|
|
2535
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
|
+
|
|
2536
2795
|
//#endregion
|
|
2537
2796
|
//#region src/help-agent/publish.ts
|
|
2538
2797
|
/**
|
|
@@ -2819,47 +3078,14 @@ program.name("agentrules").description("The AI Agent Directory CLI").version(pac
|
|
|
2819
3078
|
log.debug(`Failed to init context: ${getErrorMessage(error$2)}`);
|
|
2820
3079
|
}
|
|
2821
3080
|
}).showHelpAfterError();
|
|
2822
|
-
program.command("add <item>").description("Download and install a preset or rule from the registry
|
|
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) => {
|
|
2823
3082
|
const dryRun = Boolean(options.dryRun);
|
|
2824
|
-
if (isRuleReference(item)) {
|
|
2825
|
-
const slug = extractRuleSlug(item);
|
|
2826
|
-
const spinner$2 = await log.spinner(`Fetching rule "${slug}"...`);
|
|
2827
|
-
try {
|
|
2828
|
-
const result$1 = await addRule({
|
|
2829
|
-
slug,
|
|
2830
|
-
global: Boolean(options.global),
|
|
2831
|
-
directory: options.dir,
|
|
2832
|
-
force: Boolean(options.force || options.yes),
|
|
2833
|
-
dryRun
|
|
2834
|
-
});
|
|
2835
|
-
spinner$2.stop();
|
|
2836
|
-
if (result$1.status === "conflict") {
|
|
2837
|
-
log.error(`File already exists: ${result$1.targetPath}. Use ${ui.command("--force")} to overwrite.`);
|
|
2838
|
-
process.exitCode = 1;
|
|
2839
|
-
return;
|
|
2840
|
-
}
|
|
2841
|
-
if (result$1.status === "unchanged") {
|
|
2842
|
-
log.info(`Already up to date: ${ui.path(result$1.targetPath)}`);
|
|
2843
|
-
return;
|
|
2844
|
-
}
|
|
2845
|
-
const verb$1 = dryRun ? "Would install" : "Installed";
|
|
2846
|
-
const action = result$1.status === "overwritten" ? "updated" : "created";
|
|
2847
|
-
log.print(ui.fileStatus(action, result$1.targetPath, { dryRun }));
|
|
2848
|
-
log.print("");
|
|
2849
|
-
log.success(`${verb$1} ${ui.bold(result$1.title)} ${ui.muted(`(${result$1.platform}/${result$1.type})`)}`);
|
|
2850
|
-
if (dryRun) log.print(ui.hint("\nDry run complete. No files were written."));
|
|
2851
|
-
} catch (err) {
|
|
2852
|
-
spinner$2.stop();
|
|
2853
|
-
throw err;
|
|
2854
|
-
}
|
|
2855
|
-
return;
|
|
2856
|
-
}
|
|
2857
3083
|
const platform = options.platform ? normalizePlatformInput(options.platform) : void 0;
|
|
2858
|
-
const spinner$1 = await log.spinner("
|
|
3084
|
+
const spinner$1 = await log.spinner("Resolving...");
|
|
2859
3085
|
let result;
|
|
2860
3086
|
try {
|
|
2861
|
-
result = await
|
|
2862
|
-
|
|
3087
|
+
result = await add({
|
|
3088
|
+
slug: item,
|
|
2863
3089
|
platform,
|
|
2864
3090
|
version: options.version,
|
|
2865
3091
|
global: Boolean(options.global),
|
|
@@ -2874,22 +3100,28 @@ program.command("add <item>").description("Download and install a preset or rule
|
|
|
2874
3100
|
throw err;
|
|
2875
3101
|
}
|
|
2876
3102
|
spinner$1.stop();
|
|
2877
|
-
const
|
|
3103
|
+
const conflicts = result.files.filter((f) => f.status === "conflict");
|
|
3104
|
+
const hasBlockingConflicts = conflicts.length > 0 && !options.skipConflicts && !dryRun;
|
|
2878
3105
|
if (hasBlockingConflicts) {
|
|
2879
|
-
const count$1 =
|
|
3106
|
+
const count$1 = conflicts.length === 1 ? "1 file has" : `${conflicts.length} files have`;
|
|
2880
3107
|
const forceHint = `Use ${ui.command("--force")} to overwrite ${ui.muted("(--no-backup to skip backups)")}`;
|
|
2881
3108
|
log.error(`${count$1} conflicts. ${forceHint}`);
|
|
2882
3109
|
log.print("");
|
|
2883
|
-
for (const conflict of
|
|
3110
|
+
for (const conflict of conflicts.slice(0, 3)) {
|
|
2884
3111
|
log.print(` ${ui.muted("•")} ${conflict.path}`);
|
|
2885
3112
|
if (conflict.diff) log.print(conflict.diff.split("\n").map((l) => ` ${l}`).join("\n"));
|
|
2886
3113
|
}
|
|
2887
|
-
if (
|
|
3114
|
+
if (conflicts.length > 3) log.print(`\n ${ui.muted(`...and ${conflicts.length - 3} more`)}`);
|
|
2888
3115
|
log.print("");
|
|
2889
3116
|
log.print(forceHint);
|
|
2890
3117
|
process.exitCode = 1;
|
|
2891
3118
|
return;
|
|
2892
3119
|
}
|
|
3120
|
+
const allUnchanged = result.files.every((f) => f.status === "unchanged");
|
|
3121
|
+
if (allUnchanged) {
|
|
3122
|
+
log.info("Already up to date.");
|
|
3123
|
+
return;
|
|
3124
|
+
}
|
|
2893
3125
|
if (result.backups.length > 0) {
|
|
2894
3126
|
log.print("");
|
|
2895
3127
|
for (const backup of result.backups) log.print(ui.backupStatus(backup.originalPath, backup.backupPath, { dryRun }));
|
|
@@ -2902,10 +3134,12 @@ program.command("add <item>").description("Download and install a preset or rule
|
|
|
2902
3134
|
}
|
|
2903
3135
|
log.print("");
|
|
2904
3136
|
const verb = dryRun ? "Would install" : "Installed";
|
|
2905
|
-
|
|
2906
|
-
|
|
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`);
|
|
2907
3141
|
if (dryRun) log.print(ui.hint("\nDry run complete. No files were written."));
|
|
2908
|
-
if (result.bundle.installMessage) log.print(`\n${result.bundle.installMessage}`);
|
|
3142
|
+
if (result.kind === "preset" && result.bundle.installMessage) log.print(`\n${result.bundle.installMessage}`);
|
|
2909
3143
|
}));
|
|
2910
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) => {
|
|
2911
3145
|
const targetDir = directory ?? process.cwd();
|
|
@@ -2971,10 +3205,11 @@ program.command("validate").description("Validate an agentrules.json configurati
|
|
|
2971
3205
|
const result = await validatePreset({ path: path$1 });
|
|
2972
3206
|
if (result.valid && result.preset) {
|
|
2973
3207
|
const p$1 = result.preset;
|
|
3208
|
+
const platforms = p$1.platforms.map((entry) => entry.platform).join(", ");
|
|
2974
3209
|
log.success(p$1.title);
|
|
2975
3210
|
log.print(ui.keyValue("Description", p$1.description));
|
|
2976
3211
|
log.print(ui.keyValue("License", p$1.license));
|
|
2977
|
-
log.print(ui.keyValue("
|
|
3212
|
+
log.print(ui.keyValue("Platforms", platforms));
|
|
2978
3213
|
if (p$1.tags?.length) log.print(ui.keyValue("Tags", p$1.tags.join(", ")));
|
|
2979
3214
|
} else if (!result.valid) log.error(`Invalid: ${ui.path(result.configPath)}`);
|
|
2980
3215
|
if (result.errors.length > 0) {
|
|
@@ -3011,15 +3246,15 @@ registry.command("build").description("Build registry from preset directories").
|
|
|
3011
3246
|
validateOnly: options.validateOnly
|
|
3012
3247
|
});
|
|
3013
3248
|
if (result.validateOnly) {
|
|
3014
|
-
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`)}`);
|
|
3015
3250
|
return;
|
|
3016
3251
|
}
|
|
3017
3252
|
if (!result.outputDir) {
|
|
3018
|
-
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`);
|
|
3019
3254
|
log.print(ui.hint(`Use ${ui.command("--out <path>")} to write files`));
|
|
3020
3255
|
return;
|
|
3021
3256
|
}
|
|
3022
|
-
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`)}`);
|
|
3023
3258
|
log.print(ui.keyValue("Output", ui.path(result.outputDir)));
|
|
3024
3259
|
}));
|
|
3025
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) => {
|
|
@@ -3097,7 +3332,7 @@ program.command("whoami").description("Show the currently authenticated user").a
|
|
|
3097
3332
|
log.print(ui.keyValue("Session", `expires in ${daysUntilExpiry} day${daysUntilExpiry === 1 ? "" : "s"}`));
|
|
3098
3333
|
}
|
|
3099
3334
|
}));
|
|
3100
|
-
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) => {
|
|
3101
3336
|
const result = await publish({
|
|
3102
3337
|
path: path$1,
|
|
3103
3338
|
version: options.version,
|
|
@@ -3105,11 +3340,28 @@ program.command("publish").description("Publish a preset to the registry").argum
|
|
|
3105
3340
|
});
|
|
3106
3341
|
if (!result.success) process.exitCode = 1;
|
|
3107
3342
|
}));
|
|
3108
|
-
program.command("
|
|
3109
|
-
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) => {
|
|
3110
3363
|
const result = await unpublish({
|
|
3111
3364
|
preset,
|
|
3112
|
-
platform,
|
|
3113
3365
|
version: options.version
|
|
3114
3366
|
});
|
|
3115
3367
|
if (!result.success) process.exitCode = 1;
|