@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,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,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,83 @@
|
|
|
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 mergeConfig(partial) {
|
|
59
|
+
const defaults = defaultScopeConfig();
|
|
60
|
+
const schema_version = partial.schema_version === undefined ? defaults.schema_version : partial.schema_version;
|
|
61
|
+
const marketplaces = partial.marketplaces === undefined ? {} : partial.marketplaces;
|
|
62
|
+
const plugins = partial.plugins === undefined ? {} : partial.plugins;
|
|
63
|
+
const skills = partial.skills === undefined ? {} : partial.skills;
|
|
64
|
+
const au = partial.auto_update;
|
|
65
|
+
const auto_update = {
|
|
66
|
+
crtr: au && au.crtr !== undefined ? au.crtr : defaults.auto_update.crtr,
|
|
67
|
+
content: au && au.content !== undefined ? au.content : defaults.auto_update.content,
|
|
68
|
+
interval_hours: au && au.interval_hours !== undefined ? au.interval_hours : defaults.auto_update.interval_hours,
|
|
69
|
+
};
|
|
70
|
+
return { schema_version, marketplaces, plugins, skills, auto_update };
|
|
71
|
+
}
|
|
72
|
+
export function updateConfig(scope, mutate) {
|
|
73
|
+
const cfg = readConfig(scope);
|
|
74
|
+
mutate(cfg);
|
|
75
|
+
writeConfig(scope, cfg);
|
|
76
|
+
return cfg;
|
|
77
|
+
}
|
|
78
|
+
export function updateState(scope, mutate) {
|
|
79
|
+
const s = readState(scope);
|
|
80
|
+
mutate(s);
|
|
81
|
+
writeState(scope, s);
|
|
82
|
+
return s;
|
|
83
|
+
}
|
|
@@ -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,84 @@
|
|
|
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 currentKey = null;
|
|
15
|
+
let listBuffer = null;
|
|
16
|
+
for (const raw of lines) {
|
|
17
|
+
if (!raw.trim())
|
|
18
|
+
continue;
|
|
19
|
+
if (raw.startsWith(' - ') || raw.startsWith('- ')) {
|
|
20
|
+
const value = raw.replace(/^\s*-\s+/, '').trim();
|
|
21
|
+
if (currentKey && listBuffer)
|
|
22
|
+
listBuffer.push(stripQuotes(value));
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
const idx = raw.indexOf(':');
|
|
26
|
+
if (idx === -1)
|
|
27
|
+
continue;
|
|
28
|
+
if (currentKey && listBuffer) {
|
|
29
|
+
out[currentKey] = listBuffer;
|
|
30
|
+
listBuffer = null;
|
|
31
|
+
}
|
|
32
|
+
const key = raw.slice(0, idx).trim();
|
|
33
|
+
const rest = raw.slice(idx + 1).trim();
|
|
34
|
+
currentKey = key;
|
|
35
|
+
if (rest === '') {
|
|
36
|
+
listBuffer = [];
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (rest.startsWith('[') && rest.endsWith(']')) {
|
|
40
|
+
out[key] = rest
|
|
41
|
+
.slice(1, -1)
|
|
42
|
+
.split(',')
|
|
43
|
+
.map((s) => stripQuotes(s.trim()))
|
|
44
|
+
.filter(Boolean);
|
|
45
|
+
currentKey = null;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
out[key] = stripQuotes(rest);
|
|
49
|
+
currentKey = null;
|
|
50
|
+
}
|
|
51
|
+
if (currentKey && listBuffer)
|
|
52
|
+
out[currentKey] = listBuffer;
|
|
53
|
+
const fm = {
|
|
54
|
+
name: typeof out.name === 'string' ? out.name : '',
|
|
55
|
+
description: typeof out.description === 'string' ? out.description : undefined,
|
|
56
|
+
keywords: Array.isArray(out.keywords) ? out.keywords : undefined,
|
|
57
|
+
};
|
|
58
|
+
return fm;
|
|
59
|
+
}
|
|
60
|
+
function stripQuotes(s) {
|
|
61
|
+
if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
|
|
62
|
+
return s.slice(1, -1);
|
|
63
|
+
}
|
|
64
|
+
return s;
|
|
65
|
+
}
|
|
66
|
+
export function serializeFrontmatter(data) {
|
|
67
|
+
const lines = ['---'];
|
|
68
|
+
lines.push(`name: ${quoteIfNeeded(data.name)}`);
|
|
69
|
+
if (data.description !== undefined) {
|
|
70
|
+
lines.push(`description: ${quoteIfNeeded(data.description)}`);
|
|
71
|
+
}
|
|
72
|
+
if (data.keywords && data.keywords.length) {
|
|
73
|
+
const inline = `[${data.keywords.map(quoteIfNeeded).join(', ')}]`;
|
|
74
|
+
lines.push(`keywords: ${inline}`);
|
|
75
|
+
}
|
|
76
|
+
lines.push('---');
|
|
77
|
+
return lines.join('\n') + '\n';
|
|
78
|
+
}
|
|
79
|
+
function quoteIfNeeded(s) {
|
|
80
|
+
if (/[:#\-\[\]{},&*?|<>=!%@`]/.test(s) || /^\s/.test(s) || /\s$/.test(s)) {
|
|
81
|
+
return `"${s.replace(/"/g, '\\"')}"`;
|
|
82
|
+
}
|
|
83
|
+
return s;
|
|
84
|
+
}
|
|
@@ -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;
|
|
@@ -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
|
+
}
|