@bagelink/workspace 1.8.26 → 1.8.30

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.
@@ -0,0 +1,483 @@
1
+ import { existsSync, mkdirSync, writeFileSync, readdirSync, readFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import process from 'node:process';
4
+ import prompts from 'prompts';
5
+
6
+ async function initWorkspace(root = process.cwd()) {
7
+ console.log("\n\u{1F680} Creating Bagel workspace...\n");
8
+ const response = await prompts([
9
+ {
10
+ type: "text",
11
+ name: "workspaceName",
12
+ message: "Workspace name:",
13
+ initial: "my-workspace"
14
+ },
15
+ {
16
+ type: "text",
17
+ name: "projectId",
18
+ message: "Bagel project ID:",
19
+ initial: "my-project"
20
+ },
21
+ {
22
+ type: "confirm",
23
+ name: "createFirstProject",
24
+ message: "Create first project?",
25
+ initial: true
26
+ },
27
+ {
28
+ type: (prev) => prev ? "text" : null,
29
+ name: "firstProjectName",
30
+ message: "First project name:",
31
+ initial: "web"
32
+ }
33
+ ]);
34
+ if (!response || !response.workspaceName) {
35
+ console.log("\n\u274C Workspace creation cancelled.\n");
36
+ process.exit(1);
37
+ }
38
+ const { workspaceName, projectId, createFirstProject, firstProjectName } = response;
39
+ const workspaceDir = resolve(root, workspaceName);
40
+ createWorkspaceRoot(root, workspaceName, projectId);
41
+ createSharedPackage(workspaceDir);
42
+ if (createFirstProject && firstProjectName) {
43
+ await addProject(firstProjectName, workspaceDir);
44
+ }
45
+ console.log("\n\u2705 Workspace created successfully!");
46
+ console.log("\nNext steps:");
47
+ console.log(` cd ${workspaceName}`);
48
+ console.log(" bun install");
49
+ if (createFirstProject) {
50
+ console.log(` bun run dev:${firstProjectName}`);
51
+ } else {
52
+ console.log(" bgl add <project-name> # Add a project");
53
+ }
54
+ console.log("");
55
+ }
56
+ function createWorkspaceRoot(root, name, projectId) {
57
+ const workspaceDir = resolve(root, name);
58
+ if (existsSync(workspaceDir)) {
59
+ console.error(`\u274C Directory ${name} already exists`);
60
+ process.exit(1);
61
+ }
62
+ mkdirSync(workspaceDir, { recursive: true });
63
+ const packageJson = {
64
+ name,
65
+ private: true,
66
+ workspaces: ["*", "!node_modules"],
67
+ scripts: {
68
+ "dev": "bgl dev",
69
+ "dev:local": "bgl dev --mode localhost",
70
+ "dev:verbose": "bun run --filter './!shared*' dev",
71
+ "build": "bgl build",
72
+ "typecheck": "tsc --noEmit",
73
+ "lint": "eslint . --cache",
74
+ "lint:fix": "eslint . --cache --fix"
75
+ },
76
+ dependencies: {
77
+ "@bagelink/auth": "latest",
78
+ "@bagelink/sdk": "latest",
79
+ "@bagelink/vue": "latest",
80
+ "pinia": "latest",
81
+ "vue": "latest",
82
+ "vue-router": "latest"
83
+ },
84
+ devDependencies: {
85
+ "@bagelink/lint-config": "latest",
86
+ "@bagelink/workspace": "latest",
87
+ "@vitejs/plugin-vue": "latest",
88
+ "eslint": "latest",
89
+ "typescript": "^5.0.0",
90
+ "vite": "latest"
91
+ }
92
+ };
93
+ writeFileSync(
94
+ resolve(workspaceDir, "package.json"),
95
+ `${JSON.stringify(packageJson, null, 2)}
96
+ `
97
+ );
98
+ const bglConfig = `import { defineWorkspace } from '@bagelink/workspace'
99
+
100
+ export default defineWorkspace({
101
+ localhost: {
102
+ host: 'http://localhost:8000',
103
+ proxy: '/api',
104
+ openapi_url: 'http://localhost:8000/openapi.json',
105
+ },
106
+ development: {
107
+ host: 'https://${projectId}.bagel.to',
108
+ proxy: '/api',
109
+ openapi_url: 'https://${projectId}.bagel.to/openapi.json',
110
+ },
111
+ production: {
112
+ host: 'https://${projectId}.bagel.to',
113
+ proxy: '/api',
114
+ openapi_url: 'https://${projectId}.bagel.to/openapi.json',
115
+ },
116
+ })
117
+ `;
118
+ writeFileSync(resolve(workspaceDir, "bgl.config.ts"), bglConfig);
119
+ const tsConfig = {
120
+ compilerOptions: {
121
+ target: "ES2020",
122
+ useDefineForClassFields: true,
123
+ module: "ESNext",
124
+ lib: ["ES2020", "DOM", "DOM.Iterable"],
125
+ skipLibCheck: true,
126
+ moduleResolution: "bundler",
127
+ allowImportingTsExtensions: true,
128
+ resolveJsonModule: true,
129
+ isolatedModules: true,
130
+ noEmit: true,
131
+ jsx: "preserve",
132
+ strict: true,
133
+ noUnusedLocals: true,
134
+ noUnusedParameters: true,
135
+ noFallthroughCasesInSwitch: true,
136
+ baseUrl: ".",
137
+ paths: {
138
+ "shared/*": ["./shared/*"]
139
+ }
140
+ }
141
+ };
142
+ writeFileSync(
143
+ resolve(workspaceDir, "tsconfig.json"),
144
+ `${JSON.stringify(tsConfig, null, 2)}
145
+ `
146
+ );
147
+ const gitignore = `node_modules
148
+ dist
149
+ .DS_Store
150
+ *.local
151
+ .env.local
152
+ .vite
153
+ `;
154
+ writeFileSync(resolve(workspaceDir, ".gitignore"), gitignore);
155
+ const scriptsDir = resolve(workspaceDir, "scripts");
156
+ mkdirSync(scriptsDir, { recursive: true });
157
+ const devRunnerContent = `#!/usr/bin/env bun
158
+ import { spawn } from 'bun'
159
+ import { readdir } from 'fs/promises'
160
+ import { resolve } from 'path'
161
+
162
+ const projectsRoot = process.cwd()
163
+ const projects = (await readdir(projectsRoot, { withFileTypes: true }))
164
+ .filter(
165
+ item =>
166
+ item.isDirectory()
167
+ && item.name !== 'node_modules'
168
+ && item.name !== 'shared'
169
+ && item.name !== 'scripts'
170
+ && item.name !== '.git'
171
+ && !item.name.startsWith('.'),
172
+ )
173
+ .map(item => item.name)
174
+
175
+ console.log(\`\\n\u{1F680} Starting \${projects.length} project\${projects.length > 1 ? 's' : ''}...\\n\`)
176
+
177
+ const urlPattern = /Local:\\s+(http:\\/\\/localhost:\\d+)/
178
+
179
+ projects.forEach((project) => {
180
+ const proc = spawn({
181
+ cmd: ['bun', 'run', 'dev'],
182
+ cwd: resolve(projectsRoot, project),
183
+ stdout: 'pipe',
184
+ stderr: 'pipe',
185
+ })
186
+
187
+ const decoder = new TextDecoder()
188
+
189
+ proc.stdout.pipeTo(
190
+ new WritableStream({
191
+ write(chunk) {
192
+ const text = decoder.decode(chunk)
193
+ const match = text.match(urlPattern)
194
+ if (match) {
195
+ console.log(\` \u2713 \${project.padEnd(15)} \u2192 \${match[1]}\`)
196
+ }
197
+ },
198
+ }),
199
+ )
200
+
201
+ proc.stderr.pipeTo(
202
+ new WritableStream({
203
+ write(chunk) {
204
+ const text = decoder.decode(chunk)
205
+ if (text.includes('error') || text.includes('Error')) {
206
+ console.error(\` \u2717 \${project}: \${text.trim()}\`)
207
+ }
208
+ },
209
+ }),
210
+ )
211
+ })
212
+
213
+ console.log('\\n\u{1F4A1} Press Ctrl+C to stop all servers\\n')
214
+
215
+ process.on('SIGINT', () => {
216
+ console.log('\\n\\n\u{1F44B} Stopping all servers...\\n')
217
+ process.exit()
218
+ })
219
+ `;
220
+ writeFileSync(resolve(scriptsDir, "dev.ts"), devRunnerContent);
221
+ console.log(`\u2705 Created workspace: ${name}`);
222
+ }
223
+ function createSharedPackage(root) {
224
+ const sharedDir = resolve(root, "shared");
225
+ mkdirSync(sharedDir, { recursive: true });
226
+ const packageJson = {
227
+ name: "shared",
228
+ version: "1.0.0",
229
+ type: "module",
230
+ exports: {
231
+ ".": "./index.ts",
232
+ "./utils": "./utils/index.ts",
233
+ "./types": "./types/index.ts"
234
+ }
235
+ };
236
+ writeFileSync(
237
+ resolve(sharedDir, "package.json"),
238
+ `${JSON.stringify(packageJson, null, 2)}
239
+ `
240
+ );
241
+ writeFileSync(
242
+ resolve(sharedDir, "index.ts"),
243
+ "// Shared utilities and exports\nexport * from './utils'\n"
244
+ );
245
+ mkdirSync(resolve(sharedDir, "utils"), { recursive: true });
246
+ writeFileSync(
247
+ resolve(sharedDir, "utils", "index.ts"),
248
+ "// Shared utility functions\nexport function formatDate(date: Date): string {\n return date.toISOString()\n}\n"
249
+ );
250
+ mkdirSync(resolve(sharedDir, "types"), { recursive: true });
251
+ writeFileSync(
252
+ resolve(sharedDir, "types", "index.ts"),
253
+ "// Shared types\nexport interface User {\n id: string\n name: string\n}\n"
254
+ );
255
+ console.log("\u2705 Created shared package");
256
+ }
257
+ async function addProject(name, root = process.cwd()) {
258
+ const projectDir = resolve(root, name);
259
+ if (existsSync(projectDir)) {
260
+ console.error(`\u274C Project ${name} already exists`);
261
+ process.exit(1);
262
+ }
263
+ console.log(`
264
+ \u{1F4E6} Creating project: ${name}
265
+ `);
266
+ mkdirSync(projectDir, { recursive: true });
267
+ const isWorkspace = existsSync(resolve(root, "bgl.config.ts"));
268
+ const packageJson = {
269
+ name,
270
+ type: "module",
271
+ scripts: {
272
+ dev: "vite",
273
+ build: "vite build",
274
+ preview: "vite preview"
275
+ },
276
+ dependencies: {},
277
+ devDependencies: {
278
+ "@vitejs/plugin-vue": "latest",
279
+ "vite": "latest",
280
+ "vue": "latest"
281
+ }
282
+ };
283
+ if (isWorkspace) {
284
+ packageJson.dependencies.shared = "workspace:*";
285
+ }
286
+ writeFileSync(
287
+ resolve(projectDir, "package.json"),
288
+ `${JSON.stringify(packageJson, null, 2)}
289
+ `
290
+ );
291
+ const bglConfigContent = isWorkspace ? `import { defineWorkspace } from '@bagelink/workspace'
292
+ import rootWorkspace from '../bgl.config'
293
+
294
+ export default defineWorkspace({
295
+ localhost: rootWorkspace('localhost'),
296
+ development: rootWorkspace('development'),
297
+ production: rootWorkspace('production'),
298
+ })
299
+ ` : `import { defineWorkspace } from '@bagelink/workspace'
300
+
301
+ export default defineWorkspace({
302
+ localhost: {
303
+ host: 'http://localhost:8000',
304
+ proxy: '/api',
305
+ },
306
+ development: {
307
+ host: 'https://my-project.bagel.to',
308
+ proxy: '/api',
309
+ },
310
+ production: {
311
+ host: 'https://my-project.bagel.to',
312
+ proxy: '/api',
313
+ },
314
+ })
315
+ `;
316
+ writeFileSync(resolve(projectDir, "bgl.config.ts"), bglConfigContent);
317
+ const viteConfig = `import { defineConfig } from 'vite'
318
+ import vue from '@vitejs/plugin-vue'
319
+ import { bagelink } from '@bagelink/workspace/vite'
320
+ import workspace from './bgl.config'
321
+
322
+ export default defineConfig({
323
+ plugins: [
324
+ vue(),
325
+ bagelink({ workspace }),
326
+ ],
327
+ })
328
+ `;
329
+ writeFileSync(resolve(projectDir, "vite.config.ts"), viteConfig);
330
+ const srcDir = resolve(projectDir, "src");
331
+ mkdirSync(srcDir, { recursive: true });
332
+ const indexHtml = `<!DOCTYPE html>
333
+ <html lang="en">
334
+ <head>
335
+ <meta charset="UTF-8">
336
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
337
+ <title>${name}</title>
338
+ </head>
339
+ <body>
340
+ <div id="app"></div>
341
+ <script type="module" src="/src/main.ts"><\/script>
342
+ </body>
343
+ </html>
344
+ `;
345
+ writeFileSync(resolve(projectDir, "index.html"), indexHtml);
346
+ const mainTs = `import { createApp } from 'vue'
347
+ import App from './App.vue'
348
+
349
+ createApp(App).mount('#app')
350
+ `;
351
+ writeFileSync(resolve(srcDir, "main.ts"), mainTs);
352
+ const appVue = `<script setup lang="ts">
353
+ import { ref } from 'vue'
354
+ ${isWorkspace ? "import { formatDate } from 'shared/utils'\n" : ""}
355
+ const count = ref(0)
356
+ <\/script>
357
+
358
+ <template>
359
+ <div>
360
+ <h1>${name}</h1>
361
+ <button @click="count++">Count: {{ count }}</button>
362
+ ${isWorkspace ? "<p>{{ formatDate(new Date()) }}</p>" : ""}
363
+ </div>
364
+ </template>
365
+ `;
366
+ writeFileSync(resolve(srcDir, "App.vue"), appVue);
367
+ console.log(`\u2705 Created project: ${name}`);
368
+ if (isWorkspace) {
369
+ updateWorkspaceScripts(root, name);
370
+ }
371
+ console.log("\nNext steps:");
372
+ console.log(` cd ${name}`);
373
+ console.log(" bun install");
374
+ console.log(" bun run dev");
375
+ console.log("");
376
+ }
377
+ function updateWorkspaceScripts(root, projectName) {
378
+ const packageJsonPath = resolve(root, "package.json");
379
+ if (!existsSync(packageJsonPath)) return;
380
+ try {
381
+ const packageJson = JSON.parse(
382
+ readFileSync(packageJsonPath, "utf-8")
383
+ );
384
+ if (!packageJson.scripts) {
385
+ packageJson.scripts = {};
386
+ }
387
+ packageJson.scripts[`dev:${projectName}`] = `bun --filter ${projectName} dev`;
388
+ packageJson.scripts[`build:${projectName}`] = `bun --filter ${projectName} build`;
389
+ writeFileSync(
390
+ packageJsonPath,
391
+ `${JSON.stringify(packageJson, null, 2)}
392
+ `
393
+ );
394
+ console.log(`\u2705 Added scripts: dev:${projectName}, build:${projectName}`);
395
+ } catch (error) {
396
+ console.warn("\u26A0\uFE0F Could not update workspace scripts");
397
+ }
398
+ }
399
+ function listProjects(root = process.cwd()) {
400
+ try {
401
+ const items = readdirSync(root, { withFileTypes: true });
402
+ return items.filter(
403
+ (item) => item.isDirectory() && item.name !== "node_modules" && item.name !== "shared" && item.name !== "scripts" && item.name !== ".git" && !item.name.startsWith(".")
404
+ ).map((item) => item.name).sort();
405
+ } catch {
406
+ return [];
407
+ }
408
+ }
409
+
410
+ function generateNetlifyRedirect(config) {
411
+ const redirect = `# API Proxy Configuration
412
+ # Environment variables are set in Netlify UI or netlify.toml [build.environment] section
413
+
414
+ [[redirects]]
415
+ from = "${config.proxy}/*"
416
+ to = "${config.host}/:splat"
417
+ status = 200
418
+ force = true
419
+ headers = {X-From = "Netlify"}
420
+ `;
421
+ return redirect;
422
+ }
423
+ function generateNetlifyConfigTemplate() {
424
+ return `# Standard Netlify configuration for Bagelink projects
425
+ # Uses environment variables for proxy configuration
426
+
427
+ [build.environment]
428
+ # Set these in Netlify UI or override here
429
+ # BGL_PROXY_PATH = "/api"
430
+ # BGL_API_HOST = "https://your-project.bagel.to"
431
+
432
+ [[redirects]]
433
+ # Proxy API requests to backend
434
+ # Uses BGL_PROXY_PATH and BGL_API_HOST from environment
435
+ from = "/api/*"
436
+ to = "https://your-project.bagel.to/:splat"
437
+ status = 200
438
+ force = true
439
+ headers = {X-From = "Netlify"}
440
+
441
+ # Example: Multiple API backends
442
+ # [[redirects]]
443
+ # from = "/api/v2/*"
444
+ # to = "https://api-v2.example.com/:splat"
445
+ # status = 200
446
+ # force = true
447
+ `;
448
+ }
449
+ function generateNetlifyConfig(config, additionalConfig, useTemplate = false) {
450
+ if (useTemplate) {
451
+ return generateNetlifyConfigTemplate();
452
+ }
453
+ const redirect = generateNetlifyRedirect(config);
454
+ if (additionalConfig !== void 0 && additionalConfig !== "") {
455
+ return `${redirect}
456
+ ${additionalConfig}`;
457
+ }
458
+ return redirect;
459
+ }
460
+ function writeNetlifyConfig(config, outPath = "./netlify.toml", additionalConfig, useTemplate = false) {
461
+ const content = generateNetlifyConfig(config, additionalConfig, useTemplate);
462
+ const resolvedPath = resolve(outPath);
463
+ writeFileSync(resolvedPath, content, "utf-8");
464
+ console.log(`\u2713 Generated netlify.toml at ${resolvedPath}`);
465
+ if (!useTemplate) {
466
+ console.log("\n\u{1F4A1} Tip: For environment-based config, set these in Netlify UI:");
467
+ console.log(` BGL_PROXY_PATH = "${config.proxy}"`);
468
+ console.log(` BGL_API_HOST = "${config.host}"`);
469
+ if (config.openapi_url) {
470
+ console.log(` BGL_OPENAPI_URL = "${config.openapi_url}"`);
471
+ }
472
+ console.log("");
473
+ }
474
+ }
475
+ function setBuildEnvVars(config) {
476
+ process.env.BGL_PROXY_PATH = config.proxy;
477
+ process.env.BGL_API_HOST = config.host;
478
+ if (config.openapi_url !== void 0 && config.openapi_url !== "") {
479
+ process.env.BGL_OPENAPI_URL = config.openapi_url;
480
+ }
481
+ }
482
+
483
+ export { addProject as a, generateNetlifyConfig as g, initWorkspace as i, listProjects as l, setBuildEnvVars as s, writeNetlifyConfig as w };
package/dist/vite.cjs CHANGED
@@ -1,10 +1,11 @@
1
1
  'use strict';
2
2
 
3
+ const node_path = require('node:path');
3
4
  const process = require('node:process');
4
5
  const node_url = require('node:url');
5
- const netlify = require('./shared/workspace.Dx9_TIij.cjs');
6
+ const netlify = require('./shared/workspace.CuE882NE.cjs');
6
7
  require('node:fs');
7
- require('node:path');
8
+ require('prompts');
8
9
 
9
10
  function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; }
