@crouton-kit/crouter 0.1.1 → 0.1.2
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 +34 -4
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +126 -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 +404 -0
- package/dist/commands/spec.d.ts +2 -0
- package/dist/commands/spec.js +9 -0
- package/dist/commands/update.d.ts +2 -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/config.d.ts +10 -0
- package/dist/core/config.js +83 -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 +84 -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 +99 -0
- package/dist/prompts/spec.d.ts +1 -0
- package/dist/prompts/spec.js +106 -0
- package/dist/types.d.ts +114 -0
- package/dist/types.js +33 -0
- package/package.json +8 -5
package/bin/crouter
ADDED
package/bin/crtr
ADDED
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,38 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from 'commander';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
|
+
import { registerSkillCommands } from './commands/skill.js';
|
|
7
|
+
import { registerPluginCommands } from './commands/plugin.js';
|
|
8
|
+
import { registerMarketplaceCommands } from './commands/marketplace.js';
|
|
9
|
+
import { registerConfigCommands } from './commands/config.js';
|
|
10
|
+
import { registerUpdateCommand } from './commands/update.js';
|
|
11
|
+
import { registerDoctorCommand } from './commands/doctor.js';
|
|
12
|
+
import { registerPlanCommand } from './commands/plan.js';
|
|
13
|
+
import { registerSpecCommand } from './commands/spec.js';
|
|
14
|
+
function readPackageVersion() {
|
|
15
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const pkgPath = join(here, '..', 'package.json');
|
|
17
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
18
|
+
if (typeof pkg.version === 'string')
|
|
19
|
+
return pkg.version;
|
|
20
|
+
return '0.0.0';
|
|
21
|
+
}
|
|
3
22
|
const program = new Command();
|
|
4
23
|
program
|
|
5
|
-
.name('
|
|
6
|
-
.description('
|
|
7
|
-
.version('
|
|
8
|
-
program
|
|
24
|
+
.name('crtr')
|
|
25
|
+
.description('crtr — fast access to skills, plugins, and marketplaces')
|
|
26
|
+
.version(readPackageVersion(), '-v, --version');
|
|
27
|
+
registerSkillCommands(program);
|
|
28
|
+
registerPluginCommands(program);
|
|
29
|
+
registerMarketplaceCommands(program);
|
|
30
|
+
registerConfigCommands(program);
|
|
31
|
+
registerUpdateCommand(program);
|
|
32
|
+
registerDoctorCommand(program);
|
|
33
|
+
registerPlanCommand(program);
|
|
34
|
+
registerSpecCommand(program);
|
|
35
|
+
program.parseAsync().catch((err) => {
|
|
36
|
+
process.stderr.write(`crtr: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { readConfig, writeConfig, configPath } from '../core/config.js';
|
|
2
|
+
import { usage, notFound } from '../core/errors.js';
|
|
3
|
+
import { out, jsonOut, handleError } from '../core/output.js';
|
|
4
|
+
import { scopeRoot, listScopes } from '../core/scope.js';
|
|
5
|
+
const TOP_LEVEL_KEYS = new Set(['auto_update', 'marketplaces', 'plugins']);
|
|
6
|
+
function getNestedValue(obj, key) {
|
|
7
|
+
const parts = key.split('.');
|
|
8
|
+
let current = obj;
|
|
9
|
+
for (const part of parts) {
|
|
10
|
+
if (current === null || typeof current !== 'object')
|
|
11
|
+
return undefined;
|
|
12
|
+
current = current[part];
|
|
13
|
+
}
|
|
14
|
+
return current;
|
|
15
|
+
}
|
|
16
|
+
function parseValue(raw) {
|
|
17
|
+
if (raw === 'true')
|
|
18
|
+
return true;
|
|
19
|
+
if (raw === 'false')
|
|
20
|
+
return false;
|
|
21
|
+
if (/^-?\d+$/.test(raw))
|
|
22
|
+
return parseInt(raw, 10);
|
|
23
|
+
return raw;
|
|
24
|
+
}
|
|
25
|
+
function setNestedValue(cfg, key, value) {
|
|
26
|
+
const parts = key.split('.');
|
|
27
|
+
const topKey = parts[0];
|
|
28
|
+
if (!TOP_LEVEL_KEYS.has(topKey)) {
|
|
29
|
+
throw usage(`unknown config key: ${topKey} (expected: ${[...TOP_LEVEL_KEYS].join('|')})`);
|
|
30
|
+
}
|
|
31
|
+
if (key === 'auto_update.content') {
|
|
32
|
+
if (value !== 'notify' && value !== 'apply' && value !== false) {
|
|
33
|
+
throw usage(`auto_update.content must be 'notify', 'apply', or false`);
|
|
34
|
+
}
|
|
35
|
+
cfg.auto_update.content = value;
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (parts.length === 1) {
|
|
39
|
+
cfg[topKey] = value;
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (parts.length === 2 && topKey === 'auto_update') {
|
|
43
|
+
const subKey = parts[1];
|
|
44
|
+
cfg.auto_update[subKey] = value;
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
throw usage(`unsupported key path for set: ${key}`);
|
|
48
|
+
}
|
|
49
|
+
export function registerConfigCommands(program) {
|
|
50
|
+
const config = program
|
|
51
|
+
.command('config')
|
|
52
|
+
.description('read and write crtr configuration');
|
|
53
|
+
config
|
|
54
|
+
.command('get <key>')
|
|
55
|
+
.description('print a config value by dotted key (default scope: user)')
|
|
56
|
+
.option('--scope <scope>', 'user|project (default: user)')
|
|
57
|
+
.action(async (key, opts) => {
|
|
58
|
+
try {
|
|
59
|
+
const scope = opts.scope === 'project' ? 'project' : 'user';
|
|
60
|
+
const cfg = readConfig(scope);
|
|
61
|
+
const value = getNestedValue(cfg, key);
|
|
62
|
+
if (value === undefined) {
|
|
63
|
+
throw notFound(`config key not found: ${key}`);
|
|
64
|
+
}
|
|
65
|
+
if (typeof value === 'object') {
|
|
66
|
+
out(JSON.stringify(value));
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
out(String(value));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
handleError(e);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
config
|
|
77
|
+
.command('set <key> <value>')
|
|
78
|
+
.description('set a config value by dotted key (default scope: user)')
|
|
79
|
+
.option('--scope <scope>', 'user|project (default: user)')
|
|
80
|
+
.action(async (key, rawValue, opts) => {
|
|
81
|
+
try {
|
|
82
|
+
const scope = opts.scope === 'project' ? 'project' : 'user';
|
|
83
|
+
const cfg = readConfig(scope);
|
|
84
|
+
const parsed = parseValue(rawValue);
|
|
85
|
+
setNestedValue(cfg, key, parsed);
|
|
86
|
+
writeConfig(scope, cfg);
|
|
87
|
+
}
|
|
88
|
+
catch (e) {
|
|
89
|
+
handleError(e);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
config
|
|
93
|
+
.command('path')
|
|
94
|
+
.description('print the absolute path(s) to config.json')
|
|
95
|
+
.option('--scope <scope>', 'user|project|all (default: all)')
|
|
96
|
+
.option('--json', 'emit JSON')
|
|
97
|
+
.action(async (opts) => {
|
|
98
|
+
try {
|
|
99
|
+
const scopes = listScopes(opts.scope);
|
|
100
|
+
if (opts.json) {
|
|
101
|
+
const paths = scopes
|
|
102
|
+
.map((s) => {
|
|
103
|
+
const root = scopeRoot(s);
|
|
104
|
+
if (!root)
|
|
105
|
+
return null;
|
|
106
|
+
const p = configPath(s);
|
|
107
|
+
if (!p)
|
|
108
|
+
return null;
|
|
109
|
+
return { scope: s, path: p };
|
|
110
|
+
})
|
|
111
|
+
.filter((x) => x !== null);
|
|
112
|
+
jsonOut({ paths });
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
for (const s of scopes) {
|
|
116
|
+
const p = configPath(s);
|
|
117
|
+
if (p) {
|
|
118
|
+
out(p);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch (e) {
|
|
123
|
+
handleError(e, { json: opts.json });
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { out, jsonOut, handleError, stdoutColor } from '../core/output.js';
|
|
3
|
+
import { scopeRoot, pluginsDir, marketplacesDir, listScopes } from '../core/scope.js';
|
|
4
|
+
import { readConfig, updateConfig } from '../core/config.js';
|
|
5
|
+
import { listInstalledPlugins, listSkillsInPlugin } from '../core/resolver.js';
|
|
6
|
+
import { pathExists, listDirs, removePath } from '../core/fs-utils.js';
|
|
7
|
+
import { readPluginManifest, readMarketplaceManifest } from '../core/manifest.js';
|
|
8
|
+
import { lsRemote } from '../core/git.js';
|
|
9
|
+
import { ExitCode } from '../types.js';
|
|
10
|
+
function pass(scope, name, message) {
|
|
11
|
+
return { scope, name, status: 'pass', message };
|
|
12
|
+
}
|
|
13
|
+
function fail(scope, name, message) {
|
|
14
|
+
return { scope, name, status: 'fail', message };
|
|
15
|
+
}
|
|
16
|
+
function warn(scope, name, message) {
|
|
17
|
+
return { scope, name, status: 'warn', message };
|
|
18
|
+
}
|
|
19
|
+
function runChecksForScope(scope, opts) {
|
|
20
|
+
const results = [];
|
|
21
|
+
const root = scopeRoot(scope);
|
|
22
|
+
if (!root)
|
|
23
|
+
return results;
|
|
24
|
+
const cfg = readConfig(scope);
|
|
25
|
+
// Check: every config marketplace entry has a corresponding directory
|
|
26
|
+
const mktDir = marketplacesDir(scope);
|
|
27
|
+
for (const name of Object.keys(cfg.marketplaces)) {
|
|
28
|
+
if (!mktDir) {
|
|
29
|
+
results.push(fail(scope, `marketplace:${name}:dir`, `marketplaces directory unavailable`));
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const dir = join(mktDir, name);
|
|
33
|
+
if (!pathExists(dir)) {
|
|
34
|
+
if (opts.fix) {
|
|
35
|
+
updateConfig(scope, (c) => {
|
|
36
|
+
delete c.marketplaces[name];
|
|
37
|
+
});
|
|
38
|
+
results.push({ scope, name: `marketplace:${name}:dir`, status: 'fail', message: `directory missing — removed stale config entry`, fixed: true });
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
results.push(fail(scope, `marketplace:${name}:dir`, `directory missing: ${dir}`));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
results.push(pass(scope, `marketplace:${name}:dir`, `directory exists`));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Check: every config plugin entry has a corresponding directory
|
|
49
|
+
const plugDir = pluginsDir(scope);
|
|
50
|
+
for (const name of Object.keys(cfg.plugins)) {
|
|
51
|
+
if (!plugDir) {
|
|
52
|
+
results.push(fail(scope, `plugin:${name}:dir`, `plugins directory unavailable`));
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const dir = join(plugDir, name);
|
|
56
|
+
if (!pathExists(dir)) {
|
|
57
|
+
if (opts.fix) {
|
|
58
|
+
updateConfig(scope, (c) => {
|
|
59
|
+
delete c.plugins[name];
|
|
60
|
+
});
|
|
61
|
+
results.push({ scope, name: `plugin:${name}:dir`, status: 'fail', message: `directory missing — removed stale config entry`, fixed: true });
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
results.push(fail(scope, `plugin:${name}:dir`, `directory missing: ${dir}`));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
results.push(pass(scope, `plugin:${name}:dir`, `directory exists`));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Check: every marketplace directory has a valid manifest
|
|
72
|
+
if (mktDir && pathExists(mktDir)) {
|
|
73
|
+
for (const name of listDirs(mktDir)) {
|
|
74
|
+
const dir = join(mktDir, name);
|
|
75
|
+
const manifest = readMarketplaceManifest(dir);
|
|
76
|
+
if (!manifest) {
|
|
77
|
+
if (opts.fix) {
|
|
78
|
+
removePath(dir);
|
|
79
|
+
results.push({ scope, name: `marketplace:${name}:manifest`, status: 'fail', message: `no valid marketplace.json — removed dangling directory`, fixed: true });
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
results.push(fail(scope, `marketplace:${name}:manifest`, `no valid marketplace.json in ${dir}`));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
results.push(pass(scope, `marketplace:${name}:manifest`, `manifest valid`));
|
|
87
|
+
// Check: marketplace plugins[].source paths resolve (relative paths only)
|
|
88
|
+
for (const entry of manifest.plugins) {
|
|
89
|
+
if (entry.source.startsWith('http://') || entry.source.startsWith('https://') || entry.source.startsWith('git@')) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const resolved = join(dir, entry.source);
|
|
93
|
+
if (!pathExists(resolved)) {
|
|
94
|
+
results.push(fail(scope, `marketplace:${name}:plugin-source:${entry.name}`, `source path does not resolve: ${resolved}`));
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
results.push(pass(scope, `marketplace:${name}:plugin-source:${entry.name}`, `source resolves`));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Check: every plugin directory has a valid manifest + no duplicate names
|
|
104
|
+
const seenPluginNames = new Map();
|
|
105
|
+
if (plugDir && pathExists(plugDir)) {
|
|
106
|
+
for (const name of listDirs(plugDir)) {
|
|
107
|
+
const dir = join(plugDir, name);
|
|
108
|
+
const manifest = readPluginManifest(dir);
|
|
109
|
+
if (!manifest) {
|
|
110
|
+
if (opts.fix) {
|
|
111
|
+
removePath(dir);
|
|
112
|
+
results.push({ scope, name: `plugin:${name}:manifest`, status: 'fail', message: `no valid plugin.json — removed dangling directory`, fixed: true });
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
results.push(fail(scope, `plugin:${name}:manifest`, `no valid plugin.json in ${dir}`));
|
|
116
|
+
}
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
results.push(pass(scope, `plugin:${name}:manifest`, `manifest valid`));
|
|
120
|
+
// Duplicate names
|
|
121
|
+
if (seenPluginNames.has(name)) {
|
|
122
|
+
results.push(fail(scope, `plugin:${name}:duplicate`, `duplicate plugin name within scope (also at ${seenPluginNames.get(name)})`));
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
seenPluginNames.set(name, dir);
|
|
126
|
+
}
|
|
127
|
+
// Check: skills frontmatter parses and name matches directory
|
|
128
|
+
const plugin = listInstalledPlugins(scope).find((p) => p.name === name);
|
|
129
|
+
if (plugin) {
|
|
130
|
+
const skills = listSkillsInPlugin(plugin);
|
|
131
|
+
for (const skill of skills) {
|
|
132
|
+
if (!skill.frontmatter.name) {
|
|
133
|
+
results.push(fail(scope, `plugin:${name}:skill:${skill.name}:frontmatter`, `frontmatter missing or name field empty`));
|
|
134
|
+
}
|
|
135
|
+
else if (skill.frontmatter.name !== skill.name) {
|
|
136
|
+
results.push(warn(scope, `plugin:${name}:skill:${skill.name}:frontmatter`, `name mismatch: frontmatter says "${skill.frontmatter.name}", directory is "${skill.name}"`));
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
results.push(pass(scope, `plugin:${name}:skill:${skill.name}:frontmatter`, `frontmatter valid`));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// Git remote check (slow, opt-in)
|
|
144
|
+
if (opts.remote && manifest.source) {
|
|
145
|
+
const res = lsRemote(manifest.source);
|
|
146
|
+
if (res.status !== 0) {
|
|
147
|
+
results.push(fail(scope, `plugin:${name}:remote`, `git remote unreachable: ${manifest.source}`));
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
results.push(pass(scope, `plugin:${name}:remote`, `git remote reachable`));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return results;
|
|
156
|
+
}
|
|
157
|
+
function printResults(results) {
|
|
158
|
+
const byScopeMap = new Map();
|
|
159
|
+
for (const r of results) {
|
|
160
|
+
const existing = byScopeMap.get(r.scope);
|
|
161
|
+
if (existing) {
|
|
162
|
+
existing.push(r);
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
byScopeMap.set(r.scope, [r]);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
for (const [scope, checks] of byScopeMap) {
|
|
169
|
+
out(stdoutColor.bold(`[${scope}]`));
|
|
170
|
+
for (const c of checks) {
|
|
171
|
+
if (c.status === 'pass') {
|
|
172
|
+
out(stdoutColor.green(` PASS ${c.name}: ${c.message}`));
|
|
173
|
+
}
|
|
174
|
+
else if (c.status === 'warn') {
|
|
175
|
+
out(stdoutColor.yellow(` WARN ${c.name}: ${c.message}`));
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
const fixSuffix = c.fixed ? ' (fixed)' : '';
|
|
179
|
+
out(stdoutColor.red(` FAIL ${c.name}: ${c.message}${fixSuffix}`));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
export function registerDoctorCommand(program) {
|
|
185
|
+
program
|
|
186
|
+
.command('doctor')
|
|
187
|
+
.description('diagnose missing manifests, broken config entries, and skill frontmatter drift')
|
|
188
|
+
.option('--fix', 'drop stale config entries and prune directories without manifests')
|
|
189
|
+
.option('--remote', 'check git remotes with ls-remote (slow)')
|
|
190
|
+
.option('--scope <scope>', 'user|project|all (default: all)')
|
|
191
|
+
.option('--json', 'emit JSON')
|
|
192
|
+
.action(async (opts) => {
|
|
193
|
+
try {
|
|
194
|
+
const scopes = listScopes(opts.scope);
|
|
195
|
+
const fix = opts.fix === true;
|
|
196
|
+
const remote = opts.remote === true;
|
|
197
|
+
const allResults = [];
|
|
198
|
+
for (const scope of scopes) {
|
|
199
|
+
const results = runChecksForScope(scope, { fix, remote });
|
|
200
|
+
allResults.push(...results);
|
|
201
|
+
}
|
|
202
|
+
if (opts.json) {
|
|
203
|
+
jsonOut({ checks: allResults });
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
printResults(allResults);
|
|
207
|
+
const anyUnresolvedFail = allResults.some((r) => r.status === 'fail' && r.fixed !== true);
|
|
208
|
+
if (anyUnresolvedFail) {
|
|
209
|
+
process.exit(ExitCode.GENERAL);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
catch (e) {
|
|
213
|
+
handleError(e, { json: opts.json });
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
}
|