@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.
Files changed (2) hide show
  1. package/bin/canvas-flow.js +172 -41
  2. package/package.json +1 -1
@@ -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
- console.log(`Canvas Flow ready: ${publicUrl}`);
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
- console.log('Canvas Flow startup: health check is still pending. Keep this terminal open and watch the backend logs above.');
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) return;
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
- console.log('MongoDB preflight: connected');
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
- console.log(`MongoDB preflight waiting (${attempt}/${attempts}): ${result.message}`);
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
- assertBundleExists();
1200
- addSourceDependencyFallback();
1201
- if (flags['with-docker'] === true || flags.infra === true) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@igoruehara/canvas-flow",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "Standalone npm launcher for Canvas Flow multi-agent GenAI workflows.",
5
5
  "homepage": "https://github.com/igoruehara/canvas-flow#readme",
6
6
  "repository": {