@fluxy-chat/create-fluxy-chat 0.4.0 → 0.5.0

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/CHANGELOG.md ADDED
@@ -0,0 +1,16 @@
1
+ # Changelog
2
+
3
+ ## [0.5.0] - 2026-08-19
4
+
5
+ ### Added
6
+
7
+ - `--full` / `--template full` — Vite app with realtime chat, `@assistant` invoke, and tool thread
8
+ - `--mode hosted` — guest JWT via `GET /demo/session` (no local wrangler)
9
+ - `--mode local` — provision against a local worker (`POST /dev/provision`)
10
+ - Template scripts: `pnpm setup`, `pnpm setup:hosted`, `pnpm doctor`, `pnpm dev`
11
+ - CLI next-steps link to `/onboarding?from=cli` (keep / import `.env`)
12
+
13
+ ### Changed
14
+
15
+ - Interactive picker lists Full stack first
16
+ - README hero documents hosted vs local paths
package/dist/index.js CHANGED
@@ -15,6 +15,7 @@ import {
15
15
  // src/utils.ts
16
16
  import fs from "fs";
17
17
  import path from "path";
18
+ import { fileURLToPath } from "url";
18
19
  var PACKAGE_NAME_PATTERN = /^[a-z0-9][a-z0-9._-]*$/;
19
20
  var PACKAGE_MANAGERS = /* @__PURE__ */ new Set(["npm", "yarn", "pnpm"]);
20
21
  function validateProjectName(value) {
@@ -30,7 +31,7 @@ function isPackageManager(value) {
30
31
  return PACKAGE_MANAGERS.has(value);
31
32
  }
32
33
  function isAdapterType(value) {
33
- return ["basic", "slack", "telegram", "discord", "web", "react", "hr-feedback"].includes(value);
34
+ return ["basic", "slack", "telegram", "discord", "web", "react", "hr-feedback", "full"].includes(value);
34
35
  }
35
36
  function detectPackageManagerFromLockfiles(cwd) {
36
37
  if (fs.existsSync(path.join(cwd, "pnpm-lock.yaml"))) {
@@ -92,20 +93,19 @@ function copyDir(source, destination) {
92
93
  }
93
94
  }
94
95
  function templatesDir() {
95
- return path.resolve(
96
- path.dirname(new URL(import.meta.url).pathname.replace(/^\//, "")),
97
- "..",
98
- "templates"
99
- );
96
+ return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "templates");
100
97
  }
101
98
 
102
99
  // src/prompts.ts
103
100
  var DEFAULT_PROJECT_NAME = "my-fluxy-bot";
101
+ var DEFAULT_FULL_PROJECT_NAME = "my-fluxy-app";
104
102
  async function runPrompts(inputs) {
103
+ const full = inputs.full ?? inputs.adapter === "full";
104
+ const mode = inputs.mode ?? (full ? "local" : void 0);
105
105
  let name = inputs.name;
106
106
  if (!name) {
107
107
  if (inputs.yes) {
108
- name = DEFAULT_PROJECT_NAME;
108
+ name = full ? DEFAULT_FULL_PROJECT_NAME : DEFAULT_PROJECT_NAME;
109
109
  } else {
110
110
  const result = await text({
111
111
  message: "Project name:",
@@ -122,14 +122,17 @@ async function runPrompts(inputs) {
122
122
  }
123
123
  const minimal = inputs.minimal ?? false;
124
124
  let adapter = inputs.adapter;
125
- if (!minimal && !adapter) {
125
+ if (full) {
126
+ adapter = "full";
127
+ } else if (!minimal && !adapter) {
126
128
  if (inputs.yes) {
127
129
  adapter = "react";
128
130
  } else {
129
131
  const result = await select({
130
- message: "Select an adapter:",
132
+ message: "Select a template:",
131
133
  options: [
132
- { label: "Minimal chat widget (ui-kit, recommended)", value: "minimal" },
134
+ { label: "Full stack \u2014 chat + @assistant + setup (recommended)", value: "full" },
135
+ { label: "Minimal chat widget (ui-kit)", value: "minimal" },
133
136
  { label: "React chat app (Vite + useChat)", value: "react" },
134
137
  { label: "HR anonymous feedback (compliance starter)", value: "hr-feedback" },
135
138
  { label: "Basic (Cloudflare Workers bot)", value: "basic" },
@@ -147,7 +150,8 @@ async function runPrompts(inputs) {
147
150
  }
148
151
  }
149
152
  let language = inputs.language;
150
- if (!language) {
153
+ const isWorkerBot = !minimal && adapter !== "react" && adapter !== "full" && adapter !== "hr-feedback";
154
+ if (isWorkerBot && !language) {
151
155
  if (inputs.yes) {
152
156
  language = "typescript";
153
157
  } else {
@@ -161,6 +165,8 @@ async function runPrompts(inputs) {
161
165
  if (isCancel(result)) return null;
162
166
  language = result;
163
167
  }
168
+ } else if (!language) {
169
+ language = "typescript";
164
170
  }
165
171
  let packageManager = inputs.packageManager;
166
172
  if (!packageManager) {
@@ -195,10 +201,12 @@ async function runPrompts(inputs) {
195
201
  name,
196
202
  adapter: adapter ?? "react",
197
203
  packageManager,
198
- language,
204
+ language: language ?? "typescript",
199
205
  shouldInstall,
200
206
  shouldInitGit,
201
- minimal: minimal || inputs.minimal === true
207
+ minimal: minimal || inputs.minimal === true,
208
+ full: full || adapter === "full",
209
+ mode: mode ?? (adapter === "full" ? "local" : void 0)
202
210
  };
203
211
  }
204
212
 
@@ -799,10 +807,12 @@ import path2 from "path";
799
807
  import { exec } from "child_process";
800
808
  import { promisify } from "util";
801
809
  var execAsync = promisify(exec);
810
+ var TEMPLATE_CHOICES = "react, full, basic, slack, telegram, discord, web, hr-feedback";
802
811
  function parseArgs(argv) {
803
812
  const args = {
804
813
  yes: false,
805
814
  minimal: false,
815
+ full: false,
806
816
  skipInstall: false,
807
817
  noGit: false,
808
818
  help: false
@@ -816,6 +826,21 @@ function parseArgs(argv) {
816
826
  args.yes = true;
817
827
  } else if (arg === "--minimal") {
818
828
  args.minimal = true;
829
+ } else if (arg === "--full") {
830
+ args.full = true;
831
+ args.adapter = "full";
832
+ } else if (arg === "--mode") {
833
+ const value = argv[++i]?.trim().toLowerCase();
834
+ if (value === "local" || value === "hosted") {
835
+ args.mode = value;
836
+ if (value === "hosted") {
837
+ args.full = true;
838
+ args.adapter = "full";
839
+ }
840
+ } else {
841
+ console.error(`Invalid mode: ${value}. Choose: local, hosted`);
842
+ process.exit(1);
843
+ }
819
844
  } else if (arg === "--skip-install") {
820
845
  args.skipInstall = true;
821
846
  } else if (arg === "--no-git") {
@@ -829,7 +854,7 @@ function parseArgs(argv) {
829
854
  } else if (value === "hr-feedback") {
830
855
  args.adapter = "hr-feedback";
831
856
  } else {
832
- console.error(`Invalid template: ${value}. Choose: react, basic, slack, telegram, discord, web, hr-feedback`);
857
+ console.error(`Invalid template: ${value}. Choose: ${TEMPLATE_CHOICES}`);
833
858
  process.exit(1);
834
859
  }
835
860
  } else if (arg === "--adapter" || arg === "-a") {
@@ -837,7 +862,7 @@ function parseArgs(argv) {
837
862
  if (value && isAdapterType(value)) {
838
863
  args.adapter = value;
839
864
  } else {
840
- console.error(`Invalid adapter: ${value}. Choose: react, basic, slack, telegram, discord, web, hr-feedback`);
865
+ console.error(`Invalid adapter: ${value}. Choose: ${TEMPLATE_CHOICES}`);
841
866
  process.exit(1);
842
867
  }
843
868
  } else if (arg === "--pm" || arg === "--package-manager") {
@@ -875,17 +900,21 @@ ${pc.bold("Usage:")}
875
900
  npx create-fluxy-chat [project-name] [options]
876
901
 
877
902
  ${pc.bold("Options:")}
878
- -a, --adapter <type> Adapter: react, basic, slack, telegram, discord, web, hr-feedback
879
- -t, --template <type> Alias for --adapter (e.g. react, hr-feedback)
903
+ -a, --adapter <type> Adapter: ${TEMPLATE_CHOICES}
904
+ -t, --template <type> Alias for --adapter (e.g. full, react)
880
905
  --pm <manager> Package manager: npm, pnpm, yarn
881
906
  -l, --language <lang> Language: typescript (default) or javascript
882
907
  -y, --yes Skip prompts and accept defaults
908
+ --full Full stack: chat + @assistant + setup scripts (recommended)
909
+ --mode <local|hosted> hosted = no wrangler (uses fluxychat.com demo session)
883
910
  --minimal Chat-only widget (ui-kit) \u2014 no platform modules
884
911
  --skip-install Skip dependency installation
885
912
  --no-git Skip git repository initialization
886
913
  -h, --help Show this help
887
914
 
888
915
  ${pc.bold("Examples:")}
916
+ ${pc.cyan("npx create-fluxy-chat my-app --mode hosted -y")}
917
+ ${pc.cyan("npx create-fluxy-chat my-app --full -y")}
889
918
  ${pc.cyan("npx create-fluxy-chat my-chat --minimal")}
890
919
  ${pc.cyan("npx create-fluxy-chat my-hr-bot --template hr-feedback")}
891
920
  ${pc.cyan("npx create-fluxy-chat my-chat --template react")}
@@ -908,6 +937,8 @@ async function main() {
908
937
  language: args.language,
909
938
  yes: args.yes,
910
939
  minimal: args.minimal,
940
+ full: args.full,
941
+ mode: args.mode,
911
942
  shouldInstall: args.skipInstall ? false : void 0,
912
943
  shouldInitGit: args.noGit ? false : void 0
913
944
  });
@@ -938,6 +969,26 @@ async function main() {
938
969
  pkg.name = config.name;
939
970
  writeJson(projectDir, "package.json", pkg);
940
971
  s.stop("Minimal chat widget created.");
972
+ } else if (config.full || config.adapter === "full") {
973
+ const templateRoot = path2.join(templatesDir(), "full");
974
+ copyDir(templateRoot, projectDir);
975
+ const pkgPath = path2.join(projectDir, "package.json");
976
+ const pkg = JSON.parse(fs2.readFileSync(pkgPath, "utf8"));
977
+ pkg.name = config.name;
978
+ writeJson(projectDir, "package.json", pkg);
979
+ if (config.full || config.adapter === "full") {
980
+ fs2.mkdirSync(path2.join(projectDir, ".fluxy"), { recursive: true });
981
+ const setupMode = config.mode === "hosted" ? "hosted" : "local";
982
+ writeFile(
983
+ projectDir,
984
+ ".fluxy/mode",
985
+ `${setupMode}
986
+ `
987
+ );
988
+ }
989
+ s.stop(
990
+ config.mode === "hosted" ? "Full stack app created (hosted mode \u2014 run pnpm setup:hosted)." : "Full stack app created (chat + agent + setup scripts)."
991
+ );
941
992
  } else if (config.adapter === "react") {
942
993
  const templateRoot = path2.join(templatesDir(), "react");
943
994
  copyDir(templateRoot, projectDir);
@@ -1001,19 +1052,31 @@ async function main() {
1001
1052
  );
1002
1053
  }
1003
1054
  }
1004
- note(
1005
- config.adapter === "react" ? [
1006
- `cd ${config.name}`,
1007
- "cp .env.example .env",
1008
- "# Set VITE_FLUXYCHAT_WORKER_URL + VITE_FLUXYCHAT_PUBLIC_ROOM_ID (guest) or MEMBER_JWT",
1009
- `${config.packageManager === "npm" ? "npm run" : config.packageManager} dev`
1010
- ].join("\n") : [
1011
- `cd ${config.name}`,
1012
- "cp .env.example .dev.vars",
1013
- `${config.packageManager === "npm" ? "npm run" : config.packageManager} dev`
1014
- ].join("\n"),
1015
- "Next steps"
1016
- );
1055
+ const devCmd = config.packageManager === "npm" ? "npm run" : config.packageManager;
1056
+ const nextSteps = config.full || config.adapter === "full" ? config.mode === "hosted" ? [
1057
+ `cd ${config.name}`,
1058
+ `${devCmd} setup:hosted # guest JWT from fluxychat.com`,
1059
+ `${devCmd} dev # http://localhost:5173`,
1060
+ `# Keep this project: https://fluxychat.com/onboarding?from=cli`
1061
+ ].join("\n") : [
1062
+ `# Terminal 1 \u2014 FluxyChat monorepo (if not already running)`,
1063
+ `pnpm --filter @fluxy-chat/worker dev`,
1064
+ ``,
1065
+ `cd ${config.name}`,
1066
+ `${devCmd} setup # provision worker \u2192 writes .env`,
1067
+ `${devCmd} dev # http://localhost:5173 (+ dashboard if monorepo nearby)`,
1068
+ `# Keep / import .env: http://localhost:3000/onboarding?from=cli`
1069
+ ].join("\n") : config.adapter === "react" || config.minimal ? [
1070
+ `cd ${config.name}`,
1071
+ "cp .env.example .env",
1072
+ "# Set VITE_FLUXYCHAT_WORKER_URL + JWT or public room ID",
1073
+ `${devCmd} dev`
1074
+ ].join("\n") : [
1075
+ `cd ${config.name}`,
1076
+ "cp .env.example .dev.vars",
1077
+ `${devCmd} dev`
1078
+ ].join("\n");
1079
+ note(nextSteps, "Next steps");
1017
1080
  outro(
1018
1081
  `${pc.green("Done!")} Visit ${pc.cyan("https://github.com/AlessandroFare/fluxychat")} for the docs.`
1019
1082
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fluxy-chat/create-fluxy-chat",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Scaffold a new FluxyChat bot project with a single command",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -13,7 +13,8 @@
13
13
  "files": [
14
14
  "dist",
15
15
  "templates",
16
- "README.md"
16
+ "README.md",
17
+ "CHANGELOG.md"
17
18
  ],
18
19
  "dependencies": {
19
20
  "@clack/prompts": "^0.7.0",
package/readme.md CHANGED
@@ -5,19 +5,24 @@ Scaffold a new [FluxyChat](https://github.com/AlessandroFare/fluxychat) bot proj
5
5
  ## Quick start
6
6
 
7
7
  ```bash
8
- # Minimal chat widget (recommended: 3 lines in App.tsx)
8
+ # Full stack — chat + @assistant + setup scripts (recommended)
9
+ npx create-fluxy-chat my-app --full -y
10
+ cd my-app && pnpm setup && pnpm dev
11
+
12
+ # Minimal chat widget
9
13
  npx create-fluxy-chat my-chat --minimal
10
14
 
11
- # React + useChat (guest room ~60s or member JWT)
15
+ # React + useChat only (bring your own worker URL)
12
16
  npx create-fluxy-chat my-chat --template react
13
17
  ```
14
18
 
15
- Interactive mode defaults to the **React chat app** template.
16
-
17
19
  ## Non-interactive usage
18
20
 
19
21
  ```bash
20
- # React + Vite + useChat (recommended)
22
+ # Full stack (chat + agent + setup)
23
+ npx create-fluxy-chat my-app --full -y
24
+
25
+ # React + Vite + useChat
21
26
  npx create-fluxy-chat my-chat --template react -y
22
27
 
23
28
  # Create a Slack bot with pnpm
@@ -37,11 +42,12 @@ npx create-fluxy-chat my-bot --adapter basic
37
42
 
38
43
  | Flag | Short | Description |
39
44
  | --- | --- | --- |
40
- | `--adapter <type>` | `-a` | Adapter: `react`, `basic`, `slack`, `telegram`, `discord`, `web` |
41
- | `--template <type>` | `-t` | Alias for `--adapter (e.g. react)` |
45
+ | `--adapter <type>` | `-a` | Adapter: `full`, `react`, `basic`, `slack`, `telegram`, `discord`, `web`, `hr-feedback` |
46
+ | `--template <type>` | `-t` | Alias for `--adapter (e.g. full, react)` |
42
47
  | `--pm <manager>` | | Package manager: `npm`, `pnpm`, `yarn` |
43
48
  | `--language <lang>` | `-l` | Language: `typescript` (default) or `javascript` |
44
49
  | `--yes` | `-y` | Skip prompts and accept defaults |
50
+ | `--full` | | Full stack template: chat + `@assistant` + `pnpm setup` / `pnpm dev` |
45
51
  | `--minimal` | | Chat-only widget (`@fluxy-chat/ui-kit`), no platform modules |
46
52
  | `--skip-install` | | Skip dependency installation |
47
53
  | `--no-git` | | Skip git repository initialization |
@@ -0,0 +1,10 @@
1
+ # Generated by `pnpm setup` — or copy manually after onboarding / first-message.
2
+
3
+ VITE_FLUXYCHAT_WORKER_URL=http://127.0.0.1:8787
4
+ VITE_FLUXYCHAT_MEMBER_JWT=
5
+ VITE_FLUXYCHAT_ROOM_ID=general
6
+ VITE_FLUXYCHAT_AGENT_ID=
7
+ VITE_FLUXYCHAT_AGENT_HANDLE=@assistant
8
+ VITE_FLUXYCHAT_PROJECT_ID=
9
+ VITE_FLUXYCHAT_CONSOLE_URL=http://localhost:3000
10
+ VITE_FLUXYCHAT_USER_ID=demo-user
@@ -0,0 +1,66 @@
1
+ # FluxyChat — full stack starter
2
+
3
+ Chat + AI agent + tool calls in one Vite app. Provisions against a local FluxyChat worker in ~60 seconds.
4
+
5
+ ## Quick start
6
+
7
+ **Hosted (no wrangler):**
8
+
9
+ ```bash
10
+ npx create-fluxy-chat my-app --mode hosted -y
11
+ cd my-app
12
+ pnpm install
13
+ pnpm setup:hosted
14
+ pnpm dev
15
+ ```
16
+
17
+ **Local (full control):**
18
+
19
+ ```bash
20
+ # Terminal 1 — monorepo worker
21
+ pnpm --filter @fluxy-chat/worker dev
22
+
23
+ # Terminal 2 — new project
24
+ npx create-fluxy-chat my-app --full -y
25
+ cd my-app
26
+ pnpm install
27
+ pnpm setup
28
+ pnpm dev
29
+ ```
30
+
31
+ Optional: `pnpm doctor` checks `.env`, worker health, room access.
32
+
33
+ ## What you get
34
+
35
+ - Realtime chat (`useChat` + WebSocket)
36
+ - `@assistant` invoke with tool call thread in the UI
37
+ - Sidebar with project/room/agent metadata
38
+ - Link to operator console
39
+
40
+ ## Scripts
41
+
42
+ | Command | Description |
43
+ |---------|-------------|
44
+ | `pnpm setup:hosted` | Guest JWT via `GET /demo/session` on api.fluxychat.com |
45
+ | `pnpm setup:local` | Local `/dev/provision` (same as `pnpm setup`) |
46
+ | `pnpm doctor` | Health check: .env, worker, room |
47
+ | `pnpm dev` | Start Vite; auto-start worker if FluxyChat monorepo is detected nearby |
48
+ | `pnpm dev:app` | Vite only |
49
+
50
+ ## Environment
51
+
52
+ See `.env.example`. Generated by `pnpm setup`:
53
+
54
+ - `VITE_FLUXYCHAT_WORKER_URL` — default `http://127.0.0.1:8787`
55
+ - `VITE_FLUXYCHAT_MEMBER_JWT` — member token with admin roles
56
+ - `VITE_FLUXYCHAT_ROOM_ID` — `{projectId}-general`
57
+ - `VITE_FLUXYCHAT_AGENT_ID` — built-in `@assistant`
58
+
59
+ ## Hosted credentials
60
+
61
+ Use [fluxychat.com/onboarding](https://fluxychat.com/onboarding) and copy values into `.env` manually.
62
+
63
+ ## Learn more
64
+
65
+ - [One-click product roadmap](https://github.com/AlessandroFare/fluxychat/blob/main/docs/ONE-CLICK-PRODUCT-ROADMAP.md)
66
+ - [Docs](https://docs.fluxychat.com)
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>FluxyChat</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "fluxy-chat-full",
3
+ "private": true,
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "setup": "node scripts/fluxy-setup.mjs",
8
+ "setup:hosted": "node scripts/fluxy-setup.mjs --mode hosted",
9
+ "setup:local": "node scripts/fluxy-setup.mjs --mode local",
10
+ "doctor": "node scripts/fluxy-doctor.mjs",
11
+ "dev": "node scripts/fluxy-dev.mjs",
12
+ "dev:app": "vite",
13
+ "build": "tsc -b && vite build",
14
+ "preview": "vite preview"
15
+ },
16
+ "dependencies": {
17
+ "@fluxy-chat/react": "^0.1.1",
18
+ "@fluxy-chat/sdk": "^0.6.1",
19
+ "react": "^19.0.0",
20
+ "react-dom": "^19.0.0"
21
+ },
22
+ "devDependencies": {
23
+ "@types/react": "^19.0.0",
24
+ "@types/react-dom": "^19.0.0",
25
+ "@vitejs/plugin-react": "^4.3.0",
26
+ "typescript": "^5.6.0",
27
+ "vite": "^6.0.0"
28
+ }
29
+ }
@@ -0,0 +1,169 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Start Vite app; optionally worker + dashboard from detected monorepo.
4
+ * Auto-runs setup when .env is missing and worker is reachable.
5
+ */
6
+ import { spawn, spawnSync } from "node:child_process";
7
+ import { existsSync, readFileSync } from "node:fs";
8
+ import { dirname, join, resolve } from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+
11
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
12
+ const envPath = join(root, ".env");
13
+ const modePath = join(root, ".fluxy", "mode");
14
+ const WORKER_URL = process.env.FLUXY_WORKER_URL || "http://127.0.0.1:8787";
15
+ const POLL_MS = 500;
16
+ const POLL_MAX = 90_000;
17
+ const START_DASHBOARD = process.env.FLUXY_START_DASHBOARD !== "0";
18
+
19
+ function findMonorepo(startDir) {
20
+ const explicit = process.env.FLUXYCHAT_ROOT?.trim();
21
+ if (explicit && existsSync(join(explicit, "apps", "worker", "package.json"))) {
22
+ return explicit;
23
+ }
24
+ let dir = startDir;
25
+ for (let i = 0; i < 10; i++) {
26
+ const workerPkg = join(dir, "apps", "worker", "package.json");
27
+ if (existsSync(workerPkg)) {
28
+ try {
29
+ const pkg = JSON.parse(readFileSync(workerPkg, "utf8"));
30
+ if (pkg.name === "@fluxy-chat/worker") return dir;
31
+ } catch {
32
+ /* ignore */
33
+ }
34
+ }
35
+ const parent = dirname(dir);
36
+ if (parent === dir) break;
37
+ dir = parent;
38
+ }
39
+ return null;
40
+ }
41
+
42
+ function readSetupMode() {
43
+ if (existsSync(modePath)) {
44
+ const m = readFileSync(modePath, "utf8").trim().toLowerCase();
45
+ if (m === "hosted" || m === "local") return m;
46
+ }
47
+ return "local";
48
+ }
49
+
50
+ async function isWorkerUp(url = WORKER_URL) {
51
+ try {
52
+ const r = await fetch(`${url}/health`, { signal: AbortSignal.timeout(1500) });
53
+ return r.ok;
54
+ } catch {
55
+ return false;
56
+ }
57
+ }
58
+
59
+ function sleep(ms) {
60
+ return new Promise((r) => setTimeout(r, ms));
61
+ }
62
+
63
+ function runProc(label, cmd, args, cwd, env = {}) {
64
+ const child = spawn(cmd, args, {
65
+ cwd,
66
+ shell: process.platform === "win32",
67
+ stdio: "inherit",
68
+ env: { ...process.env, ...env },
69
+ });
70
+ child.on("exit", (code) => {
71
+ if (code && code !== 0) {
72
+ console.error(`[${label}] exited with code ${code}`);
73
+ }
74
+ });
75
+ return child;
76
+ }
77
+
78
+ function runSetup(mode) {
79
+ const args = ["scripts/fluxy-setup.mjs"];
80
+ if (mode === "hosted") args.push("--mode", "hosted");
81
+ const result = spawnSync("node", args, {
82
+ cwd: root,
83
+ stdio: "inherit",
84
+ shell: process.platform === "win32",
85
+ });
86
+ return result.status === 0;
87
+ }
88
+
89
+ async function main() {
90
+ const monorepo = findMonorepo(root);
91
+ const children = [];
92
+ const setupMode = readSetupMode();
93
+
94
+ if (!existsSync(envPath)) {
95
+ if (setupMode === "hosted") {
96
+ console.log("No .env — running hosted setup…");
97
+ if (!runSetup("hosted")) {
98
+ console.warn("Hosted setup failed. Fix network or run: pnpm setup -- --mode hosted");
99
+ }
100
+ } else if (await isWorkerUp()) {
101
+ console.log("No .env — running local setup…");
102
+ if (!runSetup("local")) {
103
+ console.warn("Setup failed. Run: pnpm setup");
104
+ }
105
+ } else if (monorepo) {
106
+ console.log("No .env — will start worker then run setup…");
107
+ } else {
108
+ console.warn(
109
+ "No .env found.\n" +
110
+ " Hosted: pnpm setup -- --mode hosted\n" +
111
+ " Local: start worker, then pnpm setup",
112
+ );
113
+ }
114
+ }
115
+
116
+ if (!(await isWorkerUp()) && setupMode === "local" && monorepo) {
117
+ console.log(`Starting worker from monorepo: ${monorepo}`);
118
+ children.push(
119
+ runProc("worker", "pnpm", ["--filter", "@fluxy-chat/worker", "dev"], monorepo, {
120
+ ALLOW_DEV_PROVISION: "true",
121
+ NODE_ENV: "development",
122
+ }),
123
+ );
124
+ const start = Date.now();
125
+ while (Date.now() - start < POLL_MAX) {
126
+ if (await isWorkerUp()) {
127
+ console.log(`Worker ready at ${WORKER_URL}`);
128
+ break;
129
+ }
130
+ await sleep(POLL_MS);
131
+ }
132
+ if (!existsSync(envPath) && (await isWorkerUp())) {
133
+ console.log("Running setup after worker start…");
134
+ runSetup("local");
135
+ }
136
+ } else if (await isWorkerUp()) {
137
+ console.log(`Worker reachable at ${WORKER_URL}`);
138
+ }
139
+
140
+ if (START_DASHBOARD && monorepo && setupMode === "local") {
141
+ console.log("Starting dashboard console on :3000");
142
+ children.push(
143
+ runProc("dashboard", "pnpm", ["--filter", "@fluxy-chat/dashboard", "dev"], monorepo),
144
+ );
145
+ }
146
+
147
+ children.push(runProc("vite", "pnpm", ["run", "dev:app"], root));
148
+
149
+ function shutdown() {
150
+ for (const child of children) {
151
+ if (child && !child.killed) {
152
+ try {
153
+ child.kill("SIGTERM");
154
+ } catch {
155
+ /* ignore */
156
+ }
157
+ }
158
+ }
159
+ process.exit(0);
160
+ }
161
+
162
+ process.on("SIGINT", shutdown);
163
+ process.on("SIGTERM", shutdown);
164
+ }
165
+
166
+ main().catch((err) => {
167
+ console.error(err);
168
+ process.exit(1);
169
+ });