sql-kite 1.0.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.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import '../src/index.js';
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "sql-kite",
3
+ "version": "1.0.0",
4
+ "description": "SQL-Kite CLI — Local-first SQLite workspace with branches, migrations and snapshots.",
5
+ "type": "module",
6
+ "bin": {
7
+ "sql-kite": "./bin/sql-kite.js"
8
+ },
9
+ "scripts": {
10
+ "dev": "node bin/sql-kite.js",
11
+ "start": "node bin/sql-kite.js"
12
+ },
13
+ "author": "D Krishna",
14
+ "license": "MIT",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/Ananta-V/sql-kite"
18
+ },
19
+ "homepage": "https://github.com/Ananta-V/sql-kite#readme",
20
+ "bugs": {
21
+ "url": "https://github.com/Ananta-V/sql-kite/issues"
22
+ },
23
+ "keywords": [
24
+ "sqlite",
25
+ "database",
26
+ "cli",
27
+ "migration",
28
+ "branching",
29
+ "snapshots",
30
+ "local-first",
31
+ "developer-tools"
32
+ ],
33
+ "engines": {
34
+ "node": ">=18"
35
+ },
36
+ "files": [
37
+ "bin/",
38
+ "src/"
39
+ ],
40
+ "dependencies": {
41
+ "better-sqlite3": "^9.2.2",
42
+ "chalk": "^5.3.0",
43
+ "commander": "^12.0.0",
44
+ "find-free-port": "^2.0.0",
45
+ "inquirer": "^9.2.12",
46
+ "open": "^10.0.3",
47
+ "ora": "^8.0.1"
48
+ }
49
+ }
@@ -0,0 +1,43 @@
1
+ import { rmSync } from 'fs';
2
+ import chalk from 'chalk';
3
+ import inquirer from 'inquirer';
4
+ import ora from 'ora';
5
+ import { getProjectPath, projectExists } from '../utils/paths.js';
6
+ import { stopCommand } from './stop.js';
7
+
8
+ export async function deleteCommand(name) {
9
+ if (!projectExists(name)) {
10
+ console.log(chalk.red(`✗ Project "${name}" does not exist`));
11
+ process.exit(1);
12
+ }
13
+
14
+ const { confirm } = await inquirer.prompt([
15
+ {
16
+ type: 'confirm',
17
+ name: 'confirm',
18
+ message: `Delete project "${name}"? This cannot be undone.`,
19
+ default: false
20
+ }
21
+ ]);
22
+
23
+ if (!confirm) {
24
+ console.log(chalk.dim('Cancelled'));
25
+ return;
26
+ }
27
+
28
+ const spinner = ora(`Deleting project "${name}"...`).start();
29
+
30
+ try {
31
+ // Stop if running
32
+ await stopCommand(name).catch(() => {});
33
+
34
+ // Delete project folder
35
+ rmSync(getProjectPath(name), { recursive: true, force: true });
36
+
37
+ spinner.succeed(chalk.green(`✓ Project "${name}" deleted`));
38
+ } catch (error) {
39
+ spinner.fail(chalk.red('✗ Failed to delete project'));
40
+ console.error(error);
41
+ process.exit(1);
42
+ }
43
+ }
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn } from 'child_process'
4
+ import { join, dirname } from 'path'
5
+ import { fileURLToPath } from 'url'
6
+ import chalk from 'chalk'
7
+
8
+ const __dirname = dirname(fileURLToPath(import.meta.url))
9
+
10
+ export default function importServerCommand(port = 3000) {
11
+ console.log(chalk.cyan('→ Starting import server...'))
12
+ console.log(chalk.dim(` Port: ${port}`))
13
+ console.log(chalk.dim(' Mode: Import-only'))
14
+ console.log('')
15
+
16
+ const serverPath = join(__dirname, '../../../server/src/index.js')
17
+
18
+ const serverProcess = spawn('node', [serverPath], {
19
+ stdio: 'inherit',
20
+ env: {
21
+ ...process.env,
22
+ PORT: port,
23
+ IMPORT_MODE: 'true'
24
+ }
25
+ })
26
+
27
+ serverProcess.on('error', (error) => {
28
+ console.error(chalk.red('✗ Failed to start import server:'), error.message)
29
+ process.exit(1)
30
+ })
31
+
32
+ serverProcess.on('exit', (code) => {
33
+ if (code !== 0) {
34
+ console.error(chalk.red(`✗ Import server exited with code ${code}`))
35
+ process.exit(code)
36
+ }
37
+ })
38
+
39
+ // Handle Ctrl+C
40
+ process.on('SIGINT', () => {
41
+ console.log(chalk.yellow('\n→ Stopping import server...'))
42
+ serverProcess.kill('SIGINT')
43
+ process.exit(0)
44
+ })
45
+
46
+ console.log(chalk.green('✓ Import server started'))
47
+ console.log(chalk.dim(` Open: http://localhost:${port}`))
48
+ console.log('')
49
+ console.log(chalk.dim('Press Ctrl+C to stop'))
50
+ }
@@ -0,0 +1,280 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { existsSync, statSync, accessSync, constants, writeFileSync, mkdirSync } from 'fs'
4
+ import { resolve, extname, basename, join, dirname } from 'path'
5
+ import { fileURLToPath } from 'url'
6
+ import Database from 'better-sqlite3'
7
+ import chalk from 'chalk'
8
+ import { spawn } from 'child_process'
9
+ import http from 'http'
10
+ import open from 'open'
11
+ import { findFreePort } from '../utils/port-finder.js'
12
+ import { ensureSqlKiteDirs, LOGS_DIR } from '../utils/paths.js'
13
+
14
+ const __dirname = dirname(fileURLToPath(import.meta.url))
15
+
16
+ export default async function importCommand(dbPath) {
17
+ ensureSqlKiteDirs()
18
+ if (!dbPath) {
19
+ console.error(chalk.red('✗ Error: Database path is required'))
20
+ console.log(chalk.dim('Usage: npm run sql-kite import <path-to-database>'))
21
+ console.log(chalk.dim(' Or: npm run sql-kite open <path-to-database>'))
22
+ process.exit(1)
23
+ }
24
+
25
+ const absolutePath = resolve(dbPath)
26
+
27
+ // ========================================
28
+ // Step 1: Preflight checks
29
+ // ========================================
30
+
31
+ console.log(chalk.cyan('→ Running preflight checks...'))
32
+
33
+ // Check file exists
34
+ if (!existsSync(absolutePath)) {
35
+ console.error(chalk.red('✗ Error: File does not exist'))
36
+ console.error(chalk.dim(` Path: ${absolutePath}`))
37
+ process.exit(1)
38
+ }
39
+
40
+ // Check it's a file, not directory
41
+ const stats = statSync(absolutePath)
42
+ if (stats.isDirectory()) {
43
+ console.error(chalk.red('✗ Error: Path is a directory, not a database file'))
44
+ console.error(chalk.dim(' Please provide a path to a .db file'))
45
+ process.exit(1)
46
+ }
47
+
48
+ // Check for symlinks (security)
49
+ if (stats.isSymbolicLink()) {
50
+ console.error(chalk.red('✗ Error: Symlinks are not supported for security reasons'))
51
+ console.error(chalk.dim(' Please provide a direct path to the database file'))
52
+ process.exit(1)
53
+ }
54
+
55
+ // Check file is readable
56
+ try {
57
+ accessSync(absolutePath, constants.R_OK)
58
+ } catch (err) {
59
+ console.error(chalk.red('✗ Error: File is not readable'))
60
+ console.error(chalk.dim(` Permission denied: ${absolutePath}`))
61
+ process.exit(1)
62
+ }
63
+
64
+ // Warn about file extension (don't block)
65
+ const ext = extname(absolutePath).toLowerCase()
66
+ if (ext !== '.db' && ext !== '.sqlite' && ext !== '.sqlite3') {
67
+ console.log(chalk.yellow('⚠ Warning: File extension is not .db, .sqlite, or .sqlite3'))
68
+ console.log(chalk.dim(` Found: ${ext || '(no extension)'}`))
69
+ console.log(chalk.dim(' Continuing anyway...'))
70
+ }
71
+
72
+ console.log(chalk.green('✓ Preflight checks passed'))
73
+
74
+ // ========================================
75
+ // Step 2: Read-only probe
76
+ // ========================================
77
+
78
+ console.log(chalk.cyan('→ Validating SQLite database...'))
79
+
80
+ let db
81
+ try {
82
+ // Open in read-only mode (no WAL writes, no side effects)
83
+ db = new Database(absolutePath, { readonly: true, fileMustExist: true })
84
+
85
+ // Test basic query
86
+ try {
87
+ db.prepare('SELECT name FROM sqlite_master LIMIT 1').all()
88
+ } catch (err) {
89
+ console.error(chalk.red('✗ Error: File is not a valid SQLite database'))
90
+ console.error(chalk.dim(` ${err.message}`))
91
+ db.close()
92
+ process.exit(1)
93
+ }
94
+
95
+ // Get metadata
96
+ const userVersion = db.prepare('PRAGMA user_version').get().user_version
97
+ const journalMode = db.prepare('PRAGMA journal_mode').get().journal_mode
98
+
99
+ console.log(chalk.green('✓ Valid SQLite database detected'))
100
+ console.log(chalk.dim(` User version: ${userVersion}`))
101
+ console.log(chalk.dim(` Journal mode: ${journalMode}`))
102
+
103
+ // Get table count
104
+ const tableCount = db.prepare(`
105
+ SELECT COUNT(*) as count
106
+ FROM sqlite_master
107
+ WHERE type='table' AND name NOT LIKE 'sqlite_%'
108
+ `).get().count
109
+
110
+ console.log(chalk.dim(` Tables: ${tableCount}`))
111
+
112
+ db.close()
113
+ } catch (err) {
114
+ if (db) db.close()
115
+ console.error(chalk.red('✗ Error: Failed to open database'))
116
+ console.error(chalk.dim(` ${err.message}`))
117
+ process.exit(1)
118
+ }
119
+
120
+ // ========================================
121
+ // Step 3: Generate project name suggestion
122
+ // ========================================
123
+
124
+ const suggestedName = basename(absolutePath, extname(absolutePath))
125
+ .toLowerCase()
126
+ .replace(/[^a-z0-9-]/g, '-')
127
+ .replace(/-+/g, '-')
128
+ .replace(/^-|-$/g, '')
129
+
130
+ // ========================================
131
+ // Step 4: Import through Studio
132
+ // ========================================
133
+
134
+ console.log('')
135
+ console.log(chalk.green('✓ Database validated successfully'))
136
+ console.log('')
137
+
138
+ // Store import session data
139
+ const homeDir = process.env.HOME || process.env.USERPROFILE
140
+ const sqlKiteDir = join(homeDir, '.sql-kite')
141
+ const sessionFile = join(sqlKiteDir, 'import-pending.json')
142
+
143
+ // Ensure .sql-kite directory exists
144
+ if (!existsSync(sqlKiteDir)) {
145
+ mkdirSync(sqlKiteDir, { recursive: true })
146
+ }
147
+
148
+ const importSession = {
149
+ sourcePath: absolutePath,
150
+ suggestedName,
151
+ validated: true,
152
+ timestamp: Date.now()
153
+ }
154
+
155
+ writeFileSync(sessionFile, JSON.stringify(importSession, null, 2))
156
+
157
+ const defaultPort = 3000
158
+ const importUrl = (port) => `http://localhost:${port}`
159
+
160
+ async function getImportServerPort() {
161
+ try {
162
+ const mode = await new Promise((resolve, reject) => {
163
+ const req = http.get(`http://localhost:${defaultPort}/api/project`, (res) => {
164
+ let raw = ''
165
+ res.on('data', (chunk) => { raw += chunk })
166
+ res.on('end', () => {
167
+ try {
168
+ const data = JSON.parse(raw)
169
+ resolve(data.mode || 'project')
170
+ } catch (e) {
171
+ resolve('project')
172
+ }
173
+ })
174
+ })
175
+ req.on('error', reject)
176
+ req.setTimeout(500, () => {
177
+ req.destroy()
178
+ reject(new Error('Timeout'))
179
+ })
180
+ })
181
+
182
+ if (mode === 'import') {
183
+ return { port: defaultPort, alreadyRunning: true }
184
+ }
185
+ } catch (e) {
186
+ // Not running on default port
187
+ }
188
+
189
+ const port = await findFreePort(defaultPort)
190
+ return { port, alreadyRunning: false }
191
+ }
192
+
193
+ async function waitForImportServer(port, maxAttempts = 60) {
194
+ for (let i = 0; i < maxAttempts; i++) {
195
+ try {
196
+ await new Promise((resolve, reject) => {
197
+ const req = http.get(`http://localhost:${port}/api/project`, (res) => {
198
+ if (res.statusCode === 200) {
199
+ resolve()
200
+ } else {
201
+ reject(new Error(`Server returned ${res.statusCode}`))
202
+ }
203
+ })
204
+ req.on('error', reject)
205
+ req.setTimeout(1000, () => {
206
+ req.destroy()
207
+ reject(new Error('Timeout'))
208
+ })
209
+ })
210
+ return true
211
+ } catch (e) {
212
+ await new Promise(resolve => setTimeout(resolve, 500))
213
+ }
214
+ }
215
+ return false
216
+ }
217
+
218
+ console.log(chalk.bold('Import session ready!'))
219
+ console.log('')
220
+ console.log(chalk.cyan('→ Next steps:'))
221
+ console.log(chalk.dim(` 1. Launching import Studio...`))
222
+ console.log(chalk.dim(` 2. Complete the import wizard`))
223
+ console.log('')
224
+ console.log(chalk.dim(`Session saved to: ${sessionFile}`))
225
+
226
+ const studioPath = join(__dirname, '../../../studio/out')
227
+ if (!existsSync(studioPath)) {
228
+ console.log(chalk.red(`\n✗ Studio UI not built yet`))
229
+ console.log(chalk.dim(` Run: ${chalk.cyan(`cd packages/studio && npm run build`)}`))
230
+ return
231
+ }
232
+
233
+ try {
234
+ const { port, alreadyRunning } = await getImportServerPort()
235
+
236
+ if (!alreadyRunning) {
237
+ const serverPath = join(__dirname, '../../../server/src/index.js')
238
+ const logPath = join(LOGS_DIR, `import-server-${Date.now()}.log`)
239
+ const out = []
240
+ const serverProcess = spawn('node', [serverPath], {
241
+ detached: true,
242
+ stdio: ['ignore', 'pipe', 'pipe'],
243
+ env: {
244
+ ...process.env,
245
+ PORT: port.toString(),
246
+ IMPORT_MODE: 'true'
247
+ }
248
+ })
249
+
250
+ serverProcess.stdout.on('data', (chunk) => {
251
+ out.push(chunk.toString())
252
+ })
253
+
254
+ serverProcess.stderr.on('data', (chunk) => {
255
+ out.push(chunk.toString())
256
+ })
257
+
258
+ serverProcess.unref()
259
+
260
+ const ready = await waitForImportServer(port)
261
+ if (!ready) {
262
+ if (out.length > 0) {
263
+ writeFileSync(logPath, out.join(''))
264
+ console.log(chalk.red('✗ Import server failed to start in time'))
265
+ console.log(chalk.dim(` Log: ${logPath}`))
266
+ } else {
267
+ console.log(chalk.red('✗ Import server failed to start in time'))
268
+ console.log(chalk.dim(' No logs captured. The server may not have started.'))
269
+ }
270
+ return
271
+ }
272
+ }
273
+
274
+ console.log(chalk.cyan(`Opening ${importUrl(port)}...`))
275
+ await open(importUrl(port))
276
+ } catch (error) {
277
+ console.log(chalk.red('✗ Failed to launch import Studio'))
278
+ console.log(chalk.dim(error.message))
279
+ }
280
+ }
@@ -0,0 +1,193 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ import chalk from 'chalk';
4
+ import ora from 'ora';
5
+ import inquirer from 'inquirer';
6
+
7
+ const ENGINE_DEV_TEMPLATE = `/**
8
+ * Development Database Engine
9
+ * Connects to SQL-Kite server via HTTP
10
+ * Used during development only
11
+ *
12
+ * ✅ LOCKED TO MAIN BRANCH
13
+ * The API is hardcoded to ONLY query the 'main' branch.
14
+ * You can switch branches in SQL-Kite Studio without affecting your app.
15
+ * Your app will always query main.
16
+ *
17
+ * Port Configuration:
18
+ * 1. Set SQL_KITE_PORT environment variable
19
+ * 2. Or update DEFAULT_PORT below
20
+ * 3. Port is shown when you run: sql-kite start <project>
21
+ */
22
+
23
+ const DEFAULT_PORT = 3000;
24
+
25
+ // Auto-detect port from environment or use default
26
+ const getApiUrl = () => {
27
+ const port = process.env.SQL_KITE_PORT || DEFAULT_PORT;
28
+ return "http://localhost:" + port;
29
+ };
30
+
31
+ export async function runDevQuery(sql, params = []) {
32
+ try {
33
+ const apiUrl = getApiUrl();
34
+ const res = await fetch(apiUrl + "/api/query", {
35
+ method: "POST",
36
+ headers: { "Content-Type": "application/json" },
37
+ body: JSON.stringify({ sql, params }),
38
+ });
39
+
40
+ const data = await res.json();
41
+
42
+ if (!data.ok) {
43
+ throw new Error(data.error || "Query failed");
44
+ }
45
+
46
+ return data.result;
47
+ } catch (error) {
48
+ console.error("Dev database error:", error);
49
+ console.error("Make sure SQL-Kite is running on port " + (process.env.SQL_KITE_PORT || DEFAULT_PORT));
50
+ throw error;
51
+ }
52
+ }
53
+ `;
54
+
55
+ const ENGINE_LOCAL_TEMPLATE = `/**
56
+ * Local Database Engine
57
+ * Connects to local SQLite database using expo-sqlite
58
+ * Used in production builds
59
+ *
60
+ * IMPORTANT: Install expo-sqlite first:
61
+ * npx expo install expo-sqlite
62
+ */
63
+
64
+ import * as SQLite from "expo-sqlite";
65
+
66
+ const db = SQLite.openDatabase("main.db");
67
+
68
+ export function runLocalQuery(sql, params = []) {
69
+ return new Promise((resolve, reject) => {
70
+ db.transaction(tx => {
71
+ tx.executeSql(
72
+ sql,
73
+ params,
74
+ (_, result) => {
75
+ resolve(result.rows._array);
76
+ },
77
+ (_, error) => {
78
+ reject(error);
79
+ return false;
80
+ }
81
+ );
82
+ });
83
+ });
84
+ }
85
+ `;
86
+
87
+ const INDEX_TEMPLATE = `/**
88
+ * Unified Database Layer
89
+ * Automatically switches between dev and production engines
90
+ *
91
+ * Usage in your app:
92
+ *
93
+ * import { runQuery } from '@/lib/database';
94
+ *
95
+ * const users = await runQuery(
96
+ * "SELECT * FROM users WHERE active = ?",
97
+ * [1]
98
+ * );
99
+ */
100
+
101
+ import { runDevQuery } from "./engine.dev";
102
+ import { runLocalQuery } from "./engine.local";
103
+
104
+ // In Expo, __DEV__ is available globally
105
+ const isDev = typeof __DEV__ !== 'undefined' ? __DEV__ : process.env.NODE_ENV === 'development';
106
+
107
+ export async function runQuery(sql, params = []) {
108
+ try {
109
+ if (isDev) {
110
+ // Development: Use HTTP connection to SQL-Kite
111
+ return await runDevQuery(sql, params);
112
+ } else {
113
+ // Production: Use local SQLite database
114
+ return await runLocalQuery(sql, params);
115
+ }
116
+ } catch (error) {
117
+ console.error("Database Error:", error);
118
+ throw error;
119
+ }
120
+ }
121
+
122
+ // Export individual engines for advanced use cases
123
+ export { runDevQuery, runLocalQuery };
124
+ `;
125
+
126
+ export async function initCommand() {
127
+ const spinner = ora('Initializing database layer...').start();
128
+
129
+ try {
130
+ // Determine target directory
131
+ const targetDir = join(process.cwd(), 'lib', 'database');
132
+ const relativeDir = 'lib/database';
133
+
134
+ // Check if directory already exists
135
+ if (existsSync(targetDir)) {
136
+ spinner.stop();
137
+
138
+ const { overwrite } = await inquirer.prompt([
139
+ {
140
+ type: 'confirm',
141
+ name: 'overwrite',
142
+ message: chalk.yellow('Directory ' + relativeDir + ' already exists. Overwrite?'),
143
+ default: false
144
+ }
145
+ ]);
146
+
147
+ if (!overwrite) {
148
+ console.log(chalk.dim(' Cancelled.'));
149
+ return;
150
+ }
151
+
152
+ spinner.start('Creating database layer...');
153
+ }
154
+
155
+ // Create directory
156
+ mkdirSync(targetDir, { recursive: true });
157
+
158
+ // Write files
159
+ writeFileSync(join(targetDir, 'engine.dev.js'), ENGINE_DEV_TEMPLATE);
160
+ writeFileSync(join(targetDir, 'engine.local.js'), ENGINE_LOCAL_TEMPLATE);
161
+ writeFileSync(join(targetDir, 'index.js'), INDEX_TEMPLATE);
162
+
163
+ spinner.succeed('Database layer created successfully!');
164
+
165
+ // Print instructions
166
+ console.log('');
167
+ console.log(chalk.bold('📁 Files created:'));
168
+ console.log(chalk.dim(' ' + relativeDir + '/index.js'));
169
+ console.log(chalk.dim(' ' + relativeDir + '/engine.dev.js'));
170
+ console.log(chalk.dim(' ' + relativeDir + '/engine.local.js'));
171
+ console.log('');
172
+ console.log(chalk.bold('🚀 Usage:'));
173
+ console.log(chalk.cyan(" import { runQuery } from '@/lib/database';"));
174
+ console.log(chalk.cyan(' const users = await runQuery("SELECT * FROM users");'));
175
+ console.log('');
176
+ console.log(chalk.bold('📖 Next steps:'));
177
+ console.log(chalk.dim(' 1. Start SQL-Kite: ' + chalk.cyan('npm run sql-kite start <project>')));
178
+ console.log(chalk.dim(' 2. Note the port (e.g., localhost:3001)'));
179
+ console.log(chalk.dim(' 3. Set port: ' + chalk.cyan('SQL_KITE_PORT=3001 in .env')));
180
+ console.log(chalk.dim(' 4. Use runQuery() in your app'));
181
+ console.log(chalk.dim(' 5. For production: ' + chalk.cyan('npx expo install expo-sqlite')));
182
+ console.log('');
183
+ console.log(chalk.bold('⚙️ Port configuration:'));
184
+ console.log(chalk.dim(' • AUTO: Set SQL_KITE_PORT env variable (recommended)'));
185
+ console.log(chalk.dim(' • MANUAL: Edit DEFAULT_PORT in engine.dev.js'));
186
+ console.log('');
187
+
188
+ } catch (error) {
189
+ spinner.fail('Failed to initialize database layer');
190
+ console.error(chalk.red(' Error: ' + error.message));
191
+ process.exit(1);
192
+ }
193
+ }
@@ -0,0 +1,33 @@
1
+ import { readdirSync, existsSync, readFileSync } from 'fs';
2
+ import chalk from 'chalk';
3
+ import { RUNTIME_DIR, getProjectServerInfoPath } from '../utils/paths.js';
4
+
5
+ export async function listCommand() {
6
+ if (!existsSync(RUNTIME_DIR)) {
7
+ console.log(chalk.dim('No projects yet'));
8
+ return;
9
+ }
10
+
11
+ const projects = readdirSync(RUNTIME_DIR);
12
+
13
+ if (projects.length === 0) {
14
+ console.log(chalk.dim('No projects yet'));
15
+ return;
16
+ }
17
+
18
+ console.log(chalk.bold('\nProjects:\n'));
19
+
20
+ projects.forEach(name => {
21
+ const serverInfoPath = getProjectServerInfoPath(name);
22
+ const isRunning = existsSync(serverInfoPath);
23
+
24
+ if (isRunning) {
25
+ const serverInfo = JSON.parse(readFileSync(serverInfoPath, 'utf-8'));
26
+ console.log(` ${chalk.green('●')} ${chalk.bold(name)} ${chalk.dim(`(port ${serverInfo.port})`)}`);
27
+ } else {
28
+ console.log(` ${chalk.gray('○')} ${name}`);
29
+ }
30
+ });
31
+
32
+ console.log();
33
+ }