singleton-pipeline 0.4.0-beta.13 → 0.4.0-beta.14
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/dist/packages/cli/src/assets/singleton-logo.txt +10 -0
- package/dist/packages/cli/src/commands/new.js +763 -0
- package/dist/packages/cli/src/commands/repl.js +557 -0
- package/dist/packages/cli/src/commands/usage.js +49 -0
- package/dist/packages/cli/src/executor/debug-loop.js +525 -0
- package/dist/packages/cli/src/executor/inputs.js +226 -0
- package/dist/packages/cli/src/executor/outputs.js +134 -0
- package/dist/packages/cli/src/executor/preflight.js +605 -0
- package/dist/packages/cli/src/executor/replay-loop.js +120 -0
- package/dist/packages/cli/src/executor/run-report.js +209 -0
- package/dist/packages/cli/src/executor/run-setup.js +114 -0
- package/dist/packages/cli/src/executor/security-review.js +97 -0
- package/dist/packages/cli/src/executor/snapshot-manager.js +349 -0
- package/dist/packages/cli/src/executor/step-runner.js +241 -0
- package/dist/packages/cli/src/executor.js +584 -0
- package/dist/packages/cli/src/index.js +107 -0
- package/dist/packages/cli/src/parser.js +89 -0
- package/dist/packages/cli/src/runners/_shared.js +96 -0
- package/dist/packages/cli/src/runners/claude.js +103 -0
- package/dist/packages/cli/src/runners/codex-instructions.js +69 -0
- package/dist/packages/cli/src/runners/codex.js +141 -0
- package/dist/packages/cli/src/runners/copilot.js +209 -0
- package/dist/packages/cli/src/runners/index.js +18 -0
- package/dist/packages/cli/src/runners/opencode.js +240 -0
- package/dist/packages/cli/src/scanner.js +43 -0
- package/dist/packages/cli/src/security/policy.js +115 -0
- package/dist/packages/cli/src/sentinels.js +1 -0
- package/dist/packages/cli/src/shell.js +753 -0
- package/dist/packages/cli/src/theme.js +39 -0
- package/dist/packages/cli/src/timeline.js +238 -0
- package/dist/packages/cli/src/types.js +1 -0
- package/dist/packages/cli/src/usage/aggregator.js +44 -0
- package/dist/packages/cli/src/usage/reader.js +30 -0
- package/dist/packages/cli/src/usage/types.js +1 -0
- package/dist/packages/server/src/index.js +36 -0
- package/dist/packages/server/src/routes/agents.js +31 -0
- package/dist/packages/server/src/routes/files.js +45 -0
- package/dist/packages/server/src/routes/pipelines.js +74 -0
- package/docs/reference.md +28 -0
- package/package.json +15 -14
- package/packages/web/dist/assets/{index-CnKytBly.js → index-9S0goZlQ.js} +1 -1
- package/packages/web/dist/assets/{index-CCFWfCA2.css → index-iV4UtXoN.css} +1 -1
- package/packages/web/dist/assets/logo-COSyZmgk.png +0 -0
- package/packages/web/dist/index.html +2 -2
- package/packages/cli/package.json +0 -18
- package/packages/cli/src/commands/new.js +0 -786
- package/packages/cli/src/commands/repl.js +0 -548
- package/packages/cli/src/executor/debug-loop.js +0 -587
- package/packages/cli/src/executor/inputs.js +0 -202
- package/packages/cli/src/executor/outputs.js +0 -140
- package/packages/cli/src/executor/preflight.js +0 -459
- package/packages/cli/src/executor/replay-loop.js +0 -172
- package/packages/cli/src/executor/run-report.js +0 -189
- package/packages/cli/src/executor/run-setup.js +0 -93
- package/packages/cli/src/executor/security-review.js +0 -108
- package/packages/cli/src/executor/snapshot-manager.js +0 -335
- package/packages/cli/src/executor/step-runner.js +0 -266
- package/packages/cli/src/executor.js +0 -652
- package/packages/cli/src/index.js +0 -107
- package/packages/cli/src/parser.js +0 -78
- package/packages/cli/src/runners/_shared.js +0 -83
- package/packages/cli/src/runners/claude.js +0 -122
- package/packages/cli/src/runners/codex-instructions.js +0 -75
- package/packages/cli/src/runners/codex.js +0 -165
- package/packages/cli/src/runners/copilot.js +0 -224
- package/packages/cli/src/runners/index.js +0 -20
- package/packages/cli/src/runners/opencode.js +0 -265
- package/packages/cli/src/scanner.js +0 -47
- package/packages/cli/src/security/policy.js +0 -126
- package/packages/cli/src/shell.js +0 -732
- package/packages/cli/src/theme.js +0 -46
- package/packages/cli/src/timeline.js +0 -180
- package/packages/server/package.json +0 -11
- package/packages/server/src/index.js +0 -43
- package/packages/server/src/routes/agents.js +0 -32
- package/packages/server/src/routes/files.js +0 -42
- package/packages/server/src/routes/pipelines.js +0 -74
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
|
-
|
|
3
|
-
// ============================================================
|
|
4
|
-
// CLI theme — semantic tokens. Change palette here.
|
|
5
|
-
// ============================================================
|
|
6
|
-
|
|
7
|
-
const accent = chalk.hex('#AF87FF');
|
|
8
|
-
|
|
9
|
-
// -- Semantic styles ----------------------------------------
|
|
10
|
-
export const style = {
|
|
11
|
-
// Informational
|
|
12
|
-
title: (s) => accent.bold(s),
|
|
13
|
-
heading: (s) => chalk.bold(s),
|
|
14
|
-
muted: (s) => chalk.hex('#676498')(s),
|
|
15
|
-
dim: (s) => chalk.gray.italic(s),
|
|
16
|
-
|
|
17
|
-
// Status
|
|
18
|
-
success: (s) => chalk.green(s),
|
|
19
|
-
warn: (s) => chalk.yellow(s),
|
|
20
|
-
error: (s) => chalk.red(s),
|
|
21
|
-
info: (s) => accent(s),
|
|
22
|
-
|
|
23
|
-
// Data accents
|
|
24
|
-
accent: (s) => accent(s),
|
|
25
|
-
id: (s) => accent.bold(s),
|
|
26
|
-
path: (s) => chalk.gray(s),
|
|
27
|
-
value: (s) => chalk.white(s),
|
|
28
|
-
code: (s) => chalk.magenta(s)
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
// -- Semantic markers (prefix glyphs) -----------------------
|
|
32
|
-
export const mark = {
|
|
33
|
-
success: '✓',
|
|
34
|
-
error: '✕',
|
|
35
|
-
warn: '!',
|
|
36
|
-
info: '›',
|
|
37
|
-
bullet: '·'
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
// -- Pre-composed line helpers ------------------------------
|
|
41
|
-
export const line = {
|
|
42
|
-
success: (msg) => `${style.success(mark.success)} ${msg}`,
|
|
43
|
-
error: (msg) => `${style.error(mark.error)} ${msg}`,
|
|
44
|
-
warn: (msg) => `${style.warn(mark.warn)} ${msg}`,
|
|
45
|
-
info: (msg) => `${style.info(mark.info)} ${msg}`
|
|
46
|
-
};
|
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
import blessed from 'blessed';
|
|
2
|
-
import { S } from './shell.js';
|
|
3
|
-
|
|
4
|
-
const FRAMES = ['⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷'];
|
|
5
|
-
|
|
6
|
-
// widgets: { screen, logPanel, statusBox } — provided by shell when running inside the TUI.
|
|
7
|
-
// If omitted, a standalone blessed screen is created.
|
|
8
|
-
export function createTimeline(stepNames, widgets = null) {
|
|
9
|
-
const N = stepNames.length;
|
|
10
|
-
const statuses = stepNames.map(() => 'pending');
|
|
11
|
-
const meta = stepNames.map(() => '');
|
|
12
|
-
let runningIdx = -1;
|
|
13
|
-
let spinnerInterval = null;
|
|
14
|
-
let spinnerFrame = 0;
|
|
15
|
-
|
|
16
|
-
let screen, logPanel, statusBox, setLabel = null, mirror = null, ownScreen = false;
|
|
17
|
-
|
|
18
|
-
if (widgets) {
|
|
19
|
-
({ screen, logPanel, statusBox, setLabel = null, mirror = null } = widgets);
|
|
20
|
-
} else {
|
|
21
|
-
ownScreen = true;
|
|
22
|
-
screen = blessed.screen({ smartCSR: true, title: 'Singleton' });
|
|
23
|
-
|
|
24
|
-
logPanel = blessed.log({
|
|
25
|
-
top: 0, left: 0,
|
|
26
|
-
width: '100%', height: '100%-6',
|
|
27
|
-
tags: true,
|
|
28
|
-
scrollable: true,
|
|
29
|
-
alwaysScroll: true,
|
|
30
|
-
padding: { left: 2, top: 1, right: 2 }
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
const separator = blessed.line({
|
|
34
|
-
orientation: 'horizontal',
|
|
35
|
-
bottom: 5, left: 0,
|
|
36
|
-
width: '100%',
|
|
37
|
-
style: { fg: S.border }
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
statusBox = blessed.box({
|
|
41
|
-
bottom: 1, left: 0,
|
|
42
|
-
width: '100%', height: 4,
|
|
43
|
-
tags: true,
|
|
44
|
-
padding: { left: 2, right: 2 }
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
screen.append(logPanel);
|
|
48
|
-
screen.append(separator);
|
|
49
|
-
screen.append(statusBox);
|
|
50
|
-
|
|
51
|
-
screen.key(['C-c'], () => { screen.destroy(); process.exit(0); });
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function dot(status, frame = 0) {
|
|
55
|
-
if (status === 'done') return `{${S.success}-fg}●{/}`;
|
|
56
|
-
if (status === 'running') return `{${S.text}-fg}${FRAMES[frame % FRAMES.length]}{/}`;
|
|
57
|
-
if (status === 'paused') return `{${S.warning}-fg}●{/}`;
|
|
58
|
-
if (status === 'error') return `{${S.error}-fg}●{/}`;
|
|
59
|
-
return `{${S.subtle}-fg}○{/}`;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function shimmerName(text, frame = 0) {
|
|
63
|
-
const peak = frame % (text.length + 6);
|
|
64
|
-
return text.split('').map((ch, i) => {
|
|
65
|
-
const dist = Math.abs(i - peak);
|
|
66
|
-
let color = S.accent;
|
|
67
|
-
if (dist === 0) color = S.text;
|
|
68
|
-
else if (dist === 1) color = '#EDD9FF';
|
|
69
|
-
else if (dist === 2) color = '#D4B0FE';
|
|
70
|
-
return ch === ' ' ? ch : `{${color}-fg}{bold}${ch}{/}`;
|
|
71
|
-
}).join('');
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function compactDots(frame = 0) {
|
|
75
|
-
return stepNames.map((_name, i) => dot(statuses[i], frame)).join(` {${S.subtle}-fg}─{/} `);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function renderTimeline(frame = 0) {
|
|
79
|
-
const currentMeta = runningIdx >= 0 && meta[runningIdx]
|
|
80
|
-
? ` {${S.muted}-fg}${meta[runningIdx]}{/}`
|
|
81
|
-
: '';
|
|
82
|
-
const isPaused = runningIdx >= 0 && statuses[runningIdx] === 'paused';
|
|
83
|
-
const activityLabel = isPaused
|
|
84
|
-
? `{${S.warning}-fg}{bold}Paused{/}`
|
|
85
|
-
: `{bold}Running{/}`;
|
|
86
|
-
const activityIcon = isPaused
|
|
87
|
-
? `{${S.warning}-fg}●{/}`
|
|
88
|
-
: `{${S.text}-fg}${FRAMES[frame % FRAMES.length]}{/}`;
|
|
89
|
-
const runningLabel = runningIdx >= 0
|
|
90
|
-
? `${activityLabel} ${activityIcon} ${shimmerName(stepNames[runningIdx], frame)}${currentMeta}`
|
|
91
|
-
: `{bold}Running:{/} {${S.muted}-fg}idle{/}`;
|
|
92
|
-
const statusLines = [
|
|
93
|
-
'',
|
|
94
|
-
runningLabel,
|
|
95
|
-
'',
|
|
96
|
-
compactDots(frame)
|
|
97
|
-
];
|
|
98
|
-
statusBox.setContent(statusLines.join('\n'));
|
|
99
|
-
if (setLabel) {
|
|
100
|
-
if (runningIdx < 0) {
|
|
101
|
-
setLabel('');
|
|
102
|
-
} else if (runningIdx === 0) {
|
|
103
|
-
// Index 0 is always the preflight pseudo-step — it's not a "real" pipeline step,
|
|
104
|
-
// so show "preflight" rather than fold it into the X/N count.
|
|
105
|
-
const labelText = isPaused ? 'preflight — paused' : 'preflight';
|
|
106
|
-
setLabel(`{${S.text}-fg}{bold}${labelText}{/}`);
|
|
107
|
-
} else {
|
|
108
|
-
// Real steps: 1..(N-1). Subtract 1 from N to exclude preflight from the total.
|
|
109
|
-
const labelText = isPaused
|
|
110
|
-
? `step ${runningIdx}/${N - 1} — paused`
|
|
111
|
-
: `step ${runningIdx}/${N - 1}`;
|
|
112
|
-
setLabel(`{${S.text}-fg}{bold}${labelText}{/}`);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
screen.render();
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
renderTimeline();
|
|
119
|
-
|
|
120
|
-
return {
|
|
121
|
-
log(text) { const s = `{${S.keyword}-fg}${text}{/}`; logPanel.log(s); mirror?.(s); screen.render(); },
|
|
122
|
-
logMuted(text) { const s = `{${S.muted}-fg}${text}{/}`; logPanel.log(s); mirror?.(s); screen.render(); },
|
|
123
|
-
logSuccess(text) { const s = `{${S.success}-fg}${text}{/}`; logPanel.log(s); mirror?.(s); screen.render(); },
|
|
124
|
-
logError(text) { const s = `{${S.error}-fg}${text}{/}`; logPanel.log(s); mirror?.(s); screen.render(); },
|
|
125
|
-
logDiffLine(raw) {
|
|
126
|
-
const text = String(raw ?? '');
|
|
127
|
-
const body = text.replace(/^\s+/, '');
|
|
128
|
-
// Default to S.subtle for all non-signal lines (meta git, context, untracked previews) —
|
|
129
|
-
// one consistent gray instead of two slightly different ones. Check git meta starts before
|
|
130
|
-
// +/- because +++ and --- would otherwise match the body coloring.
|
|
131
|
-
let color = S.muted;
|
|
132
|
-
if (/^(diff --git|index |--- |\+\+\+ )/.test(body)) color = S.muted;
|
|
133
|
-
else if (body.startsWith('@@')) color = S.keyword;
|
|
134
|
-
else if (body.startsWith('+')) color = S.success;
|
|
135
|
-
else if (body.startsWith('-')) color = S.error;
|
|
136
|
-
const s = `{${color}-fg}${text}{/}`;
|
|
137
|
-
logPanel.log(s);
|
|
138
|
-
mirror?.(s);
|
|
139
|
-
screen.render();
|
|
140
|
-
},
|
|
141
|
-
|
|
142
|
-
setRunning(i, info = '') {
|
|
143
|
-
if (spinnerInterval) { clearInterval(spinnerInterval); spinnerInterval = null; }
|
|
144
|
-
runningIdx = i;
|
|
145
|
-
statuses[i] = 'running';
|
|
146
|
-
meta[i] = info;
|
|
147
|
-
renderTimeline();
|
|
148
|
-
spinnerInterval = setInterval(() => { spinnerFrame++; renderTimeline(spinnerFrame); }, 80);
|
|
149
|
-
},
|
|
150
|
-
|
|
151
|
-
setPaused(i, info = '') {
|
|
152
|
-
if (spinnerInterval) { clearInterval(spinnerInterval); spinnerInterval = null; }
|
|
153
|
-
runningIdx = i;
|
|
154
|
-
statuses[i] = 'paused';
|
|
155
|
-
meta[i] = info;
|
|
156
|
-
renderTimeline();
|
|
157
|
-
},
|
|
158
|
-
|
|
159
|
-
setDone(i, info = '') {
|
|
160
|
-
if (spinnerInterval) { clearInterval(spinnerInterval); spinnerInterval = null; }
|
|
161
|
-
statuses[i] = 'done';
|
|
162
|
-
meta[i] = info;
|
|
163
|
-
if (runningIdx === i) runningIdx = -1;
|
|
164
|
-
renderTimeline();
|
|
165
|
-
},
|
|
166
|
-
|
|
167
|
-
setError(i, info = '') {
|
|
168
|
-
if (spinnerInterval) { clearInterval(spinnerInterval); spinnerInterval = null; }
|
|
169
|
-
statuses[i] = 'error';
|
|
170
|
-
meta[i] = info;
|
|
171
|
-
if (runningIdx === i) runningIdx = -1;
|
|
172
|
-
renderTimeline();
|
|
173
|
-
},
|
|
174
|
-
|
|
175
|
-
end() {
|
|
176
|
-
if (spinnerInterval) { clearInterval(spinnerInterval); spinnerInterval = null; }
|
|
177
|
-
if (ownScreen) screen.destroy();
|
|
178
|
-
}
|
|
179
|
-
};
|
|
180
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import express from 'express';
|
|
2
|
-
import cors from 'cors';
|
|
3
|
-
import fs from 'node:fs/promises';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import { fileURLToPath } from 'node:url';
|
|
6
|
-
|
|
7
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
-
const WEB_DIST = path.resolve(__dirname, '../../web/dist');
|
|
9
|
-
import { agentsRouter } from './routes/agents.js';
|
|
10
|
-
import { pipelinesRouter } from './routes/pipelines.js';
|
|
11
|
-
import { filesRouter } from './routes/files.js';
|
|
12
|
-
|
|
13
|
-
export async function startServer({ port = 4317, root = process.cwd(), logger = console.log } = {}) {
|
|
14
|
-
const app = express();
|
|
15
|
-
app.use(cors());
|
|
16
|
-
app.use(express.json({ limit: '2mb' }));
|
|
17
|
-
|
|
18
|
-
const ctx = {
|
|
19
|
-
root,
|
|
20
|
-
pipelinesDir: path.join(root, '.singleton', 'pipelines'),
|
|
21
|
-
agentsCacheFile: path.join(root, '.singleton', 'agents.json')
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
app.get('/api/health', (_req, res) => res.json({ ok: true, root }));
|
|
25
|
-
app.use('/api/agents', agentsRouter(ctx));
|
|
26
|
-
app.use('/api/pipelines', pipelinesRouter(ctx));
|
|
27
|
-
app.use('/api/files', filesRouter(ctx));
|
|
28
|
-
|
|
29
|
-
app.use(express.static(WEB_DIST));
|
|
30
|
-
app.get('*', (_req, res) => res.sendFile(path.join(WEB_DIST, 'index.html')));
|
|
31
|
-
|
|
32
|
-
return new Promise((resolve) => {
|
|
33
|
-
const server = app.listen(port, () => {
|
|
34
|
-
logger(`Singleton server listening on http://localhost:${port}`);
|
|
35
|
-
logger(`Project root: ${root}`);
|
|
36
|
-
resolve(server);
|
|
37
|
-
});
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
42
|
-
startServer({ port: Number(process.env.PORT) || 4317, root: process.cwd() });
|
|
43
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { Router } from 'express';
|
|
2
|
-
import fs from 'node:fs/promises';
|
|
3
|
-
import { scanAgents } from '../../../cli/src/scanner.js';
|
|
4
|
-
|
|
5
|
-
export function agentsRouter(ctx) {
|
|
6
|
-
const r = Router();
|
|
7
|
-
|
|
8
|
-
r.get('/', async (_req, res) => {
|
|
9
|
-
try {
|
|
10
|
-
try {
|
|
11
|
-
const raw = await fs.readFile(ctx.agentsCacheFile, 'utf8');
|
|
12
|
-
return res.json(JSON.parse(raw));
|
|
13
|
-
} catch {
|
|
14
|
-
const agents = await scanAgents(ctx.root);
|
|
15
|
-
return res.json({ scannedAt: new Date().toISOString(), root: ctx.root, agents });
|
|
16
|
-
}
|
|
17
|
-
} catch (err) {
|
|
18
|
-
res.status(500).json({ error: err.message });
|
|
19
|
-
}
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
r.post('/rescan', async (_req, res) => {
|
|
23
|
-
try {
|
|
24
|
-
const agents = await scanAgents(ctx.root);
|
|
25
|
-
res.json({ scannedAt: new Date().toISOString(), root: ctx.root, agents });
|
|
26
|
-
} catch (err) {
|
|
27
|
-
res.status(500).json({ error: err.message });
|
|
28
|
-
}
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
return r;
|
|
32
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { Router } from 'express';
|
|
2
|
-
import fs from 'node:fs/promises';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
|
|
5
|
-
const SKIP_DIRS = new Set(['node_modules', '.git', '.singleton', 'dist', 'build', '.next', '.cache']);
|
|
6
|
-
|
|
7
|
-
async function walk(root, rel = '', out = []) {
|
|
8
|
-
const abs = path.join(root, rel);
|
|
9
|
-
const entries = await fs.readdir(abs, { withFileTypes: true });
|
|
10
|
-
for (const e of entries) {
|
|
11
|
-
if (e.name.startsWith('.') && e.name !== '.singleton') {
|
|
12
|
-
if (e.isDirectory()) continue;
|
|
13
|
-
}
|
|
14
|
-
if (e.isDirectory()) {
|
|
15
|
-
if (SKIP_DIRS.has(e.name)) continue;
|
|
16
|
-
await walk(root, path.join(rel, e.name), out);
|
|
17
|
-
} else if (e.isFile()) {
|
|
18
|
-
out.push(path.join(rel, e.name));
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
return out;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function filesRouter(ctx) {
|
|
25
|
-
const r = Router();
|
|
26
|
-
|
|
27
|
-
r.get('/', async (req, res) => {
|
|
28
|
-
try {
|
|
29
|
-
const ext = String(req.query.ext || '').replace(/^\./, '').toLowerCase();
|
|
30
|
-
const all = await walk(ctx.root);
|
|
31
|
-
const filtered = ext
|
|
32
|
-
? all.filter((f) => f.toLowerCase().endsWith(`.${ext}`))
|
|
33
|
-
: all;
|
|
34
|
-
filtered.sort();
|
|
35
|
-
res.json({ root: ctx.root, files: filtered });
|
|
36
|
-
} catch (err) {
|
|
37
|
-
res.status(500).json({ error: err.message });
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
return r;
|
|
42
|
-
}
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import { Router } from 'express';
|
|
2
|
-
import fs from 'node:fs/promises';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
|
|
5
|
-
function safeName(name) {
|
|
6
|
-
return String(name).replace(/[^a-zA-Z0-9_-]+/g, '-').slice(0, 80) || 'pipeline';
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function pipelinesRouter(ctx) {
|
|
10
|
-
const r = Router();
|
|
11
|
-
|
|
12
|
-
r.get('/', async (_req, res) => {
|
|
13
|
-
try {
|
|
14
|
-
await fs.mkdir(ctx.pipelinesDir, { recursive: true });
|
|
15
|
-
const files = (await fs.readdir(ctx.pipelinesDir)).filter((f) => f.endsWith('.json'));
|
|
16
|
-
const items = (await Promise.all(files.map(async (f) => {
|
|
17
|
-
try {
|
|
18
|
-
const raw = await fs.readFile(path.join(ctx.pipelinesDir, f), 'utf8');
|
|
19
|
-
const parsed = JSON.parse(raw);
|
|
20
|
-
if (!parsed.name) return null;
|
|
21
|
-
return { file: f, ...parsed };
|
|
22
|
-
} catch {
|
|
23
|
-
return null;
|
|
24
|
-
}
|
|
25
|
-
}))).filter(Boolean);
|
|
26
|
-
res.json({ pipelines: items });
|
|
27
|
-
} catch (err) {
|
|
28
|
-
res.status(500).json({ error: err.message });
|
|
29
|
-
}
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
r.get('/:name', async (req, res) => {
|
|
33
|
-
try {
|
|
34
|
-
const file = path.join(ctx.pipelinesDir, `${safeName(req.params.name)}.json`);
|
|
35
|
-
const raw = await fs.readFile(file, 'utf8');
|
|
36
|
-
res.json(JSON.parse(raw));
|
|
37
|
-
} catch (err) {
|
|
38
|
-
res.status(404).json({ error: 'not found' });
|
|
39
|
-
}
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
r.post('/', async (req, res) => {
|
|
43
|
-
try {
|
|
44
|
-
const { name, steps = [], nodes, edges } = req.body || {};
|
|
45
|
-
if (!name) return res.status(400).json({ error: 'name required' });
|
|
46
|
-
await fs.mkdir(ctx.pipelinesDir, { recursive: true });
|
|
47
|
-
const safe = safeName(name);
|
|
48
|
-
const file = path.join(ctx.pipelinesDir, `${safe}.json`);
|
|
49
|
-
const payload = {
|
|
50
|
-
name: safe,
|
|
51
|
-
created: new Date().toISOString(),
|
|
52
|
-
steps,
|
|
53
|
-
nodes,
|
|
54
|
-
edges
|
|
55
|
-
};
|
|
56
|
-
await fs.writeFile(file, JSON.stringify(payload, null, 2));
|
|
57
|
-
res.json({ ok: true, file: `${safe}.json`, ...payload });
|
|
58
|
-
} catch (err) {
|
|
59
|
-
res.status(500).json({ error: err.message });
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
r.delete('/:name', async (req, res) => {
|
|
64
|
-
try {
|
|
65
|
-
const file = path.join(ctx.pipelinesDir, `${safeName(req.params.name)}.json`);
|
|
66
|
-
await fs.unlink(file);
|
|
67
|
-
res.json({ ok: true });
|
|
68
|
-
} catch (err) {
|
|
69
|
-
res.status(404).json({ error: 'not found' });
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
return r;
|
|
74
|
-
}
|