@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,187 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
4
|
+
import { writeFileSync } from 'node:fs';
|
|
5
|
+
import { CRTR_DIR_NAME } from '../types.js';
|
|
6
|
+
import { ensureDir, pathExists, readText, walkFiles } from './fs-utils.js';
|
|
7
|
+
import { usage, notFound, general } from './errors.js';
|
|
8
|
+
import { out, hint, jsonOut, handleError } from './output.js';
|
|
9
|
+
export function mangleCwd(cwd = process.cwd()) {
|
|
10
|
+
return cwd.replace(/\//g, '-');
|
|
11
|
+
}
|
|
12
|
+
export function artifactsRoot(kind, cwd) {
|
|
13
|
+
return join(homedir(), CRTR_DIR_NAME, mangleCwd(cwd), kind);
|
|
14
|
+
}
|
|
15
|
+
export function sanitizeName(raw) {
|
|
16
|
+
const trimmed = raw.trim().replace(/^\/+|\/+$/g, '');
|
|
17
|
+
if (trimmed === '')
|
|
18
|
+
throw usage('name must not be empty');
|
|
19
|
+
if (trimmed.split('/').some((seg) => seg === '..' || seg === '.')) {
|
|
20
|
+
throw usage(`name must not contain "." or ".." segments: ${raw}`);
|
|
21
|
+
}
|
|
22
|
+
if (trimmed.startsWith('/') || /^[A-Za-z]:[\\/]/.test(trimmed)) {
|
|
23
|
+
throw usage(`name must not be absolute: ${raw}`);
|
|
24
|
+
}
|
|
25
|
+
return trimmed;
|
|
26
|
+
}
|
|
27
|
+
export function artifactPath(kind, name, cwd) {
|
|
28
|
+
return join(artifactsRoot(kind, cwd), `${sanitizeName(name)}.md`);
|
|
29
|
+
}
|
|
30
|
+
export function inTmux() {
|
|
31
|
+
return Boolean(process.env.TMUX);
|
|
32
|
+
}
|
|
33
|
+
export function openInTmuxPane(path) {
|
|
34
|
+
const result = spawnSync('termrender', ['--tmux', path], {
|
|
35
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
36
|
+
});
|
|
37
|
+
if (result.error) {
|
|
38
|
+
const code = result.error.code;
|
|
39
|
+
if (code === 'ENOENT') {
|
|
40
|
+
hint('termrender not found on $PATH — install it to auto-open in tmux');
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
hint(`termrender failed: ${result.error.message}`);
|
|
44
|
+
}
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (result.status !== 0) {
|
|
48
|
+
const stderrText = result.stderr.toString().trim();
|
|
49
|
+
hint(`termrender exited with ${result.status}${stderrText ? `: ${stderrText}` : ''}`);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const paneId = result.stdout.toString().trim();
|
|
53
|
+
if (paneId)
|
|
54
|
+
hint(`opened in tmux pane ${paneId}`);
|
|
55
|
+
}
|
|
56
|
+
async function readStdin() {
|
|
57
|
+
const chunks = [];
|
|
58
|
+
for await (const chunk of process.stdin) {
|
|
59
|
+
chunks.push(chunk);
|
|
60
|
+
}
|
|
61
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
62
|
+
}
|
|
63
|
+
function listArtifactNames(kind) {
|
|
64
|
+
const root = artifactsRoot(kind);
|
|
65
|
+
if (!pathExists(root))
|
|
66
|
+
return [];
|
|
67
|
+
return walkFiles(root, (n) => n.endsWith('.md'))
|
|
68
|
+
.map((abs) => abs.substring(root.length + 1).replace(/\.md$/, ''))
|
|
69
|
+
.sort();
|
|
70
|
+
}
|
|
71
|
+
export function registerArtifactCommand(program, opts) {
|
|
72
|
+
const { command, kind, promptFn } = opts;
|
|
73
|
+
const cmd = program
|
|
74
|
+
.command(`${command} [content]`)
|
|
75
|
+
.description(`print the ${command} prompt, or save a ${command} with --name`)
|
|
76
|
+
.option('--name <name>', `save the ${command} under this name`)
|
|
77
|
+
.action(async (content, options) => {
|
|
78
|
+
try {
|
|
79
|
+
if (options.name === undefined) {
|
|
80
|
+
if (content !== undefined) {
|
|
81
|
+
throw usage(`positional content requires --name (try \`crtr ${command} --name <name> ...\`)`);
|
|
82
|
+
}
|
|
83
|
+
out(promptFn(artifactsRoot(kind)));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
let body;
|
|
87
|
+
if (content !== undefined) {
|
|
88
|
+
body = content;
|
|
89
|
+
}
|
|
90
|
+
else if (!process.stdin.isTTY) {
|
|
91
|
+
body = await readStdin();
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
throw usage(`no content provided. Pipe via stdin (heredoc) or pass as a positional arg:\n` +
|
|
95
|
+
` crtr ${command} --name <name> <<'EOF'\n <content>\n EOF`);
|
|
96
|
+
}
|
|
97
|
+
if (body.trim() === '') {
|
|
98
|
+
throw usage('content is empty');
|
|
99
|
+
}
|
|
100
|
+
const filePath = artifactPath(kind, options.name);
|
|
101
|
+
ensureDir(dirname(filePath));
|
|
102
|
+
writeFileSync(filePath, body.endsWith('\n') ? body : body + '\n', 'utf8');
|
|
103
|
+
out(filePath);
|
|
104
|
+
if (inTmux())
|
|
105
|
+
openInTmuxPane(filePath);
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
handleError(e);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
cmd
|
|
112
|
+
.command('list')
|
|
113
|
+
.description(`list ${kind} for the current directory`)
|
|
114
|
+
.option('--json', 'emit JSON')
|
|
115
|
+
.action(async (options) => {
|
|
116
|
+
try {
|
|
117
|
+
const names = listArtifactNames(kind);
|
|
118
|
+
if (options.json) {
|
|
119
|
+
jsonOut({
|
|
120
|
+
[kind]: names.map((name) => ({ name, path: artifactPath(kind, name) })),
|
|
121
|
+
});
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
for (const n of names)
|
|
125
|
+
out(n);
|
|
126
|
+
}
|
|
127
|
+
catch (e) {
|
|
128
|
+
handleError(e, { json: options.json });
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
cmd
|
|
132
|
+
.command('show <name>')
|
|
133
|
+
.description(`print the body of a ${command}`)
|
|
134
|
+
.action(async (name) => {
|
|
135
|
+
try {
|
|
136
|
+
const filePath = artifactPath(kind, name);
|
|
137
|
+
if (!pathExists(filePath)) {
|
|
138
|
+
throw notFound(`${command} not found: ${name} (looked at ${filePath})`);
|
|
139
|
+
}
|
|
140
|
+
out(readText(filePath));
|
|
141
|
+
hint(`crtr: edit with \`crtr ${command} edit ${name}\` (${filePath})`);
|
|
142
|
+
}
|
|
143
|
+
catch (e) {
|
|
144
|
+
handleError(e);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
cmd
|
|
148
|
+
.command('path [name]')
|
|
149
|
+
.description(`print the absolute path of a ${command} or the ${kind} directory`)
|
|
150
|
+
.action(async (name) => {
|
|
151
|
+
try {
|
|
152
|
+
out(name === undefined ? artifactsRoot(kind) : artifactPath(kind, name));
|
|
153
|
+
}
|
|
154
|
+
catch (e) {
|
|
155
|
+
handleError(e);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
cmd
|
|
159
|
+
.command('edit <name>')
|
|
160
|
+
.description(`open the ${command} in $EDITOR`)
|
|
161
|
+
.action(async (name) => {
|
|
162
|
+
try {
|
|
163
|
+
const filePath = artifactPath(kind, name);
|
|
164
|
+
if (!pathExists(filePath)) {
|
|
165
|
+
throw notFound(`${command} not found: ${name} (looked at ${filePath})`);
|
|
166
|
+
}
|
|
167
|
+
const editor = process.env.EDITOR !== undefined && process.env.EDITOR !== ''
|
|
168
|
+
? process.env.EDITOR
|
|
169
|
+
: 'vi';
|
|
170
|
+
await new Promise((resolve, reject) => {
|
|
171
|
+
const child = spawn(editor, [filePath], { stdio: 'inherit' });
|
|
172
|
+
child.on('error', (e) => reject(general(`failed to launch editor: ${e.message}`)));
|
|
173
|
+
child.on('close', (code) => {
|
|
174
|
+
if (code !== 0 && code !== null) {
|
|
175
|
+
reject(general(`editor exited with code ${code}`));
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
resolve();
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
catch (e) {
|
|
184
|
+
handleError(e);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function maybeAutoUpdate(argv: string[]): void;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { readConfig, readState, updateState } from './config.js';
|
|
3
|
+
import { nowIso } from './fs-utils.js';
|
|
4
|
+
import { info } from './output.js';
|
|
5
|
+
import { selfCheck, contentCheck } from '../commands/update.js';
|
|
6
|
+
const HOUR_MS = 60 * 60 * 1000;
|
|
7
|
+
const SKIP_SUBCOMMANDS = new Set([
|
|
8
|
+
'update',
|
|
9
|
+
'help',
|
|
10
|
+
'--help',
|
|
11
|
+
'-h',
|
|
12
|
+
'--version',
|
|
13
|
+
'-v',
|
|
14
|
+
]);
|
|
15
|
+
function shouldSkipForArgv(argv) {
|
|
16
|
+
const sub = argv[2];
|
|
17
|
+
if (sub === undefined)
|
|
18
|
+
return true;
|
|
19
|
+
return SKIP_SUBCOMMANDS.has(sub);
|
|
20
|
+
}
|
|
21
|
+
function withinInterval(lastIso, intervalHours) {
|
|
22
|
+
if (!lastIso)
|
|
23
|
+
return false;
|
|
24
|
+
const last = Date.parse(lastIso);
|
|
25
|
+
if (!Number.isFinite(last))
|
|
26
|
+
return false;
|
|
27
|
+
return Date.now() - last < intervalHours * HOUR_MS;
|
|
28
|
+
}
|
|
29
|
+
function spawnDetachedSelfUpdate() {
|
|
30
|
+
const child = spawn('npm', ['i', '-g', '@crouton-kit/crtr@latest'], {
|
|
31
|
+
detached: true,
|
|
32
|
+
stdio: 'ignore',
|
|
33
|
+
});
|
|
34
|
+
child.unref();
|
|
35
|
+
}
|
|
36
|
+
function spawnDetachedContentUpdate() {
|
|
37
|
+
const child = spawn('crtr', ['update', '--content'], {
|
|
38
|
+
detached: true,
|
|
39
|
+
stdio: 'ignore',
|
|
40
|
+
});
|
|
41
|
+
child.unref();
|
|
42
|
+
}
|
|
43
|
+
export function maybeAutoUpdate(argv) {
|
|
44
|
+
try {
|
|
45
|
+
if (process.env.CRTR_NO_AUTO_UPDATE === '1')
|
|
46
|
+
return;
|
|
47
|
+
if (shouldSkipForArgv(argv))
|
|
48
|
+
return;
|
|
49
|
+
const cfg = readConfig('user');
|
|
50
|
+
const { crtr, content, interval_hours } = cfg.auto_update;
|
|
51
|
+
if (crtr === false && content === false)
|
|
52
|
+
return;
|
|
53
|
+
const state = readState('user');
|
|
54
|
+
if (state.last_self_check === undefined) {
|
|
55
|
+
updateState('user', (s) => {
|
|
56
|
+
s.last_self_check = nowIso();
|
|
57
|
+
});
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (withinInterval(state.last_self_check, interval_hours))
|
|
61
|
+
return;
|
|
62
|
+
updateState('user', (s) => {
|
|
63
|
+
s.last_self_check = nowIso();
|
|
64
|
+
});
|
|
65
|
+
if (crtr === 'notify') {
|
|
66
|
+
selfCheck();
|
|
67
|
+
}
|
|
68
|
+
else if (crtr === 'apply') {
|
|
69
|
+
info('applying self-update in background');
|
|
70
|
+
spawnDetachedSelfUpdate();
|
|
71
|
+
}
|
|
72
|
+
if (content === 'notify') {
|
|
73
|
+
contentCheck();
|
|
74
|
+
}
|
|
75
|
+
else if (content === 'apply') {
|
|
76
|
+
info('applying content updates in background');
|
|
77
|
+
spawnDetachedContentUpdate();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
if (process.env.CRTR_DEBUG === '1') {
|
|
82
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
83
|
+
process.stderr.write(`crtr: auto-update hook error: ${msg}\n`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Scope, ScopeConfig, ScopeState } from '../types.js';
|
|
2
|
+
export declare function configPath(scope: Scope): string | null;
|
|
3
|
+
export declare function statePath(scope: Scope): string | null;
|
|
4
|
+
export declare function readConfig(scope: Scope): ScopeConfig;
|
|
5
|
+
export declare function readState(scope: Scope): ScopeState;
|
|
6
|
+
export declare function writeConfig(scope: Scope, config: ScopeConfig): void;
|
|
7
|
+
export declare function writeState(scope: Scope, state: ScopeState): void;
|
|
8
|
+
export declare function ensureScopeInitialized(scope: Scope, root: string): void;
|
|
9
|
+
export declare function updateConfig(scope: Scope, mutate: (cfg: ScopeConfig) => void): ScopeConfig;
|
|
10
|
+
export declare function updateState(scope: Scope, mutate: (s: ScopeState) => void): ScopeState;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { CONFIG_FILE, STATE_FILE, defaultScopeConfig, defaultScopeState } from '../types.js';
|
|
3
|
+
import { readJsonIfExists, writeJson, ensureDir } from './fs-utils.js';
|
|
4
|
+
import { scopeRoot, requireScopeRoot } from './scope.js';
|
|
5
|
+
function configPathFor(root) {
|
|
6
|
+
return join(root, CONFIG_FILE);
|
|
7
|
+
}
|
|
8
|
+
function statePathFor(root) {
|
|
9
|
+
return join(root, STATE_FILE);
|
|
10
|
+
}
|
|
11
|
+
export function configPath(scope) {
|
|
12
|
+
const root = scopeRoot(scope);
|
|
13
|
+
return root ? configPathFor(root) : null;
|
|
14
|
+
}
|
|
15
|
+
export function statePath(scope) {
|
|
16
|
+
const root = scopeRoot(scope);
|
|
17
|
+
return root ? statePathFor(root) : null;
|
|
18
|
+
}
|
|
19
|
+
export function readConfig(scope) {
|
|
20
|
+
const root = scopeRoot(scope);
|
|
21
|
+
if (!root)
|
|
22
|
+
return defaultScopeConfig();
|
|
23
|
+
const existing = readJsonIfExists(configPathFor(root));
|
|
24
|
+
if (!existing)
|
|
25
|
+
return defaultScopeConfig();
|
|
26
|
+
return mergeConfig(existing);
|
|
27
|
+
}
|
|
28
|
+
export function readState(scope) {
|
|
29
|
+
const root = scopeRoot(scope);
|
|
30
|
+
if (!root)
|
|
31
|
+
return defaultScopeState();
|
|
32
|
+
const existing = readJsonIfExists(statePathFor(root));
|
|
33
|
+
if (!existing)
|
|
34
|
+
return defaultScopeState();
|
|
35
|
+
return {
|
|
36
|
+
marketplaces: existing.marketplaces ?? {},
|
|
37
|
+
plugins: existing.plugins ?? {},
|
|
38
|
+
last_self_check: existing.last_self_check,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export function writeConfig(scope, config) {
|
|
42
|
+
const root = requireScopeRoot(scope);
|
|
43
|
+
ensureDir(root);
|
|
44
|
+
writeJson(configPathFor(root), config);
|
|
45
|
+
}
|
|
46
|
+
export function writeState(scope, state) {
|
|
47
|
+
const root = requireScopeRoot(scope);
|
|
48
|
+
ensureDir(root);
|
|
49
|
+
writeJson(statePathFor(root), state);
|
|
50
|
+
}
|
|
51
|
+
export function ensureScopeInitialized(scope, root) {
|
|
52
|
+
ensureDir(root);
|
|
53
|
+
const cfgPath = configPathFor(root);
|
|
54
|
+
if (!readJsonIfExists(cfgPath)) {
|
|
55
|
+
writeJson(cfgPath, defaultScopeConfig());
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function normalizeMode(value, fallback) {
|
|
59
|
+
if (value === true)
|
|
60
|
+
return 'notify';
|
|
61
|
+
if (value === false)
|
|
62
|
+
return false;
|
|
63
|
+
if (value === 'notify' || value === 'apply')
|
|
64
|
+
return value;
|
|
65
|
+
return fallback;
|
|
66
|
+
}
|
|
67
|
+
function mergeConfig(partial) {
|
|
68
|
+
const defaults = defaultScopeConfig();
|
|
69
|
+
const schema_version = partial.schema_version === undefined ? defaults.schema_version : partial.schema_version;
|
|
70
|
+
const marketplaces = partial.marketplaces === undefined ? {} : partial.marketplaces;
|
|
71
|
+
const plugins = partial.plugins === undefined ? {} : partial.plugins;
|
|
72
|
+
const skills = partial.skills === undefined ? {} : partial.skills;
|
|
73
|
+
const au = partial.auto_update;
|
|
74
|
+
const rawInterval = au && typeof au.interval_hours === 'number' ? au.interval_hours : undefined;
|
|
75
|
+
const interval_hours = rawInterval !== undefined && Number.isFinite(rawInterval) && rawInterval >= 0
|
|
76
|
+
? rawInterval
|
|
77
|
+
: defaults.auto_update.interval_hours;
|
|
78
|
+
const auto_update = {
|
|
79
|
+
crtr: normalizeMode(au?.crtr, defaults.auto_update.crtr),
|
|
80
|
+
content: normalizeMode(au?.content, defaults.auto_update.content),
|
|
81
|
+
interval_hours,
|
|
82
|
+
};
|
|
83
|
+
return { schema_version, marketplaces, plugins, skills, auto_update };
|
|
84
|
+
}
|
|
85
|
+
export function updateConfig(scope, mutate) {
|
|
86
|
+
const cfg = readConfig(scope);
|
|
87
|
+
mutate(cfg);
|
|
88
|
+
writeConfig(scope, cfg);
|
|
89
|
+
return cfg;
|
|
90
|
+
}
|
|
91
|
+
export function updateState(scope, mutate) {
|
|
92
|
+
const s = readState(scope);
|
|
93
|
+
mutate(s);
|
|
94
|
+
writeState(scope, s);
|
|
95
|
+
return s;
|
|
96
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type ExitCodeValue } from '../types.js';
|
|
2
|
+
export declare class CrtrError extends Error {
|
|
3
|
+
code: string;
|
|
4
|
+
exitCode: ExitCodeValue;
|
|
5
|
+
details?: Record<string, unknown>;
|
|
6
|
+
constructor(code: string, message: string, exitCode?: ExitCodeValue, details?: Record<string, unknown>);
|
|
7
|
+
}
|
|
8
|
+
export declare function notFound(message: string, details?: Record<string, unknown>): CrtrError;
|
|
9
|
+
export declare function usage(message: string, details?: Record<string, unknown>): CrtrError;
|
|
10
|
+
export declare function ambiguous(message: string, details?: Record<string, unknown>): CrtrError;
|
|
11
|
+
export declare function network(message: string, details?: Record<string, unknown>): CrtrError;
|
|
12
|
+
export declare function general(message: string, details?: Record<string, unknown>): CrtrError;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ExitCode } from '../types.js';
|
|
2
|
+
export class CrtrError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
exitCode;
|
|
5
|
+
details;
|
|
6
|
+
constructor(code, message, exitCode = ExitCode.GENERAL, details) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = 'CrtrError';
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.exitCode = exitCode;
|
|
11
|
+
this.details = details;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function notFound(message, details) {
|
|
15
|
+
return new CrtrError('not_found', message, ExitCode.NOT_FOUND, details);
|
|
16
|
+
}
|
|
17
|
+
export function usage(message, details) {
|
|
18
|
+
return new CrtrError('usage', message, ExitCode.USAGE, details);
|
|
19
|
+
}
|
|
20
|
+
export function ambiguous(message, details) {
|
|
21
|
+
return new CrtrError('ambiguous', message, ExitCode.AMBIGUOUS, details);
|
|
22
|
+
}
|
|
23
|
+
export function network(message, details) {
|
|
24
|
+
return new CrtrError('network', message, ExitCode.NETWORK, details);
|
|
25
|
+
}
|
|
26
|
+
export function general(message, details) {
|
|
27
|
+
return new CrtrError('error', message, ExitCode.GENERAL, details);
|
|
28
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { SkillFrontmatter } from '../types.js';
|
|
2
|
+
export interface ParsedFrontmatter {
|
|
3
|
+
data: SkillFrontmatter | null;
|
|
4
|
+
body: string;
|
|
5
|
+
raw: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function parseFrontmatter(source: string): ParsedFrontmatter;
|
|
8
|
+
export declare function serializeFrontmatter(data: SkillFrontmatter): string;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
const FRONTMATTER_RE = /^---\s*\r?\n([\s\S]*?)\r?\n---\s*\r?\n?/;
|
|
2
|
+
export function parseFrontmatter(source) {
|
|
3
|
+
const match = source.match(FRONTMATTER_RE);
|
|
4
|
+
if (!match) {
|
|
5
|
+
return { data: null, body: source, raw: '' };
|
|
6
|
+
}
|
|
7
|
+
const raw = match[1];
|
|
8
|
+
const body = source.slice(match[0].length);
|
|
9
|
+
return { data: parseSimpleYaml(raw), body, raw };
|
|
10
|
+
}
|
|
11
|
+
function parseSimpleYaml(yaml) {
|
|
12
|
+
const lines = yaml.split(/\r?\n/);
|
|
13
|
+
const out = {};
|
|
14
|
+
let i = 0;
|
|
15
|
+
while (i < lines.length) {
|
|
16
|
+
const raw = lines[i];
|
|
17
|
+
if (!raw.trim()) {
|
|
18
|
+
i++;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
const idx = raw.indexOf(':');
|
|
22
|
+
if (idx === -1) {
|
|
23
|
+
i++;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const key = raw.slice(0, idx).trim();
|
|
27
|
+
const rest = raw.slice(idx + 1).trim();
|
|
28
|
+
// Block scalar: `key: |` or `key: >` with optional chomp indicator (-/+)
|
|
29
|
+
const blockMatch = rest.match(/^([|>])([-+]?)\s*$/);
|
|
30
|
+
if (blockMatch) {
|
|
31
|
+
const style = blockMatch[1];
|
|
32
|
+
const chomp = blockMatch[2];
|
|
33
|
+
const collected = [];
|
|
34
|
+
let blockIndent = null;
|
|
35
|
+
let j = i + 1;
|
|
36
|
+
while (j < lines.length) {
|
|
37
|
+
const r = lines[j];
|
|
38
|
+
if (r.trim() === '') {
|
|
39
|
+
collected.push('');
|
|
40
|
+
j++;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const ind = r.match(/^(\s*)/)?.[1].length ?? 0;
|
|
44
|
+
if (blockIndent === null) {
|
|
45
|
+
if (ind === 0)
|
|
46
|
+
break;
|
|
47
|
+
blockIndent = ind;
|
|
48
|
+
}
|
|
49
|
+
if (ind < blockIndent)
|
|
50
|
+
break;
|
|
51
|
+
collected.push(r.slice(blockIndent));
|
|
52
|
+
j++;
|
|
53
|
+
}
|
|
54
|
+
while (collected.length > 0 && collected[collected.length - 1] === '')
|
|
55
|
+
collected.pop();
|
|
56
|
+
let value;
|
|
57
|
+
if (style === '|') {
|
|
58
|
+
value = collected.join('\n');
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
const parts = [];
|
|
62
|
+
let para = [];
|
|
63
|
+
for (const ln of collected) {
|
|
64
|
+
if (ln === '') {
|
|
65
|
+
if (para.length > 0) {
|
|
66
|
+
parts.push(para.join(' '));
|
|
67
|
+
para = [];
|
|
68
|
+
}
|
|
69
|
+
parts.push('');
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
para.push(ln);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (para.length > 0)
|
|
76
|
+
parts.push(para.join(' '));
|
|
77
|
+
const folded = [];
|
|
78
|
+
for (let k = 0; k < parts.length; k++) {
|
|
79
|
+
if (parts[k] === '' && (k === 0 || parts[k - 1] === ''))
|
|
80
|
+
continue;
|
|
81
|
+
folded.push(parts[k]);
|
|
82
|
+
}
|
|
83
|
+
value = folded.join('\n').replace(/\n+$/, '');
|
|
84
|
+
}
|
|
85
|
+
if (chomp !== '+')
|
|
86
|
+
value = value.replace(/\n+$/, '');
|
|
87
|
+
out[key] = value;
|
|
88
|
+
i = j;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
// Empty value: could be a list on subsequent lines
|
|
92
|
+
if (rest === '') {
|
|
93
|
+
const buf = [];
|
|
94
|
+
let j = i + 1;
|
|
95
|
+
while (j < lines.length) {
|
|
96
|
+
const r = lines[j];
|
|
97
|
+
if (r.trim() === '') {
|
|
98
|
+
j++;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (/^\s*-\s+/.test(r)) {
|
|
102
|
+
buf.push(stripQuotes(r.replace(/^\s*-\s+/, '').trim()));
|
|
103
|
+
j++;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
if (buf.length > 0)
|
|
109
|
+
out[key] = buf;
|
|
110
|
+
i = j;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (rest.startsWith('[') && rest.endsWith(']')) {
|
|
114
|
+
out[key] = rest
|
|
115
|
+
.slice(1, -1)
|
|
116
|
+
.split(',')
|
|
117
|
+
.map((s) => stripQuotes(s.trim()))
|
|
118
|
+
.filter(Boolean);
|
|
119
|
+
i++;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
out[key] = stripQuotes(rest);
|
|
123
|
+
i++;
|
|
124
|
+
}
|
|
125
|
+
const fm = {
|
|
126
|
+
name: typeof out.name === 'string' ? out.name : '',
|
|
127
|
+
description: typeof out.description === 'string' ? out.description : undefined,
|
|
128
|
+
keywords: Array.isArray(out.keywords) ? out.keywords : undefined,
|
|
129
|
+
};
|
|
130
|
+
return fm;
|
|
131
|
+
}
|
|
132
|
+
function stripQuotes(s) {
|
|
133
|
+
if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
|
|
134
|
+
return s.slice(1, -1);
|
|
135
|
+
}
|
|
136
|
+
return s;
|
|
137
|
+
}
|
|
138
|
+
export function serializeFrontmatter(data) {
|
|
139
|
+
const lines = ['---'];
|
|
140
|
+
lines.push(`name: ${quoteIfNeeded(data.name)}`);
|
|
141
|
+
if (data.description !== undefined) {
|
|
142
|
+
lines.push(`description: ${quoteIfNeeded(data.description)}`);
|
|
143
|
+
}
|
|
144
|
+
if (data.keywords && data.keywords.length) {
|
|
145
|
+
const inline = `[${data.keywords.map(quoteIfNeeded).join(', ')}]`;
|
|
146
|
+
lines.push(`keywords: ${inline}`);
|
|
147
|
+
}
|
|
148
|
+
lines.push('---');
|
|
149
|
+
return lines.join('\n') + '\n';
|
|
150
|
+
}
|
|
151
|
+
function quoteIfNeeded(s) {
|
|
152
|
+
if (/[:#\-\[\]{},&*?|<>=!%@`]/.test(s) || /^\s/.test(s) || /\s$/.test(s)) {
|
|
153
|
+
return `"${s.replace(/"/g, '\\"')}"`;
|
|
154
|
+
}
|
|
155
|
+
return s;
|
|
156
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare function ensureDir(dir: string): void;
|
|
2
|
+
export declare function writeJson(path: string, data: unknown): void;
|
|
3
|
+
export declare function readJson<T = unknown>(path: string): T;
|
|
4
|
+
export declare function readJsonIfExists<T = unknown>(path: string): T | null;
|
|
5
|
+
export declare function readTextIfExists(path: string): string | null;
|
|
6
|
+
export declare function readText(path: string): string;
|
|
7
|
+
export declare function isDir(path: string): boolean;
|
|
8
|
+
export declare function isSymlink(path: string): boolean;
|
|
9
|
+
export declare function pathExists(path: string): boolean;
|
|
10
|
+
export declare function listDirs(path: string): string[];
|
|
11
|
+
export declare function listEntries(path: string): string[];
|
|
12
|
+
export declare function removePath(path: string): void;
|
|
13
|
+
export declare function linkOrCopy(target: string, linkPath: string, opts?: {
|
|
14
|
+
noSymlink?: boolean;
|
|
15
|
+
}): 'symlink' | 'copy';
|
|
16
|
+
export declare function readSymlinkTarget(path: string): string | null;
|
|
17
|
+
export declare function walkFiles(root: string, predicate?: (name: string) => boolean): string[];
|
|
18
|
+
export declare function nowIso(): string;
|