@cmodjs/core 0.1.13 → 0.2.0
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/docs/commands.md +31 -19
- package/docs/index.md +14 -4
- package/docs/install-steps.md +30 -4
- package/docs/jobs.md +25 -9
- package/docs/mod.md +103 -37
- package/docs/options.md +93 -0
- package/docs/permissions.md +88 -0
- package/docs/state.md +4 -31
- package/docs/testing.md +13 -2
- package/installer.d.ts +10 -0
- package/installer.d.ts.map +1 -0
- package/installer.js +8 -0
- package/jobs/context.d.ts +2 -2
- package/jobs/context.d.ts.map +1 -1
- package/jobs/context.js +5 -3
- package/jobs/permissions.js +1 -1
- package/jobs/program.d.ts.map +1 -1
- package/jobs/program.js +5 -1
- package/jobs/prompt.js +5 -5
- package/jobs/schedule.d.ts.map +1 -1
- package/jobs/schedule.js +2 -1
- package/jobs/slash-command.d.ts.map +1 -1
- package/jobs/slash-command.js +3 -2
- package/jobs/status-line.d.ts.map +1 -1
- package/jobs/status-line.js +3 -2
- package/jobs/tool.d.ts.map +1 -1
- package/jobs/tool.js +3 -2
- package/mod.d.ts +48 -10
- package/mod.d.ts.map +1 -1
- package/mod.js +3 -0
- package/options.d.ts +58 -0
- package/options.d.ts.map +1 -0
- package/options.js +66 -0
- package/package.json +1 -1
- package/records.d.ts +14 -1
- package/records.d.ts.map +1 -1
- package/records.js +106 -7
- package/register.d.ts +4 -2
- package/register.d.ts.map +1 -1
- package/register.js +8 -2
- package/runtime/claude.d.ts +5 -1
- package/runtime/claude.d.ts.map +1 -1
- package/runtime/grants.d.ts +28 -0
- package/runtime/grants.d.ts.map +1 -0
- package/runtime/grants.js +286 -0
- package/runtime/installer.d.ts +23 -0
- package/runtime/installer.d.ts.map +1 -0
- package/runtime/installer.js +113 -0
- package/runtime/lifecycle.d.ts +6 -3
- package/runtime/lifecycle.d.ts.map +1 -1
- package/runtime/lifecycle.js +220 -36
- package/runtime/metadata.d.ts +20 -0
- package/runtime/metadata.d.ts.map +1 -0
- package/runtime/metadata.js +143 -0
- package/runtime/options.d.ts +21 -0
- package/runtime/options.d.ts.map +1 -0
- package/runtime/options.js +100 -0
- package/runtime/state.d.ts +1 -0
- package/runtime/state.d.ts.map +1 -1
- package/runtime/state.js +5 -75
- package/src/installer.ts +17 -0
- package/src/jobs/context.ts +5 -3
- package/src/jobs/permissions.ts +1 -1
- package/src/jobs/program.ts +4 -1
- package/src/jobs/prompt.ts +5 -5
- package/src/jobs/schedule.ts +2 -1
- package/src/jobs/slash-command.ts +3 -2
- package/src/jobs/status-line.ts +3 -2
- package/src/jobs/tool.ts +3 -2
- package/src/mod.ts +46 -11
- package/src/options.ts +91 -0
- package/src/records.ts +117 -8
- package/src/register.ts +12 -3
- package/src/runtime/claude.ts +2 -0
- package/src/runtime/grants.ts +306 -0
- package/src/runtime/installer.ts +163 -0
- package/src/runtime/lifecycle.ts +220 -45
- package/src/runtime/metadata.ts +153 -0
- package/src/runtime/options.ts +102 -0
- package/src/runtime/state.ts +7 -70
- package/src/testing/fake-claude.ts +6 -1
- package/src/testing/fake-files.ts +9 -3
- package/src/testing.ts +40 -15
- package/src/utils/metadata.ts +242 -0
- package/testing/fake-claude.d.ts +4 -0
- package/testing/fake-claude.d.ts.map +1 -1
- package/testing/fake-claude.js +5 -1
- package/testing/fake-files.d.ts.map +1 -1
- package/testing/fake-files.js +9 -3
- package/testing.d.ts +6 -2
- package/testing.d.ts.map +1 -1
- package/testing.js +29 -13
- package/utils/metadata.d.ts +21 -0
- package/utils/metadata.d.ts.map +1 -0
- package/utils/metadata.js +250 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
export class MetadataError extends Error {
|
|
2
|
+
line;
|
|
3
|
+
constructor(line, message) {
|
|
4
|
+
super(`line ${line}: ${message}`);
|
|
5
|
+
this.line = line;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
const markdown = ['md', 'markdown', 'mdx'];
|
|
9
|
+
const hashComments = ['sh', 'bash', 'zsh', 'fish', 'py', 'rb', 'pl', 'r', 'yaml', 'yml', 'toml', 'ps1', 'nu', 'tcl', 'mk', 'conf', 'cfg', 'env'];
|
|
10
|
+
const slashComments = ['ts', 'tsx', 'mts', 'cts', 'js', 'jsx', 'mjs', 'cjs', 'go', 'rs', 'swift', 'kt', 'kts', 'java', 'c', 'h', 'cc', 'cpp', 'hpp', 'cs', 'scala', 'dart', 'zig', 'jsonc', 'json5'];
|
|
11
|
+
const dashComments = ['lua', 'sql', 'hs', 'elm'];
|
|
12
|
+
const hashCommentNames = ['Makefile', 'Dockerfile', 'Brewfile', 'Gemfile', 'Rakefile', 'Justfile', 'justfile'];
|
|
13
|
+
export const sidecarSuffix = '.meta';
|
|
14
|
+
export function formatOf(path, firstLine) {
|
|
15
|
+
const name = path.slice(path.lastIndexOf('/') + 1);
|
|
16
|
+
const dot = name.lastIndexOf('.');
|
|
17
|
+
const extension = dot <= 0 ? '' : name.slice(dot + 1).toLowerCase();
|
|
18
|
+
if (markdown.includes(extension))
|
|
19
|
+
return { kind: 'frontmatter' };
|
|
20
|
+
if (hashComments.includes(extension) || hashCommentNames.includes(name))
|
|
21
|
+
return { kind: 'comment', prefix: '#' };
|
|
22
|
+
if (slashComments.includes(extension))
|
|
23
|
+
return { kind: 'comment', prefix: '//' };
|
|
24
|
+
if (dashComments.includes(extension))
|
|
25
|
+
return { kind: 'comment', prefix: '--' };
|
|
26
|
+
if (extension === '' && firstLine?.startsWith('#!') === true)
|
|
27
|
+
return { kind: 'comment', prefix: '#' };
|
|
28
|
+
return { kind: 'sidecar' };
|
|
29
|
+
}
|
|
30
|
+
export function readMetadata(text, format) {
|
|
31
|
+
const lines = text.split('\n');
|
|
32
|
+
if (format.kind === 'comment') {
|
|
33
|
+
const region = commentRegion(lines, format.prefix);
|
|
34
|
+
return { metadata: region === undefined ? {} : asRecord(mapEntries(region.lines, region.start + 2, 0)) };
|
|
35
|
+
}
|
|
36
|
+
const region = format.kind === 'frontmatter' ? frontmatterRegion(lines) : { start: -1, end: lines.length, lines };
|
|
37
|
+
if (region === undefined)
|
|
38
|
+
return { metadata: {} };
|
|
39
|
+
const block = metadataBlock(region);
|
|
40
|
+
const name = format.kind === 'frontmatter' ? topLevelScalar(region, 'name') : undefined;
|
|
41
|
+
return { metadata: block === undefined ? {} : asRecord(block.entries), ...(name === undefined ? {} : { name }) };
|
|
42
|
+
}
|
|
43
|
+
export function writeMetadata(text, format, metadata) {
|
|
44
|
+
const lines = text.split('\n');
|
|
45
|
+
const entries = Object.entries(metadata);
|
|
46
|
+
if (format.kind === 'comment') {
|
|
47
|
+
const { prefix } = format;
|
|
48
|
+
const block = entries.length === 0 ? [] : [`${prefix} /// metadata`, ...entries.map(([key, value]) => `${prefix} ${scalar(key)}: ${scalar(value)}`), `${prefix} ///`];
|
|
49
|
+
const region = commentRegion(lines, prefix);
|
|
50
|
+
if (region !== undefined)
|
|
51
|
+
return [...lines.slice(0, region.start), ...block, ...lines.slice(region.end + 1)].join('\n');
|
|
52
|
+
if (block.length === 0)
|
|
53
|
+
return text;
|
|
54
|
+
const at = lines[0]?.startsWith('#!') === true ? 1 : 0;
|
|
55
|
+
return [...lines.slice(0, at), ...block, ...lines.slice(at)].join('\n');
|
|
56
|
+
}
|
|
57
|
+
const map = entries.length === 0 ? [] : ['metadata:', ...entries.map(([key, value]) => ` ${scalar(key)}: ${scalar(value)}`)];
|
|
58
|
+
if (format.kind === 'sidecar') {
|
|
59
|
+
const block = metadataBlock({ start: -1, end: lines.length, lines });
|
|
60
|
+
const rest = block === undefined ? lines : [...lines.slice(0, block.start), ...lines.slice(block.end)];
|
|
61
|
+
const kept = rest.join('\n').trim();
|
|
62
|
+
return [...(kept === '' ? [] : [kept]), ...map].join('\n') + (map.length === 0 && kept === '' ? '' : '\n');
|
|
63
|
+
}
|
|
64
|
+
const region = frontmatterRegion(lines);
|
|
65
|
+
if (region === undefined)
|
|
66
|
+
return map.length === 0 ? text : ['---', ...map, '---', ...lines].join('\n');
|
|
67
|
+
const block = metadataBlock(region);
|
|
68
|
+
const inner = block === undefined ? [...region.lines, ...map] : [...lines.slice(region.start + 1, block.start), ...map, ...lines.slice(block.end, region.end)];
|
|
69
|
+
if (inner.every((line) => line.trim() === ''))
|
|
70
|
+
return lines.slice(region.end + 1).join('\n');
|
|
71
|
+
return [...lines.slice(0, region.start + 1), ...inner, ...lines.slice(region.end)].join('\n');
|
|
72
|
+
}
|
|
73
|
+
function frontmatterRegion(lines) {
|
|
74
|
+
if (lines[0]?.replace(/^\uFEFF/, '').trimEnd() !== '---')
|
|
75
|
+
return undefined;
|
|
76
|
+
const end = lines.findIndex((line, index) => index > 0 && line.trimEnd() === '---');
|
|
77
|
+
if (end === -1)
|
|
78
|
+
throw new MetadataError(1, 'the frontmatter starts with --- and never ends. Close it with a --- line.');
|
|
79
|
+
return { start: 0, end, lines: lines.slice(1, end) };
|
|
80
|
+
}
|
|
81
|
+
function commentRegion(lines, prefix) {
|
|
82
|
+
const start = lines.findIndex((line) => line.trimEnd() === `${prefix} /// metadata`);
|
|
83
|
+
if (start === -1)
|
|
84
|
+
return undefined;
|
|
85
|
+
const content = [];
|
|
86
|
+
for (let index = start + 1; index < lines.length; index += 1) {
|
|
87
|
+
const line = (lines[index] ?? '').trimEnd();
|
|
88
|
+
if (line === `${prefix} ///`)
|
|
89
|
+
return { start, end: index, lines: content };
|
|
90
|
+
if (line !== prefix && !line.startsWith(`${prefix} `))
|
|
91
|
+
break;
|
|
92
|
+
content.push(line.slice(prefix.length + 1));
|
|
93
|
+
}
|
|
94
|
+
throw new MetadataError(start + 1, `the metadata block never ends. Close it with a "${prefix} ///" line.`);
|
|
95
|
+
}
|
|
96
|
+
function metadataBlock(region) {
|
|
97
|
+
const at = region.lines.findIndex((line) => /^metadata\s*:/.test(line));
|
|
98
|
+
if (at === -1)
|
|
99
|
+
return undefined;
|
|
100
|
+
const lineNumber = region.start + 2 + at;
|
|
101
|
+
const rest = withoutComment((region.lines[at] ?? '').replace(/^metadata\s*:/, '')).trim();
|
|
102
|
+
const start = region.start + 1 + at;
|
|
103
|
+
if (rest.startsWith('{'))
|
|
104
|
+
return { start, end: start + 1, entries: flowEntries(rest, lineNumber) };
|
|
105
|
+
if (rest !== '')
|
|
106
|
+
throw new MetadataError(lineNumber, 'metadata holds a single value. Write a map of names to text, such as metadata: { mymod.key: value }.');
|
|
107
|
+
let index = at + 1;
|
|
108
|
+
while (index < region.lines.length && (/^\s/.test(region.lines[index] ?? '') || (region.lines[index] ?? '').trim() === ''))
|
|
109
|
+
index += 1;
|
|
110
|
+
while (index > at + 1 && (region.lines[index - 1] ?? '').trim() === '')
|
|
111
|
+
index -= 1;
|
|
112
|
+
const children = region.lines.slice(at + 1, index);
|
|
113
|
+
return { start, end: region.start + 1 + index, entries: mapEntries(children, lineNumber + 1, indentOf(children)) };
|
|
114
|
+
}
|
|
115
|
+
function indentOf(lines) {
|
|
116
|
+
const first = lines.find((line) => line.trim() !== '' && !line.trim().startsWith('#'));
|
|
117
|
+
return first === undefined ? 0 : (/^\s*/.exec(first)?.[0].length ?? 0);
|
|
118
|
+
}
|
|
119
|
+
function mapEntries(lines, firstLine, indent) {
|
|
120
|
+
const entries = [];
|
|
121
|
+
lines.forEach((raw, index) => {
|
|
122
|
+
const lineNumber = firstLine + index;
|
|
123
|
+
const line = withoutComment(raw);
|
|
124
|
+
if (line.trim() === '')
|
|
125
|
+
return;
|
|
126
|
+
const depth = /^\s*/.exec(line)?.[0].length ?? 0;
|
|
127
|
+
if (depth !== indent)
|
|
128
|
+
throw new MetadataError(lineNumber, 'a metadata value holds a list or a map. Each value is one line of text, and a list is one space-separated text.');
|
|
129
|
+
const body = line.trim();
|
|
130
|
+
if (body.startsWith('- '))
|
|
131
|
+
throw new MetadataError(lineNumber, 'metadata holds a list. Write a map of names to text, and a list as one space-separated text.');
|
|
132
|
+
entries.push(entryOf(body, lineNumber));
|
|
133
|
+
});
|
|
134
|
+
return entries;
|
|
135
|
+
}
|
|
136
|
+
function flowEntries(text, lineNumber) {
|
|
137
|
+
if (!text.endsWith('}'))
|
|
138
|
+
throw new MetadataError(lineNumber, 'the metadata map starts with { and does not end on the same line. Close it with }, or write one entry per line.');
|
|
139
|
+
const body = text.slice(1, -1).trim();
|
|
140
|
+
if (body === '')
|
|
141
|
+
return [];
|
|
142
|
+
return splitOutsideQuotes(body, ',', lineNumber)
|
|
143
|
+
.map((part) => part.trim())
|
|
144
|
+
.filter((part) => part !== '')
|
|
145
|
+
.map((part) => entryOf(part, lineNumber));
|
|
146
|
+
}
|
|
147
|
+
function entryOf(text, lineNumber) {
|
|
148
|
+
const [keyText, ...rest] = splitOutsideQuotes(text, ':', lineNumber, true);
|
|
149
|
+
if (keyText === undefined || rest.length === 0)
|
|
150
|
+
throw new MetadataError(lineNumber, `"${text}" has no ":". Write each entry as name: value.`);
|
|
151
|
+
const valueText = rest.join(':').trim();
|
|
152
|
+
if (valueText.startsWith('{') || valueText.startsWith('[') || valueText === '|' || valueText === '>')
|
|
153
|
+
throw new MetadataError(lineNumber, `${keyText.trim()} holds a list or a map. Each value is one line of text, and a list is one space-separated text.`);
|
|
154
|
+
return { key: unquoted(keyText.trim(), lineNumber), value: unquoted(valueText, lineNumber) };
|
|
155
|
+
}
|
|
156
|
+
function splitOutsideQuotes(text, separator, lineNumber, needsSpace = false) {
|
|
157
|
+
const parts = [];
|
|
158
|
+
let quote;
|
|
159
|
+
let current = '';
|
|
160
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
161
|
+
const char = text[index];
|
|
162
|
+
if (quote !== undefined) {
|
|
163
|
+
current += char;
|
|
164
|
+
if (char === '\\' && quote === '"') {
|
|
165
|
+
current += text[index + 1] ?? '';
|
|
166
|
+
index += 1;
|
|
167
|
+
}
|
|
168
|
+
else if (char === quote)
|
|
169
|
+
quote = undefined;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const before = current.trimEnd().at(-1);
|
|
173
|
+
if ((char === '"' || char === "'") && (before === undefined || before === ':'))
|
|
174
|
+
quote = char;
|
|
175
|
+
const next = text[index + 1];
|
|
176
|
+
if (char === separator && (!needsSpace || next === undefined || next === ' ' || next === '\t')) {
|
|
177
|
+
parts.push(current);
|
|
178
|
+
current = '';
|
|
179
|
+
if (needsSpace) {
|
|
180
|
+
parts.push(text.slice(index + 1));
|
|
181
|
+
return parts;
|
|
182
|
+
}
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
current += char;
|
|
186
|
+
}
|
|
187
|
+
if (quote !== undefined)
|
|
188
|
+
throw new MetadataError(lineNumber, `a ${quote === '"' ? 'double' : 'single'} quote never closes.`);
|
|
189
|
+
parts.push(current);
|
|
190
|
+
return parts;
|
|
191
|
+
}
|
|
192
|
+
function withoutComment(line) {
|
|
193
|
+
let quote;
|
|
194
|
+
for (let index = 0; index < line.length; index += 1) {
|
|
195
|
+
const char = line[index];
|
|
196
|
+
if (quote !== undefined) {
|
|
197
|
+
if (char === '\\' && quote === '"')
|
|
198
|
+
index += 1;
|
|
199
|
+
else if (char === quote)
|
|
200
|
+
quote = undefined;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (char === '"' || char === "'")
|
|
204
|
+
quote = char;
|
|
205
|
+
if (char === '#' && (index === 0 || /\s/.test(line[index - 1] ?? '')))
|
|
206
|
+
return line.slice(0, index);
|
|
207
|
+
}
|
|
208
|
+
return line;
|
|
209
|
+
}
|
|
210
|
+
function unquoted(text, lineNumber) {
|
|
211
|
+
if (text.startsWith('"')) {
|
|
212
|
+
if (!text.endsWith('"') || text.length < 2)
|
|
213
|
+
throw new MetadataError(lineNumber, `${text} opens a double quote that never closes.`);
|
|
214
|
+
try {
|
|
215
|
+
return JSON.parse(text);
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
throw new MetadataError(lineNumber, `${text} is not a double-quoted text. Escape a " inside it as \\".`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (text.startsWith("'")) {
|
|
222
|
+
if (!text.endsWith("'") || text.length < 2)
|
|
223
|
+
throw new MetadataError(lineNumber, `${text} opens a single quote that never closes.`);
|
|
224
|
+
return text.slice(1, -1).replaceAll("''", "'");
|
|
225
|
+
}
|
|
226
|
+
return text;
|
|
227
|
+
}
|
|
228
|
+
function topLevelScalar(region, key) {
|
|
229
|
+
const at = region.lines.findIndex((line) => line.startsWith(`${key}:`));
|
|
230
|
+
if (at === -1)
|
|
231
|
+
return undefined;
|
|
232
|
+
const text = withoutComment((region.lines[at] ?? '').slice(key.length + 1)).trim();
|
|
233
|
+
if (text === '')
|
|
234
|
+
return undefined;
|
|
235
|
+
try {
|
|
236
|
+
return unquoted(text, region.start + 2 + at);
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
return undefined;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function asRecord(entries) {
|
|
243
|
+
return Object.freeze(Object.fromEntries(entries.map(({ key, value }) => [key, value])));
|
|
244
|
+
}
|
|
245
|
+
const plain = /^[A-Za-z0-9_./+~$%^()=\\][^#]*$/;
|
|
246
|
+
const special = /^(true|false|yes|no|on|off|null|~|[-+]?(\d[\d_]*)?\.?\d+([eE][-+]?\d+)?|0x[0-9a-fA-F]+|\.inf|\.nan)$/i;
|
|
247
|
+
function scalar(text) {
|
|
248
|
+
const isPlain = plain.test(text) && text === text.trim() && !text.includes(': ') && !text.endsWith(':') && !text.includes(' #') && !special.test(text) && !/["',{}[\]]/.test(text);
|
|
249
|
+
return isPlain ? text : JSON.stringify(text);
|
|
250
|
+
}
|