m8flow 1.1.5 → 1.1.7
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/bundled/backend/api/routes/nodes.py +38 -0
- package/bundled/backend/core/code_validator.py +11 -5
- package/bundled/backend/templates.py +54 -0
- package/bundled/frontend-dist/assets/index-BdX84Y93.css +1 -0
- package/bundled/frontend-dist/assets/index-DBpYQWaZ.js +47 -0
- package/bundled/frontend-dist/index.html +2 -2
- package/lib/logger.js +84 -30
- package/lib/run.js +67 -69
- package/package.json +1 -1
- package/bundled/frontend-dist/assets/index-CwaU33vI.js +0 -45
- package/bundled/frontend-dist/assets/index-D9h1Krrv.css +0 -1
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
9
9
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
10
10
|
<title>M8Flow — ML Pipeline Builder</title>
|
|
11
|
-
<script type="module" crossorigin src="/assets/index-
|
|
12
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
11
|
+
<script type="module" crossorigin src="/assets/index-DBpYQWaZ.js"></script>
|
|
12
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BdX84Y93.css">
|
|
13
13
|
</head>
|
|
14
14
|
<body>
|
|
15
15
|
<div id="root"></div>
|
package/lib/logger.js
CHANGED
|
@@ -1,19 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Unified logger —
|
|
3
|
-
*
|
|
2
|
+
* Unified logger — consistent symbol language, correct box alignment.
|
|
3
|
+
*
|
|
4
|
+
* Symbol convention:
|
|
5
|
+
* ✔ success / complete
|
|
6
|
+
* ℹ info / neutral
|
|
7
|
+
* ⚠ warning
|
|
8
|
+
* ✖ error / fatal
|
|
9
|
+
* spinning dots — active task (ora)
|
|
4
10
|
*/
|
|
5
11
|
import chalk from 'chalk';
|
|
6
12
|
import ora from 'ora';
|
|
7
13
|
|
|
8
14
|
// ── Palette ───────────────────────────────────────────────────────────────────
|
|
9
15
|
const c = {
|
|
10
|
-
brand:
|
|
11
|
-
ok:
|
|
12
|
-
warn:
|
|
13
|
-
error:
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
brand: chalk.hex('#6366f1'),
|
|
17
|
+
ok: chalk.hex('#22c55e'),
|
|
18
|
+
warn: chalk.hex('#f59e0b'),
|
|
19
|
+
error: chalk.hex('#ef4444'),
|
|
20
|
+
info: chalk.hex('#38bdf8'),
|
|
21
|
+
dim: chalk.dim,
|
|
22
|
+
bold: chalk.bold,
|
|
17
23
|
};
|
|
18
24
|
|
|
19
25
|
// ── Symbols ───────────────────────────────────────────────────────────────────
|
|
@@ -21,10 +27,23 @@ export const sym = {
|
|
|
21
27
|
ok: c.ok('✔'),
|
|
22
28
|
warn: c.warn('⚠'),
|
|
23
29
|
error: c.error('✖'),
|
|
30
|
+
info: c.info('ℹ'),
|
|
24
31
|
arrow: c.brand('›'),
|
|
25
|
-
dot: c.dim('·'),
|
|
26
32
|
};
|
|
27
33
|
|
|
34
|
+
// ── Strip ANSI for accurate visible-width measurement ─────────────────────────
|
|
35
|
+
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
36
|
+
|
|
37
|
+
function visibleLen(s) {
|
|
38
|
+
return s.replace(ANSI_RE, '').length;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Pad `str` so its VISIBLE length equals `width`, then return the padded string. */
|
|
42
|
+
function padVisible(str, width) {
|
|
43
|
+
const extra = width - visibleLen(str);
|
|
44
|
+
return extra > 0 ? str + ' '.repeat(extra) : str;
|
|
45
|
+
}
|
|
46
|
+
|
|
28
47
|
// ── Banner ────────────────────────────────────────────────────────────────────
|
|
29
48
|
export function banner(version) {
|
|
30
49
|
console.log();
|
|
@@ -42,9 +61,9 @@ export function banner(version) {
|
|
|
42
61
|
// ── Step spinner ──────────────────────────────────────────────────────────────
|
|
43
62
|
export function spin(text) {
|
|
44
63
|
const spinner = ora({
|
|
45
|
-
text:
|
|
46
|
-
spinner:
|
|
47
|
-
color:
|
|
64
|
+
text: ` ${text}`,
|
|
65
|
+
spinner: 'dots',
|
|
66
|
+
color: 'magenta',
|
|
48
67
|
prefixText: ' ',
|
|
49
68
|
}).start();
|
|
50
69
|
|
|
@@ -52,6 +71,7 @@ export function spin(text) {
|
|
|
52
71
|
succeed: (msg) => spinner.succeed(chalk.white(` ${msg ?? text}`)),
|
|
53
72
|
fail: (msg) => spinner.fail(c.error(` ${msg ?? text}`)),
|
|
54
73
|
warn: (msg) => spinner.warn(c.warn(` ${msg ?? text}`)),
|
|
74
|
+
info: (msg) => spinner.info(c.info(` ${msg ?? text}`)),
|
|
55
75
|
update: (msg) => { spinner.text = ` ${msg}`; },
|
|
56
76
|
stop: () => spinner.stop(),
|
|
57
77
|
};
|
|
@@ -59,28 +79,62 @@ export function spin(text) {
|
|
|
59
79
|
|
|
60
80
|
// ── Simple log helpers ────────────────────────────────────────────────────────
|
|
61
81
|
export const log = {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
warn: (...a) => console.warn(` ${sym.warn}
|
|
65
|
-
error: (...a) => console.error(` ${sym.error}
|
|
66
|
-
section: (t) =>
|
|
82
|
+
ok: (...a) => console.log(` ${sym.ok} `, ...a),
|
|
83
|
+
info: (...a) => console.log(` ${sym.info} `, ...a),
|
|
84
|
+
warn: (...a) => console.warn(` ${sym.warn} `, ...a),
|
|
85
|
+
error: (...a) => console.error(` ${sym.error} `, ...a),
|
|
86
|
+
section: (t) => {
|
|
87
|
+
console.log();
|
|
88
|
+
console.log(` ${c.bold(c.brand('─── ' + t))}`);
|
|
89
|
+
console.log();
|
|
90
|
+
},
|
|
67
91
|
blank: () => console.log(),
|
|
68
92
|
dim: (...a) => console.log(` ${c.dim(a.join(' '))}`),
|
|
93
|
+
tree: (last, msg) => console.log(` ${c.dim(last ? '└─' : '├─')} ${msg}`),
|
|
69
94
|
};
|
|
70
95
|
|
|
71
|
-
// ── Ready box
|
|
72
|
-
export function readyBox(frontendUrl, backendUrl) {
|
|
73
|
-
const
|
|
74
|
-
|
|
96
|
+
// ── Ready box — pixel-perfect alignment via visible-width padding ──────────────
|
|
97
|
+
export function readyBox(frontendUrl, backendUrl, timings = {}) {
|
|
98
|
+
const W = 46; // visible content width inside the box (between the ║ chars)
|
|
99
|
+
const br = c.brand;
|
|
100
|
+
|
|
101
|
+
/** Build one padded box row — content is measured BEFORE ANSI codes */
|
|
102
|
+
const row = (content) => {
|
|
103
|
+
const inner = padVisible(content, W);
|
|
104
|
+
return br(' ║') + inner + br('║');
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const sep = br(' ╠' + '═'.repeat(W) + '╣');
|
|
108
|
+
const top = br(' ╔' + '═'.repeat(W) + '╗');
|
|
109
|
+
const bot = br(' ╚' + '═'.repeat(W) + '╝');
|
|
110
|
+
|
|
111
|
+
const appLine = ` ${c.dim('App')} ${c.dim('→')} ${c.brand(frontendUrl)}`;
|
|
112
|
+
const apiLine = ` ${c.dim('API')} ${c.dim('→')} ${c.brand(backendUrl)}`;
|
|
113
|
+
|
|
114
|
+
console.log();
|
|
115
|
+
console.log(top);
|
|
116
|
+
console.log(row(''));
|
|
117
|
+
console.log(row(` ✨ ${c.bold('M8Flow is running!')}`));
|
|
118
|
+
console.log(row(''));
|
|
119
|
+
console.log(sep);
|
|
120
|
+
console.log(row(appLine));
|
|
121
|
+
console.log(row(apiLine));
|
|
122
|
+
console.log(row(''));
|
|
123
|
+
if (timings.total) {
|
|
124
|
+
console.log(row(` ${c.dim('Started in')} ${c.ok(timings.total + 's')}`));
|
|
125
|
+
console.log(row(''));
|
|
126
|
+
}
|
|
127
|
+
console.log(row(` ${c.dim('Press Ctrl+C to stop')}`));
|
|
128
|
+
console.log(bot);
|
|
129
|
+
console.log();
|
|
130
|
+
}
|
|
75
131
|
|
|
132
|
+
// ── Shutdown sequence ─────────────────────────────────────────────────────────
|
|
133
|
+
export function printShutdown() {
|
|
76
134
|
console.log();
|
|
77
|
-
console.log(
|
|
78
|
-
console.log(
|
|
79
|
-
console.log(
|
|
80
|
-
console.log(
|
|
81
|
-
console.log(chalk.hex('#6366f1')(' ║') + line(' API →', backendUrl).padEnd(54) + chalk.hex('#6366f1')('║'));
|
|
82
|
-
console.log(chalk.hex('#6366f1')(' ║') + ' ' + chalk.hex('#6366f1')('║'));
|
|
83
|
-
console.log(chalk.hex('#6366f1')(' ║') + c.dim(' Press Ctrl+C to stop. ') + chalk.hex('#6366f1')('║'));
|
|
84
|
-
console.log(chalk.hex('#6366f1')(' ╚══════════════════════════════════════════════╝'));
|
|
135
|
+
console.log(` ${c.dim('Shutting down M8Flow…')}`);
|
|
136
|
+
console.log(` ${c.dim('├─')} Stopping backend…`);
|
|
137
|
+
console.log(` ${c.dim('├─')} Stopping frontend…`);
|
|
138
|
+
console.log(` ${c.dim('└─')} ${c.ok('Done.')} Goodbye! 👋`);
|
|
85
139
|
console.log();
|
|
86
140
|
}
|
package/lib/run.js
CHANGED
|
@@ -1,17 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `m8flow run` — full startup orchestration.
|
|
3
|
-
*
|
|
4
|
-
* Steps:
|
|
5
|
-
* 1. Validate Python
|
|
6
|
-
* 2. Create storage dirs
|
|
7
|
-
* 3. Ensure venv
|
|
8
|
-
* 4. pip install (skip when requirements unchanged)
|
|
9
|
-
* 5. Resolve ports (auto-shift if busy)
|
|
10
|
-
* 6. Start FastAPI backend
|
|
11
|
-
* 7. Serve React frontend
|
|
12
|
-
* 8. Wait for backend health
|
|
13
|
-
* 9. Open browser
|
|
14
|
-
* 10. Keep alive until Ctrl+C
|
|
2
|
+
* `m8flow run` — full startup orchestration with timing metrics.
|
|
15
3
|
*/
|
|
16
4
|
import path from 'path';
|
|
17
5
|
import os from 'os';
|
|
@@ -24,7 +12,7 @@ import { checkPython, ensureVenv, installDeps } from './setup.js';
|
|
|
24
12
|
import { spawnBackend } from './backend.js';
|
|
25
13
|
import { serveStatic } from './server.js';
|
|
26
14
|
import { resolvePort, waitForPort, killProcessOnPort } from './ports.js';
|
|
27
|
-
import { banner, log, spin, readyBox }
|
|
15
|
+
import { banner, log, spin, readyBox, printShutdown } from './logger.js';
|
|
28
16
|
|
|
29
17
|
const require = createRequire(import.meta.url);
|
|
30
18
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -34,110 +22,126 @@ const BACKEND_DIR = path.join(PKG_DIR, 'bundled', 'backend');
|
|
|
34
22
|
const FRONTEND_DIR = path.join(PKG_DIR, 'bundled', 'frontend-dist');
|
|
35
23
|
const M8FLOW_HOME = path.join(os.homedir(), '.m8flow');
|
|
36
24
|
|
|
25
|
+
/** Returns elapsed seconds since `start` (Date.now()) as a string like "2.4s" */
|
|
26
|
+
function elapsed(start) { return ((Date.now() - start) / 1000).toFixed(1) + 's'; }
|
|
27
|
+
|
|
37
28
|
export async function run({
|
|
38
29
|
frontendPort = 3000,
|
|
39
30
|
backendPort = 8000,
|
|
40
31
|
openBrowser = true,
|
|
41
32
|
verbose = false,
|
|
42
33
|
} = {}) {
|
|
43
|
-
// Ensure any unhandled failure prints cleanly and exits — no lingering process
|
|
44
34
|
process.on('uncaughtException', (err) => {
|
|
45
35
|
log.error(err.message);
|
|
46
36
|
process.exit(1);
|
|
47
37
|
});
|
|
48
38
|
|
|
39
|
+
const totalStart = Date.now();
|
|
49
40
|
const pkg = require('../package.json');
|
|
50
41
|
banner(pkg.version);
|
|
51
42
|
|
|
52
|
-
// ── Guard: bundled assets present ────────────────────────────────────────
|
|
53
43
|
assertBundled();
|
|
54
44
|
|
|
55
|
-
// ──
|
|
56
|
-
log.section('
|
|
45
|
+
// ── Environment ──────────────────────────────────────────────────────────────
|
|
46
|
+
log.section('Checking environment');
|
|
47
|
+
|
|
48
|
+
let t = Date.now();
|
|
57
49
|
const sp1 = spin('Locating Python 3.8+…');
|
|
58
50
|
const pythonExe = await checkPython().catch((e) => { sp1.fail(e.message); throw e; });
|
|
59
|
-
sp1.succeed(`Python found (${pythonExe})`);
|
|
51
|
+
sp1.succeed(`Python found (${pythonExe}) ${elapsed(t)}`);
|
|
60
52
|
|
|
61
|
-
// ── 2. Storage dirs ───────────────────────────────────────────────────────
|
|
62
53
|
ensureStorageDirs();
|
|
63
|
-
log.ok(`Storage ready (${M8FLOW_HOME})`);
|
|
54
|
+
log.ok(`Storage dirs ready (${M8FLOW_HOME})`);
|
|
64
55
|
|
|
65
|
-
// ──
|
|
66
|
-
|
|
56
|
+
// ── Runtime setup ─────────────────────────────────────────────────────────────
|
|
57
|
+
log.section('Preparing runtime');
|
|
67
58
|
|
|
68
|
-
|
|
59
|
+
t = Date.now();
|
|
60
|
+
const venvPython = await ensureVenv(pythonExe, M8FLOW_HOME);
|
|
69
61
|
await installDeps(venvPython, BACKEND_DIR, M8FLOW_HOME);
|
|
70
62
|
|
|
71
|
-
// ──
|
|
72
|
-
log.section('
|
|
63
|
+
// ── Resolving ports ───────────────────────────────────────────────────────────
|
|
64
|
+
log.section('Resolving ports');
|
|
73
65
|
|
|
74
|
-
// Kill any leftover process from a previous session BEFORE checking port availability.
|
|
75
|
-
// On Windows, Ctrl+C sometimes leaves the Python/uvicorn process running.
|
|
76
66
|
await killProcessOnPort(backendPort);
|
|
77
67
|
await killProcessOnPort(frontendPort);
|
|
78
68
|
|
|
79
69
|
const fe = await resolvePort(frontendPort, 'frontend');
|
|
80
70
|
const be = await resolvePort(backendPort, 'backend');
|
|
81
71
|
|
|
82
|
-
if (fe.changed) log.warn(`Port ${fe.from}
|
|
83
|
-
if (be.changed) log.warn(`Port ${be.from}
|
|
72
|
+
if (fe.changed) log.warn(`Port ${fe.from} busy → using ${fe.port}`);
|
|
73
|
+
if (be.changed) log.warn(`Port ${be.from} busy → using ${be.port}`);
|
|
84
74
|
|
|
85
|
-
log.ok(`Frontend →
|
|
86
|
-
log.ok(`Backend →
|
|
75
|
+
log.ok(`Frontend → http://localhost:${fe.port}`);
|
|
76
|
+
log.ok(`Backend → http://localhost:${be.port}`);
|
|
87
77
|
|
|
88
|
-
// ──
|
|
89
|
-
log.section('
|
|
90
|
-
const sp6 = spin('Launching backend…');
|
|
91
|
-
const { proc: backendProc, mode: backendMode } = await spawnBackend(
|
|
92
|
-
venvPython, BACKEND_DIR, be.port, M8FLOW_HOME, verbose
|
|
93
|
-
);
|
|
78
|
+
// ── Starting servers ──────────────────────────────────────────────────────────
|
|
79
|
+
log.section('Initializing ML engine');
|
|
94
80
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
81
|
+
// Warn about Docker fallback BEFORE the spinner so it reads naturally
|
|
82
|
+
const { proc: backendProc, mode: backendMode } = await (async () => {
|
|
83
|
+
const { execa } = await import('execa');
|
|
84
|
+
const dockerOk = await execa('docker', ['info'], { reject: false, timeout: 3_000 })
|
|
85
|
+
.then(r => r.exitCode === 0).catch(() => false);
|
|
86
|
+
if (!dockerOk) {
|
|
87
|
+
log.info('Docker not found — falling back to Python venv');
|
|
88
|
+
}
|
|
89
|
+
return spawnBackend(venvPython, BACKEND_DIR, be.port, M8FLOW_HOME, verbose);
|
|
90
|
+
})();
|
|
91
|
+
|
|
92
|
+
const beStart = Date.now();
|
|
93
|
+
const sp6 = spin(
|
|
94
|
+
backendMode === 'docker'
|
|
95
|
+
? 'Starting backend (Docker — warm environment)…'
|
|
96
|
+
: 'Starting backend (Python venv)…'
|
|
97
|
+
);
|
|
100
98
|
|
|
101
99
|
backendProc.on('exit', (code) => {
|
|
102
|
-
if (code !== 0 && code !== null) process.exit(1);
|
|
100
|
+
if (code !== 0 && code !== null) process.exit(1);
|
|
103
101
|
});
|
|
104
102
|
|
|
105
|
-
|
|
103
|
+
const feStart = Date.now();
|
|
104
|
+
const sp7 = spin('Starting frontend server…');
|
|
106
105
|
const httpServer = await serveStatic(FRONTEND_DIR, fe.port);
|
|
106
|
+
sp7.succeed(`Frontend ready ${elapsed(feStart)}`);
|
|
107
107
|
|
|
108
|
-
// ──
|
|
109
|
-
|
|
110
|
-
// The frontend shows its own "Initializing" overlay during this window.
|
|
111
|
-
sp6.update('Waiting for backend to accept connections…');
|
|
108
|
+
// ── Health check ──────────────────────────────────────────────────────────────
|
|
109
|
+
sp6.update('Performing health checks…');
|
|
112
110
|
await waitForPort(be.port, '127.0.0.1', 120_000).catch(() => {
|
|
113
111
|
log.warn('Backend is taking longer than expected — the UI will retry automatically.');
|
|
114
112
|
});
|
|
115
|
-
sp6.succeed(
|
|
116
|
-
|
|
113
|
+
sp6.succeed(
|
|
114
|
+
backendMode === 'docker'
|
|
115
|
+
? `Backend ready (Docker — warm) ${elapsed(beStart)}`
|
|
116
|
+
: `Backend ready (Python venv) ${elapsed(beStart)}`
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const totalTime = ((Date.now() - totalStart) / 1000).toFixed(1);
|
|
120
|
+
log.ok(`M8Flow launched in ${totalTime}s`);
|
|
117
121
|
|
|
118
|
-
// ──
|
|
122
|
+
// ── Ready ─────────────────────────────────────────────────────────────────────
|
|
119
123
|
const appUrl = `http://localhost:${fe.port}`;
|
|
120
124
|
const apiUrl = `http://localhost:${be.port}`;
|
|
121
125
|
|
|
122
|
-
readyBox(appUrl, apiUrl);
|
|
126
|
+
readyBox(appUrl, apiUrl, { total: totalTime });
|
|
127
|
+
|
|
128
|
+
log.info('Open the browser to start building pipelines');
|
|
129
|
+
log.info('Drag nodes onto the canvas to create ML workflows');
|
|
130
|
+
log.blank();
|
|
123
131
|
|
|
124
132
|
if (openBrowser) {
|
|
125
133
|
try {
|
|
126
134
|
const { default: open } = await import('open');
|
|
127
135
|
await open(appUrl);
|
|
128
136
|
} catch {
|
|
129
|
-
log.dim(`
|
|
137
|
+
log.dim(`Navigate to ${appUrl}`);
|
|
130
138
|
}
|
|
131
139
|
}
|
|
132
140
|
|
|
133
|
-
// ──
|
|
134
|
-
const shutdown = (
|
|
135
|
-
|
|
136
|
-
log.info(`Received ${signal} — shutting down…`);
|
|
141
|
+
// ── Graceful shutdown ─────────────────────────────────────────────────────────
|
|
142
|
+
const shutdown = (_signal) => {
|
|
143
|
+
printShutdown();
|
|
137
144
|
|
|
138
|
-
// On Windows, .kill() only kills the execa wrapper — the Python subprocess
|
|
139
|
-
// often survives, leaving the port busy on the next run.
|
|
140
|
-
// Use taskkill /T to kill the entire process tree.
|
|
141
145
|
if (process.platform === 'win32' && backendProc.pid) {
|
|
142
146
|
try { execSync(`taskkill /PID ${backendProc.pid} /F /T`, { stdio: 'ignore' }); } catch {}
|
|
143
147
|
} else {
|
|
@@ -145,14 +149,12 @@ export async function run({
|
|
|
145
149
|
}
|
|
146
150
|
|
|
147
151
|
try { httpServer.close(); } catch {}
|
|
148
|
-
log.ok('Goodbye!\n');
|
|
149
152
|
process.exit(0);
|
|
150
153
|
};
|
|
151
154
|
|
|
152
155
|
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
153
156
|
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
154
157
|
|
|
155
|
-
// Keep the Node process alive
|
|
156
158
|
await new Promise(() => {});
|
|
157
159
|
}
|
|
158
160
|
|
|
@@ -168,14 +170,10 @@ function assertBundled() {
|
|
|
168
170
|
const missing = [];
|
|
169
171
|
if (!fs.existsSync(BACKEND_DIR)) missing.push('bundled/backend');
|
|
170
172
|
if (!fs.existsSync(FRONTEND_DIR)) missing.push('bundled/frontend-dist');
|
|
171
|
-
|
|
172
173
|
if (missing.length === 0) return;
|
|
173
|
-
|
|
174
174
|
throw new Error(
|
|
175
175
|
`Missing bundled assets: ${missing.join(', ')}\n\n` +
|
|
176
|
-
`
|
|
177
|
-
`
|
|
178
|
-
` Then install locally:\n` +
|
|
179
|
-
` cd cli && npm install -g .`
|
|
176
|
+
` Run: node cli/scripts/build.js\n` +
|
|
177
|
+
` Then: cd cli && npm install -g .`
|
|
180
178
|
);
|
|
181
179
|
}
|
package/package.json
CHANGED