10
11
 
@@ -81,9 +82,27 @@ function bagelink(options) {
81
82
  if (config.additionalAliases) {
82
83
  Object.assign(alias, config.additionalAliases);
83
84
  }
84
- const server = config.configureProxy !== false ? {
85
- proxy: createViteProxy(workspaceConfig)
86
- } : void 0;
85
+ let port;
86
+ try {
87
+ const currentDir = process__default.cwd();
88
+ const projectName = node_path.basename(currentDir);
89
+ const parentDir = node_url.fileURLToPath(new URL("..", `file://${currentDir}/`));
90
+ const allProjects = netlify.listProjects(parentDir);
91
+ const projectIndex = allProjects.indexOf(projectName);
92
+ if (projectIndex !== -1) {
93
+ port = 5173 + projectIndex;
94
+ }
95
+ } catch {
96
+ }
97
+ const server = {
98
+ ...config.configureProxy !== false && {
99
+ proxy: createViteProxy(workspaceConfig)
100
+ },
101
+ ...port !== void 0 && {
102
+ port,
103
+ strictPort: true
104
+ }
105
+ };
87
106
  const define = {
88
107
  "import.meta.env.VITE_BGL_PROXY": JSON.stringify(workspaceConfig.proxy),
89
108
  "import.meta.env.VITE_BGL_HOST": JSON.stringify(workspaceConfig.host),
@@ -96,7 +115,7 @@ function bagelink(options) {
96
115
  alias
97
116
  },
98
117
  define,
99
- ...server && { server }
118
+ server
100
119
  };
101
120
  }
