@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.
Files changed (46) hide show
  1. package/bin/crouter +2 -0
  2. package/bin/crtr +2 -0
  3. package/dist/cli.js +34 -4
  4. package/dist/commands/config.d.ts +2 -0
  5. package/dist/commands/config.js +126 -0
  6. package/dist/commands/doctor.d.ts +2 -0
  7. package/dist/commands/doctor.js +216 -0
  8. package/dist/commands/marketplace.d.ts +2 -0
  9. package/dist/commands/marketplace.js +365 -0
  10. package/dist/commands/plan.d.ts +2 -0
  11. package/dist/commands/plan.js +9 -0
  12. package/dist/commands/plugin.d.ts +2 -0
  13. package/dist/commands/plugin.js +364 -0
  14. package/dist/commands/skill.d.ts +2 -0
  15. package/dist/commands/skill.js +404 -0
  16. package/dist/commands/spec.d.ts +2 -0
  17. package/dist/commands/spec.js +9 -0
  18. package/dist/commands/update.d.ts +2 -0
  19. package/dist/commands/update.js +140 -0
  20. package/dist/core/artifact.d.ts +14 -0
  21. package/dist/core/artifact.js +187 -0
  22. package/dist/core/config.d.ts +10 -0
  23. package/dist/core/config.js +83 -0
  24. package/dist/core/errors.d.ts +12 -0
  25. package/dist/core/errors.js +28 -0
  26. package/dist/core/frontmatter.d.ts +8 -0
  27. package/dist/core/frontmatter.js +84 -0
  28. package/dist/core/fs-utils.d.ts +18 -0
  29. package/dist/core/fs-utils.js +115 -0
  30. package/dist/core/git.d.ts +18 -0
  31. package/dist/core/git.js +71 -0
  32. package/dist/core/manifest.d.ts +5 -0
  33. package/dist/core/manifest.js +15 -0
  34. package/dist/core/output.d.ts +35 -0
  35. package/dist/core/output.js +99 -0
  36. package/dist/core/resolver.d.ts +28 -0
  37. package/dist/core/resolver.js +228 -0
  38. package/dist/core/scope.d.ts +12 -0
  39. package/dist/core/scope.js +87 -0
  40. package/dist/prompts/plan.d.ts +1 -0
  41. package/dist/prompts/plan.js +99 -0
  42. package/dist/prompts/spec.d.ts +1 -0
  43. package/dist/prompts/spec.js +106 -0
  44. package/dist/types.d.ts +114 -0
  45. package/dist/types.js +33 -0
  46. package/package.json +8 -5
package/bin/crouter ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import('../dist/cli.js');
package/bin/crtr ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import('../dist/cli.js');
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('crouter')
6
- .description('crouter CLI')
7
- .version('0.1.0');
8
- program.parse();
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,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerConfigCommands(program: Command): void;
@@ -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,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerDoctorCommand(program: Command): void;
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerMarketplaceCommands(program: Command): void;