@igoruehara/canvas-flow 0.1.11 → 0.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/bin/canvas-flow.js +172 -41
- package/package.json +1 -1
package/bin/canvas-flow.js
CHANGED
|
@@ -20,6 +20,56 @@ const INFRA_PROJECT_NAME = 'canvas-flow';
|
|
|
20
20
|
const INFRA_BASE_SERVICES = ['mongo'];
|
|
21
21
|
const INFRA_FULL_SERVICES = ['mongo', 'etcd', 'minio', 'milvus'];
|
|
22
22
|
|
|
23
|
+
const STARTUP_BANNER = [
|
|
24
|
+
' ______ ________ ',
|
|
25
|
+
' / ____/___ _____ _ ______ ______/ ____/ /___ _ __ ',
|
|
26
|
+
" / / / __ '/ __ \\ | / / __ '/ ___/ /_ / / __ \\ | /| / / ",
|
|
27
|
+
'/ /___/ /_/ / / / / |/ / /_/ (__ ) __/ / / /_/ / |/ |/ / ',
|
|
28
|
+
'\\____/\\__,_/_/ /_/|___/\\__,_/____/_/ /_/\\____/|__/|__/ ',
|
|
29
|
+
].join('\n');
|
|
30
|
+
|
|
31
|
+
function envFlagEnabled(name) {
|
|
32
|
+
return ['1', 'true', 'yes', 'sim', 'on'].includes(String(process.env[name] || '').trim().toLowerCase());
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function shouldUseAnsiColor() {
|
|
36
|
+
if (process.env.NO_COLOR) return false;
|
|
37
|
+
return Boolean(process.stdout.isTTY || process.env.FORCE_COLOR);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function colorAnsi(text, code) {
|
|
41
|
+
return shouldUseAnsiColor() ? `\x1b[${code}m${text}\x1b[0m` : text;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function shouldPrintStartupBanner(flags = {}) {
|
|
45
|
+
if (flags.banner === false || envFlagEnabled('CANVAS_FLOW_NO_BANNER')) return false;
|
|
46
|
+
if (process.env.CI && !envFlagEnabled('CANVAS_FLOW_BANNER')) return false;
|
|
47
|
+
return Boolean(process.stdout.isTTY || envFlagEnabled('CANVAS_FLOW_BANNER'));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function boxLine(text, width = 74) {
|
|
51
|
+
const safeText = String(text || '').slice(0, width - 4);
|
|
52
|
+
return `| ${safeText.padEnd(width - 4, ' ')} |`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function printStartupBanner(flags = {}) {
|
|
56
|
+
if (!shouldPrintStartupBanner(flags)) return;
|
|
57
|
+
|
|
58
|
+
const border = '+'.padEnd(75, '-') + '+';
|
|
59
|
+
const box = [
|
|
60
|
+
border,
|
|
61
|
+
boxLine('Canvas Flow standalone runtime'),
|
|
62
|
+
boxLine('Tip: use --with-docker for local Mongo, or --full for Mongo + Milvus.'),
|
|
63
|
+
boxLine('Docs: https://igoruehara.github.io/canvas-flow/'),
|
|
64
|
+
border,
|
|
65
|
+
].join('\n');
|
|
66
|
+
|
|
67
|
+
console.log('');
|
|
68
|
+
console.log(colorAnsi(STARTUP_BANNER.trimEnd(), '95'));
|
|
69
|
+
console.log(colorAnsi(box, '36'));
|
|
70
|
+
console.log('');
|
|
71
|
+
}
|
|
72
|
+
|
|
23
73
|
function printHelp() {
|
|
24
74
|
console.log(`
|
|
25
75
|
Canvas Flow standalone
|
|
@@ -44,6 +94,7 @@ Options:
|
|
|
44
94
|
--public-url <url> Override server.publicUrl
|
|
45
95
|
--open Open the browser after starting
|
|
46
96
|
--no-open Do not open the browser
|
|
97
|
+
--no-banner Do not print the startup banner
|
|
47
98
|
--with-docker Start local Docker infrastructure before Canvas Flow
|
|
48
99
|
--full Include Milvus, MinIO and etcd with Docker infrastructure
|
|
49
100
|
--show Show config content with "init" or "config"
|
|
@@ -835,6 +886,70 @@ function sleep(ms) {
|
|
|
835
886
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
836
887
|
}
|
|
837
888
|
|
|
889
|
+
function createStartupProgress() {
|
|
890
|
+
const frames = ['-', '\\', '|', '/'];
|
|
891
|
+
const useTty = Boolean(process.stdout.isTTY && !process.env.CI);
|
|
892
|
+
let frameIndex = 0;
|
|
893
|
+
let lastLength = 0;
|
|
894
|
+
let interval;
|
|
895
|
+
let percent = 0;
|
|
896
|
+
let message = 'starting';
|
|
897
|
+
|
|
898
|
+
const line = () => `${frames[frameIndex]} Canvas Flow startup ${String(percent).padStart(3, ' ')}% - ${message}`;
|
|
899
|
+
const clearLine = () => {
|
|
900
|
+
if (!useTty || !lastLength) return;
|
|
901
|
+
process.stdout.write(`\r${' '.repeat(lastLength)}\r`);
|
|
902
|
+
lastLength = 0;
|
|
903
|
+
};
|
|
904
|
+
const render = () => {
|
|
905
|
+
if (!useTty) return;
|
|
906
|
+
const text = line();
|
|
907
|
+
const padded = text.padEnd(lastLength, ' ');
|
|
908
|
+
lastLength = Math.max(lastLength, text.length);
|
|
909
|
+
process.stdout.write(`\r${padded}`);
|
|
910
|
+
};
|
|
911
|
+
const ensureInterval = () => {
|
|
912
|
+
if (!useTty || interval) return;
|
|
913
|
+
interval = setInterval(() => {
|
|
914
|
+
frameIndex = (frameIndex + 1) % frames.length;
|
|
915
|
+
render();
|
|
916
|
+
}, 140);
|
|
917
|
+
if (typeof interval.unref === 'function') interval.unref();
|
|
918
|
+
};
|
|
919
|
+
const stopInterval = () => {
|
|
920
|
+
if (!interval) return;
|
|
921
|
+
clearInterval(interval);
|
|
922
|
+
interval = undefined;
|
|
923
|
+
};
|
|
924
|
+
|
|
925
|
+
return {
|
|
926
|
+
update(nextPercent, nextMessage) {
|
|
927
|
+
percent = Math.max(percent, Math.min(99, Number(nextPercent) || percent));
|
|
928
|
+
message = nextMessage || message;
|
|
929
|
+
ensureInterval();
|
|
930
|
+
if (useTty) render();
|
|
931
|
+
else console.log(`Canvas Flow startup ${percent}% - ${message}`);
|
|
932
|
+
},
|
|
933
|
+
log(text) {
|
|
934
|
+
clearLine();
|
|
935
|
+
console.log(text);
|
|
936
|
+
render();
|
|
937
|
+
},
|
|
938
|
+
done(text) {
|
|
939
|
+
percent = 100;
|
|
940
|
+
message = 'ready';
|
|
941
|
+
stopInterval();
|
|
942
|
+
clearLine();
|
|
943
|
+
console.log(text || 'Canvas Flow ready (100%)');
|
|
944
|
+
},
|
|
945
|
+
fail(text) {
|
|
946
|
+
stopInterval();
|
|
947
|
+
clearLine();
|
|
948
|
+
if (text) console.log(text);
|
|
949
|
+
},
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
|
|
838
953
|
async function checkHttpOk(url, timeoutMs = 1200) {
|
|
839
954
|
const controller = new AbortController();
|
|
840
955
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
@@ -851,37 +966,37 @@ async function checkHttpOk(url, timeoutMs = 1200) {
|
|
|
851
966
|
function startStartupStatus(publicUrl, options = {}) {
|
|
852
967
|
const healthUrl = `${String(publicUrl || '').replace(/\/$/, '')}/health`;
|
|
853
968
|
const startedAt = Date.now();
|
|
969
|
+
const progress = options.progress || createStartupProgress();
|
|
854
970
|
let stopped = false;
|
|
855
|
-
|
|
856
|
-
console.log('Canvas Flow startup: loading application bundle...');
|
|
857
|
-
console.log(`Canvas Flow startup: waiting for backend health at ${healthUrl}`);
|
|
858
|
-
|
|
859
|
-
const interval = setInterval(() => {
|
|
860
|
-
const elapsedSeconds = Math.max(1, Math.round((Date.now() - startedAt) / 1000));
|
|
861
|
-
console.log(`Canvas Flow startup: still starting (${elapsedSeconds}s elapsed)...`);
|
|
862
|
-
}, 2500);
|
|
971
|
+
let nextHealthLogAt = 0;
|
|
863
972
|
|
|
864
973
|
const stop = () => {
|
|
865
974
|
if (stopped) return;
|
|
866
975
|
stopped = true;
|
|
867
|
-
clearInterval(interval);
|
|
868
976
|
};
|
|
869
977
|
|
|
870
978
|
void (async () => {
|
|
871
979
|
const deadline = Date.now() + 90000;
|
|
980
|
+
progress.update(90, `waiting for backend health at ${healthUrl}`);
|
|
872
981
|
while (!stopped && Date.now() < deadline) {
|
|
873
982
|
if (await checkHttpOk(healthUrl)) {
|
|
874
983
|
stop();
|
|
875
|
-
|
|
984
|
+
progress.done(`Canvas Flow ready (100%): ${publicUrl}`);
|
|
876
985
|
if (options.openBrowser) openBrowser(publicUrl);
|
|
877
986
|
return;
|
|
878
987
|
}
|
|
988
|
+
const elapsedSeconds = Math.max(1, Math.round((Date.now() - startedAt) / 1000));
|
|
989
|
+
if (elapsedSeconds >= nextHealthLogAt) {
|
|
990
|
+
const nextPercent = Math.min(98, 90 + Math.floor(elapsedSeconds / 15));
|
|
991
|
+
progress.update(nextPercent, `waiting for backend health (${elapsedSeconds}s elapsed)`);
|
|
992
|
+
nextHealthLogAt = elapsedSeconds + 3;
|
|
993
|
+
}
|
|
879
994
|
await sleep(500);
|
|
880
995
|
}
|
|
881
996
|
|
|
882
997
|
if (!stopped) {
|
|
883
998
|
stop();
|
|
884
|
-
|
|
999
|
+
progress.fail('Canvas Flow startup: health check is still pending. Keep this terminal open and watch the backend logs above.');
|
|
885
1000
|
}
|
|
886
1001
|
})();
|
|
887
1002
|
|
|
@@ -1161,8 +1276,11 @@ async function doctor(flags) {
|
|
|
1161
1276
|
reporter.finish();
|
|
1162
1277
|
}
|
|
1163
1278
|
|
|
1164
|
-
async function waitForMongo(config, flags, paths) {
|
|
1165
|
-
if (flags['skip-mongo-check'] === true)
|
|
1279
|
+
async function waitForMongo(config, flags, paths, progress) {
|
|
1280
|
+
if (flags['skip-mongo-check'] === true) {
|
|
1281
|
+
if (progress) progress.update(45, 'MongoDB preflight skipped');
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1166
1284
|
if (!config.database.mongoUrl) {
|
|
1167
1285
|
throw new Error(`database.mongoUrl is required. Edit the config with: canvas-flow config --edit`);
|
|
1168
1286
|
}
|
|
@@ -1174,15 +1292,18 @@ async function waitForMongo(config, flags, paths) {
|
|
|
1174
1292
|
let lastMessage = '';
|
|
1175
1293
|
|
|
1176
1294
|
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
1295
|
+
if (progress) progress.update(30, `checking MongoDB (${attempt}/${attempts})`);
|
|
1177
1296
|
const result = await checkMongoConnection(config.database.mongoUrl, options);
|
|
1178
1297
|
if (result.ok) {
|
|
1179
|
-
|
|
1298
|
+
if (progress) progress.update(55, 'MongoDB connected');
|
|
1299
|
+
else console.log('MongoDB preflight: connected');
|
|
1180
1300
|
return;
|
|
1181
1301
|
}
|
|
1182
1302
|
|
|
1183
1303
|
lastMessage = result.message;
|
|
1184
1304
|
if (attempt < attempts) {
|
|
1185
|
-
|
|
1305
|
+
if (progress) progress.update(35, `waiting for MongoDB (${attempt}/${attempts})`);
|
|
1306
|
+
else console.log(`MongoDB preflight waiting (${attempt}/${attempts}): ${result.message}`);
|
|
1186
1307
|
await sleep(1500);
|
|
1187
1308
|
}
|
|
1188
1309
|
}
|
|
@@ -1196,35 +1317,45 @@ async function waitForMongo(config, flags, paths) {
|
|
|
1196
1317
|
}
|
|
1197
1318
|
|
|
1198
1319
|
async function start(flags) {
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
infra('up', flags);
|
|
1203
|
-
}
|
|
1204
|
-
const paths = resolvePaths(flags);
|
|
1205
|
-
ensureDir(paths.homeDir);
|
|
1206
|
-
const configExisted = fs.existsSync(paths.configPath);
|
|
1207
|
-
const config = loadConfig(paths.configPath);
|
|
1208
|
-
const runtime = applyEnvironment(config, paths, flags);
|
|
1209
|
-
await waitForMongo(config, flags, paths);
|
|
1210
|
-
|
|
1211
|
-
process.chdir(paths.homeDir);
|
|
1212
|
-
|
|
1213
|
-
console.log(`Canvas Flow config: ${paths.configPath}`);
|
|
1214
|
-
console.log(`Canvas Flow home: ${paths.homeDir}`);
|
|
1215
|
-
console.log(`Canvas Flow URL: ${runtime.publicUrl}`);
|
|
1216
|
-
if (!configExisted) {
|
|
1217
|
-
console.log('Created the default config.json.');
|
|
1218
|
-
console.log('Edit it with: canvas-flow config --edit');
|
|
1219
|
-
console.log('Show it with: canvas-flow config --show');
|
|
1220
|
-
}
|
|
1221
|
-
|
|
1222
|
-
const shouldOpen = flags.open === true || (flags.open !== false && config.server.openBrowser === true);
|
|
1223
|
-
const startupStatus = startStartupStatus(runtime.publicUrl, { openBrowser: shouldOpen });
|
|
1320
|
+
printStartupBanner(flags);
|
|
1321
|
+
const progress = createStartupProgress();
|
|
1322
|
+
let startupStatus;
|
|
1224
1323
|
try {
|
|
1324
|
+
progress.update(5, 'checking package bundle');
|
|
1325
|
+
assertBundleExists();
|
|
1326
|
+
progress.update(10, 'loading runtime dependencies');
|
|
1327
|
+
addSourceDependencyFallback();
|
|
1328
|
+
if (flags['with-docker'] === true || flags.infra === true) {
|
|
1329
|
+
progress.log('Canvas Flow startup: starting Docker infrastructure...');
|
|
1330
|
+
infra('up', flags);
|
|
1331
|
+
}
|
|
1332
|
+
progress.update(15, 'loading config');
|
|
1333
|
+
const paths = resolvePaths(flags);
|
|
1334
|
+
ensureDir(paths.homeDir);
|
|
1335
|
+
const configExisted = fs.existsSync(paths.configPath);
|
|
1336
|
+
const config = loadConfig(paths.configPath);
|
|
1337
|
+
progress.update(25, 'applying environment');
|
|
1338
|
+
const runtime = applyEnvironment(config, paths, flags);
|
|
1339
|
+
await waitForMongo(config, flags, paths, progress);
|
|
1340
|
+
|
|
1341
|
+
process.chdir(paths.homeDir);
|
|
1342
|
+
|
|
1343
|
+
progress.log(`Canvas Flow config: ${paths.configPath}`);
|
|
1344
|
+
progress.log(`Canvas Flow home: ${paths.homeDir}`);
|
|
1345
|
+
progress.log(`Canvas Flow URL: ${runtime.publicUrl}`);
|
|
1346
|
+
if (!configExisted) {
|
|
1347
|
+
progress.log('Created the default config.json.');
|
|
1348
|
+
progress.log('Edit it with: canvas-flow config --edit');
|
|
1349
|
+
progress.log('Show it with: canvas-flow config --show');
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
const shouldOpen = flags.open === true || (flags.open !== false && config.server.openBrowser === true);
|
|
1353
|
+
progress.update(75, 'starting Canvas Flow API');
|
|
1354
|
+
startupStatus = startStartupStatus(runtime.publicUrl, { openBrowser: shouldOpen, progress });
|
|
1225
1355
|
require(SERVER_ENTRY);
|
|
1226
1356
|
} catch (error) {
|
|
1227
|
-
startupStatus.stop();
|
|
1357
|
+
if (startupStatus) startupStatus.stop();
|
|
1358
|
+
progress.fail('Canvas Flow startup failed.');
|
|
1228
1359
|
throw error;
|
|
1229
1360
|
}
|
|
1230
1361
|
}
|
package/package.json
CHANGED