102
121
  };
package/dist/vite.mjs CHANGED
@@ -1,8 +1,10 @@
1
+ import { basename } from 'node:path';
1
2
  import process from 'node:process';
2
3
  import { fileURLToPath } from 'node:url';
3
- export { g as generateNetlifyConfig, s as setBuildEnvVars, w as writeNetlifyConfig } from './shared/workspace.Twuo1PFw.mjs';
4
+ import { l as listProjects } from './shared/workspace.ThBu2HJO.mjs';
5
+ export { g as generateNetlifyConfig, s as setBuildEnvVars, w as writeNetlifyConfig } from './shared/workspace.ThBu2HJO.mjs';
4
6
  import 'node:fs';
5
- import 'node:path';
7
+ import 'prompts';
6
8
 
7
9
  function createViteProxy(config) {
8
10
  const proxy = {};
@@ -75,9 +77,27 @@ function bagelink(options) {
75
77
  if (config.additionalAliases) {
76
78
  Object.assign(alias, config.additionalAliases);
77
79
  }
78
- const server = config.configureProxy !== false ? {
79
- proxy: createViteProxy(workspaceConfig)
80
- } : void 0;
80
+ let port;
81
+ try {
82
+ const currentDir = process.cwd();
83
+ const projectName = basename(currentDir);
84
+ const parentDir = fileURLToPath(new URL("..", `file://${currentDir}/`));
85
+ const allProjects = listProjects(parentDir);
86
+ const projectIndex = allProjects.indexOf(projectName);
87
+ if (projectIndex !== -1) {
88
+ port = 5173 + projectIndex;
89
+ }
90
+ } catch {
91
+ }
92
+ const server = {
93
+ ...config.configureProxy !== false && {
94
+ proxy: createViteProxy(workspaceConfig)
95
+ },
96
+ ...port !== void 0 && {
97
+ port,
98
+ strictPort: true
99
+ }
100
+ };
81
101
  const define = {
82
102
  "import.meta.env.VITE_BGL_PROXY": JSON.stringify(workspaceConfig.proxy),
83
103
  "import.meta.env.VITE_BGL_HOST": JSON.stringify(workspaceConfig.host),
@@ -90,7 +110,7 @@ function bagelink(options) {
90
110
  alias
91
111
  },
92
112
  define,
93
- ...server && { server }
113
+ server
94
114
  };
95
115
  }
96
116
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@bagelink/workspace",
3
3
  "type": "module",
4
- "version": "1.8.26",
4
+ "version": "1.8.30",
5
5
  "description": "Monorepo workspace tooling for Bagel projects with proxy and config management",
6
6
  "author": {
7
7
  "name": "Bagel Studio",
package/src/dev.ts CHANGED
@@ -4,12 +4,16 @@ import { listProjects } from './workspace.js'
4
4
 
5
5
  interface ServerInfo {
6
6
  name: string
7
- port?: number
7
+ assignedPort: number
8
+ actualPort?: number
8
9
  status: 'starting' | 'ready'
10
+ order: number
9
11
  }
10
12
 
11
13
  const servers = new Map<string, ServerInfo>()
14
+ const projectToPort = new Map<string, number>()
12
15
  const seenLines = new Set<string>()
16
+ const START_PORT = 5173
13
17
 
14
18
  // ANSI colors
15
19
  const colors = {
@@ -29,13 +33,16 @@ function clearAndPrintServers() {
29
33
 
30
34
  console.log(`${colors.cyan}🚀 Development Servers${colors.reset}\n`)
31
35
 
32
- for (const [name, info] of servers.entries()) {
33
- if (info.status === 'ready' && info.port) {
34
- const url = `http://localhost:${info.port}`
36
+ // Sort by order to maintain consistent display
37
+ const sortedServers = Array.from(servers.entries()).sort((a, b) => a[1].order - b[1].order)
38
+
39
+ for (const [name, info] of sortedServers) {
40
+ const url = `http://localhost:${info.assignedPort}`
41
+ if (info.status === 'ready') {
35
42
  console.log(`${colors.green}●${colors.reset} ${colors.cyan}${name}${colors.reset} ${colors.dim}→${colors.reset} ${url}`)
36
43
  }
37
44
  else {
38
- console.log(`${colors.yellow}○${colors.reset} ${colors.dim}${name} (starting...)${colors.reset}`)
45
+ console.log(`${colors.yellow}○${colors.reset} ${colors.dim}${name} (starting...)${colors.reset} ${colors.dim}${url}${colors.reset}`)
39
46
  }
40
47
  }
41
48
 
@@ -50,9 +57,23 @@ export async function runDev(
50
57
  const resolvedFilters = resolveFilters(filter)
51
58
  if (!resolvedFilters || resolvedFilters.length === 0) return 1
52
59
 
60
+ // Pre-assign ports based on alphabetical order
61
+ const projectNames = resolvedFilters.map(f => f.replace('./', '')).sort()
62
+ projectNames.forEach((name, index) => {
63
+ const assignedPort = START_PORT + index
64
+ servers.set(name, {
65
+ name,
66
+ assignedPort,
67
+ status: 'starting',
68
+ order: index,
69
+ })
70
+ projectToPort.set(name, assignedPort)
71
+ })
72
+
73
+ clearAndPrintServers()
74
+
53
75
  const filterArgs = resolvedFilters.map(f => `--filter '${f}'`).join(' ')
54
- const projectNames = resolvedFilters.map(f => f.replace('./', '')).join(', ')
55
- console.log(`${colors.dim}Starting dev servers: ${projectNames}${argsStr}${colors.reset}\n`)
76
+ const projectNamesDisplay = projectNames.join(', ')
56
77
 
57
78
  const command = `bun run ${filterArgs} dev${argsStr}`
58
79
  const proc = spawn(command, {
@@ -71,17 +92,6 @@ export async function runDev(
71
92
  if (seenLines.has(line)) return
72
93
  seenLines.add(line)
73
94
 
74
- // Extract project name from "project dev: ..."
75
- const projectMatch = line.match(/^(\w+)\s+dev:/)
76
- if (projectMatch) {
77
- const name = projectMatch[1]
78
- if (!servers.has(name)) {
79
- console.log(`${colors.dim}[DEBUG] Detected project: ${name}${colors.reset}`)
80
- servers.set(name, { name, status: 'starting' })
81
- clearAndPrintServers()
82
- }
83
- }
84
-
85
95
  // Extract port from "project dev: ➜ Local: http://localhost:5173/"
86
96
  const portMatch = line.match(/Local:\s+http:\/\/localhost:(\d+)/)
87
97
  if (portMatch) {
@@ -92,9 +102,8 @@ export async function runDev(
92
102
  if (projectInLine) {
93
103
  const name = projectInLine[1]
94
104
  const info = servers.get(name)
95
- console.log(`${colors.dim}[DEBUG] Port ${port} for project: ${name}, exists: ${!!info}, hasPort: ${info?.port}${colors.reset}`)
96
- if (info && !info.port) {
97
- info.port = port
105
+ if (info && info.assignedPort === port && !info.actualPort) {
106
+ info.actualPort = port
98
107
  info.status = 'ready'
99
108
  clearAndPrintServers()
100
109
  }