@sporesec/arcana 2.2.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 +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +219 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/audit.d.ts +17 -0
- package/dist/commands/audit.d.ts.map +1 -0
- package/dist/commands/audit.js +157 -0
- package/dist/commands/audit.js.map +1 -0
- package/dist/commands/audit.test.d.ts +2 -0
- package/dist/commands/audit.test.d.ts.map +1 -0
- package/dist/commands/audit.test.js +217 -0
- package/dist/commands/audit.test.js.map +1 -0
- package/dist/commands/clean.d.ts +5 -0
- package/dist/commands/clean.d.ts.map +1 -0
- package/dist/commands/clean.js +125 -0
- package/dist/commands/clean.js.map +1 -0
- package/dist/commands/config.d.ts +4 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +135 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/create.d.ts +2 -0
- package/dist/commands/create.d.ts.map +1 -0
- package/dist/commands/create.js +100 -0
- package/dist/commands/create.js.map +1 -0
- package/dist/commands/doctor.d.ts +6 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +213 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/info.d.ts +5 -0
- package/dist/commands/info.d.ts.map +1 -0
- package/dist/commands/info.js +114 -0
- package/dist/commands/info.js.map +1 -0
- package/dist/commands/init.d.ts +13 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +216 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/install.d.ts +8 -0
- package/dist/commands/install.d.ts.map +1 -0
- package/dist/commands/install.js +404 -0
- package/dist/commands/install.js.map +1 -0
- package/dist/commands/list.d.ts +8 -0
- package/dist/commands/list.d.ts.map +1 -0
- package/dist/commands/list.js +100 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/providers.d.ts +6 -0
- package/dist/commands/providers.d.ts.map +1 -0
- package/dist/commands/providers.js +103 -0
- package/dist/commands/providers.js.map +1 -0
- package/dist/commands/scan.d.ts +5 -0
- package/dist/commands/scan.d.ts.map +1 -0
- package/dist/commands/scan.js +110 -0
- package/dist/commands/scan.js.map +1 -0
- package/dist/commands/search.d.ts +6 -0
- package/dist/commands/search.d.ts.map +1 -0
- package/dist/commands/search.js +58 -0
- package/dist/commands/search.js.map +1 -0
- package/dist/commands/stats.d.ts +4 -0
- package/dist/commands/stats.d.ts.map +1 -0
- package/dist/commands/stats.js +143 -0
- package/dist/commands/stats.js.map +1 -0
- package/dist/commands/uninstall.d.ts +6 -0
- package/dist/commands/uninstall.d.ts.map +1 -0
- package/dist/commands/uninstall.js +163 -0
- package/dist/commands/uninstall.js.map +1 -0
- package/dist/commands/update.d.ts +7 -0
- package/dist/commands/update.d.ts.map +1 -0
- package/dist/commands/update.js +348 -0
- package/dist/commands/update.js.map +1 -0
- package/dist/commands/validate.d.ts +6 -0
- package/dist/commands/validate.d.ts.map +1 -0
- package/dist/commands/validate.js +140 -0
- package/dist/commands/validate.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +39 -0
- package/dist/index.js.map +1 -0
- package/dist/interactive.d.ts +2 -0
- package/dist/interactive.d.ts.map +1 -0
- package/dist/interactive.js +812 -0
- package/dist/interactive.js.map +1 -0
- package/dist/providers/arcana.d.ts +5 -0
- package/dist/providers/arcana.d.ts.map +1 -0
- package/dist/providers/arcana.js +11 -0
- package/dist/providers/arcana.js.map +1 -0
- package/dist/providers/base.d.ts +11 -0
- package/dist/providers/base.d.ts.map +1 -0
- package/dist/providers/base.js +10 -0
- package/dist/providers/base.js.map +1 -0
- package/dist/providers/github.d.ts +25 -0
- package/dist/providers/github.d.ts.map +1 -0
- package/dist/providers/github.js +146 -0
- package/dist/providers/github.js.map +1 -0
- package/dist/registry.d.ts +9 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +71 -0
- package/dist/registry.js.map +1 -0
- package/dist/types.d.ts +67 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/atomic.d.ts +2 -0
- package/dist/utils/atomic.d.ts.map +1 -0
- package/dist/utils/atomic.js +20 -0
- package/dist/utils/atomic.js.map +1 -0
- package/dist/utils/atomic.test.d.ts +2 -0
- package/dist/utils/atomic.test.d.ts.map +1 -0
- package/dist/utils/atomic.test.js +31 -0
- package/dist/utils/atomic.test.js.map +1 -0
- package/dist/utils/cache.d.ts +4 -0
- package/dist/utils/cache.d.ts.map +1 -0
- package/dist/utils/cache.js +47 -0
- package/dist/utils/cache.js.map +1 -0
- package/dist/utils/config.d.ts +6 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +90 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/config.test.d.ts +2 -0
- package/dist/utils/config.test.d.ts.map +1 -0
- package/dist/utils/config.test.js +38 -0
- package/dist/utils/config.test.js.map +1 -0
- package/dist/utils/errors.d.ts +6 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +11 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/frontmatter.d.ts +12 -0
- package/dist/utils/frontmatter.d.ts.map +1 -0
- package/dist/utils/frontmatter.js +172 -0
- package/dist/utils/frontmatter.js.map +1 -0
- package/dist/utils/frontmatter.test.d.ts +2 -0
- package/dist/utils/frontmatter.test.d.ts.map +1 -0
- package/dist/utils/frontmatter.test.js +152 -0
- package/dist/utils/frontmatter.test.js.map +1 -0
- package/dist/utils/fs.d.ts +16 -0
- package/dist/utils/fs.d.ts.map +1 -0
- package/dist/utils/fs.js +118 -0
- package/dist/utils/fs.js.map +1 -0
- package/dist/utils/fs.test.d.ts +2 -0
- package/dist/utils/fs.test.d.ts.map +1 -0
- package/dist/utils/fs.test.js +145 -0
- package/dist/utils/fs.test.js.map +1 -0
- package/dist/utils/help.d.ts +6 -0
- package/dist/utils/help.d.ts.map +1 -0
- package/dist/utils/help.js +117 -0
- package/dist/utils/help.js.map +1 -0
- package/dist/utils/help.test.d.ts +2 -0
- package/dist/utils/help.test.d.ts.map +1 -0
- package/dist/utils/help.test.js +66 -0
- package/dist/utils/help.test.js.map +1 -0
- package/dist/utils/history.d.ts +10 -0
- package/dist/utils/history.d.ts.map +1 -0
- package/dist/utils/history.js +58 -0
- package/dist/utils/history.js.map +1 -0
- package/dist/utils/http.d.ts +17 -0
- package/dist/utils/http.d.ts.map +1 -0
- package/dist/utils/http.js +165 -0
- package/dist/utils/http.js.map +1 -0
- package/dist/utils/http.test.d.ts +2 -0
- package/dist/utils/http.test.d.ts.map +1 -0
- package/dist/utils/http.test.js +55 -0
- package/dist/utils/http.test.js.map +1 -0
- package/dist/utils/parallel.d.ts +2 -0
- package/dist/utils/parallel.d.ts.map +1 -0
- package/dist/utils/parallel.js +17 -0
- package/dist/utils/parallel.js.map +1 -0
- package/dist/utils/scanner.d.ts +27 -0
- package/dist/utils/scanner.d.ts.map +1 -0
- package/dist/utils/scanner.js +195 -0
- package/dist/utils/scanner.js.map +1 -0
- package/dist/utils/ui.d.ts +27 -0
- package/dist/utils/ui.d.ts.map +1 -0
- package/dist/utils/ui.js +99 -0
- package/dist/utils/ui.js.map +1 -0
- package/dist/utils/ui.test.d.ts +2 -0
- package/dist/utils/ui.test.d.ts.map +1 -0
- package/dist/utils/ui.test.js +31 -0
- package/dist/utils/ui.test.js.map +1 -0
- package/dist/utils/validate.d.ts +2 -0
- package/dist/utils/validate.d.ts.map +1 -0
- package/dist/utils/validate.js +7 -0
- package/dist/utils/validate.js.map +1 -0
- package/package.json +62 -0
|
@@ -0,0 +1,812 @@
|
|
|
1
|
+
import { existsSync, readdirSync, statSync, rmSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import * as p from "@clack/prompts";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import semver from "semver";
|
|
6
|
+
import { renderBanner } from "./utils/help.js";
|
|
7
|
+
import { ui } from "./utils/ui.js";
|
|
8
|
+
import { getProvider, getProviders } from "./registry.js";
|
|
9
|
+
import { isSkillInstalled, readSkillMeta, installSkill, writeSkillMeta, getInstallDir, getSkillDir, } from "./utils/fs.js";
|
|
10
|
+
import { loadConfig } from "./utils/config.js";
|
|
11
|
+
import { runDoctorChecks } from "./commands/doctor.js";
|
|
12
|
+
import { removeSymlinksFor } from "./commands/uninstall.js";
|
|
13
|
+
import { appendHistory } from "./utils/history.js";
|
|
14
|
+
import { clearProviderCache } from "./registry.js";
|
|
15
|
+
const AMBER = chalk.hex("#d4943a");
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Category map (7 categories, 4-12 skills each, no orphans)
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
const SKILL_CATEGORIES = {
|
|
20
|
+
"Code Quality & Review": [
|
|
21
|
+
"code-reviewer", "codebase-dissection", "testing-strategy",
|
|
22
|
+
"refactoring-patterns", "git-workflow", "pre-production-review",
|
|
23
|
+
"frontend-code-review", "dependency-audit", "performance-optimization",
|
|
24
|
+
],
|
|
25
|
+
"Security & Infrastructure": [
|
|
26
|
+
"security-review", "local-security", "container-security",
|
|
27
|
+
"docker-kubernetes", "ci-cd-pipelines", "ci-cd-automation",
|
|
28
|
+
"monitoring-observability", "incident-response",
|
|
29
|
+
],
|
|
30
|
+
"Languages & Frameworks": [
|
|
31
|
+
"golang-pro", "go-linter-configuration", "typescript", "typescript-advanced",
|
|
32
|
+
"python-best-practices", "rust-best-practices", "frontend-design",
|
|
33
|
+
"fullstack-developer", "remotion-best-practices", "npm-package",
|
|
34
|
+
],
|
|
35
|
+
"API, Data & Docs": [
|
|
36
|
+
"api-design", "api-testing", "programming-architecture",
|
|
37
|
+
"database-design", "env-config", "cost-optimization",
|
|
38
|
+
"docx", "xlsx", "doc-generation", "update-docs",
|
|
39
|
+
],
|
|
40
|
+
"Game Design & Production": [
|
|
41
|
+
"game-design-theory", "game-engines", "game-programming-languages",
|
|
42
|
+
"gameplay-mechanics", "level-design", "game-tools-workflows",
|
|
43
|
+
"game-servers", "networking-servers", "synchronization-algorithms",
|
|
44
|
+
"monetization-systems", "publishing-platforms", "daw-music",
|
|
45
|
+
],
|
|
46
|
+
"Graphics, Audio & Performance": [
|
|
47
|
+
"graphics-rendering", "shader-techniques", "particle-systems",
|
|
48
|
+
"audio-systems", "asset-optimization", "optimization-performance",
|
|
49
|
+
"memory-management",
|
|
50
|
+
],
|
|
51
|
+
"Skill Development": [
|
|
52
|
+
"skill-creation-guide", "skill-creator", "find-skills", "project-migration",
|
|
53
|
+
],
|
|
54
|
+
};
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Helpers
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
function cancelAndExit() {
|
|
59
|
+
clearProviderCache();
|
|
60
|
+
p.cancel("Goodbye.");
|
|
61
|
+
process.exit(0);
|
|
62
|
+
}
|
|
63
|
+
function handleCancel(value) {
|
|
64
|
+
if (p.isCancel(value))
|
|
65
|
+
cancelAndExit();
|
|
66
|
+
}
|
|
67
|
+
function countInstalled() {
|
|
68
|
+
const dir = getInstallDir();
|
|
69
|
+
if (!existsSync(dir))
|
|
70
|
+
return 0;
|
|
71
|
+
return readdirSync(dir).filter(d => {
|
|
72
|
+
try {
|
|
73
|
+
return statSync(join(dir, d)).isDirectory();
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}).length;
|
|
79
|
+
}
|
|
80
|
+
function truncate(str, max) {
|
|
81
|
+
return str.length > max ? str.slice(0, max) + "..." : str;
|
|
82
|
+
}
|
|
83
|
+
function getCategoryFor(skillName) {
|
|
84
|
+
for (const [cat, skills] of Object.entries(SKILL_CATEGORIES)) {
|
|
85
|
+
if (skills.includes(skillName))
|
|
86
|
+
return cat;
|
|
87
|
+
}
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
function getRelatedSkills(skillName, limit = 3) {
|
|
91
|
+
const cat = getCategoryFor(skillName);
|
|
92
|
+
if (!cat)
|
|
93
|
+
return [];
|
|
94
|
+
return (SKILL_CATEGORIES[cat] ?? [])
|
|
95
|
+
.filter(s => s !== skillName && !isSkillInstalled(s))
|
|
96
|
+
.slice(0, limit);
|
|
97
|
+
}
|
|
98
|
+
function getInstalledNames() {
|
|
99
|
+
const dir = getInstallDir();
|
|
100
|
+
if (!existsSync(dir))
|
|
101
|
+
return [];
|
|
102
|
+
return readdirSync(dir)
|
|
103
|
+
.filter(d => {
|
|
104
|
+
try {
|
|
105
|
+
return statSync(join(dir, d)).isDirectory();
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
.sort();
|
|
112
|
+
}
|
|
113
|
+
function buildMenuOptions(installedCount, availableCount) {
|
|
114
|
+
const isNew = installedCount === 0;
|
|
115
|
+
const options = [];
|
|
116
|
+
if (isNew) {
|
|
117
|
+
options.push({ value: "setup", label: AMBER("Get Started"), hint: "detect project, install recommended skills" });
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
options.push({ value: "installed", label: "Manage installed skills", hint: `${installedCount} installed` });
|
|
121
|
+
}
|
|
122
|
+
options.push({ value: "browse", label: "Browse skills by category" });
|
|
123
|
+
options.push({ value: "search", label: "Search for a skill" });
|
|
124
|
+
if (!isNew) {
|
|
125
|
+
options.push({ value: "setup", label: "Get Started", hint: "detect project, add more skills" });
|
|
126
|
+
}
|
|
127
|
+
options.push({ value: "health", label: "Check environment health" });
|
|
128
|
+
options.push({ value: "ref", label: "CLI reference" });
|
|
129
|
+
options.push({ value: "exit", label: "Exit" });
|
|
130
|
+
return options;
|
|
131
|
+
}
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
// Atomic operations
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
async function doInstall(skillName, providerName) {
|
|
136
|
+
const provider = getProvider(providerName);
|
|
137
|
+
const s = p.spinner();
|
|
138
|
+
s.start(`Installing ${chalk.bold(skillName)}...`);
|
|
139
|
+
try {
|
|
140
|
+
const files = await provider.fetch(skillName);
|
|
141
|
+
installSkill(skillName, files);
|
|
142
|
+
const remote = await provider.info(skillName);
|
|
143
|
+
writeSkillMeta(skillName, {
|
|
144
|
+
version: remote?.version ?? "0.0.0",
|
|
145
|
+
installedAt: new Date().toISOString(),
|
|
146
|
+
source: providerName,
|
|
147
|
+
description: remote?.description,
|
|
148
|
+
fileCount: files.length,
|
|
149
|
+
sizeBytes: files.reduce((s2, f) => s2 + f.content.length, 0),
|
|
150
|
+
});
|
|
151
|
+
s.stop(`Installed ${chalk.bold(skillName)} (${files.length} files)`);
|
|
152
|
+
appendHistory("install", skillName);
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
s.stop(`Failed to install ${skillName}`);
|
|
157
|
+
if (err instanceof Error)
|
|
158
|
+
p.log.error(ui.dim(err.message));
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
async function doBatchInstall(names, providerName) {
|
|
163
|
+
if (names.length === 0)
|
|
164
|
+
return 0;
|
|
165
|
+
const provider = getProvider(providerName);
|
|
166
|
+
const s = p.spinner();
|
|
167
|
+
s.start(`Installing ${names.length} skill${names.length > 1 ? "s" : ""}...`);
|
|
168
|
+
let installed = 0;
|
|
169
|
+
for (const name of names) {
|
|
170
|
+
try {
|
|
171
|
+
s.message(`Installing ${chalk.bold(name)} (${installed + 1}/${names.length})...`);
|
|
172
|
+
const files = await provider.fetch(name);
|
|
173
|
+
installSkill(name, files);
|
|
174
|
+
const remote = await provider.info(name);
|
|
175
|
+
writeSkillMeta(name, {
|
|
176
|
+
version: remote?.version ?? "0.0.0",
|
|
177
|
+
installedAt: new Date().toISOString(),
|
|
178
|
+
source: providerName,
|
|
179
|
+
description: remote?.description,
|
|
180
|
+
fileCount: files.length,
|
|
181
|
+
sizeBytes: files.reduce((s2, f) => s2 + f.content.length, 0),
|
|
182
|
+
});
|
|
183
|
+
installed++;
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
s.stop(`Failed: ${name}`);
|
|
187
|
+
if (err instanceof Error)
|
|
188
|
+
p.log.error(ui.dim(err.message));
|
|
189
|
+
if (installed + 1 < names.length)
|
|
190
|
+
s.start(`Installing next...`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
s.stop(`Installed ${installed} skill${installed !== 1 ? "s" : ""}`);
|
|
194
|
+
return installed;
|
|
195
|
+
}
|
|
196
|
+
function doUninstall(skillName) {
|
|
197
|
+
const skillDir = getSkillDir(skillName);
|
|
198
|
+
if (!existsSync(skillDir))
|
|
199
|
+
return false;
|
|
200
|
+
try {
|
|
201
|
+
rmSync(skillDir, { recursive: true, force: true });
|
|
202
|
+
removeSymlinksFor(skillName);
|
|
203
|
+
appendHistory("uninstall", skillName);
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
// Skill Detail View (central action point)
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
async function skillDetailFlow(skillName, allSkills, providerName) {
|
|
214
|
+
const info = allSkills.find(s => s.name === skillName);
|
|
215
|
+
const installed = isSkillInstalled(skillName);
|
|
216
|
+
const meta = installed ? readSkillMeta(skillName) : null;
|
|
217
|
+
// Build info block
|
|
218
|
+
const lines = [];
|
|
219
|
+
lines.push(`${chalk.bold(skillName)} ${info ? `v${info.version}` : ""}`);
|
|
220
|
+
if (info?.description)
|
|
221
|
+
lines.push(info.description);
|
|
222
|
+
lines.push("");
|
|
223
|
+
const category = getCategoryFor(skillName);
|
|
224
|
+
if (category)
|
|
225
|
+
lines.push(`Category: ${category}`);
|
|
226
|
+
if (info?.source)
|
|
227
|
+
lines.push(`Source: ${info.source}`);
|
|
228
|
+
if (installed && meta) {
|
|
229
|
+
const date = meta.installedAt ? new Date(meta.installedAt).toLocaleDateString() : "";
|
|
230
|
+
lines.push(`Status: ${chalk.green("installed")} (v${meta.version}${date ? `, ${date}` : ""})`);
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
lines.push(`Status: ${chalk.dim("not installed")}`);
|
|
234
|
+
}
|
|
235
|
+
const related = getRelatedSkills(skillName);
|
|
236
|
+
if (related.length > 0) {
|
|
237
|
+
lines.push(`Related: ${related.join(", ")}`);
|
|
238
|
+
}
|
|
239
|
+
p.note(lines.join("\n"), skillName);
|
|
240
|
+
// Action menu
|
|
241
|
+
const actions = [];
|
|
242
|
+
if (installed) {
|
|
243
|
+
actions.push({ value: "reinstall", label: "Reinstall (overwrite)" });
|
|
244
|
+
actions.push({ value: "uninstall", label: "Uninstall (remove files)" });
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
actions.push({ value: "install", label: "Install this skill" });
|
|
248
|
+
}
|
|
249
|
+
actions.push({ value: "back", label: "Back" });
|
|
250
|
+
const action = await p.select({ message: "Action", options: actions });
|
|
251
|
+
handleCancel(action);
|
|
252
|
+
switch (action) {
|
|
253
|
+
case "install":
|
|
254
|
+
case "reinstall": {
|
|
255
|
+
await doInstall(skillName, providerName);
|
|
256
|
+
return "back";
|
|
257
|
+
}
|
|
258
|
+
case "uninstall": {
|
|
259
|
+
const ok = await p.confirm({ message: `Uninstall ${chalk.bold(skillName)}?` });
|
|
260
|
+
handleCancel(ok);
|
|
261
|
+
if (ok) {
|
|
262
|
+
const success = doUninstall(skillName);
|
|
263
|
+
if (success) {
|
|
264
|
+
p.log.success(`Removed ${chalk.bold(skillName)}`);
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
p.log.error(`Failed to remove ${skillName}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return "back";
|
|
271
|
+
}
|
|
272
|
+
default:
|
|
273
|
+
return "back";
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
// ---------------------------------------------------------------------------
|
|
277
|
+
// Browse by Category
|
|
278
|
+
// ---------------------------------------------------------------------------
|
|
279
|
+
async function browseByCategory(allSkills, providerName) {
|
|
280
|
+
const availableNames = new Set(allSkills.map(s => s.name));
|
|
281
|
+
while (true) {
|
|
282
|
+
const categoryOptions = Object.entries(SKILL_CATEGORIES).map(([name, skills]) => {
|
|
283
|
+
const valid = skills.filter(s => availableNames.has(s));
|
|
284
|
+
const installedCount = valid.filter(s => isSkillInstalled(s)).length;
|
|
285
|
+
return {
|
|
286
|
+
value: name,
|
|
287
|
+
label: name,
|
|
288
|
+
hint: `${valid.length} skills, ${installedCount} installed`,
|
|
289
|
+
};
|
|
290
|
+
});
|
|
291
|
+
const category = await p.select({
|
|
292
|
+
message: "Browse by category",
|
|
293
|
+
options: [
|
|
294
|
+
...categoryOptions,
|
|
295
|
+
{ value: "__back", label: "Back" },
|
|
296
|
+
],
|
|
297
|
+
});
|
|
298
|
+
handleCancel(category);
|
|
299
|
+
if (category === "__back")
|
|
300
|
+
return;
|
|
301
|
+
await categorySkillList(category, SKILL_CATEGORIES[category] ?? [], allSkills, providerName);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
async function categorySkillList(categoryName, skillNames, allSkills, providerName) {
|
|
305
|
+
const availableNames = new Set(allSkills.map(s => s.name));
|
|
306
|
+
const validSkills = skillNames.filter(s => availableNames.has(s));
|
|
307
|
+
if (validSkills.length === 0) {
|
|
308
|
+
p.log.warn("No skills found in this category.");
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
while (true) {
|
|
312
|
+
const options = validSkills.map(name => {
|
|
313
|
+
const info = allSkills.find(s => s.name === name);
|
|
314
|
+
const installed = isSkillInstalled(name);
|
|
315
|
+
return {
|
|
316
|
+
value: name,
|
|
317
|
+
label: `${name}${installed ? chalk.green(" \u2713") : ""}`,
|
|
318
|
+
hint: truncate(info?.description ?? "", 50),
|
|
319
|
+
};
|
|
320
|
+
});
|
|
321
|
+
const notInstalled = validSkills.filter(s => !isSkillInstalled(s));
|
|
322
|
+
const extraOptions = [];
|
|
323
|
+
if (notInstalled.length > 0) {
|
|
324
|
+
extraOptions.push({
|
|
325
|
+
value: "__install_all",
|
|
326
|
+
label: `Install all uninstalled`,
|
|
327
|
+
hint: `${notInstalled.length} skill${notInstalled.length > 1 ? "s" : ""}`,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
extraOptions.push({ value: "__back", label: "Back to categories" });
|
|
331
|
+
const picked = await p.select({
|
|
332
|
+
message: `${categoryName} (${validSkills.length} skills)`,
|
|
333
|
+
options: [...options, ...extraOptions],
|
|
334
|
+
});
|
|
335
|
+
handleCancel(picked);
|
|
336
|
+
if (picked === "__back")
|
|
337
|
+
return;
|
|
338
|
+
if (picked === "__install_all") {
|
|
339
|
+
await doBatchInstall(notInstalled, providerName);
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
const result = await skillDetailFlow(picked, allSkills, providerName);
|
|
343
|
+
if (result === "menu")
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
// ---------------------------------------------------------------------------
|
|
348
|
+
// Search
|
|
349
|
+
// ---------------------------------------------------------------------------
|
|
350
|
+
async function searchFlow(allSkills, providerName) {
|
|
351
|
+
while (true) {
|
|
352
|
+
const query = await p.text({
|
|
353
|
+
message: "Search for:",
|
|
354
|
+
placeholder: "e.g. testing, review, golang",
|
|
355
|
+
validate: (v) => (!v || v.trim().length === 0 ? "Enter a search term" : undefined),
|
|
356
|
+
});
|
|
357
|
+
handleCancel(query);
|
|
358
|
+
const provider = getProvider(providerName);
|
|
359
|
+
const s = p.spinner();
|
|
360
|
+
s.start(`Searching "${query}"...`);
|
|
361
|
+
let results;
|
|
362
|
+
try {
|
|
363
|
+
results = await provider.search(query);
|
|
364
|
+
}
|
|
365
|
+
catch (err) {
|
|
366
|
+
s.stop("Search failed");
|
|
367
|
+
if (err instanceof Error)
|
|
368
|
+
p.log.error(ui.dim(err.message));
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
s.stop(`${results.length} result${results.length !== 1 ? "s" : ""}`);
|
|
372
|
+
appendHistory("search", query);
|
|
373
|
+
if (results.length === 0) {
|
|
374
|
+
p.log.info("No skills matched. Try a different query.");
|
|
375
|
+
const again = await p.confirm({ message: "Search again?" });
|
|
376
|
+
handleCancel(again);
|
|
377
|
+
if (!again)
|
|
378
|
+
return;
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
const nav = await searchResultsPicker(results, allSkills, providerName);
|
|
382
|
+
if (nav === "done")
|
|
383
|
+
return;
|
|
384
|
+
// "search" continues the loop
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
async function searchResultsPicker(results, allSkills, providerName) {
|
|
388
|
+
while (true) {
|
|
389
|
+
const options = results.map(skill => ({
|
|
390
|
+
value: skill.name,
|
|
391
|
+
label: `${skill.name}${isSkillInstalled(skill.name) ? chalk.green(" \u2713") : ""}`,
|
|
392
|
+
hint: truncate(skill.description, 50),
|
|
393
|
+
}));
|
|
394
|
+
const picked = await p.select({
|
|
395
|
+
message: "Pick a skill for details",
|
|
396
|
+
options: [
|
|
397
|
+
...options,
|
|
398
|
+
{ value: "__search", label: "Search again" },
|
|
399
|
+
{ value: "__back", label: "Back" },
|
|
400
|
+
],
|
|
401
|
+
});
|
|
402
|
+
handleCancel(picked);
|
|
403
|
+
if (picked === "__search")
|
|
404
|
+
return "search";
|
|
405
|
+
if (picked === "__back")
|
|
406
|
+
return "done";
|
|
407
|
+
const result = await skillDetailFlow(picked, allSkills, providerName);
|
|
408
|
+
if (result === "menu")
|
|
409
|
+
return "done";
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
// ---------------------------------------------------------------------------
|
|
413
|
+
// Quick Setup / Get Started
|
|
414
|
+
// ---------------------------------------------------------------------------
|
|
415
|
+
async function quickSetup(allSkills, providerName) {
|
|
416
|
+
const { detectProject, SKILL_SUGGESTIONS, SKILL_SUGGESTIONS_DEFAULT } = await import("./commands/init.js");
|
|
417
|
+
const proj = detectProject(process.cwd());
|
|
418
|
+
p.log.step(`Detected: ${chalk.cyan(proj.name)} (${proj.type})`);
|
|
419
|
+
const suggestions = SKILL_SUGGESTIONS[proj.type] ?? SKILL_SUGGESTIONS_DEFAULT;
|
|
420
|
+
const availableNames = new Set(allSkills.map(s => s.name));
|
|
421
|
+
const validSuggestions = suggestions.filter((s) => availableNames.has(s));
|
|
422
|
+
if (validSuggestions.length === 0) {
|
|
423
|
+
p.log.info("No specific recommendations for this project type.");
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
const notInstalled = validSuggestions.filter((s) => !isSkillInstalled(s));
|
|
427
|
+
if (notInstalled.length === 0) {
|
|
428
|
+
p.log.success("All recommended skills are already installed.");
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
const options = validSuggestions.map((name) => {
|
|
432
|
+
const installed = isSkillInstalled(name);
|
|
433
|
+
return {
|
|
434
|
+
value: name,
|
|
435
|
+
label: `${name}${installed ? chalk.green(" \u2713 installed") : ""}`,
|
|
436
|
+
hint: installed ? "already installed" : "not installed",
|
|
437
|
+
};
|
|
438
|
+
});
|
|
439
|
+
const selected = await p.multiselect({
|
|
440
|
+
message: `Recommended skills for ${proj.type}`,
|
|
441
|
+
options,
|
|
442
|
+
required: false,
|
|
443
|
+
});
|
|
444
|
+
handleCancel(selected);
|
|
445
|
+
const toInstall = selected.filter((s) => !isSkillInstalled(s));
|
|
446
|
+
if (toInstall.length > 0) {
|
|
447
|
+
await doBatchInstall(toInstall, providerName);
|
|
448
|
+
}
|
|
449
|
+
else if (selected.length > 0) {
|
|
450
|
+
p.log.info("All selected skills are already installed.");
|
|
451
|
+
}
|
|
452
|
+
if (!existsSync(join(process.cwd(), "CLAUDE.md"))) {
|
|
453
|
+
p.log.info(`Tip: Run ${chalk.cyan("arcana init")} to create project config files.`);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
// ---------------------------------------------------------------------------
|
|
457
|
+
// Manage Installed Skills
|
|
458
|
+
// ---------------------------------------------------------------------------
|
|
459
|
+
async function manageInstalled(allSkills, providerName) {
|
|
460
|
+
while (true) {
|
|
461
|
+
const names = getInstalledNames();
|
|
462
|
+
if (names.length === 0) {
|
|
463
|
+
p.log.info("No skills installed.");
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
// Group installed skills by category
|
|
467
|
+
const groups = [];
|
|
468
|
+
const categorized = new Set();
|
|
469
|
+
for (const [cat, catSkills] of Object.entries(SKILL_CATEGORIES)) {
|
|
470
|
+
const installed = catSkills.filter(s => names.includes(s));
|
|
471
|
+
if (installed.length > 0) {
|
|
472
|
+
groups.push({ cat, skills: installed });
|
|
473
|
+
installed.forEach(s => categorized.add(s));
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
const uncategorized = names.filter(s => !categorized.has(s));
|
|
477
|
+
if (uncategorized.length > 0) {
|
|
478
|
+
groups.push({ cat: "Other", skills: uncategorized });
|
|
479
|
+
}
|
|
480
|
+
const options = groups.map(g => ({
|
|
481
|
+
value: g.cat,
|
|
482
|
+
label: g.cat,
|
|
483
|
+
hint: `${g.skills.length} installed`,
|
|
484
|
+
}));
|
|
485
|
+
const picked = await p.select({
|
|
486
|
+
message: `Installed skills (${names.length})`,
|
|
487
|
+
options: [
|
|
488
|
+
...options,
|
|
489
|
+
{ value: "__update", label: chalk.cyan("Check for updates") },
|
|
490
|
+
{ value: "__bulk_uninstall", label: "Uninstall multiple..." },
|
|
491
|
+
{ value: "__back", label: "Back" },
|
|
492
|
+
],
|
|
493
|
+
});
|
|
494
|
+
handleCancel(picked);
|
|
495
|
+
if (picked === "__back")
|
|
496
|
+
return;
|
|
497
|
+
if (picked === "__update") {
|
|
498
|
+
await updateAll(providerName);
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
if (picked === "__bulk_uninstall") {
|
|
502
|
+
await bulkUninstall(names);
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
const group = groups.find(g => g.cat === picked);
|
|
506
|
+
if (group) {
|
|
507
|
+
await installedCategoryList(group.cat, group.skills, allSkills, providerName);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
async function installedCategoryList(categoryName, installedNames, allSkills, providerName) {
|
|
512
|
+
while (true) {
|
|
513
|
+
const stillInstalled = installedNames.filter(s => isSkillInstalled(s));
|
|
514
|
+
if (stillInstalled.length === 0) {
|
|
515
|
+
p.log.info("No skills remaining in this category.");
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
const options = stillInstalled.map(name => {
|
|
519
|
+
const meta = readSkillMeta(name);
|
|
520
|
+
const ver = meta ? `v${meta.version}` : "";
|
|
521
|
+
const date = meta?.installedAt ? new Date(meta.installedAt).toLocaleDateString() : "";
|
|
522
|
+
return {
|
|
523
|
+
value: name,
|
|
524
|
+
label: name,
|
|
525
|
+
hint: `${ver}${date ? ` ${date}` : ""}`,
|
|
526
|
+
};
|
|
527
|
+
});
|
|
528
|
+
const picked = await p.select({
|
|
529
|
+
message: `${categoryName} (${stillInstalled.length} installed)`,
|
|
530
|
+
options: [...options, { value: "__back", label: "Back" }],
|
|
531
|
+
});
|
|
532
|
+
handleCancel(picked);
|
|
533
|
+
if (picked === "__back")
|
|
534
|
+
return;
|
|
535
|
+
const result = await skillDetailFlow(picked, allSkills, providerName);
|
|
536
|
+
if (result === "menu")
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
async function bulkUninstall(installedNames) {
|
|
541
|
+
const selected = await p.multiselect({
|
|
542
|
+
message: "Select skills to uninstall",
|
|
543
|
+
options: installedNames.map(name => ({ value: name, label: name })),
|
|
544
|
+
required: false,
|
|
545
|
+
maxItems: 15,
|
|
546
|
+
});
|
|
547
|
+
handleCancel(selected);
|
|
548
|
+
const names = selected;
|
|
549
|
+
if (names.length === 0)
|
|
550
|
+
return;
|
|
551
|
+
const ok = await p.confirm({
|
|
552
|
+
message: `Uninstall ${names.length} skill${names.length > 1 ? "s" : ""}?`,
|
|
553
|
+
});
|
|
554
|
+
handleCancel(ok);
|
|
555
|
+
if (!ok)
|
|
556
|
+
return;
|
|
557
|
+
let removed = 0;
|
|
558
|
+
for (const name of names) {
|
|
559
|
+
if (doUninstall(name))
|
|
560
|
+
removed++;
|
|
561
|
+
}
|
|
562
|
+
p.log.success(`Removed ${removed} skill${removed !== 1 ? "s" : ""}`);
|
|
563
|
+
}
|
|
564
|
+
// ---------------------------------------------------------------------------
|
|
565
|
+
// Update All (called from Manage Installed)
|
|
566
|
+
// ---------------------------------------------------------------------------
|
|
567
|
+
async function updateAll(providerName) {
|
|
568
|
+
const installed = getInstalledNames();
|
|
569
|
+
if (installed.length === 0) {
|
|
570
|
+
p.log.info("No skills installed.");
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
const s = p.spinner();
|
|
574
|
+
s.start(`Checking ${installed.length} skill${installed.length !== 1 ? "s" : ""} for updates...`);
|
|
575
|
+
const provider = getProvider(providerName);
|
|
576
|
+
let remoteSkills;
|
|
577
|
+
try {
|
|
578
|
+
remoteSkills = await provider.list();
|
|
579
|
+
}
|
|
580
|
+
catch (err) {
|
|
581
|
+
s.stop("Failed to fetch remote skill list");
|
|
582
|
+
if (err instanceof Error)
|
|
583
|
+
p.log.error(ui.dim(err.message));
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
const remoteMap = new Map(remoteSkills.map(rs => [rs.name, rs]));
|
|
587
|
+
const updates = [];
|
|
588
|
+
for (const name of installed) {
|
|
589
|
+
const remote = remoteMap.get(name);
|
|
590
|
+
if (!remote)
|
|
591
|
+
continue;
|
|
592
|
+
const meta = readSkillMeta(name);
|
|
593
|
+
const localVer = semver.valid(semver.coerce(meta?.version)) ?? "0.0.0";
|
|
594
|
+
const remoteVer = semver.valid(semver.coerce(remote.version)) ?? "0.0.0";
|
|
595
|
+
if (semver.gt(remoteVer, localVer)) {
|
|
596
|
+
updates.push({ name, from: meta?.version ?? "0.0.0", to: remote.version });
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
s.stop(`Checked ${installed.length} skills`);
|
|
600
|
+
if (updates.length === 0) {
|
|
601
|
+
p.log.success("All skills are up to date.");
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
p.log.step(`${updates.length} update${updates.length !== 1 ? "s" : ""} available:`);
|
|
605
|
+
for (const u of updates) {
|
|
606
|
+
p.log.info(` ${chalk.bold(u.name)}: v${u.from} -> v${u.to}`);
|
|
607
|
+
}
|
|
608
|
+
const ok = await p.confirm({ message: "Apply updates?" });
|
|
609
|
+
handleCancel(ok);
|
|
610
|
+
if (!ok)
|
|
611
|
+
return;
|
|
612
|
+
const spin = p.spinner();
|
|
613
|
+
spin.start("Updating...");
|
|
614
|
+
let updated = 0;
|
|
615
|
+
for (const u of updates) {
|
|
616
|
+
try {
|
|
617
|
+
spin.message(`Updating ${chalk.bold(u.name)}...`);
|
|
618
|
+
const files = await provider.fetch(u.name);
|
|
619
|
+
installSkill(u.name, files);
|
|
620
|
+
const remote = remoteMap.get(u.name);
|
|
621
|
+
writeSkillMeta(u.name, {
|
|
622
|
+
version: remote.version,
|
|
623
|
+
installedAt: new Date().toISOString(),
|
|
624
|
+
source: providerName,
|
|
625
|
+
description: remote.description,
|
|
626
|
+
fileCount: files.length,
|
|
627
|
+
sizeBytes: files.reduce((s2, f) => s2 + f.content.length, 0),
|
|
628
|
+
});
|
|
629
|
+
updated++;
|
|
630
|
+
}
|
|
631
|
+
catch (err) {
|
|
632
|
+
if (err instanceof Error)
|
|
633
|
+
p.log.error(`Failed to update ${u.name}: ${err.message}`);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
spin.stop(`Updated ${updated} skill${updated !== 1 ? "s" : ""}`);
|
|
637
|
+
}
|
|
638
|
+
// ---------------------------------------------------------------------------
|
|
639
|
+
// Check Environment Health
|
|
640
|
+
// ---------------------------------------------------------------------------
|
|
641
|
+
async function checkHealth() {
|
|
642
|
+
const checks = runDoctorChecks();
|
|
643
|
+
p.log.step(chalk.bold("Environment Health Check"));
|
|
644
|
+
for (const check of checks) {
|
|
645
|
+
const icon = check.status === "pass" ? chalk.green("OK")
|
|
646
|
+
: check.status === "warn" ? chalk.yellow("!!")
|
|
647
|
+
: chalk.red("XX");
|
|
648
|
+
p.log.info(`${icon} ${chalk.bold(check.name)}: ${check.message}`);
|
|
649
|
+
if (check.fix)
|
|
650
|
+
p.log.info(chalk.dim(` Fix: ${check.fix}`));
|
|
651
|
+
}
|
|
652
|
+
const fails = checks.filter(c => c.status === "fail").length;
|
|
653
|
+
const warns = checks.filter(c => c.status === "warn").length;
|
|
654
|
+
if (fails > 0) {
|
|
655
|
+
p.log.error(`${fails} issue${fails > 1 ? "s" : ""} found`);
|
|
656
|
+
}
|
|
657
|
+
else if (warns > 0) {
|
|
658
|
+
p.log.warn(`${warns} warning${warns > 1 ? "s" : ""}`);
|
|
659
|
+
}
|
|
660
|
+
else {
|
|
661
|
+
p.log.success("All checks passed");
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
// Offer fixes once - no loop. User can re-enter health check to verify.
|
|
665
|
+
const fixChecks = checks.filter(c => c.fix && c.status !== "pass");
|
|
666
|
+
if (fixChecks.length === 0)
|
|
667
|
+
return;
|
|
668
|
+
const fixOptions = fixChecks.map(c => {
|
|
669
|
+
const cmd = c.fix.replace(/^Run:\s*/, "");
|
|
670
|
+
return { value: cmd, label: `Run: ${cmd}`, hint: c.name };
|
|
671
|
+
});
|
|
672
|
+
const fixAction = await p.select({
|
|
673
|
+
message: "Run a fix?",
|
|
674
|
+
options: [...fixOptions, { value: "__skip", label: "Skip" }],
|
|
675
|
+
});
|
|
676
|
+
handleCancel(fixAction);
|
|
677
|
+
if (fixAction !== "__skip") {
|
|
678
|
+
const cmd = fixAction;
|
|
679
|
+
p.log.info(chalk.dim(`Running: ${cmd}`));
|
|
680
|
+
try {
|
|
681
|
+
const { execSync } = await import("node:child_process");
|
|
682
|
+
execSync(cmd, { stdio: "inherit" });
|
|
683
|
+
}
|
|
684
|
+
catch {
|
|
685
|
+
// Non-zero exit expected for some commands
|
|
686
|
+
}
|
|
687
|
+
p.log.info(chalk.dim("Run health check again to verify."));
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
// ---------------------------------------------------------------------------
|
|
691
|
+
// Show CLI Reference
|
|
692
|
+
// ---------------------------------------------------------------------------
|
|
693
|
+
function showCliReference() {
|
|
694
|
+
const ref = [
|
|
695
|
+
"arcana list [--installed] [--all]",
|
|
696
|
+
"arcana search <query>",
|
|
697
|
+
"arcana install <skill> [--all] [--force]",
|
|
698
|
+
"arcana uninstall <skill> [--yes]",
|
|
699
|
+
"arcana update [--all] [--dry-run]",
|
|
700
|
+
"arcana info <skill>",
|
|
701
|
+
"arcana init [--tool <name>]",
|
|
702
|
+
"arcana create <name>",
|
|
703
|
+
"arcana validate [--all] [--fix]",
|
|
704
|
+
"arcana scan [skill] [--all] [--json]",
|
|
705
|
+
"arcana audit [--all]",
|
|
706
|
+
"arcana config [key] [value]",
|
|
707
|
+
"arcana providers [--add owner/repo]",
|
|
708
|
+
"arcana doctor",
|
|
709
|
+
"arcana clean [--dry-run]",
|
|
710
|
+
"arcana stats",
|
|
711
|
+
].join("\n");
|
|
712
|
+
p.note(ref, "CLI Reference");
|
|
713
|
+
}
|
|
714
|
+
// ---------------------------------------------------------------------------
|
|
715
|
+
// Main session loop
|
|
716
|
+
// ---------------------------------------------------------------------------
|
|
717
|
+
export async function showInteractiveMenu(version) {
|
|
718
|
+
const config = loadConfig();
|
|
719
|
+
const providerName = config.defaultProvider;
|
|
720
|
+
// Fetch skill list once for the session
|
|
721
|
+
let allSkills = [];
|
|
722
|
+
let availableCount = 0;
|
|
723
|
+
try {
|
|
724
|
+
const providers = getProviders();
|
|
725
|
+
for (const provider of providers) {
|
|
726
|
+
const skills = await provider.list();
|
|
727
|
+
allSkills.push(...skills);
|
|
728
|
+
}
|
|
729
|
+
availableCount = allSkills.length;
|
|
730
|
+
}
|
|
731
|
+
catch {
|
|
732
|
+
// Offline mode
|
|
733
|
+
}
|
|
734
|
+
// Banner (shown once)
|
|
735
|
+
const installedOnEntry = countInstalled();
|
|
736
|
+
console.log();
|
|
737
|
+
console.log(renderBanner());
|
|
738
|
+
console.log();
|
|
739
|
+
console.log(` ${AMBER.bold("arcana")} ${chalk.dim(`v${version}`)}`);
|
|
740
|
+
console.log(` ${chalk.dim("Expert skills for AI coding agents. Install what you need.")}`);
|
|
741
|
+
console.log();
|
|
742
|
+
if (availableCount > 0 && installedOnEntry > 0) {
|
|
743
|
+
if (installedOnEntry > availableCount) {
|
|
744
|
+
// More installed than in marketplace (local test skills, etc.)
|
|
745
|
+
console.log(` ${chalk.dim(`${installedOnEntry} installed (${availableCount} in marketplace) | provider: ${providerName}`)}`);
|
|
746
|
+
}
|
|
747
|
+
else {
|
|
748
|
+
const pct = Math.round((installedOnEntry / availableCount) * 100);
|
|
749
|
+
console.log(` ${chalk.dim(`${installedOnEntry}/${availableCount} installed (${pct}%) | provider: ${providerName}`)}`);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
else if (availableCount > 0) {
|
|
753
|
+
console.log(` ${chalk.dim(`${availableCount} skills across ${Object.keys(SKILL_CATEGORIES).length} categories`)}`);
|
|
754
|
+
}
|
|
755
|
+
else {
|
|
756
|
+
console.log(` ${chalk.dim(`${installedOnEntry} installed | offline mode`)}`);
|
|
757
|
+
}
|
|
758
|
+
console.log();
|
|
759
|
+
// First-time guided setup
|
|
760
|
+
if (installedOnEntry === 0 && availableCount > 0) {
|
|
761
|
+
const wantsSetup = await p.confirm({
|
|
762
|
+
message: "First time? Let's find the right skills for your project.",
|
|
763
|
+
initialValue: true,
|
|
764
|
+
});
|
|
765
|
+
if (!p.isCancel(wantsSetup) && wantsSetup) {
|
|
766
|
+
await quickSetup(allSkills, providerName);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
// Main loop - simple re-render, no double menus
|
|
770
|
+
while (true) {
|
|
771
|
+
const installedCount = countInstalled();
|
|
772
|
+
const options = buildMenuOptions(installedCount, availableCount);
|
|
773
|
+
const selected = await p.select({
|
|
774
|
+
message: "What would you like to do?",
|
|
775
|
+
options,
|
|
776
|
+
});
|
|
777
|
+
if (p.isCancel(selected) || selected === "exit") {
|
|
778
|
+
clearProviderCache();
|
|
779
|
+
p.outro(chalk.dim("Until next time."));
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
console.log();
|
|
783
|
+
try {
|
|
784
|
+
switch (selected) {
|
|
785
|
+
case "browse":
|
|
786
|
+
await browseByCategory(allSkills, providerName);
|
|
787
|
+
break;
|
|
788
|
+
case "search":
|
|
789
|
+
await searchFlow(allSkills, providerName);
|
|
790
|
+
break;
|
|
791
|
+
case "setup":
|
|
792
|
+
await quickSetup(allSkills, providerName);
|
|
793
|
+
break;
|
|
794
|
+
case "installed":
|
|
795
|
+
await manageInstalled(allSkills, providerName);
|
|
796
|
+
break;
|
|
797
|
+
case "health":
|
|
798
|
+
await checkHealth();
|
|
799
|
+
break;
|
|
800
|
+
case "ref":
|
|
801
|
+
showCliReference();
|
|
802
|
+
break;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
catch (err) {
|
|
806
|
+
if (err instanceof Error) {
|
|
807
|
+
p.log.error(err.message);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
//# sourceMappingURL=interactive.js.map
|