@sporesec/arcana 2.4.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.d.ts +0 -1
- package/dist/cli.js +120 -9
- package/dist/command-registry.d.ts +10 -0
- package/dist/command-registry.js +65 -0
- package/dist/commands/audit.d.ts +0 -1
- package/dist/commands/audit.js +16 -6
- package/dist/commands/benchmark.d.ts +4 -0
- package/dist/commands/benchmark.js +178 -0
- package/dist/commands/clean.d.ts +0 -1
- package/dist/commands/clean.js +19 -8
- package/dist/commands/compact.d.ts +2 -1
- package/dist/commands/compact.js +74 -14
- package/dist/commands/completions.d.ts +3 -0
- package/dist/commands/completions.js +104 -0
- package/dist/commands/config.d.ts +0 -1
- package/dist/commands/config.js +15 -6
- package/dist/commands/create.d.ts +0 -1
- package/dist/commands/create.js +1 -1
- package/dist/commands/diff.d.ts +4 -0
- package/dist/commands/diff.js +166 -0
- package/dist/commands/doctor.d.ts +0 -1
- package/dist/commands/doctor.js +64 -23
- package/dist/commands/export-cmd.d.ts +4 -0
- package/dist/commands/export-cmd.js +66 -0
- package/dist/commands/import-cmd.d.ts +4 -0
- package/dist/commands/import-cmd.js +131 -0
- package/dist/commands/info.d.ts +0 -1
- package/dist/commands/info.js +29 -4
- package/dist/commands/init.d.ts +0 -1
- package/dist/commands/init.js +26 -33
- package/dist/commands/install.d.ts +1 -1
- package/dist/commands/install.js +118 -205
- package/dist/commands/list.d.ts +0 -1
- package/dist/commands/list.js +12 -4
- package/dist/commands/lock.d.ts +4 -0
- package/dist/commands/lock.js +171 -0
- package/dist/commands/optimize.d.ts +0 -1
- package/dist/commands/optimize.js +111 -20
- package/dist/commands/outdated.d.ts +4 -0
- package/dist/commands/outdated.js +159 -0
- package/dist/commands/profile.d.ts +3 -0
- package/dist/commands/profile.js +274 -0
- package/dist/commands/providers.d.ts +0 -1
- package/dist/commands/providers.js +1 -4
- package/dist/commands/recommend.d.ts +5 -0
- package/dist/commands/recommend.js +96 -0
- package/dist/commands/scan.d.ts +0 -1
- package/dist/commands/scan.js +13 -7
- package/dist/commands/search.d.ts +2 -1
- package/dist/commands/search.js +32 -9
- package/dist/commands/stats.d.ts +0 -1
- package/dist/commands/stats.js +24 -20
- package/dist/commands/team.d.ts +3 -0
- package/dist/commands/team.js +291 -0
- package/dist/commands/uninstall.d.ts +0 -1
- package/dist/commands/uninstall.js +18 -4
- package/dist/commands/update.d.ts +0 -1
- package/dist/commands/update.js +155 -155
- package/dist/commands/validate.d.ts +0 -1
- package/dist/commands/validate.js +14 -6
- package/dist/commands/verify.d.ts +4 -0
- package/dist/commands/verify.js +116 -0
- package/dist/constants.d.ts +10 -0
- package/dist/constants.js +13 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/interactive/browse.d.ts +4 -0
- package/dist/interactive/browse.js +103 -0
- package/dist/interactive/categories.d.ts +4 -0
- package/dist/interactive/categories.js +87 -0
- package/dist/interactive/health.d.ts +1 -0
- package/dist/interactive/health.js +57 -0
- package/dist/interactive/helpers.d.ts +11 -0
- package/dist/interactive/helpers.js +66 -0
- package/dist/interactive/index.d.ts +1 -0
- package/dist/interactive/index.js +1 -0
- package/dist/interactive/manage.d.ts +2 -0
- package/dist/interactive/manage.js +187 -0
- package/dist/interactive/menu.d.ts +1 -0
- package/dist/interactive/menu.js +107 -0
- package/dist/interactive/search.d.ts +2 -0
- package/dist/interactive/search.js +66 -0
- package/dist/interactive/setup.d.ts +2 -0
- package/dist/interactive/setup.js +48 -0
- package/dist/interactive/skill-detail.d.ts +5 -0
- package/dist/interactive/skill-detail.js +126 -0
- package/dist/interactive.d.ts +0 -1
- package/dist/interactive.js +89 -66
- package/dist/providers/arcana.d.ts +0 -1
- package/dist/providers/arcana.js +0 -1
- package/dist/providers/base.d.ts +0 -1
- package/dist/providers/base.js +0 -1
- package/dist/providers/github.d.ts +0 -1
- package/dist/providers/github.js +8 -3
- package/dist/registry.d.ts +0 -1
- package/dist/registry.js +1 -4
- package/dist/types.d.ts +10 -1
- package/dist/types.js +0 -1
- package/dist/utils/atomic.d.ts +0 -1
- package/dist/utils/atomic.js +3 -2
- package/dist/utils/cache.d.ts +0 -1
- package/dist/utils/cache.js +3 -2
- package/dist/utils/config.d.ts +2 -1
- package/dist/utils/config.js +30 -5
- package/dist/utils/conflict-check.d.ts +8 -0
- package/dist/utils/conflict-check.js +72 -0
- package/dist/utils/errors.d.ts +0 -1
- package/dist/utils/errors.js +0 -1
- package/dist/utils/frontmatter.d.ts +0 -1
- package/dist/utils/frontmatter.js +37 -10
- package/dist/utils/fs.d.ts +0 -1
- package/dist/utils/fs.js +30 -11
- package/dist/utils/help.d.ts +0 -1
- package/dist/utils/help.js +15 -28
- package/dist/utils/history.d.ts +0 -1
- package/dist/utils/history.js +0 -1
- package/dist/utils/http.d.ts +0 -1
- package/dist/utils/http.js +14 -5
- package/dist/utils/install-core.d.ts +48 -0
- package/dist/utils/install-core.js +108 -0
- package/dist/utils/integrity.d.ts +17 -0
- package/dist/utils/integrity.js +84 -0
- package/dist/utils/parallel.d.ts +0 -1
- package/dist/utils/parallel.js +0 -1
- package/dist/utils/project-context.d.ts +19 -0
- package/dist/utils/project-context.js +283 -0
- package/dist/utils/scanner.d.ts +0 -1
- package/dist/utils/scanner.js +138 -10
- package/dist/utils/scoring.d.ts +10 -0
- package/dist/utils/scoring.js +84 -0
- package/dist/utils/ui.d.ts +0 -1
- package/dist/utils/ui.js +11 -4
- package/dist/utils/validate.d.ts +0 -1
- package/dist/utils/validate.js +4 -1
- package/package.json +74 -62
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/commands/audit.d.ts.map +0 -1
- package/dist/commands/audit.js.map +0 -1
- package/dist/commands/audit.test.d.ts +0 -2
- package/dist/commands/audit.test.d.ts.map +0 -1
- package/dist/commands/audit.test.js +0 -217
- package/dist/commands/audit.test.js.map +0 -1
- package/dist/commands/clean.d.ts.map +0 -1
- package/dist/commands/clean.js.map +0 -1
- package/dist/commands/compact.d.ts.map +0 -1
- package/dist/commands/compact.js.map +0 -1
- package/dist/commands/config.d.ts.map +0 -1
- package/dist/commands/config.js.map +0 -1
- package/dist/commands/create.d.ts.map +0 -1
- package/dist/commands/create.js.map +0 -1
- package/dist/commands/doctor.d.ts.map +0 -1
- package/dist/commands/doctor.js.map +0 -1
- package/dist/commands/info.d.ts.map +0 -1
- package/dist/commands/info.js.map +0 -1
- package/dist/commands/init.d.ts.map +0 -1
- package/dist/commands/init.js.map +0 -1
- package/dist/commands/install.d.ts.map +0 -1
- package/dist/commands/install.js.map +0 -1
- package/dist/commands/list.d.ts.map +0 -1
- package/dist/commands/list.js.map +0 -1
- package/dist/commands/optimize.d.ts.map +0 -1
- package/dist/commands/optimize.js.map +0 -1
- package/dist/commands/providers.d.ts.map +0 -1
- package/dist/commands/providers.js.map +0 -1
- package/dist/commands/scan.d.ts.map +0 -1
- package/dist/commands/scan.js.map +0 -1
- package/dist/commands/search.d.ts.map +0 -1
- package/dist/commands/search.js.map +0 -1
- package/dist/commands/stats.d.ts.map +0 -1
- package/dist/commands/stats.js.map +0 -1
- package/dist/commands/uninstall.d.ts.map +0 -1
- package/dist/commands/uninstall.js.map +0 -1
- package/dist/commands/update.d.ts.map +0 -1
- package/dist/commands/update.js.map +0 -1
- package/dist/commands/validate.d.ts.map +0 -1
- package/dist/commands/validate.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/interactive.d.ts.map +0 -1
- package/dist/interactive.js.map +0 -1
- package/dist/providers/arcana.d.ts.map +0 -1
- package/dist/providers/arcana.js.map +0 -1
- package/dist/providers/base.d.ts.map +0 -1
- package/dist/providers/base.js.map +0 -1
- package/dist/providers/github.d.ts.map +0 -1
- package/dist/providers/github.js.map +0 -1
- package/dist/registry.d.ts.map +0 -1
- package/dist/registry.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
- package/dist/utils/atomic.d.ts.map +0 -1
- package/dist/utils/atomic.js.map +0 -1
- package/dist/utils/atomic.test.d.ts +0 -2
- package/dist/utils/atomic.test.d.ts.map +0 -1
- package/dist/utils/atomic.test.js +0 -31
- package/dist/utils/atomic.test.js.map +0 -1
- package/dist/utils/cache.d.ts.map +0 -1
- package/dist/utils/cache.js.map +0 -1
- package/dist/utils/config.d.ts.map +0 -1
- package/dist/utils/config.js.map +0 -1
- package/dist/utils/config.test.d.ts +0 -2
- package/dist/utils/config.test.d.ts.map +0 -1
- package/dist/utils/config.test.js +0 -38
- package/dist/utils/config.test.js.map +0 -1
- package/dist/utils/errors.d.ts.map +0 -1
- package/dist/utils/errors.js.map +0 -1
- package/dist/utils/frontmatter.d.ts.map +0 -1
- package/dist/utils/frontmatter.js.map +0 -1
- package/dist/utils/frontmatter.test.d.ts +0 -2
- package/dist/utils/frontmatter.test.d.ts.map +0 -1
- package/dist/utils/frontmatter.test.js +0 -152
- package/dist/utils/frontmatter.test.js.map +0 -1
- package/dist/utils/fs.d.ts.map +0 -1
- package/dist/utils/fs.js.map +0 -1
- package/dist/utils/fs.test.d.ts +0 -2
- package/dist/utils/fs.test.d.ts.map +0 -1
- package/dist/utils/fs.test.js +0 -145
- package/dist/utils/fs.test.js.map +0 -1
- package/dist/utils/help.d.ts.map +0 -1
- package/dist/utils/help.js.map +0 -1
- package/dist/utils/help.test.d.ts +0 -2
- package/dist/utils/help.test.d.ts.map +0 -1
- package/dist/utils/help.test.js +0 -66
- package/dist/utils/help.test.js.map +0 -1
- package/dist/utils/history.d.ts.map +0 -1
- package/dist/utils/history.js.map +0 -1
- package/dist/utils/http.d.ts.map +0 -1
- package/dist/utils/http.js.map +0 -1
- package/dist/utils/http.test.d.ts +0 -2
- package/dist/utils/http.test.d.ts.map +0 -1
- package/dist/utils/http.test.js +0 -55
- package/dist/utils/http.test.js.map +0 -1
- package/dist/utils/parallel.d.ts.map +0 -1
- package/dist/utils/parallel.js.map +0 -1
- package/dist/utils/scanner.d.ts.map +0 -1
- package/dist/utils/scanner.js.map +0 -1
- package/dist/utils/ui.d.ts.map +0 -1
- package/dist/utils/ui.js.map +0 -1
- package/dist/utils/ui.test.d.ts +0 -2
- package/dist/utils/ui.test.d.ts.map +0 -1
- package/dist/utils/ui.test.js +0 -31
- package/dist/utils/ui.test.js.map +0 -1
- package/dist/utils/validate.d.ts.map +0 -1
- package/dist/utils/validate.js.map +0 -1
package/dist/registry.d.ts
CHANGED
package/dist/registry.js
CHANGED
|
@@ -64,8 +64,5 @@ export function getProviders(name) {
|
|
|
64
64
|
if (name)
|
|
65
65
|
return [getProvider(name)];
|
|
66
66
|
const config = loadConfig();
|
|
67
|
-
return config.providers
|
|
68
|
-
.filter((p) => p.enabled)
|
|
69
|
-
.map((p) => createProvider(p.name, p.type, p.url));
|
|
67
|
+
return config.providers.filter((p) => p.enabled).map((p) => createProvider(p.name, p.type, p.url));
|
|
70
68
|
}
|
|
71
|
-
//# sourceMappingURL=registry.js.map
|
package/dist/types.d.ts
CHANGED
|
@@ -4,6 +4,11 @@ export interface SkillInfo {
|
|
|
4
4
|
version: string;
|
|
5
5
|
source: string;
|
|
6
6
|
repo?: string;
|
|
7
|
+
tags?: string[];
|
|
8
|
+
conflicts?: string[];
|
|
9
|
+
companions?: string[];
|
|
10
|
+
verified?: boolean;
|
|
11
|
+
author?: string;
|
|
7
12
|
}
|
|
8
13
|
export interface SkillFile {
|
|
9
14
|
path: string;
|
|
@@ -26,6 +31,11 @@ export interface MarketplacePlugin {
|
|
|
26
31
|
source: string;
|
|
27
32
|
description: string;
|
|
28
33
|
version: string;
|
|
34
|
+
tags?: string[];
|
|
35
|
+
conflicts?: string[];
|
|
36
|
+
companions?: string[];
|
|
37
|
+
verified?: boolean;
|
|
38
|
+
author?: string;
|
|
29
39
|
}
|
|
30
40
|
export interface ProviderConfig {
|
|
31
41
|
name: string;
|
|
@@ -64,4 +74,3 @@ export interface DoctorCheck {
|
|
|
64
74
|
message: string;
|
|
65
75
|
fix?: string;
|
|
66
76
|
}
|
|
67
|
-
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.js
CHANGED
package/dist/utils/atomic.d.ts
CHANGED
package/dist/utils/atomic.js
CHANGED
package/dist/utils/cache.d.ts
CHANGED
package/dist/utils/cache.js
CHANGED
package/dist/utils/config.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ArcanaConfig, ProviderConfig } from "../types.js";
|
|
2
|
+
/** Validate config and return warnings for invalid fields. */
|
|
3
|
+
export declare function validateConfig(config: ArcanaConfig): string[];
|
|
2
4
|
export declare function loadConfig(): ArcanaConfig;
|
|
3
5
|
export declare function saveConfig(config: ArcanaConfig): void;
|
|
4
6
|
export declare function addProvider(provider: ProviderConfig): void;
|
|
5
7
|
export declare function removeProvider(name: string): boolean;
|
|
6
|
-
//# sourceMappingURL=config.d.ts.map
|
package/dist/utils/config.js
CHANGED
|
@@ -4,6 +4,8 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import { ui } from "./ui.js";
|
|
5
5
|
import { atomicWriteSync } from "./atomic.js";
|
|
6
6
|
const CONFIG_PATH = join(homedir(), ".arcana", "config.json");
|
|
7
|
+
/** Matches owner/repo slug format (e.g. "medy-gribkov/arcana") */
|
|
8
|
+
const SLUG_RE = /^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/;
|
|
7
9
|
const DEFAULT_CONFIG = {
|
|
8
10
|
defaultProvider: "arcana",
|
|
9
11
|
installDir: join(homedir(), ".agents", "skills"),
|
|
@@ -17,7 +19,27 @@ const DEFAULT_CONFIG = {
|
|
|
17
19
|
],
|
|
18
20
|
};
|
|
19
21
|
function cloneConfig(config) {
|
|
20
|
-
return { ...config, providers: config.providers.map(p => ({ ...p })) };
|
|
22
|
+
return { ...config, providers: config.providers.map((p) => ({ ...p })) };
|
|
23
|
+
}
|
|
24
|
+
/** Validate config and return warnings for invalid fields. */
|
|
25
|
+
export function validateConfig(config) {
|
|
26
|
+
const warnings = [];
|
|
27
|
+
// Validate providers have valid owner/repo slugs
|
|
28
|
+
for (const p of config.providers) {
|
|
29
|
+
if (p.type === "github" && !SLUG_RE.test(p.url)) {
|
|
30
|
+
warnings.push(`Provider "${p.name}" has invalid URL "${p.url}". Expected owner/repo format.`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// Validate installDir is absolute
|
|
34
|
+
if (!isAbsolute(config.installDir)) {
|
|
35
|
+
warnings.push(`installDir "${config.installDir}" is not an absolute path.`);
|
|
36
|
+
}
|
|
37
|
+
// Validate defaultProvider matches a configured provider or valid slug
|
|
38
|
+
const providerNames = config.providers.map((p) => p.name);
|
|
39
|
+
if (!providerNames.includes(config.defaultProvider) && !SLUG_RE.test(config.defaultProvider)) {
|
|
40
|
+
warnings.push(`defaultProvider "${config.defaultProvider}" doesn't match any configured provider and isn't a valid slug.`);
|
|
41
|
+
}
|
|
42
|
+
return warnings;
|
|
21
43
|
}
|
|
22
44
|
export function loadConfig() {
|
|
23
45
|
if (!existsSync(CONFIG_PATH)) {
|
|
@@ -29,7 +51,7 @@ export function loadConfig() {
|
|
|
29
51
|
const config = {
|
|
30
52
|
...DEFAULT_CONFIG,
|
|
31
53
|
...loaded,
|
|
32
|
-
providers: loaded.providers ?? DEFAULT_CONFIG.providers.map(p => ({ ...p })),
|
|
54
|
+
providers: loaded.providers ?? DEFAULT_CONFIG.providers.map((p) => ({ ...p })),
|
|
33
55
|
};
|
|
34
56
|
return applyEnvOverrides(config);
|
|
35
57
|
}
|
|
@@ -51,11 +73,15 @@ function applyEnvOverrides(base) {
|
|
|
51
73
|
}
|
|
52
74
|
const envProvider = process.env.ARCANA_DEFAULT_PROVIDER;
|
|
53
75
|
if (envProvider) {
|
|
54
|
-
|
|
76
|
+
const trimmed = envProvider.trim();
|
|
77
|
+
if (trimmed.length === 0) {
|
|
55
78
|
console.error(ui.warn(" Warning: ARCANA_DEFAULT_PROVIDER is empty. Ignoring."));
|
|
56
79
|
}
|
|
80
|
+
else if (!SLUG_RE.test(trimmed) && !config.providers.some((p) => p.name === trimmed)) {
|
|
81
|
+
console.error(ui.warn(` Warning: ARCANA_DEFAULT_PROVIDER "${trimmed}" is not a valid slug. Ignoring.`));
|
|
82
|
+
}
|
|
57
83
|
else {
|
|
58
|
-
config.defaultProvider =
|
|
84
|
+
config.defaultProvider = trimmed;
|
|
59
85
|
}
|
|
60
86
|
}
|
|
61
87
|
return config;
|
|
@@ -87,4 +113,3 @@ export function removeProvider(name) {
|
|
|
87
113
|
saveConfig(config);
|
|
88
114
|
return true;
|
|
89
115
|
}
|
|
90
|
-
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { SkillInfo } from "../types.js";
|
|
2
|
+
import type { ProjectContext } from "./project-context.js";
|
|
3
|
+
export interface ConflictWarning {
|
|
4
|
+
type: "rule-overlap" | "skill-conflict" | "preference-clash";
|
|
5
|
+
message: string;
|
|
6
|
+
severity: "warn" | "block";
|
|
7
|
+
}
|
|
8
|
+
export declare function checkConflicts(skillName: string, skillInfo: SkillInfo | null, skillContent: string | null, context: ProjectContext): ConflictWarning[];
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/** Known opposing preference pairs. If a skill promotes one, and CLAUDE.md has the other, warn. */
|
|
2
|
+
const OPPOSING_PAIRS = [
|
|
3
|
+
["callbacks", "async/await"],
|
|
4
|
+
["any", "strict typing"],
|
|
5
|
+
["abbreviations", "meaningful names"],
|
|
6
|
+
["classes", "functional"],
|
|
7
|
+
["oop", "functional programming"],
|
|
8
|
+
["semicolons", "no semicolons"],
|
|
9
|
+
["tabs", "spaces"],
|
|
10
|
+
];
|
|
11
|
+
export function checkConflicts(skillName, skillInfo, skillContent, context) {
|
|
12
|
+
const warnings = [];
|
|
13
|
+
// 1. Explicit skill-level conflicts from marketplace metadata
|
|
14
|
+
if (skillInfo?.conflicts && skillInfo.conflicts.length > 0) {
|
|
15
|
+
const installed = context.installedSkills;
|
|
16
|
+
const conflicting = skillInfo.conflicts.filter((c) => installed.includes(c));
|
|
17
|
+
for (const c of conflicting) {
|
|
18
|
+
warnings.push({
|
|
19
|
+
type: "skill-conflict",
|
|
20
|
+
message: `"${skillName}" conflicts with installed skill "${c}".`,
|
|
21
|
+
severity: "block",
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
// 2. Rule overlap: skill name matches existing .claude/rules/*.md filename
|
|
26
|
+
const ruleBasenames = context.ruleFiles.map((f) => f.replace(/\.md$/, "").toLowerCase());
|
|
27
|
+
if (ruleBasenames.includes(skillName.toLowerCase())) {
|
|
28
|
+
warnings.push({
|
|
29
|
+
type: "rule-overlap",
|
|
30
|
+
message: `A rule file "${skillName}.md" already exists in .claude/rules/. This skill may duplicate existing instructions.`,
|
|
31
|
+
severity: "warn",
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
// Also check if skill tags overlap heavily with rule file names
|
|
35
|
+
if (skillInfo?.tags) {
|
|
36
|
+
for (const tag of skillInfo.tags) {
|
|
37
|
+
if (ruleBasenames.includes(tag.toLowerCase()) && tag.toLowerCase() !== skillName.toLowerCase()) {
|
|
38
|
+
warnings.push({
|
|
39
|
+
type: "rule-overlap",
|
|
40
|
+
message: `Skill tag "${tag}" matches existing rule "${tag}.md". May overlap.`,
|
|
41
|
+
severity: "warn",
|
|
42
|
+
});
|
|
43
|
+
break; // one warning is enough
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// 3. Preference clash: check skill content against CLAUDE.md preferences
|
|
48
|
+
if (skillContent && context.preferences.length > 0) {
|
|
49
|
+
const contentLower = skillContent.toLowerCase();
|
|
50
|
+
for (const [a, b] of OPPOSING_PAIRS) {
|
|
51
|
+
const skillHasA = contentLower.includes(a);
|
|
52
|
+
const skillHasB = contentLower.includes(b);
|
|
53
|
+
const prefsHaveA = context.preferences.some((p) => p.toLowerCase().includes(a));
|
|
54
|
+
const prefsHaveB = context.preferences.some((p) => p.toLowerCase().includes(b));
|
|
55
|
+
if (skillHasA && prefsHaveB) {
|
|
56
|
+
warnings.push({
|
|
57
|
+
type: "preference-clash",
|
|
58
|
+
message: `Skill mentions "${a}" but your project prefers "${b}".`,
|
|
59
|
+
severity: "warn",
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
else if (skillHasB && prefsHaveA) {
|
|
63
|
+
warnings.push({
|
|
64
|
+
type: "preference-clash",
|
|
65
|
+
message: `Skill mentions "${b}" but your project prefers "${a}".`,
|
|
66
|
+
severity: "warn",
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return warnings;
|
|
72
|
+
}
|
package/dist/utils/errors.d.ts
CHANGED
package/dist/utils/errors.js
CHANGED
|
@@ -9,4 +9,3 @@ export declare function extractFrontmatter(content: string): {
|
|
|
9
9
|
export declare function parseFrontmatter(raw: string): SkillFrontmatter | null;
|
|
10
10
|
export declare function fixSkillFrontmatter(content: string): string;
|
|
11
11
|
export declare function validateSkillDir(skillDir: string, skillName: string): ValidationResult;
|
|
12
|
-
//# sourceMappingURL=frontmatter.d.ts.map
|
|
@@ -40,7 +40,13 @@ export function parseFrontmatter(raw) {
|
|
|
40
40
|
if (descMatch?.[1] !== undefined) {
|
|
41
41
|
let value = descMatch[1].trim();
|
|
42
42
|
// Handle YAML multiline: |, >, or bare indented continuation
|
|
43
|
-
if (value === "|" ||
|
|
43
|
+
if (value === "|" ||
|
|
44
|
+
value === ">" ||
|
|
45
|
+
value === "|-" ||
|
|
46
|
+
value === "|+" ||
|
|
47
|
+
value === ">-" ||
|
|
48
|
+
value === ">+" ||
|
|
49
|
+
value === "") {
|
|
44
50
|
const multilineLines = [];
|
|
45
51
|
for (let j = i + 1; j < lines.length; j++) {
|
|
46
52
|
const next = lines[j];
|
|
@@ -93,12 +99,7 @@ export function fixSkillFrontmatter(content) {
|
|
|
93
99
|
if (!parsed)
|
|
94
100
|
return content;
|
|
95
101
|
// Rebuild clean frontmatter with only name and description
|
|
96
|
-
const cleanFm = [
|
|
97
|
-
FM_DELIMITER,
|
|
98
|
-
`name: ${parsed.name}`,
|
|
99
|
-
`description: ${parsed.description}`,
|
|
100
|
-
FM_DELIMITER,
|
|
101
|
-
].join("\n");
|
|
102
|
+
const cleanFm = [FM_DELIMITER, `name: ${parsed.name}`, `description: ${parsed.description}`, FM_DELIMITER].join("\n");
|
|
102
103
|
return cleanFm + "\n" + extracted.body.replace(/^\n+/, "\n");
|
|
103
104
|
}
|
|
104
105
|
export function validateSkillDir(skillDir, skillName) {
|
|
@@ -145,12 +146,29 @@ export function validateSkillDir(skillDir, skillName) {
|
|
|
145
146
|
else if (parsed.description.length > MAX_DESC_LENGTH) {
|
|
146
147
|
result.warnings.push(`Description too long (${parsed.description.length} chars, max ${MAX_DESC_LENGTH})`);
|
|
147
148
|
}
|
|
148
|
-
// Check for non-standard fields
|
|
149
|
+
// Check for non-standard fields (metadata is invalid per spec)
|
|
149
150
|
const standardFields = ["name", "description"];
|
|
151
|
+
const VALID_FIELDS = [
|
|
152
|
+
"name",
|
|
153
|
+
"description",
|
|
154
|
+
"argument-hint",
|
|
155
|
+
"compatibility",
|
|
156
|
+
"disable-model-invocation",
|
|
157
|
+
"license",
|
|
158
|
+
"user-invokable",
|
|
159
|
+
];
|
|
150
160
|
for (const line of extracted.raw.split("\n")) {
|
|
151
161
|
const keyMatch = line.match(/^(\w[\w-]*):/);
|
|
152
162
|
if (keyMatch?.[1] && !standardFields.includes(keyMatch[1])) {
|
|
153
|
-
|
|
163
|
+
if (keyMatch[1] === "metadata") {
|
|
164
|
+
result.warnings.push("Invalid field: metadata (not allowed in frontmatter)");
|
|
165
|
+
}
|
|
166
|
+
else if (!VALID_FIELDS.includes(keyMatch[1])) {
|
|
167
|
+
result.infos.push(`Non-standard field: ${keyMatch[1]}`);
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
result.infos.push(`Optional field: ${keyMatch[1]}`);
|
|
171
|
+
}
|
|
154
172
|
}
|
|
155
173
|
}
|
|
156
174
|
if (parsed.name !== skillName) {
|
|
@@ -165,8 +183,17 @@ export function validateSkillDir(skillDir, skillName) {
|
|
|
165
183
|
if (extracted.body.trim().length >= 50 && !extracted.body.includes("##")) {
|
|
166
184
|
result.infos.push("Body has no ## headings (recommended for structure)");
|
|
167
185
|
}
|
|
186
|
+
// Check for code blocks (quality signal)
|
|
187
|
+
if (extracted.body.trim().length >= 50 && !extracted.body.includes("```")) {
|
|
188
|
+
result.infos.push("No code blocks found (procedural skills should include code examples)");
|
|
189
|
+
}
|
|
190
|
+
// Check for BAD/GOOD pattern examples
|
|
191
|
+
const hasPattern = /(?:BAD|GOOD|WRONG|RIGHT|AVOID|PREFER|DO NOT|INSTEAD)/i.test(extracted.body) ||
|
|
192
|
+
/<!--\s*(?:bad|good)\s*-->/i.test(extracted.body);
|
|
193
|
+
if (extracted.body.trim().length >= 100 && !hasPattern) {
|
|
194
|
+
result.infos.push("No BAD/GOOD contrast patterns found (recommended for teaching)");
|
|
195
|
+
}
|
|
168
196
|
if (result.errors.length > 0)
|
|
169
197
|
result.valid = false;
|
|
170
198
|
return result;
|
|
171
199
|
}
|
|
172
|
-
//# sourceMappingURL=frontmatter.js.map
|
package/dist/utils/fs.d.ts
CHANGED
package/dist/utils/fs.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, rmSync, renameSync, lstatSync, readlinkSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, rmSync, renameSync, lstatSync, readlinkSync, } from "node:fs";
|
|
2
2
|
import { join, dirname, resolve, sep } from "node:path";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { loadConfig } from "../utils/config.js";
|
|
@@ -26,10 +26,14 @@ export function getDirSize(dir) {
|
|
|
26
26
|
else
|
|
27
27
|
size += stat.size;
|
|
28
28
|
}
|
|
29
|
-
catch {
|
|
29
|
+
catch {
|
|
30
|
+
/* skip unreadable entries */
|
|
31
|
+
}
|
|
30
32
|
}
|
|
31
33
|
}
|
|
32
|
-
catch {
|
|
34
|
+
catch {
|
|
35
|
+
/* skip unreadable dirs */
|
|
36
|
+
}
|
|
33
37
|
}
|
|
34
38
|
return size;
|
|
35
39
|
}
|
|
@@ -45,14 +49,18 @@ export function installSkill(skillName, files) {
|
|
|
45
49
|
try {
|
|
46
50
|
for (const file of files) {
|
|
47
51
|
// Reject paths containing .. before resolving
|
|
48
|
-
if (file.path.includes("..") ||
|
|
52
|
+
if (file.path.includes("..") ||
|
|
53
|
+
file.path.includes("~") ||
|
|
54
|
+
file.path.startsWith("\\\\") ||
|
|
55
|
+
file.path.startsWith("//")) {
|
|
49
56
|
throw new Error(`Path traversal blocked: ${file.path}`);
|
|
50
57
|
}
|
|
51
58
|
const filePath = resolve(tempDir, file.path);
|
|
52
59
|
// Normalize to lowercase on Windows for case-insensitive comparison
|
|
53
60
|
const normalizedFile = process.platform === "win32" ? filePath.toLowerCase() : filePath;
|
|
54
61
|
const normalizedTemp = process.platform === "win32" ? (tempDir + sep).toLowerCase() : tempDir + sep;
|
|
55
|
-
if (!normalizedFile.startsWith(normalizedTemp) &&
|
|
62
|
+
if (!normalizedFile.startsWith(normalizedTemp) &&
|
|
63
|
+
normalizedFile !== (process.platform === "win32" ? tempDir.toLowerCase() : tempDir)) {
|
|
56
64
|
throw new Error(`Path traversal blocked: ${file.path}`);
|
|
57
65
|
}
|
|
58
66
|
const dir = dirname(filePath);
|
|
@@ -72,7 +80,9 @@ export function installSkill(skillName, files) {
|
|
|
72
80
|
try {
|
|
73
81
|
rmSync(tempDir, { recursive: true, force: true });
|
|
74
82
|
}
|
|
75
|
-
catch {
|
|
83
|
+
catch {
|
|
84
|
+
/* best-effort */
|
|
85
|
+
}
|
|
76
86
|
throw err;
|
|
77
87
|
}
|
|
78
88
|
return skillDir;
|
|
@@ -124,13 +134,21 @@ export function listFilesByAge(dir, ext, olderThanDays) {
|
|
|
124
134
|
continue;
|
|
125
135
|
const age = now - stat.mtimeMs;
|
|
126
136
|
if (age > cutoff) {
|
|
127
|
-
results.push({
|
|
137
|
+
results.push({
|
|
138
|
+
path: full,
|
|
139
|
+
sizeMB: stat.size / (1024 * 1024),
|
|
140
|
+
daysOld: Math.floor(age / (24 * 60 * 60 * 1000)),
|
|
141
|
+
});
|
|
128
142
|
}
|
|
129
143
|
}
|
|
130
|
-
catch {
|
|
144
|
+
catch {
|
|
145
|
+
/* skip */
|
|
146
|
+
}
|
|
131
147
|
}
|
|
132
148
|
}
|
|
133
|
-
catch {
|
|
149
|
+
catch {
|
|
150
|
+
/* skip */
|
|
151
|
+
}
|
|
134
152
|
}
|
|
135
153
|
return results;
|
|
136
154
|
}
|
|
@@ -189,8 +207,9 @@ export function listSymlinks() {
|
|
|
189
207
|
results.push({ name: entry, fullPath, target, broken: !existsSync(target) });
|
|
190
208
|
}
|
|
191
209
|
}
|
|
192
|
-
catch {
|
|
210
|
+
catch {
|
|
211
|
+
/* skip unreadable */
|
|
212
|
+
}
|
|
193
213
|
}
|
|
194
214
|
return results;
|
|
195
215
|
}
|
|
196
|
-
//# sourceMappingURL=fs.js.map
|
package/dist/utils/help.d.ts
CHANGED
package/dist/utils/help.js
CHANGED
|
@@ -4,6 +4,7 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import * as p from "@clack/prompts";
|
|
5
5
|
import chalk from "chalk";
|
|
6
6
|
import { ui } from "./ui.js";
|
|
7
|
+
import { getGroupedCommands } from "../command-registry.js";
|
|
7
8
|
const noColor = !!(process.env.NO_COLOR || process.env.TERM === "dumb");
|
|
8
9
|
function amberShade(hex, text) {
|
|
9
10
|
if (noColor)
|
|
@@ -32,30 +33,12 @@ export function renderBanner() {
|
|
|
32
33
|
}
|
|
33
34
|
return BANNER_LINES.map((line, i) => ` ${amberShade(AMBER_HEXES[i], line)}`).join("\n");
|
|
34
35
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
],
|
|
40
|
-
|
|
41
|
-
{ cmd: "list", desc: "List available skills" },
|
|
42
|
-
{ cmd: "search <query>", desc: "Search across providers" },
|
|
43
|
-
{ cmd: "info <skill>", desc: "Show skill details" },
|
|
44
|
-
{ cmd: "install [skills...]", desc: "Install one or more skills" },
|
|
45
|
-
{ cmd: "update [skills...]", desc: "Update installed skills" },
|
|
46
|
-
{ cmd: "uninstall [skills...]", desc: "Remove one or more skills" },
|
|
47
|
-
],
|
|
48
|
-
DEVELOPMENT: [
|
|
49
|
-
{ cmd: "create <name>", desc: "Create a new skill from template" },
|
|
50
|
-
{ cmd: "validate [skill]", desc: "Validate skill structure" },
|
|
51
|
-
{ cmd: "audit [skill]", desc: "Audit skill quality" },
|
|
52
|
-
],
|
|
53
|
-
CONFIGURATION: [
|
|
54
|
-
{ cmd: "config [key] [val]", desc: "View or modify configuration" },
|
|
55
|
-
{ cmd: "providers", desc: "Manage skill providers" },
|
|
56
|
-
{ cmd: "clean", desc: "Remove orphaned data" },
|
|
57
|
-
{ cmd: "stats", desc: "Show session analytics" },
|
|
58
|
-
],
|
|
36
|
+
// Help groups: subset of registry for --help display (keeps output scannable)
|
|
37
|
+
const HELP_GROUPS = {
|
|
38
|
+
"GETTING STARTED": ["init", "doctor"],
|
|
39
|
+
SKILLS: ["list", "search", "info", "install", "update", "uninstall", "recommend"],
|
|
40
|
+
DEVELOPMENT: ["create", "validate", "audit"],
|
|
41
|
+
CONFIGURATION: ["config", "providers", "clean", "stats"],
|
|
59
42
|
};
|
|
60
43
|
const EXAMPLES = [
|
|
61
44
|
"$ arcana install code-reviewer typescript golang",
|
|
@@ -74,11 +57,16 @@ export function buildCustomHelp(version) {
|
|
|
74
57
|
lines.push("");
|
|
75
58
|
lines.push(` ${ui.dim("USAGE")}`);
|
|
76
59
|
lines.push(" arcana <command> [options]");
|
|
77
|
-
|
|
60
|
+
const allCommands = getGroupedCommands();
|
|
61
|
+
const allFlat = Object.values(allCommands).flat();
|
|
62
|
+
for (const [group, names] of Object.entries(HELP_GROUPS)) {
|
|
78
63
|
lines.push("");
|
|
79
64
|
lines.push(` ${ui.dim(group)}`);
|
|
80
|
-
for (const
|
|
81
|
-
|
|
65
|
+
for (const name of names) {
|
|
66
|
+
const entry = allFlat.find((c) => c.name === name);
|
|
67
|
+
if (!entry)
|
|
68
|
+
continue;
|
|
69
|
+
lines.push(` ${ui.cyan(padRight(entry.usage, 22))}${ui.dim(entry.description)}`);
|
|
82
70
|
}
|
|
83
71
|
}
|
|
84
72
|
lines.push("");
|
|
@@ -114,4 +102,3 @@ export function showWelcome(version) {
|
|
|
114
102
|
p.log.info("They install on-demand and only load when relevant, not all at once.");
|
|
115
103
|
console.log();
|
|
116
104
|
}
|
|
117
|
-
//# sourceMappingURL=help.js.map
|
package/dist/utils/history.d.ts
CHANGED
|
@@ -7,4 +7,3 @@ export declare function readHistory(): HistoryEntry[];
|
|
|
7
7
|
export declare function appendHistory(action: string, target?: string): void;
|
|
8
8
|
export declare function clearHistory(): void;
|
|
9
9
|
export declare function getRecentSkills(limit?: number): string[];
|
|
10
|
-
//# sourceMappingURL=history.d.ts.map
|
package/dist/utils/history.js
CHANGED
package/dist/utils/http.d.ts
CHANGED
package/dist/utils/http.js
CHANGED
|
@@ -102,11 +102,15 @@ function doGet(url, timeout, redirectCount = 0) {
|
|
|
102
102
|
if (token) {
|
|
103
103
|
try {
|
|
104
104
|
const hostname = new URL(url).hostname;
|
|
105
|
-
if (hostname === "github.com" ||
|
|
105
|
+
if (hostname === "github.com" ||
|
|
106
|
+
hostname.endsWith(".github.com") ||
|
|
107
|
+
hostname.endsWith(".githubusercontent.com")) {
|
|
106
108
|
headers["Authorization"] = `token ${token}`;
|
|
107
109
|
}
|
|
108
110
|
}
|
|
109
|
-
catch {
|
|
111
|
+
catch {
|
|
112
|
+
/* invalid URL, skip auth */
|
|
113
|
+
}
|
|
110
114
|
}
|
|
111
115
|
const req = https.get(url, { headers, timeout, agent }, (res) => {
|
|
112
116
|
// Follow redirects (HTTPS only)
|
|
@@ -123,8 +127,14 @@ function doGet(url, timeout, redirectCount = 0) {
|
|
|
123
127
|
// After the existing https check, add:
|
|
124
128
|
try {
|
|
125
129
|
const redirectUrl = new URL(location);
|
|
126
|
-
const allowedHosts = [
|
|
127
|
-
|
|
130
|
+
const allowedHosts = [
|
|
131
|
+
"github.com",
|
|
132
|
+
"raw.githubusercontent.com",
|
|
133
|
+
"api.github.com",
|
|
134
|
+
"objects.githubusercontent.com",
|
|
135
|
+
"registry.npmjs.org",
|
|
136
|
+
];
|
|
137
|
+
if (!allowedHosts.some((h) => redirectUrl.hostname === h || redirectUrl.hostname.endsWith("." + h))) {
|
|
128
138
|
reject(new Error(`Redirect to untrusted host blocked: ${redirectUrl.hostname}`));
|
|
129
139
|
return;
|
|
130
140
|
}
|
|
@@ -162,4 +172,3 @@ function doGet(url, timeout, redirectCount = 0) {
|
|
|
162
172
|
});
|
|
163
173
|
});
|
|
164
174
|
}
|
|
165
|
-
//# sourceMappingURL=http.js.map
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Provider } from "../providers/base.js";
|
|
2
|
+
import type { SkillFile, SkillInfo } from "../types.js";
|
|
3
|
+
export interface InstallOneResult {
|
|
4
|
+
success: boolean;
|
|
5
|
+
skillName: string;
|
|
6
|
+
files?: SkillFile[];
|
|
7
|
+
sizeKB?: number;
|
|
8
|
+
error?: string;
|
|
9
|
+
scanBlocked?: boolean;
|
|
10
|
+
conflictBlocked?: boolean;
|
|
11
|
+
conflictWarnings?: string[];
|
|
12
|
+
alreadyInstalled?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface InstallBatchResult {
|
|
15
|
+
installed: string[];
|
|
16
|
+
skipped: string[];
|
|
17
|
+
failed: string[];
|
|
18
|
+
failedErrors: Record<string, string>;
|
|
19
|
+
}
|
|
20
|
+
/** Scan fetched files for security threats. Returns true if install should proceed. */
|
|
21
|
+
export declare function preInstallScan(_skillName: string, files: SkillFile[], force?: boolean): {
|
|
22
|
+
proceed: boolean;
|
|
23
|
+
critical: string[];
|
|
24
|
+
high: string[];
|
|
25
|
+
};
|
|
26
|
+
/** Check for conflicts with existing project context. Returns warnings/blocks. */
|
|
27
|
+
export declare function preInstallConflictCheck(skillName: string, remote: SkillInfo | null, files: SkillFile[], force?: boolean): {
|
|
28
|
+
proceed: boolean;
|
|
29
|
+
blocks: string[];
|
|
30
|
+
warnings: string[];
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Core install logic for a single skill. Handles:
|
|
34
|
+
* fetch -> security scan -> conflict check -> write files -> write meta -> update lock
|
|
35
|
+
*/
|
|
36
|
+
export declare function installOneCore(skillName: string, provider: Provider, opts: {
|
|
37
|
+
force?: boolean;
|
|
38
|
+
noCheck?: boolean;
|
|
39
|
+
}): Promise<InstallOneResult>;
|
|
40
|
+
/** Compute size warning message if skill exceeds threshold. */
|
|
41
|
+
export declare function sizeWarning(sizeKB: number): string | null;
|
|
42
|
+
/** Check if a skill can be installed (not already present or force mode). */
|
|
43
|
+
export declare function canInstall(skillName: string, force?: boolean): {
|
|
44
|
+
proceed: boolean;
|
|
45
|
+
reason?: string;
|
|
46
|
+
};
|
|
47
|
+
/** Read existing meta to detect provider change on reinstall. */
|
|
48
|
+
export declare function detectProviderChange(skillName: string, newProvider: string): string | null;
|