@crouton-kit/crouter 0.1.1 → 0.1.3
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/bin/crouter +2 -0
- package/bin/crtr +2 -0
- package/dist/cli.js +36 -4
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +134 -0
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +216 -0
- package/dist/commands/marketplace.d.ts +2 -0
- package/dist/commands/marketplace.js +365 -0
- package/dist/commands/plan.d.ts +2 -0
- package/dist/commands/plan.js +9 -0
- package/dist/commands/plugin.d.ts +2 -0
- package/dist/commands/plugin.js +364 -0
- package/dist/commands/skill.d.ts +2 -0
- package/dist/commands/skill.js +405 -0
- package/dist/commands/spec.d.ts +2 -0
- package/dist/commands/spec.js +9 -0
- package/dist/commands/update.d.ts +4 -0
- package/dist/commands/update.js +140 -0
- package/dist/core/artifact.d.ts +14 -0
- package/dist/core/artifact.js +187 -0
- package/dist/core/auto-update.d.ts +1 -0
- package/dist/core/auto-update.js +86 -0
- package/dist/core/config.d.ts +10 -0
- package/dist/core/config.js +96 -0
- package/dist/core/errors.d.ts +12 -0
- package/dist/core/errors.js +28 -0
- package/dist/core/frontmatter.d.ts +8 -0
- package/dist/core/frontmatter.js +156 -0
- package/dist/core/fs-utils.d.ts +18 -0
- package/dist/core/fs-utils.js +115 -0
- package/dist/core/git.d.ts +18 -0
- package/dist/core/git.js +71 -0
- package/dist/core/manifest.d.ts +5 -0
- package/dist/core/manifest.js +15 -0
- package/dist/core/output.d.ts +35 -0
- package/dist/core/output.js +99 -0
- package/dist/core/resolver.d.ts +28 -0
- package/dist/core/resolver.js +228 -0
- package/dist/core/scope.d.ts +12 -0
- package/dist/core/scope.js +87 -0
- package/dist/prompts/plan.d.ts +1 -0
- package/dist/prompts/plan.js +106 -0
- package/dist/prompts/skill.d.ts +1 -0
- package/dist/prompts/skill.js +49 -0
- package/dist/prompts/spec.d.ts +1 -0
- package/dist/prompts/spec.js +113 -0
- package/dist/types.d.ts +115 -0
- package/dist/types.js +33 -0
- package/package.json +8 -5
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, symlinkSync, writeFileSync, cpSync, readlinkSync, } from 'node:fs';
|
|
2
|
+
import { dirname, join, relative } from 'node:path';
|
|
3
|
+
import { platform } from 'node:os';
|
|
4
|
+
export function ensureDir(dir) {
|
|
5
|
+
mkdirSync(dir, { recursive: true });
|
|
6
|
+
}
|
|
7
|
+
export function writeJson(path, data) {
|
|
8
|
+
ensureDir(dirname(path));
|
|
9
|
+
writeFileSync(path, JSON.stringify(data, null, 2) + '\n', 'utf8');
|
|
10
|
+
}
|
|
11
|
+
export function readJson(path) {
|
|
12
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
13
|
+
}
|
|
14
|
+
export function readJsonIfExists(path) {
|
|
15
|
+
if (!existsSync(path))
|
|
16
|
+
return null;
|
|
17
|
+
return readJson(path);
|
|
18
|
+
}
|
|
19
|
+
export function readTextIfExists(path) {
|
|
20
|
+
if (!existsSync(path))
|
|
21
|
+
return null;
|
|
22
|
+
return readFileSync(path, 'utf8');
|
|
23
|
+
}
|
|
24
|
+
export function readText(path) {
|
|
25
|
+
return readFileSync(path, 'utf8');
|
|
26
|
+
}
|
|
27
|
+
export function isDir(path) {
|
|
28
|
+
try {
|
|
29
|
+
return statSync(path).isDirectory();
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export function isSymlink(path) {
|
|
36
|
+
try {
|
|
37
|
+
return lstatSync(path).isSymbolicLink();
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export function pathExists(path) {
|
|
44
|
+
return existsSync(path);
|
|
45
|
+
}
|
|
46
|
+
export function listDirs(path) {
|
|
47
|
+
if (!existsSync(path))
|
|
48
|
+
return [];
|
|
49
|
+
return readdirSync(path, { withFileTypes: true })
|
|
50
|
+
.filter((d) => d.isDirectory() || d.isSymbolicLink())
|
|
51
|
+
.map((d) => d.name);
|
|
52
|
+
}
|
|
53
|
+
export function listEntries(path) {
|
|
54
|
+
if (!existsSync(path))
|
|
55
|
+
return [];
|
|
56
|
+
return readdirSync(path);
|
|
57
|
+
}
|
|
58
|
+
export function removePath(path) {
|
|
59
|
+
if (!existsSync(path) && !isSymlink(path))
|
|
60
|
+
return;
|
|
61
|
+
rmSync(path, { recursive: true, force: true });
|
|
62
|
+
}
|
|
63
|
+
export function linkOrCopy(target, linkPath, opts = {}) {
|
|
64
|
+
ensureDir(dirname(linkPath));
|
|
65
|
+
removePath(linkPath);
|
|
66
|
+
const isWindows = platform() === 'win32';
|
|
67
|
+
if (!opts.noSymlink && !isWindows) {
|
|
68
|
+
const rel = relative(dirname(linkPath), target);
|
|
69
|
+
try {
|
|
70
|
+
symlinkSync(rel, linkPath, isDir(target) ? 'dir' : 'file');
|
|
71
|
+
return 'symlink';
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
cpSync(target, linkPath, { recursive: true });
|
|
75
|
+
return 'copy';
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
cpSync(target, linkPath, { recursive: true });
|
|
79
|
+
return 'copy';
|
|
80
|
+
}
|
|
81
|
+
export function readSymlinkTarget(path) {
|
|
82
|
+
try {
|
|
83
|
+
return readlinkSync(path);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
export function walkFiles(root, predicate = () => true) {
|
|
90
|
+
const out = [];
|
|
91
|
+
if (!existsSync(root))
|
|
92
|
+
return out;
|
|
93
|
+
const stack = [root];
|
|
94
|
+
while (stack.length) {
|
|
95
|
+
const dir = stack.pop();
|
|
96
|
+
let entries;
|
|
97
|
+
try {
|
|
98
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
for (const e of entries) {
|
|
104
|
+
const full = join(dir, e.name);
|
|
105
|
+
if (e.isDirectory())
|
|
106
|
+
stack.push(full);
|
|
107
|
+
else if (e.isFile() && predicate(e.name))
|
|
108
|
+
out.push(full);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
export function nowIso() {
|
|
114
|
+
return new Date().toISOString();
|
|
115
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface GitResult {
|
|
2
|
+
status: number;
|
|
3
|
+
stdout: string;
|
|
4
|
+
stderr: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function gitSync(args: string[], cwd?: string): GitResult;
|
|
7
|
+
export declare function gitAsync(args: string[], cwd?: string): Promise<GitResult>;
|
|
8
|
+
export declare function clone(url: string, dest: string, opts?: {
|
|
9
|
+
ref?: string;
|
|
10
|
+
depth?: number;
|
|
11
|
+
}): GitResult;
|
|
12
|
+
export declare function pull(cwd: string): GitResult;
|
|
13
|
+
export declare function fetch(cwd: string, ref?: string): GitResult;
|
|
14
|
+
export declare function lsRemote(url: string): GitResult;
|
|
15
|
+
export declare function currentSha(cwd: string): string | null;
|
|
16
|
+
export declare function remoteSha(cwd: string, ref: string): string | null;
|
|
17
|
+
export declare function isGitRepo(cwd: string): boolean;
|
|
18
|
+
export declare function deriveNameFromUrl(url: string): string;
|
package/dist/core/git.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
2
|
+
import { network } from './errors.js';
|
|
3
|
+
export function gitSync(args, cwd) {
|
|
4
|
+
const res = spawnSync('git', args, { cwd, encoding: 'utf8' });
|
|
5
|
+
const status = typeof res.status === 'number' ? res.status : 1;
|
|
6
|
+
const stdout = typeof res.stdout === 'string' ? res.stdout : '';
|
|
7
|
+
const stderr = typeof res.stderr === 'string' ? res.stderr : '';
|
|
8
|
+
return { status, stdout, stderr };
|
|
9
|
+
}
|
|
10
|
+
export async function gitAsync(args, cwd) {
|
|
11
|
+
return new Promise((resolve) => {
|
|
12
|
+
const child = spawn('git', args, { cwd });
|
|
13
|
+
let stdout = '';
|
|
14
|
+
let stderr = '';
|
|
15
|
+
child.stdout.on('data', (d) => (stdout += d.toString()));
|
|
16
|
+
child.stderr.on('data', (d) => (stderr += d.toString()));
|
|
17
|
+
child.on('close', (status) => {
|
|
18
|
+
const code = typeof status === 'number' ? status : 1;
|
|
19
|
+
resolve({ status: code, stdout, stderr });
|
|
20
|
+
});
|
|
21
|
+
child.on('error', (e) => resolve({ status: 1, stdout: '', stderr: String(e) }));
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
export function clone(url, dest, opts = {}) {
|
|
25
|
+
const args = ['clone'];
|
|
26
|
+
if (opts.depth)
|
|
27
|
+
args.push('--depth', String(opts.depth));
|
|
28
|
+
if (opts.ref)
|
|
29
|
+
args.push('--branch', opts.ref);
|
|
30
|
+
args.push(url, dest);
|
|
31
|
+
const res = gitSync(args);
|
|
32
|
+
if (res.status !== 0) {
|
|
33
|
+
throw network(`git clone failed: ${url}\n${res.stderr.trim()}`);
|
|
34
|
+
}
|
|
35
|
+
return res;
|
|
36
|
+
}
|
|
37
|
+
export function pull(cwd) {
|
|
38
|
+
return gitSync(['pull', '--ff-only'], cwd);
|
|
39
|
+
}
|
|
40
|
+
export function fetch(cwd, ref) {
|
|
41
|
+
const args = ['fetch', 'origin'];
|
|
42
|
+
if (ref)
|
|
43
|
+
args.push(ref);
|
|
44
|
+
return gitSync(args, cwd);
|
|
45
|
+
}
|
|
46
|
+
export function lsRemote(url) {
|
|
47
|
+
return gitSync(['ls-remote', url]);
|
|
48
|
+
}
|
|
49
|
+
export function currentSha(cwd) {
|
|
50
|
+
const res = gitSync(['rev-parse', 'HEAD'], cwd);
|
|
51
|
+
if (res.status !== 0)
|
|
52
|
+
return null;
|
|
53
|
+
return res.stdout.trim();
|
|
54
|
+
}
|
|
55
|
+
export function remoteSha(cwd, ref) {
|
|
56
|
+
const res = gitSync(['rev-parse', `origin/${ref}`], cwd);
|
|
57
|
+
if (res.status !== 0)
|
|
58
|
+
return null;
|
|
59
|
+
return res.stdout.trim();
|
|
60
|
+
}
|
|
61
|
+
export function isGitRepo(cwd) {
|
|
62
|
+
const res = gitSync(['rev-parse', '--is-inside-work-tree'], cwd);
|
|
63
|
+
return res.status === 0 && res.stdout.trim() === 'true';
|
|
64
|
+
}
|
|
65
|
+
export function deriveNameFromUrl(url) {
|
|
66
|
+
const trimmed = url.replace(/\.git\/?$/, '').replace(/\/$/, '');
|
|
67
|
+
const last = trimmed.split('/').pop();
|
|
68
|
+
if (!last)
|
|
69
|
+
return url;
|
|
70
|
+
return last;
|
|
71
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { MarketplaceManifest, PluginManifest } from '../types.js';
|
|
2
|
+
export declare function pluginManifestPath(pluginRoot: string): string;
|
|
3
|
+
export declare function marketplaceManifestPath(mktRoot: string): string;
|
|
4
|
+
export declare function readPluginManifest(pluginRoot: string): PluginManifest | null;
|
|
5
|
+
export declare function readMarketplaceManifest(mktRoot: string): MarketplaceManifest | null;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { MARKETPLACE_MANIFEST_DIR, MARKETPLACE_MANIFEST_FILE, PLUGIN_MANIFEST_DIR, PLUGIN_MANIFEST_FILE, } from '../types.js';
|
|
3
|
+
import { readJsonIfExists } from './fs-utils.js';
|
|
4
|
+
export function pluginManifestPath(pluginRoot) {
|
|
5
|
+
return join(pluginRoot, PLUGIN_MANIFEST_DIR, PLUGIN_MANIFEST_FILE);
|
|
6
|
+
}
|
|
7
|
+
export function marketplaceManifestPath(mktRoot) {
|
|
8
|
+
return join(mktRoot, MARKETPLACE_MANIFEST_DIR, MARKETPLACE_MANIFEST_FILE);
|
|
9
|
+
}
|
|
10
|
+
export function readPluginManifest(pluginRoot) {
|
|
11
|
+
return readJsonIfExists(pluginManifestPath(pluginRoot));
|
|
12
|
+
}
|
|
13
|
+
export function readMarketplaceManifest(mktRoot) {
|
|
14
|
+
return readJsonIfExists(marketplaceManifestPath(mktRoot));
|
|
15
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { CrtrError } from './errors.js';
|
|
2
|
+
export declare const stdoutColor: {
|
|
3
|
+
dim: (s: string) => string;
|
|
4
|
+
bold: (s: string) => string;
|
|
5
|
+
red: (s: string) => string;
|
|
6
|
+
green: (s: string) => string;
|
|
7
|
+
yellow: (s: string) => string;
|
|
8
|
+
blue: (s: string) => string;
|
|
9
|
+
cyan: (s: string) => string;
|
|
10
|
+
gray: (s: string) => string;
|
|
11
|
+
};
|
|
12
|
+
export declare const stderrColor: {
|
|
13
|
+
dim: (s: string) => string;
|
|
14
|
+
bold: (s: string) => string;
|
|
15
|
+
red: (s: string) => string;
|
|
16
|
+
green: (s: string) => string;
|
|
17
|
+
yellow: (s: string) => string;
|
|
18
|
+
blue: (s: string) => string;
|
|
19
|
+
cyan: (s: string) => string;
|
|
20
|
+
gray: (s: string) => string;
|
|
21
|
+
};
|
|
22
|
+
export declare function out(line: string): void;
|
|
23
|
+
export declare function err(line: string): void;
|
|
24
|
+
export declare function hint(line: string): void;
|
|
25
|
+
export declare function warn(line: string): void;
|
|
26
|
+
export declare function info(line: string): void;
|
|
27
|
+
export declare function jsonOut(obj: unknown): void;
|
|
28
|
+
export declare function jsonError(error: CrtrError | Error): void;
|
|
29
|
+
export declare function isTTY(): boolean;
|
|
30
|
+
export declare function isJsonRequested(opts: {
|
|
31
|
+
json?: boolean;
|
|
32
|
+
} | undefined): boolean;
|
|
33
|
+
export declare function handleError(error: unknown, opts?: {
|
|
34
|
+
json?: boolean;
|
|
35
|
+
}): never;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { CrtrError } from './errors.js';
|
|
2
|
+
import { ExitCode, SCHEMA_VERSION } from '../types.js';
|
|
3
|
+
const ANSI = {
|
|
4
|
+
reset: '\x1b[0m',
|
|
5
|
+
dim: '\x1b[2m',
|
|
6
|
+
bold: '\x1b[1m',
|
|
7
|
+
red: '\x1b[31m',
|
|
8
|
+
green: '\x1b[32m',
|
|
9
|
+
yellow: '\x1b[33m',
|
|
10
|
+
blue: '\x1b[34m',
|
|
11
|
+
cyan: '\x1b[36m',
|
|
12
|
+
gray: '\x1b[90m',
|
|
13
|
+
};
|
|
14
|
+
function shouldColor(stream) {
|
|
15
|
+
if (process.env.NO_COLOR)
|
|
16
|
+
return false;
|
|
17
|
+
if (process.env.FORCE_COLOR)
|
|
18
|
+
return true;
|
|
19
|
+
return Boolean(stream.isTTY);
|
|
20
|
+
}
|
|
21
|
+
function paint(stream, code, text) {
|
|
22
|
+
return shouldColor(stream) ? `${code}${text}${ANSI.reset}` : text;
|
|
23
|
+
}
|
|
24
|
+
export const stdoutColor = {
|
|
25
|
+
dim: (s) => paint(process.stdout, ANSI.dim, s),
|
|
26
|
+
bold: (s) => paint(process.stdout, ANSI.bold, s),
|
|
27
|
+
red: (s) => paint(process.stdout, ANSI.red, s),
|
|
28
|
+
green: (s) => paint(process.stdout, ANSI.green, s),
|
|
29
|
+
yellow: (s) => paint(process.stdout, ANSI.yellow, s),
|
|
30
|
+
blue: (s) => paint(process.stdout, ANSI.blue, s),
|
|
31
|
+
cyan: (s) => paint(process.stdout, ANSI.cyan, s),
|
|
32
|
+
gray: (s) => paint(process.stdout, ANSI.gray, s),
|
|
33
|
+
};
|
|
34
|
+
export const stderrColor = {
|
|
35
|
+
dim: (s) => paint(process.stderr, ANSI.dim, s),
|
|
36
|
+
bold: (s) => paint(process.stderr, ANSI.bold, s),
|
|
37
|
+
red: (s) => paint(process.stderr, ANSI.red, s),
|
|
38
|
+
green: (s) => paint(process.stderr, ANSI.green, s),
|
|
39
|
+
yellow: (s) => paint(process.stderr, ANSI.yellow, s),
|
|
40
|
+
blue: (s) => paint(process.stderr, ANSI.blue, s),
|
|
41
|
+
cyan: (s) => paint(process.stderr, ANSI.cyan, s),
|
|
42
|
+
gray: (s) => paint(process.stderr, ANSI.gray, s),
|
|
43
|
+
};
|
|
44
|
+
export function out(line) {
|
|
45
|
+
process.stdout.write(line.endsWith('\n') ? line : line + '\n');
|
|
46
|
+
}
|
|
47
|
+
export function err(line) {
|
|
48
|
+
process.stderr.write(line.endsWith('\n') ? line : line + '\n');
|
|
49
|
+
}
|
|
50
|
+
export function hint(line) {
|
|
51
|
+
err(stderrColor.dim(`# ${line}`));
|
|
52
|
+
}
|
|
53
|
+
export function warn(line) {
|
|
54
|
+
err(stderrColor.yellow(`crtr: ${line}`));
|
|
55
|
+
}
|
|
56
|
+
export function info(line) {
|
|
57
|
+
err(stderrColor.gray(`crtr: ${line}`));
|
|
58
|
+
}
|
|
59
|
+
export function jsonOut(obj) {
|
|
60
|
+
const enriched = typeof obj === 'object' && obj !== null && !Array.isArray(obj)
|
|
61
|
+
? { schema_version: SCHEMA_VERSION, ...obj }
|
|
62
|
+
: { schema_version: SCHEMA_VERSION, data: obj };
|
|
63
|
+
process.stdout.write(JSON.stringify(enriched, null, 2) + '\n');
|
|
64
|
+
}
|
|
65
|
+
export function jsonError(error) {
|
|
66
|
+
const e = error instanceof CrtrError ? error : new CrtrError('error', error.message, ExitCode.GENERAL);
|
|
67
|
+
process.stdout.write(JSON.stringify({
|
|
68
|
+
schema_version: SCHEMA_VERSION,
|
|
69
|
+
error: true,
|
|
70
|
+
code: e.code,
|
|
71
|
+
message: e.message,
|
|
72
|
+
...(e.details ?? {}),
|
|
73
|
+
}, null, 2) + '\n');
|
|
74
|
+
}
|
|
75
|
+
export function isTTY() {
|
|
76
|
+
return Boolean(process.stdout.isTTY);
|
|
77
|
+
}
|
|
78
|
+
export function isJsonRequested(opts) {
|
|
79
|
+
return Boolean(opts?.json);
|
|
80
|
+
}
|
|
81
|
+
export function handleError(error, opts = {}) {
|
|
82
|
+
if (error instanceof CrtrError) {
|
|
83
|
+
if (opts.json) {
|
|
84
|
+
jsonError(error);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
err(stderrColor.red(`crtr: ${error.message}`));
|
|
88
|
+
}
|
|
89
|
+
process.exit(error.exitCode);
|
|
90
|
+
}
|
|
91
|
+
const e = error;
|
|
92
|
+
if (opts.json) {
|
|
93
|
+
jsonError(e);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
err(stderrColor.red(`crtr: ${e.message ?? String(e)}`));
|
|
97
|
+
}
|
|
98
|
+
process.exit(ExitCode.GENERAL);
|
|
99
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { InstalledMarketplace, InstalledPlugin, Scope, ScopeConfig, Skill } from '../types.js';
|
|
2
|
+
export declare function listInstalledPlugins(scope: Scope): InstalledPlugin[];
|
|
3
|
+
export declare function listAllPlugins(): InstalledPlugin[];
|
|
4
|
+
export declare function findPluginByName(name: string, scope?: Scope): InstalledPlugin | null;
|
|
5
|
+
interface ScopeConfigs {
|
|
6
|
+
project?: ScopeConfig;
|
|
7
|
+
user: ScopeConfig;
|
|
8
|
+
}
|
|
9
|
+
export declare function effectiveSkillEnabled(pluginName: string, skillName: string, cfgs: ScopeConfigs): {
|
|
10
|
+
enabled: boolean;
|
|
11
|
+
disabledIn?: Scope;
|
|
12
|
+
};
|
|
13
|
+
export declare function listSkillsInPlugin(plugin: InstalledPlugin, cfgs?: ScopeConfigs): Skill[];
|
|
14
|
+
export declare function listAllSkills(scopeFilter?: Scope): Skill[];
|
|
15
|
+
export interface SkillResolutionOpts {
|
|
16
|
+
scope?: Scope;
|
|
17
|
+
pluginFilter?: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function resolveSkill(rawName: string, opts?: SkillResolutionOpts): Skill;
|
|
20
|
+
export declare function parseSkillQualifier(raw: string): {
|
|
21
|
+
plugin?: string;
|
|
22
|
+
name: string;
|
|
23
|
+
};
|
|
24
|
+
export declare function listInstalledMarketplaces(scope: Scope): InstalledMarketplace[];
|
|
25
|
+
export declare function listAllMarketplaces(): InstalledMarketplace[];
|
|
26
|
+
export declare function findMarketplaceByName(name: string, scope?: Scope): InstalledMarketplace | null;
|
|
27
|
+
export declare function scopeRootsLabel(): string;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { join, relative, sep, dirname } from 'node:path';
|
|
2
|
+
import { SKILL_ENTRY_FILE, SKILLS_DIR, skillConfigKey, } from '../types.js';
|
|
3
|
+
import { readConfig } from './config.js';
|
|
4
|
+
import { listDirs, pathExists, readText, walkFiles, } from './fs-utils.js';
|
|
5
|
+
import { readMarketplaceManifest, readPluginManifest } from './manifest.js';
|
|
6
|
+
import { parseFrontmatter } from './frontmatter.js';
|
|
7
|
+
import { ambiguous, notFound } from './errors.js';
|
|
8
|
+
import { marketplacesDir, pluginsDir, projectScopeRoot, userScopeRoot } from './scope.js';
|
|
9
|
+
export function listInstalledPlugins(scope) {
|
|
10
|
+
const dir = pluginsDir(scope);
|
|
11
|
+
if (!dir || !pathExists(dir))
|
|
12
|
+
return [];
|
|
13
|
+
const cfg = readConfig(scope);
|
|
14
|
+
const out = [];
|
|
15
|
+
for (const name of listDirs(dir)) {
|
|
16
|
+
const root = join(dir, name);
|
|
17
|
+
const manifest = readPluginManifest(root);
|
|
18
|
+
if (!manifest)
|
|
19
|
+
continue;
|
|
20
|
+
const entry = cfg.plugins[name];
|
|
21
|
+
let version;
|
|
22
|
+
if (entry && entry.version !== undefined)
|
|
23
|
+
version = entry.version;
|
|
24
|
+
else if (manifest.version !== undefined)
|
|
25
|
+
version = manifest.version;
|
|
26
|
+
out.push({
|
|
27
|
+
name,
|
|
28
|
+
scope,
|
|
29
|
+
root,
|
|
30
|
+
manifest,
|
|
31
|
+
enabled: entry ? entry.enabled : true,
|
|
32
|
+
sourceMarketplace: entry ? entry.source_marketplace : undefined,
|
|
33
|
+
version,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
export function listAllPlugins() {
|
|
39
|
+
const scopes = [];
|
|
40
|
+
if (projectScopeRoot())
|
|
41
|
+
scopes.push('project');
|
|
42
|
+
scopes.push('user');
|
|
43
|
+
return scopes.flatMap(listInstalledPlugins);
|
|
44
|
+
}
|
|
45
|
+
export function findPluginByName(name, scope) {
|
|
46
|
+
if (scope) {
|
|
47
|
+
return listInstalledPlugins(scope).find((p) => p.name === name) ?? null;
|
|
48
|
+
}
|
|
49
|
+
for (const s of ['project', 'user'].filter((sc) => sc === 'project' ? projectScopeRoot() !== null : true)) {
|
|
50
|
+
const match = listInstalledPlugins(s).find((p) => p.name === name);
|
|
51
|
+
if (match)
|
|
52
|
+
return match;
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
function loadScopeConfigs() {
|
|
57
|
+
const user = readConfig('user');
|
|
58
|
+
if (projectScopeRoot())
|
|
59
|
+
return { project: readConfig('project'), user };
|
|
60
|
+
return { user };
|
|
61
|
+
}
|
|
62
|
+
export function effectiveSkillEnabled(pluginName, skillName, cfgs) {
|
|
63
|
+
const key = skillConfigKey(pluginName, skillName);
|
|
64
|
+
if (cfgs.project && cfgs.project.skills[key] !== undefined) {
|
|
65
|
+
const e = cfgs.project.skills[key].enabled;
|
|
66
|
+
return e ? { enabled: true } : { enabled: false, disabledIn: 'project' };
|
|
67
|
+
}
|
|
68
|
+
if (cfgs.user.skills[key] !== undefined) {
|
|
69
|
+
const e = cfgs.user.skills[key].enabled;
|
|
70
|
+
return e ? { enabled: true } : { enabled: false, disabledIn: 'user' };
|
|
71
|
+
}
|
|
72
|
+
return { enabled: true };
|
|
73
|
+
}
|
|
74
|
+
export function listSkillsInPlugin(plugin, cfgs) {
|
|
75
|
+
const skillsRoot = join(plugin.root, SKILLS_DIR);
|
|
76
|
+
if (!pathExists(skillsRoot))
|
|
77
|
+
return [];
|
|
78
|
+
const configs = cfgs === undefined ? loadScopeConfigs() : cfgs;
|
|
79
|
+
const skills = [];
|
|
80
|
+
const skillFiles = walkFiles(skillsRoot, (n) => n === SKILL_ENTRY_FILE);
|
|
81
|
+
for (const file of skillFiles) {
|
|
82
|
+
const rel = relative(skillsRoot, dirname(file));
|
|
83
|
+
const name = rel.split(sep).join('/');
|
|
84
|
+
if (!name)
|
|
85
|
+
continue;
|
|
86
|
+
const source = readText(file);
|
|
87
|
+
const { data } = parseFrontmatter(source);
|
|
88
|
+
const { enabled, disabledIn } = effectiveSkillEnabled(plugin.name, name, configs);
|
|
89
|
+
skills.push({
|
|
90
|
+
name,
|
|
91
|
+
plugin: plugin.name,
|
|
92
|
+
scope: plugin.scope,
|
|
93
|
+
path: file,
|
|
94
|
+
pluginRoot: plugin.root,
|
|
95
|
+
frontmatter: data === null ? { name } : data,
|
|
96
|
+
enabled,
|
|
97
|
+
disabledIn,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return skills.sort((a, b) => a.name.localeCompare(b.name));
|
|
101
|
+
}
|
|
102
|
+
export function listAllSkills(scopeFilter) {
|
|
103
|
+
const plugins = scopeFilter ? listInstalledPlugins(scopeFilter) : listAllPlugins();
|
|
104
|
+
const cfgs = loadScopeConfigs();
|
|
105
|
+
return plugins
|
|
106
|
+
.filter((p) => p.enabled)
|
|
107
|
+
.flatMap((p) => listSkillsInPlugin(p, cfgs));
|
|
108
|
+
}
|
|
109
|
+
export function resolveSkill(rawName, opts = {}) {
|
|
110
|
+
const { plugin: pluginQualifier, name } = parseSkillQualifier(rawName);
|
|
111
|
+
const plugins = opts.scope ? listInstalledPlugins(opts.scope) : listAllPlugins();
|
|
112
|
+
const enabledPlugins = plugins.filter((p) => p.enabled);
|
|
113
|
+
const cfgs = loadScopeConfigs();
|
|
114
|
+
const ordered = orderPluginsByResolution(enabledPlugins);
|
|
115
|
+
const matches = [];
|
|
116
|
+
for (const plugin of ordered) {
|
|
117
|
+
if (pluginQualifier && plugin.name !== pluginQualifier)
|
|
118
|
+
continue;
|
|
119
|
+
if (opts.pluginFilter && plugin.name !== opts.pluginFilter)
|
|
120
|
+
continue;
|
|
121
|
+
const skillPath = join(plugin.root, SKILLS_DIR, ...name.split('/'), SKILL_ENTRY_FILE);
|
|
122
|
+
if (!pathExists(skillPath))
|
|
123
|
+
continue;
|
|
124
|
+
const source = readText(skillPath);
|
|
125
|
+
const { data } = parseFrontmatter(source);
|
|
126
|
+
const { enabled, disabledIn } = effectiveSkillEnabled(plugin.name, name, cfgs);
|
|
127
|
+
matches.push({
|
|
128
|
+
name,
|
|
129
|
+
plugin: plugin.name,
|
|
130
|
+
scope: plugin.scope,
|
|
131
|
+
path: skillPath,
|
|
132
|
+
pluginRoot: plugin.root,
|
|
133
|
+
frontmatter: data === null ? { name } : data,
|
|
134
|
+
enabled,
|
|
135
|
+
disabledIn,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
if (matches.length === 0) {
|
|
139
|
+
throw notFound(pluginQualifier
|
|
140
|
+
? `skill not found: ${pluginQualifier}:${name}`
|
|
141
|
+
: `skill not found: ${name}`, { skill: name, plugin: pluginQualifier });
|
|
142
|
+
}
|
|
143
|
+
if (matches.length === 1)
|
|
144
|
+
return matches[0];
|
|
145
|
+
const sameScopeAndPlugin = matches.every((m) => m.plugin === matches[0].plugin && m.scope === matches[0].scope);
|
|
146
|
+
if (sameScopeAndPlugin)
|
|
147
|
+
return matches[0];
|
|
148
|
+
// Resolution order picks the first; flag ambiguity only if user didn't qualify.
|
|
149
|
+
if (!pluginQualifier) {
|
|
150
|
+
return matches[0];
|
|
151
|
+
}
|
|
152
|
+
throw ambiguous(`ambiguous skill: ${name}`, {
|
|
153
|
+
skill: name,
|
|
154
|
+
candidates: matches.map((m) => ({
|
|
155
|
+
plugin: m.plugin,
|
|
156
|
+
scope: m.scope,
|
|
157
|
+
path: m.path,
|
|
158
|
+
})),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
export function parseSkillQualifier(raw) {
|
|
162
|
+
const idx = raw.indexOf(':');
|
|
163
|
+
if (idx === -1)
|
|
164
|
+
return { name: raw };
|
|
165
|
+
return { plugin: raw.slice(0, idx), name: raw.slice(idx + 1) };
|
|
166
|
+
}
|
|
167
|
+
function orderPluginsByResolution(plugins) {
|
|
168
|
+
const score = (p) => {
|
|
169
|
+
const fromMarketplace = Boolean(p.sourceMarketplace);
|
|
170
|
+
if (p.scope === 'project' && !fromMarketplace)
|
|
171
|
+
return 0;
|
|
172
|
+
if (p.scope === 'user' && !fromMarketplace)
|
|
173
|
+
return 1;
|
|
174
|
+
if (p.scope === 'project' && fromMarketplace)
|
|
175
|
+
return 2;
|
|
176
|
+
return 3;
|
|
177
|
+
};
|
|
178
|
+
return [...plugins].sort((a, b) => score(a) - score(b));
|
|
179
|
+
}
|
|
180
|
+
export function listInstalledMarketplaces(scope) {
|
|
181
|
+
const dir = marketplacesDir(scope);
|
|
182
|
+
if (!dir || !pathExists(dir))
|
|
183
|
+
return [];
|
|
184
|
+
const cfg = readConfig(scope);
|
|
185
|
+
const out = [];
|
|
186
|
+
for (const name of listDirs(dir)) {
|
|
187
|
+
const root = join(dir, name);
|
|
188
|
+
const manifest = readMarketplaceManifest(root);
|
|
189
|
+
if (!manifest)
|
|
190
|
+
continue;
|
|
191
|
+
const entry = cfg.marketplaces[name];
|
|
192
|
+
const url = entry && entry.url !== undefined ? entry.url : '';
|
|
193
|
+
const ref = entry && entry.ref !== undefined ? entry.ref : 'main';
|
|
194
|
+
out.push({
|
|
195
|
+
name,
|
|
196
|
+
scope,
|
|
197
|
+
root,
|
|
198
|
+
manifest,
|
|
199
|
+
url,
|
|
200
|
+
ref,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
return out;
|
|
204
|
+
}
|
|
205
|
+
export function listAllMarketplaces() {
|
|
206
|
+
const scopes = [];
|
|
207
|
+
if (projectScopeRoot())
|
|
208
|
+
scopes.push('project');
|
|
209
|
+
scopes.push('user');
|
|
210
|
+
return scopes.flatMap(listInstalledMarketplaces);
|
|
211
|
+
}
|
|
212
|
+
export function findMarketplaceByName(name, scope) {
|
|
213
|
+
if (scope) {
|
|
214
|
+
return listInstalledMarketplaces(scope).find((m) => m.name === name) ?? null;
|
|
215
|
+
}
|
|
216
|
+
for (const s of ['project', 'user']) {
|
|
217
|
+
if (s === 'project' && !projectScopeRoot())
|
|
218
|
+
continue;
|
|
219
|
+
const found = listInstalledMarketplaces(s).find((m) => m.name === name);
|
|
220
|
+
if (found)
|
|
221
|
+
return found;
|
|
222
|
+
}
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
export function scopeRootsLabel() {
|
|
226
|
+
const proj = projectScopeRoot();
|
|
227
|
+
return proj ? `project=${proj}, user=${userScopeRoot()}` : `user=${userScopeRoot()}`;
|
|
228
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Scope } from '../types.js';
|
|
2
|
+
export declare function userScopeRoot(): string;
|
|
3
|
+
export declare function findProjectScopeRoot(startDir?: string): string | null;
|
|
4
|
+
export declare function projectScopeRoot(startDir?: string): string | null;
|
|
5
|
+
export declare function scopeRoot(scope: Scope): string | null;
|
|
6
|
+
export declare function requireScopeRoot(scope: Scope): string;
|
|
7
|
+
export declare function ensureProjectScopeRoot(startDir?: string): string;
|
|
8
|
+
export declare function pluginsDir(scope: Scope): string | null;
|
|
9
|
+
export declare function marketplacesDir(scope: Scope): string | null;
|
|
10
|
+
export declare function resolveScopeArg(scopeArg: string | undefined): Scope | 'all';
|
|
11
|
+
export declare function listScopes(scopeArg: string | undefined): Scope[];
|
|
12
|
+
export declare function resetScopeCache(): void;
|