@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
|
@@ -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;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { existsSync, statSync } from 'node:fs';
|
|
3
|
+
import { join, resolve, dirname } from 'node:path';
|
|
4
|
+
import { CRTR_DIR_NAME } from '../types.js';
|
|
5
|
+
import { usage } from './errors.js';
|
|
6
|
+
let cachedProjectRoot;
|
|
7
|
+
export function userScopeRoot() {
|
|
8
|
+
return join(homedir(), CRTR_DIR_NAME);
|
|
9
|
+
}
|
|
10
|
+
export function findProjectScopeRoot(startDir = process.cwd()) {
|
|
11
|
+
if (cachedProjectRoot !== undefined)
|
|
12
|
+
return cachedProjectRoot;
|
|
13
|
+
const userRoot = userScopeRoot();
|
|
14
|
+
let dir = resolve(startDir);
|
|
15
|
+
while (true) {
|
|
16
|
+
const candidate = join(dir, CRTR_DIR_NAME);
|
|
17
|
+
if (candidate !== userRoot && existsSync(candidate)) {
|
|
18
|
+
try {
|
|
19
|
+
if (statSync(candidate).isDirectory()) {
|
|
20
|
+
cachedProjectRoot = candidate;
|
|
21
|
+
return candidate;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
/* fall through */
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const parent = dirname(dir);
|
|
29
|
+
if (parent === dir) {
|
|
30
|
+
cachedProjectRoot = null;
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
dir = parent;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export function projectScopeRoot(startDir) {
|
|
37
|
+
return findProjectScopeRoot(startDir);
|
|
38
|
+
}
|
|
39
|
+
export function scopeRoot(scope) {
|
|
40
|
+
return scope === 'user' ? userScopeRoot() : projectScopeRoot();
|
|
41
|
+
}
|
|
42
|
+
export function requireScopeRoot(scope) {
|
|
43
|
+
const root = scopeRoot(scope);
|
|
44
|
+
if (!root) {
|
|
45
|
+
throw usage(`no ${scope} scope available — run \`crtr init\` here or use --scope user`);
|
|
46
|
+
}
|
|
47
|
+
return root;
|
|
48
|
+
}
|
|
49
|
+
export function ensureProjectScopeRoot(startDir = process.cwd()) {
|
|
50
|
+
const found = findProjectScopeRoot(startDir);
|
|
51
|
+
if (found)
|
|
52
|
+
return found;
|
|
53
|
+
// Initialize new project scope at startDir
|
|
54
|
+
const root = join(resolve(startDir), CRTR_DIR_NAME);
|
|
55
|
+
cachedProjectRoot = root;
|
|
56
|
+
return root;
|
|
57
|
+
}
|
|
58
|
+
export function pluginsDir(scope) {
|
|
59
|
+
const root = scopeRoot(scope);
|
|
60
|
+
return root ? join(root, 'plugins') : null;
|
|
61
|
+
}
|
|
62
|
+
export function marketplacesDir(scope) {
|
|
63
|
+
const root = scopeRoot(scope);
|
|
64
|
+
return root ? join(root, 'marketplaces') : null;
|
|
65
|
+
}
|
|
66
|
+
export function resolveScopeArg(scopeArg) {
|
|
67
|
+
if (scopeArg === undefined)
|
|
68
|
+
return 'all';
|
|
69
|
+
const value = scopeArg.toLowerCase();
|
|
70
|
+
if (value === 'user' || value === 'project' || value === 'all')
|
|
71
|
+
return value;
|
|
72
|
+
throw usage(`invalid --scope: ${scopeArg} (expected user|project|all)`);
|
|
73
|
+
}
|
|
74
|
+
export function listScopes(scopeArg) {
|
|
75
|
+
const v = resolveScopeArg(scopeArg);
|
|
76
|
+
if (v === 'all') {
|
|
77
|
+
const out = [];
|
|
78
|
+
if (projectScopeRoot())
|
|
79
|
+
out.push('project');
|
|
80
|
+
out.push('user');
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
return [v];
|
|
84
|
+
}
|
|
85
|
+
export function resetScopeCache() {
|
|
86
|
+
cachedProjectRoot = undefined;
|
|
87
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function planPrompt(plansDir: string): string;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
export function planPrompt(plansDir) {
|
|
2
|
+
return `# Planning workflow
|
|
3
|
+
|
|
4
|
+
You are entering a focused planning session. The goal is to produce an
|
|
5
|
+
implementation plan that another agent (or you, in a later turn) can execute
|
|
6
|
+
without re-discovering everything. A plan is a map, not a tutorial.
|
|
7
|
+
|
|
8
|
+
Plans for this directory live at:
|
|
9
|
+
${plansDir}
|
|
10
|
+
|
|
11
|
+
If a relevant prior plan already exists there, read it first.
|
|
12
|
+
|
|
13
|
+
## Phase 1: Initial Understanding
|
|
14
|
+
|
|
15
|
+
Build a comprehensive picture of the user's request and the code involved.
|
|
16
|
+
Actively search for existing functions, utilities, and patterns that can be
|
|
17
|
+
reused — do not propose new code when a suitable implementation already
|
|
18
|
+
exists.
|
|
19
|
+
|
|
20
|
+
- **Launch up to 3 Explore subagents IN PARALLEL** (single message, multiple
|
|
21
|
+
tool calls) to cover the codebase efficiently.
|
|
22
|
+
- Use 1 agent when the task is isolated to known files, the user provided
|
|
23
|
+
specific paths, or the change is small and targeted.
|
|
24
|
+
- Use multiple agents when scope is uncertain, multiple areas of the codebase
|
|
25
|
+
are involved, or you need to understand existing patterns before planning.
|
|
26
|
+
- Quality over quantity — 3 agents maximum; usually 1 is right.
|
|
27
|
+
- When using multiple agents, give each a distinct focus (existing impls,
|
|
28
|
+
related components, test patterns) so they do not duplicate work.
|
|
29
|
+
|
|
30
|
+
## Phase 2: Design
|
|
31
|
+
|
|
32
|
+
Design the implementation approach based on Phase 1 findings.
|
|
33
|
+
|
|
34
|
+
- **Default**: launch at least 1 Plan agent — it validates your understanding
|
|
35
|
+
and surfaces alternatives.
|
|
36
|
+
- **Skip agents** only for truly trivial tasks (typo fixes, single-line
|
|
37
|
+
changes, simple renames).
|
|
38
|
+
- **Multiple agents (up to 3)** for tasks that benefit from different
|
|
39
|
+
perspectives — large refactors, architectural changes, many edge cases.
|
|
40
|
+
|
|
41
|
+
In the Plan agent prompt:
|
|
42
|
+
- Provide comprehensive background context from Phase 1, including filenames
|
|
43
|
+
and code-path traces.
|
|
44
|
+
- Describe requirements and constraints.
|
|
45
|
+
- Request a detailed implementation plan.
|
|
46
|
+
|
|
47
|
+
## Phase 3: Review
|
|
48
|
+
|
|
49
|
+
- Read the critical files identified by agents to deepen your understanding.
|
|
50
|
+
- Ensure the plan aligns with the user's original request.
|
|
51
|
+
- Use **AskUserQuestion** to clarify any remaining questions with the user.
|
|
52
|
+
Bias toward asking when a decision is non-obvious — interrupting once is
|
|
53
|
+
cheaper than building the wrong thing.
|
|
54
|
+
|
|
55
|
+
**Important:** Use AskUserQuestion ONLY to clarify requirements or choose
|
|
56
|
+
between approaches. Never use it to ask the user "is this plan okay?" or
|
|
57
|
+
"should I proceed?" — the save step below is the approval moment.
|
|
58
|
+
|
|
59
|
+
## Phase 4: Final Plan
|
|
60
|
+
|
|
61
|
+
Save the plan with \`crtr plan --name <kebab-case-name>\`. Pipe the markdown
|
|
62
|
+
body in via stdin (heredoc):
|
|
63
|
+
|
|
64
|
+
\`\`\`bash
|
|
65
|
+
crtr plan --name <kebab-case-name> <<'EOF'
|
|
66
|
+
# Plan: <one-line title>
|
|
67
|
+
|
|
68
|
+
## Context
|
|
69
|
+
<why this change is being made — the problem it addresses, what prompted it,
|
|
70
|
+
and the intended outcome>
|
|
71
|
+
|
|
72
|
+
## Recommended approach
|
|
73
|
+
<your chosen approach. Include only the recommendation, not all alternatives.
|
|
74
|
+
Be concise enough to scan, detailed enough to execute.>
|
|
75
|
+
|
|
76
|
+
## Files to modify / create
|
|
77
|
+
- \`path/to/file.ts\` — <what changes>
|
|
78
|
+
- ...
|
|
79
|
+
|
|
80
|
+
## Existing utilities to reuse
|
|
81
|
+
- \`function-name\` from \`path/to/file.ts:LL\` — <why it fits>
|
|
82
|
+
|
|
83
|
+
## Verification
|
|
84
|
+
<how to test the changes end-to-end — run the code, run tests, etc.>
|
|
85
|
+
EOF
|
|
86
|
+
\`\`\`
|
|
87
|
+
|
|
88
|
+
- Pick a short, descriptive kebab-case name. Names may be nested
|
|
89
|
+
(\`crtr plan --name auth/jwt-refresh\`) — they become subdirectories.
|
|
90
|
+
- The file lands at \`${plansDir}/<name>.md\`.
|
|
91
|
+
- If you are running inside tmux, the saved plan auto-opens in a side pane
|
|
92
|
+
via termrender. No extra step needed.
|
|
93
|
+
|
|
94
|
+
## Phase 5: Done
|
|
95
|
+
|
|
96
|
+
Your turn ends after the save command succeeds. No need to summarize the plan
|
|
97
|
+
in chat — the user can read the file.
|
|
98
|
+
`;
|
|
99
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function specPrompt(specsDir: string): string;
|