@foundation0/api 1.1.12 → 1.1.13
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/README.md +128 -128
- package/agents.ts +918 -918
- package/git.ts +20 -20
- package/libs/curl.ts +770 -770
- package/mcp/cli.mjs +37 -37
- package/mcp/cli.ts +87 -87
- package/mcp/client.ts +565 -565
- package/mcp/index.ts +15 -15
- package/mcp/server.ts +2991 -2991
- package/net.ts +170 -170
- package/package.json +13 -9
- package/projects.ts +4250 -4340
- package/taskgraph-parser.ts +217 -217
- package/libs/curl.test.ts +0 -130
- package/mcp/AGENTS.md +0 -130
- package/mcp/client.test.ts +0 -142
- package/mcp/manual.md +0 -179
- package/mcp/server.test.ts +0 -967
package/taskgraph-parser.ts
CHANGED
|
@@ -1,217 +1,217 @@
|
|
|
1
|
-
import { promises as fs } from 'node:fs';
|
|
2
|
-
import * as path from 'node:path';
|
|
3
|
-
|
|
4
|
-
export type TaskId = `TASK-${string}`;
|
|
5
|
-
|
|
6
|
-
export interface TaskGraphTask {
|
|
7
|
-
id: TaskId;
|
|
8
|
-
deps: TaskId[];
|
|
9
|
-
desc: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface TaskGraphParseResult {
|
|
13
|
-
version: 'TASKGRAPH-V1';
|
|
14
|
-
tasks: TaskGraphTask[];
|
|
15
|
-
byId: Record<string, TaskGraphTask>;
|
|
16
|
-
topo: TaskId[];
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const TASK_ID_RE = /^TASK-[0-9]{3}[A-Za-z0-9]*$/;
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Extract exactly one ```taskgraph fenced block from markdown (returns raw block contents).
|
|
23
|
-
*/
|
|
24
|
-
export function extractTaskGraphBlockFromMarkdown(markdown: string): string {
|
|
25
|
-
const matches = [...markdown.matchAll(/```taskgraph\s*\n([\s\S]*?)\n```/g)];
|
|
26
|
-
if (matches.length !== 1) {
|
|
27
|
-
throw new Error(`Expected exactly 1 taskgraph block, found ${matches.length}`);
|
|
28
|
-
}
|
|
29
|
-
return matches[0][1];
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Parse + validate TASKGRAPH-V1 block content (no fences).
|
|
34
|
-
*
|
|
35
|
-
* Returns a validated DAG plus a deterministic topological order (Kahn).
|
|
36
|
-
*/
|
|
37
|
-
export function parseTaskGraphV1(block: string): TaskGraphParseResult {
|
|
38
|
-
const lines = block.replace(/\r\n/g, '\n').split('\n');
|
|
39
|
-
|
|
40
|
-
const isIgnorable = (raw: string) => {
|
|
41
|
-
const t = raw.trim();
|
|
42
|
-
return t.length === 0 || t.startsWith('#');
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
let i = 0;
|
|
46
|
-
while (i < lines.length && isIgnorable(lines[i])) i++;
|
|
47
|
-
if (i >= lines.length) throw new Error('Missing TASKGRAPH-V1 header');
|
|
48
|
-
if (lines[i].trim() !== 'TASKGRAPH-V1') throw new Error(`Line ${i + 1}: expected TASKGRAPH-V1`);
|
|
49
|
-
i++;
|
|
50
|
-
|
|
51
|
-
const tasks: TaskGraphTask[] = [];
|
|
52
|
-
const byId = new Map<TaskId, TaskGraphTask>();
|
|
53
|
-
|
|
54
|
-
for (; i < lines.length; i++) {
|
|
55
|
-
const raw = lines[i];
|
|
56
|
-
if (isIgnorable(raw)) continue;
|
|
57
|
-
|
|
58
|
-
const parts = raw.split('|');
|
|
59
|
-
if (parts.length !== 3) throw new Error(`Line ${i + 1}: expected <id>|<deps>|<desc>`);
|
|
60
|
-
|
|
61
|
-
const idRaw = parts[0].trim();
|
|
62
|
-
const depsRaw = parts[1].trim();
|
|
63
|
-
const desc = parts[2].trim();
|
|
64
|
-
|
|
65
|
-
if (!TASK_ID_RE.test(idRaw)) throw new Error(`Line ${i + 1}: invalid id '${idRaw}'`);
|
|
66
|
-
const id = idRaw as TaskId;
|
|
67
|
-
|
|
68
|
-
if (!desc) throw new Error(`Line ${i + 1}: empty desc for '${id}'`);
|
|
69
|
-
if (byId.has(id)) throw new Error(`Line ${i + 1}: duplicate id '${id}'`);
|
|
70
|
-
if (depsRaw !== '' && /\s/.test(depsRaw)) throw new Error(`Line ${i + 1}: deps contains whitespace`);
|
|
71
|
-
|
|
72
|
-
const deps =
|
|
73
|
-
depsRaw === ''
|
|
74
|
-
? []
|
|
75
|
-
: depsRaw.split(',').map((d) => {
|
|
76
|
-
if (!TASK_ID_RE.test(d)) throw new Error(`Line ${i + 1}: invalid dep '${d}'`);
|
|
77
|
-
return d as TaskId;
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
if (deps.includes(id)) throw new Error(`Line ${i + 1}: task '${id}' cannot depend on itself`);
|
|
81
|
-
if (new Set(deps).size !== deps.length) throw new Error(`Line ${i + 1}: duplicate deps for '${id}'`);
|
|
82
|
-
|
|
83
|
-
const task: TaskGraphTask = { id, deps, desc };
|
|
84
|
-
tasks.push(task);
|
|
85
|
-
byId.set(id, task);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
for (const t of tasks) {
|
|
89
|
-
for (const d of t.deps) {
|
|
90
|
-
if (!byId.has(d)) throw new Error(`Task '${t.id}' depends on unknown task '${d}'`);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const inDeg = new Map<TaskId, number>();
|
|
95
|
-
const adj = new Map<TaskId, TaskId[]>();
|
|
96
|
-
for (const t of tasks) {
|
|
97
|
-
inDeg.set(t.id, t.deps.length);
|
|
98
|
-
adj.set(t.id, []);
|
|
99
|
-
}
|
|
100
|
-
for (const t of tasks) {
|
|
101
|
-
for (const d of t.deps) adj.get(d)!.push(t.id);
|
|
102
|
-
}
|
|
103
|
-
for (const outs of adj.values()) outs.sort();
|
|
104
|
-
|
|
105
|
-
const ready = [...inDeg.entries()]
|
|
106
|
-
.filter(([, deg]) => deg === 0)
|
|
107
|
-
.map(([id]) => id)
|
|
108
|
-
.sort();
|
|
109
|
-
|
|
110
|
-
const topo: TaskId[] = [];
|
|
111
|
-
while (ready.length > 0) {
|
|
112
|
-
const id = ready.shift()!;
|
|
113
|
-
topo.push(id);
|
|
114
|
-
for (const v of adj.get(id)!) {
|
|
115
|
-
const next = inDeg.get(v)! - 1;
|
|
116
|
-
inDeg.set(v, next);
|
|
117
|
-
if (next === 0) {
|
|
118
|
-
ready.push(v);
|
|
119
|
-
ready.sort();
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if (topo.length !== tasks.length) {
|
|
125
|
-
const remaining = [...inDeg.entries()].filter(([, deg]) => deg > 0).map(([id]) => id).sort();
|
|
126
|
-
throw new Error(`Dependency cycle detected among: ${remaining.join(',')}`);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const byIdObj: Record<string, TaskGraphTask> = {};
|
|
130
|
-
for (const [id, task] of byId.entries()) {
|
|
131
|
-
byIdObj[id] = task;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
return { version: 'TASKGRAPH-V1', tasks, byId: byIdObj, topo };
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
export function parseTaskGraphFromMarkdown(markdown: string): TaskGraphParseResult {
|
|
138
|
-
return parseTaskGraphV1(extractTaskGraphBlockFromMarkdown(markdown));
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function parseArgs(argv: string[]): { inputPath: string; outputPath?: string; indent: number } {
|
|
142
|
-
let inputPath: string | undefined;
|
|
143
|
-
let outputPath: string | undefined;
|
|
144
|
-
let indent = 2;
|
|
145
|
-
|
|
146
|
-
for (let i = 0; i < argv.length; i++) {
|
|
147
|
-
const arg = argv[i];
|
|
148
|
-
if (arg === '--help' || arg === '-h') {
|
|
149
|
-
process.stdout.write(
|
|
150
|
-
[
|
|
151
|
-
'Usage:',
|
|
152
|
-
' bun api/taskgraph-parser.ts --tracks <path>',
|
|
153
|
-
' bun api/taskgraph-parser.ts --tracks <path> --out <file>',
|
|
154
|
-
' bun scripts/taskgraph-parser.ts --tracks <path>',
|
|
155
|
-
' bun scripts/taskgraph-parser.ts --tracks <path> --out <file>',
|
|
156
|
-
'',
|
|
157
|
-
'Options:',
|
|
158
|
-
' --tracks Input tracks.md path (required)',
|
|
159
|
-
' --out Optional output JSON file path (defaults to stdout)',
|
|
160
|
-
' --indent Optional JSON indent (default: 2)',
|
|
161
|
-
' --help, -h Show this help message',
|
|
162
|
-
'',
|
|
163
|
-
].join('\n'),
|
|
164
|
-
);
|
|
165
|
-
process.exit(0);
|
|
166
|
-
}
|
|
167
|
-
if (arg === '--tracks') {
|
|
168
|
-
inputPath = argv[i + 1];
|
|
169
|
-
if (!inputPath) throw new Error('Missing value for --tracks');
|
|
170
|
-
i++;
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
|
-
if (arg === '--out') {
|
|
174
|
-
outputPath = argv[i + 1];
|
|
175
|
-
if (!outputPath) throw new Error('Missing value for --out');
|
|
176
|
-
i++;
|
|
177
|
-
continue;
|
|
178
|
-
}
|
|
179
|
-
if (arg === '--indent') {
|
|
180
|
-
const value = Number(argv[i + 1]);
|
|
181
|
-
if (!Number.isFinite(value) || value < 0) throw new Error(`Invalid --indent: ${argv[i + 1]}`);
|
|
182
|
-
indent = Math.floor(value);
|
|
183
|
-
i++;
|
|
184
|
-
continue;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (!arg.startsWith('--')) {
|
|
188
|
-
if (!inputPath) inputPath = arg;
|
|
189
|
-
continue;
|
|
190
|
-
}
|
|
191
|
-
throw new Error(`Unknown argument: ${arg}`);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
if (!inputPath) throw new Error('Missing required --tracks <path>');
|
|
195
|
-
return { inputPath: path.resolve(inputPath), outputPath: outputPath ? path.resolve(outputPath) : undefined, indent };
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
export async function main(): Promise<void> {
|
|
199
|
-
const { inputPath, outputPath, indent } = parseArgs(process.argv.slice(2));
|
|
200
|
-
const markdown = await fs.readFile(inputPath, 'utf8');
|
|
201
|
-
const graph = parseTaskGraphFromMarkdown(markdown);
|
|
202
|
-
const payload = JSON.stringify(graph, null, indent);
|
|
203
|
-
|
|
204
|
-
if (outputPath) {
|
|
205
|
-
await fs.writeFile(outputPath, payload + '\n', 'utf8');
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
process.stdout.write(payload + '\n');
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
if (import.meta.main) {
|
|
213
|
-
main().catch((err) => {
|
|
214
|
-
process.stderr.write(String(err instanceof Error ? err.stack ?? err.message : err) + '\n');
|
|
215
|
-
process.exitCode = 1;
|
|
216
|
-
});
|
|
217
|
-
}
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export type TaskId = `TASK-${string}`;
|
|
5
|
+
|
|
6
|
+
export interface TaskGraphTask {
|
|
7
|
+
id: TaskId;
|
|
8
|
+
deps: TaskId[];
|
|
9
|
+
desc: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface TaskGraphParseResult {
|
|
13
|
+
version: 'TASKGRAPH-V1';
|
|
14
|
+
tasks: TaskGraphTask[];
|
|
15
|
+
byId: Record<string, TaskGraphTask>;
|
|
16
|
+
topo: TaskId[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const TASK_ID_RE = /^TASK-[0-9]{3}[A-Za-z0-9]*$/;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Extract exactly one ```taskgraph fenced block from markdown (returns raw block contents).
|
|
23
|
+
*/
|
|
24
|
+
export function extractTaskGraphBlockFromMarkdown(markdown: string): string {
|
|
25
|
+
const matches = [...markdown.matchAll(/```taskgraph\s*\n([\s\S]*?)\n```/g)];
|
|
26
|
+
if (matches.length !== 1) {
|
|
27
|
+
throw new Error(`Expected exactly 1 taskgraph block, found ${matches.length}`);
|
|
28
|
+
}
|
|
29
|
+
return matches[0][1];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Parse + validate TASKGRAPH-V1 block content (no fences).
|
|
34
|
+
*
|
|
35
|
+
* Returns a validated DAG plus a deterministic topological order (Kahn).
|
|
36
|
+
*/
|
|
37
|
+
export function parseTaskGraphV1(block: string): TaskGraphParseResult {
|
|
38
|
+
const lines = block.replace(/\r\n/g, '\n').split('\n');
|
|
39
|
+
|
|
40
|
+
const isIgnorable = (raw: string) => {
|
|
41
|
+
const t = raw.trim();
|
|
42
|
+
return t.length === 0 || t.startsWith('#');
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
let i = 0;
|
|
46
|
+
while (i < lines.length && isIgnorable(lines[i])) i++;
|
|
47
|
+
if (i >= lines.length) throw new Error('Missing TASKGRAPH-V1 header');
|
|
48
|
+
if (lines[i].trim() !== 'TASKGRAPH-V1') throw new Error(`Line ${i + 1}: expected TASKGRAPH-V1`);
|
|
49
|
+
i++;
|
|
50
|
+
|
|
51
|
+
const tasks: TaskGraphTask[] = [];
|
|
52
|
+
const byId = new Map<TaskId, TaskGraphTask>();
|
|
53
|
+
|
|
54
|
+
for (; i < lines.length; i++) {
|
|
55
|
+
const raw = lines[i];
|
|
56
|
+
if (isIgnorable(raw)) continue;
|
|
57
|
+
|
|
58
|
+
const parts = raw.split('|');
|
|
59
|
+
if (parts.length !== 3) throw new Error(`Line ${i + 1}: expected <id>|<deps>|<desc>`);
|
|
60
|
+
|
|
61
|
+
const idRaw = parts[0].trim();
|
|
62
|
+
const depsRaw = parts[1].trim();
|
|
63
|
+
const desc = parts[2].trim();
|
|
64
|
+
|
|
65
|
+
if (!TASK_ID_RE.test(idRaw)) throw new Error(`Line ${i + 1}: invalid id '${idRaw}'`);
|
|
66
|
+
const id = idRaw as TaskId;
|
|
67
|
+
|
|
68
|
+
if (!desc) throw new Error(`Line ${i + 1}: empty desc for '${id}'`);
|
|
69
|
+
if (byId.has(id)) throw new Error(`Line ${i + 1}: duplicate id '${id}'`);
|
|
70
|
+
if (depsRaw !== '' && /\s/.test(depsRaw)) throw new Error(`Line ${i + 1}: deps contains whitespace`);
|
|
71
|
+
|
|
72
|
+
const deps =
|
|
73
|
+
depsRaw === ''
|
|
74
|
+
? []
|
|
75
|
+
: depsRaw.split(',').map((d) => {
|
|
76
|
+
if (!TASK_ID_RE.test(d)) throw new Error(`Line ${i + 1}: invalid dep '${d}'`);
|
|
77
|
+
return d as TaskId;
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (deps.includes(id)) throw new Error(`Line ${i + 1}: task '${id}' cannot depend on itself`);
|
|
81
|
+
if (new Set(deps).size !== deps.length) throw new Error(`Line ${i + 1}: duplicate deps for '${id}'`);
|
|
82
|
+
|
|
83
|
+
const task: TaskGraphTask = { id, deps, desc };
|
|
84
|
+
tasks.push(task);
|
|
85
|
+
byId.set(id, task);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
for (const t of tasks) {
|
|
89
|
+
for (const d of t.deps) {
|
|
90
|
+
if (!byId.has(d)) throw new Error(`Task '${t.id}' depends on unknown task '${d}'`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const inDeg = new Map<TaskId, number>();
|
|
95
|
+
const adj = new Map<TaskId, TaskId[]>();
|
|
96
|
+
for (const t of tasks) {
|
|
97
|
+
inDeg.set(t.id, t.deps.length);
|
|
98
|
+
adj.set(t.id, []);
|
|
99
|
+
}
|
|
100
|
+
for (const t of tasks) {
|
|
101
|
+
for (const d of t.deps) adj.get(d)!.push(t.id);
|
|
102
|
+
}
|
|
103
|
+
for (const outs of adj.values()) outs.sort();
|
|
104
|
+
|
|
105
|
+
const ready = [...inDeg.entries()]
|
|
106
|
+
.filter(([, deg]) => deg === 0)
|
|
107
|
+
.map(([id]) => id)
|
|
108
|
+
.sort();
|
|
109
|
+
|
|
110
|
+
const topo: TaskId[] = [];
|
|
111
|
+
while (ready.length > 0) {
|
|
112
|
+
const id = ready.shift()!;
|
|
113
|
+
topo.push(id);
|
|
114
|
+
for (const v of adj.get(id)!) {
|
|
115
|
+
const next = inDeg.get(v)! - 1;
|
|
116
|
+
inDeg.set(v, next);
|
|
117
|
+
if (next === 0) {
|
|
118
|
+
ready.push(v);
|
|
119
|
+
ready.sort();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (topo.length !== tasks.length) {
|
|
125
|
+
const remaining = [...inDeg.entries()].filter(([, deg]) => deg > 0).map(([id]) => id).sort();
|
|
126
|
+
throw new Error(`Dependency cycle detected among: ${remaining.join(',')}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const byIdObj: Record<string, TaskGraphTask> = {};
|
|
130
|
+
for (const [id, task] of byId.entries()) {
|
|
131
|
+
byIdObj[id] = task;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return { version: 'TASKGRAPH-V1', tasks, byId: byIdObj, topo };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function parseTaskGraphFromMarkdown(markdown: string): TaskGraphParseResult {
|
|
138
|
+
return parseTaskGraphV1(extractTaskGraphBlockFromMarkdown(markdown));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function parseArgs(argv: string[]): { inputPath: string; outputPath?: string; indent: number } {
|
|
142
|
+
let inputPath: string | undefined;
|
|
143
|
+
let outputPath: string | undefined;
|
|
144
|
+
let indent = 2;
|
|
145
|
+
|
|
146
|
+
for (let i = 0; i < argv.length; i++) {
|
|
147
|
+
const arg = argv[i];
|
|
148
|
+
if (arg === '--help' || arg === '-h') {
|
|
149
|
+
process.stdout.write(
|
|
150
|
+
[
|
|
151
|
+
'Usage:',
|
|
152
|
+
' bun api/taskgraph-parser.ts --tracks <path>',
|
|
153
|
+
' bun api/taskgraph-parser.ts --tracks <path> --out <file>',
|
|
154
|
+
' bun scripts/taskgraph-parser.ts --tracks <path>',
|
|
155
|
+
' bun scripts/taskgraph-parser.ts --tracks <path> --out <file>',
|
|
156
|
+
'',
|
|
157
|
+
'Options:',
|
|
158
|
+
' --tracks Input tracks.md path (required)',
|
|
159
|
+
' --out Optional output JSON file path (defaults to stdout)',
|
|
160
|
+
' --indent Optional JSON indent (default: 2)',
|
|
161
|
+
' --help, -h Show this help message',
|
|
162
|
+
'',
|
|
163
|
+
].join('\n'),
|
|
164
|
+
);
|
|
165
|
+
process.exit(0);
|
|
166
|
+
}
|
|
167
|
+
if (arg === '--tracks') {
|
|
168
|
+
inputPath = argv[i + 1];
|
|
169
|
+
if (!inputPath) throw new Error('Missing value for --tracks');
|
|
170
|
+
i++;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (arg === '--out') {
|
|
174
|
+
outputPath = argv[i + 1];
|
|
175
|
+
if (!outputPath) throw new Error('Missing value for --out');
|
|
176
|
+
i++;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (arg === '--indent') {
|
|
180
|
+
const value = Number(argv[i + 1]);
|
|
181
|
+
if (!Number.isFinite(value) || value < 0) throw new Error(`Invalid --indent: ${argv[i + 1]}`);
|
|
182
|
+
indent = Math.floor(value);
|
|
183
|
+
i++;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (!arg.startsWith('--')) {
|
|
188
|
+
if (!inputPath) inputPath = arg;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (!inputPath) throw new Error('Missing required --tracks <path>');
|
|
195
|
+
return { inputPath: path.resolve(inputPath), outputPath: outputPath ? path.resolve(outputPath) : undefined, indent };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export async function main(): Promise<void> {
|
|
199
|
+
const { inputPath, outputPath, indent } = parseArgs(process.argv.slice(2));
|
|
200
|
+
const markdown = await fs.readFile(inputPath, 'utf8');
|
|
201
|
+
const graph = parseTaskGraphFromMarkdown(markdown);
|
|
202
|
+
const payload = JSON.stringify(graph, null, indent);
|
|
203
|
+
|
|
204
|
+
if (outputPath) {
|
|
205
|
+
await fs.writeFile(outputPath, payload + '\n', 'utf8');
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
process.stdout.write(payload + '\n');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (import.meta.main) {
|
|
213
|
+
main().catch((err) => {
|
|
214
|
+
process.stderr.write(String(err instanceof Error ? err.stack ?? err.message : err) + '\n');
|
|
215
|
+
process.exitCode = 1;
|
|
216
|
+
});
|
|
217
|
+
}
|
package/libs/curl.test.ts
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'bun:test'
|
|
2
|
-
import { curl } from './curl'
|
|
3
|
-
|
|
4
|
-
describe('api/libs/curl', () => {
|
|
5
|
-
const withServer = async <T>(fn: (baseUrl: string) => Promise<T>): Promise<T> => {
|
|
6
|
-
const server = Bun.serve({
|
|
7
|
-
port: 0,
|
|
8
|
-
fetch: async (req) => {
|
|
9
|
-
const url = new URL(req.url)
|
|
10
|
-
|
|
11
|
-
if (url.pathname === '/hello') {
|
|
12
|
-
return new Response('hello', { headers: { 'x-test': '1' } })
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
if (url.pathname === '/echo') {
|
|
16
|
-
const body = await req.text()
|
|
17
|
-
return Response.json({
|
|
18
|
-
method: req.method,
|
|
19
|
-
body,
|
|
20
|
-
contentType: req.headers.get('content-type'),
|
|
21
|
-
ua: req.headers.get('user-agent'),
|
|
22
|
-
})
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
if (url.pathname === '/redirect') {
|
|
26
|
-
return new Response('nope', { status: 302, headers: { location: '/hello' } })
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return new Response('not found', { status: 404 })
|
|
30
|
-
},
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
try {
|
|
34
|
-
return await fn(`http://127.0.0.1:${server.port}`)
|
|
35
|
-
} finally {
|
|
36
|
-
server.stop(true)
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
it('fetches a URL and returns body on stdout', async () => {
|
|
41
|
-
await withServer(async (baseUrl) => {
|
|
42
|
-
const result = await curl(`${baseUrl}/hello`)
|
|
43
|
-
expect(result.exitCode).toBe(0)
|
|
44
|
-
expect(result.timedOut).toBe(false)
|
|
45
|
-
expect(result.stdout).toBe('hello')
|
|
46
|
-
expect(result.results?.[0]?.httpCode).toBe(200)
|
|
47
|
-
})
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
it('defaults scheme-less host URLs to http://', async () => {
|
|
51
|
-
await withServer(async (baseUrl) => {
|
|
52
|
-
const url = baseUrl.replace('http://', '')
|
|
53
|
-
const result = await curl(`${url}/hello`)
|
|
54
|
-
expect(result.exitCode).toBe(0)
|
|
55
|
-
expect(result.stdout).toBe('hello')
|
|
56
|
-
})
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
it('supports -i to include response headers', async () => {
|
|
60
|
-
await withServer(async (baseUrl) => {
|
|
61
|
-
const result = await curl('-i', `${baseUrl}/hello`)
|
|
62
|
-
expect(result.exitCode).toBe(0)
|
|
63
|
-
expect(result.stdout).toContain('HTTP/1.1 200')
|
|
64
|
-
expect(result.stdout.toLowerCase()).toContain('x-test: 1')
|
|
65
|
-
expect(result.stdout).toContain('hello')
|
|
66
|
-
})
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
it('supports -d for POST body and defaults method to POST', async () => {
|
|
70
|
-
await withServer(async (baseUrl) => {
|
|
71
|
-
const result = await curl('-d', 'a=1', `${baseUrl}/echo`)
|
|
72
|
-
expect(result.exitCode).toBe(0)
|
|
73
|
-
const payload = JSON.parse(result.stdout)
|
|
74
|
-
expect(payload.method).toBe('POST')
|
|
75
|
-
expect(payload.body).toBe('a=1')
|
|
76
|
-
expect(payload.contentType).toContain('application/x-www-form-urlencoded')
|
|
77
|
-
})
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
it('supports -G with -d to apply data as query params', async () => {
|
|
81
|
-
await withServer(async (baseUrl) => {
|
|
82
|
-
const result = await curl('-G', '-d', 'a=1', `${baseUrl}/hello`)
|
|
83
|
-
expect(result.exitCode).toBe(0)
|
|
84
|
-
expect(result.stdout).toBe('hello')
|
|
85
|
-
})
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
it('supports -L to follow redirects', async () => {
|
|
89
|
-
await withServer(async (baseUrl) => {
|
|
90
|
-
const result = await curl('-L', `${baseUrl}/redirect`)
|
|
91
|
-
expect(result.exitCode).toBe(0)
|
|
92
|
-
expect(result.results?.[0]?.redirectCount).toBe(1)
|
|
93
|
-
expect(result.results?.[0]?.urlEffective).toContain('/hello')
|
|
94
|
-
expect(result.stdout).toBe('hello')
|
|
95
|
-
})
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
it('supports -f to fail on HTTP >= 400 and suppress body', async () => {
|
|
99
|
-
await withServer(async (baseUrl) => {
|
|
100
|
-
const result = await curl('-f', `${baseUrl}/missing`)
|
|
101
|
-
expect(result.exitCode).toBe(22)
|
|
102
|
-
expect(result.stdout).toBe('')
|
|
103
|
-
expect(result.stderr).toContain('returned error')
|
|
104
|
-
})
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
it('supports -w to write out %{http_code}', async () => {
|
|
108
|
-
await withServer(async (baseUrl) => {
|
|
109
|
-
const result = await curl('-w', '%{http_code}', `${baseUrl}/hello`)
|
|
110
|
-
expect(result.exitCode).toBe(0)
|
|
111
|
-
expect(result.stdout).toBe('hello200')
|
|
112
|
-
})
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
it('accepts trailing tool options object without turning it into a curl arg', async () => {
|
|
116
|
-
await withServer(async (baseUrl) => {
|
|
117
|
-
const result = await curl(`${baseUrl}/hello`, { timeoutMs: 10_000 })
|
|
118
|
-
expect(result.exitCode).toBe(0)
|
|
119
|
-
expect(result.args).toEqual([`${baseUrl}/hello`])
|
|
120
|
-
})
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
it('returns usage exit code for unsupported flags', async () => {
|
|
124
|
-
await withServer(async (baseUrl) => {
|
|
125
|
-
const result = await curl('--version', `${baseUrl}/hello`)
|
|
126
|
-
expect(result.exitCode).toBe(2)
|
|
127
|
-
expect(result.stderr).toContain('Unsupported curl option')
|
|
128
|
-
})
|
|
129
|
-
})
|
|
130
|
-
})
|