@kosdev-code/kos-ui-cli 2.1.20 → 2.1.22

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kosdev-code/kos-ui-cli",
3
- "version": "2.1.20",
3
+ "version": "2.1.22",
4
4
  "bin": {
5
5
  "kosui": "./src/lib/cli.mjs"
6
6
  },
@@ -21,7 +21,7 @@
21
21
  "main": "./src/index.js",
22
22
  "kos": {
23
23
  "build": {
24
- "gitHash": "21d74bd5b3b2a8206284fa74551b078c7f951398"
24
+ "gitHash": "14929950a1c351744473e8cb80a2b0ec2364c590"
25
25
  }
26
26
  },
27
27
  "publishConfig": {
@@ -0,0 +1,434 @@
1
+ // generators/dev/index.mjs
2
+ import chalk from "chalk";
3
+ import { spawn } from "child_process";
4
+ import { existsSync, mkdirSync, createWriteStream } from "fs";
5
+ import http from "http";
6
+ import path from "path";
7
+ import {
8
+ getAllHosts,
9
+ getHost,
10
+ filterPlugins,
11
+ checkProjectExists,
12
+ validateDevConfig,
13
+ } from "../../utils/dev-config.mjs";
14
+
15
+ export const metadata = {
16
+ key: "dev",
17
+ name: "Development Server",
18
+ description: "Start development servers for host applications and plugins",
19
+ namedArguments: {
20
+ host: "host",
21
+ plugins: "plugins",
22
+ disable: "disable",
23
+ pluginsOnly: "pluginsOnly",
24
+ interactive: "interactive",
25
+ },
26
+ };
27
+
28
+ // Process management
29
+ const processes = [];
30
+ const verbose = process.env.VERBOSE_PLUGIN_DEV === "true";
31
+
32
+ function cleanup() {
33
+ console.log(chalk.yellow("\nCleaning up..."));
34
+ processes.forEach((proc) => {
35
+ if (proc && !proc.killed) {
36
+ if (verbose) {
37
+ console.log(chalk.cyan(` Stopping process ${proc.pid}`));
38
+ }
39
+ proc.kill();
40
+ }
41
+ });
42
+ console.log(chalk.green("Cleanup complete"));
43
+ process.exit(0);
44
+ }
45
+
46
+ // Register cleanup handlers
47
+ process.on("SIGINT", cleanup);
48
+ process.on("SIGTERM", cleanup);
49
+
50
+ /**
51
+ * Check if a port is healthy
52
+ */
53
+ async function checkPortHealth(port, endpoint = "/api/kos/ui/plugins/contexts") {
54
+ return new Promise((resolve) => {
55
+ const req = http.get(`http://localhost:${port}${endpoint}`, (res) => {
56
+ resolve(res.statusCode === 200);
57
+ });
58
+ req.on("error", () => resolve(false));
59
+ req.setTimeout(1000, () => {
60
+ req.destroy();
61
+ resolve(false);
62
+ });
63
+ });
64
+ }
65
+
66
+ /**
67
+ * Wait for server to be ready
68
+ */
69
+ async function waitForServer(port, name, maxWaitSeconds = 120) {
70
+ console.log(chalk.yellow(`Waiting for ${name} to be ready...`));
71
+
72
+ const startTime = Date.now();
73
+ const maxWaitMs = maxWaitSeconds * 1000;
74
+
75
+ while (Date.now() - startTime < maxWaitMs) {
76
+ const isHealthy = await checkPortHealth(port);
77
+ if (isHealthy) {
78
+ const elapsed = Math.floor((Date.now() - startTime) / 1000);
79
+ console.log(chalk.green(`✓ ${name} ready on port ${port}! (${elapsed}s)`));
80
+ return true;
81
+ }
82
+
83
+ const elapsed = Math.floor((Date.now() - startTime) / 1000);
84
+ if (elapsed > 0 && elapsed % 5 === 0) {
85
+ console.log(` ... still waiting (${elapsed}s)`);
86
+ }
87
+
88
+ await new Promise((resolve) => setTimeout(resolve, 1000));
89
+ }
90
+
91
+ console.log(chalk.red(`✗ Timeout waiting for ${name}`));
92
+ return false;
93
+ }
94
+
95
+ /**
96
+ * Start a dev server
97
+ */
98
+ function startDevServer(project, port, logFile, env = {}) {
99
+ console.log(chalk.blue(`Starting ${project} on port ${port}`));
100
+
101
+ const logStream = createWriteStream(logFile, { flags: "w" });
102
+
103
+ const proc = spawn("npx", ["nx", "run", `${project}:serve`], {
104
+ env: {
105
+ ...process.env,
106
+ ...env,
107
+ VERBOSE_PLUGIN_DEV: verbose ? "true" : "false",
108
+ },
109
+ stdio: ["ignore", "pipe", "pipe"],
110
+ });
111
+
112
+ proc.stdout.pipe(logStream);
113
+ proc.stderr.pipe(logStream);
114
+
115
+ proc.on("error", (err) => {
116
+ console.log(chalk.red(`Failed to start ${project}: ${err.message}`));
117
+ });
118
+
119
+ proc.on("exit", (code) => {
120
+ if (code !== 0 && code !== null) {
121
+ console.log(chalk.red(`${project} exited with code ${code}`));
122
+ }
123
+ });
124
+
125
+ processes.push(proc);
126
+
127
+ console.log(chalk.green(` ✓ PID: ${proc.pid}, Log: ${logFile}`));
128
+
129
+ return proc;
130
+ }
131
+
132
+ /**
133
+ * Start internal development mode (host + plugins)
134
+ */
135
+ async function startInternalDevelopment(hostConfig, plugins, logDir) {
136
+ console.log(chalk.green("=================================================================="));
137
+ console.log(chalk.green("Internal Development Mode"));
138
+ console.log(chalk.green(`Host: ${hostConfig.name}`));
139
+ console.log(chalk.green(`Plugins: ${plugins.length}`));
140
+ console.log(chalk.green("=================================================================="));
141
+ console.log("");
142
+
143
+ // Create log directory
144
+ if (!existsSync(logDir)) {
145
+ mkdirSync(logDir, { recursive: true });
146
+ }
147
+
148
+ // Start plugin dev servers
149
+ console.log(chalk.yellow("Starting plugin dev servers..."));
150
+ for (const plugin of plugins) {
151
+ const logFile = path.join(logDir, `${plugin.project}.log`);
152
+ startDevServer(plugin.project, plugin.port, logFile);
153
+ }
154
+
155
+ console.log("");
156
+ console.log(chalk.yellow("Waiting for plugin dev servers to start..."));
157
+ await new Promise((resolve) => setTimeout(resolve, 8000));
158
+
159
+ // Verify plugin servers
160
+ console.log(chalk.yellow("Verifying plugin dev servers..."));
161
+ let allReady = true;
162
+
163
+ for (const plugin of plugins) {
164
+ const isReady = await checkPortHealth(plugin.port);
165
+ if (isReady) {
166
+ console.log(chalk.green(` ✓ ${plugin.project} ready on port ${plugin.port}`));
167
+ } else {
168
+ console.log(chalk.red(` ✗ ${plugin.project} NOT ready on port ${plugin.port}`));
169
+ allReady = false;
170
+ }
171
+ }
172
+
173
+ if (!allReady) {
174
+ console.log(chalk.red(`\nSome plugin servers failed to start. Check logs in ${logDir}/`));
175
+ process.exit(1);
176
+ }
177
+
178
+ // Build environment variables
179
+ console.log("");
180
+ console.log(chalk.yellow("Configuring host app environment..."));
181
+
182
+ const hostEnv = {
183
+ USE_LOCAL_PLUGINS: "true",
184
+ };
185
+
186
+ // Add plugin-specific env vars
187
+ for (const plugin of plugins) {
188
+ const envVar = plugin.project.toUpperCase().replace(/-/g, "_") + "_DEV";
189
+ hostEnv[envVar] = "true";
190
+ console.log(chalk.green(` ✓ ${envVar}=true`));
191
+ }
192
+
193
+ // Start host app
194
+ console.log("");
195
+ console.log(chalk.yellow(`Starting host app (${hostConfig.project})...`));
196
+ const hostLogFile = path.join(logDir, `${hostConfig.project}.log`);
197
+ startDevServer(hostConfig.project, hostConfig.port, hostLogFile, hostEnv);
198
+ console.log("");
199
+
200
+ // Wait for host app
201
+ const hostReady = await waitForServer(hostConfig.port, hostConfig.project);
202
+
203
+ if (!hostReady) {
204
+ console.log(chalk.red("Host app failed to start"));
205
+ process.exit(1);
206
+ }
207
+
208
+ // Print summary
209
+ printSummary(hostConfig, plugins, logDir);
210
+ }
211
+
212
+ /**
213
+ * Start third-party development mode (plugins only)
214
+ */
215
+ async function startThirdPartyDevelopment(hostConfig, plugins, logDir) {
216
+ console.log(chalk.green("=================================================================="));
217
+ console.log(chalk.green("Third-Party Development Mode"));
218
+ console.log(chalk.green(`Plugin Context: ${hostConfig.pluginContext}`));
219
+ console.log(chalk.green(`Plugins: ${plugins.length}`));
220
+ console.log(chalk.green("=================================================================="));
221
+ console.log("");
222
+
223
+ // Create log directory
224
+ if (!existsSync(logDir)) {
225
+ mkdirSync(logDir, { recursive: true });
226
+ }
227
+
228
+ // Start plugin dev servers only
229
+ console.log(chalk.yellow("Starting plugin dev servers..."));
230
+ for (const plugin of plugins) {
231
+ const logFile = path.join(logDir, `${plugin.project}.log`);
232
+ startDevServer(plugin.project, plugin.port, logFile);
233
+ }
234
+
235
+ console.log("");
236
+ console.log(chalk.yellow("Waiting for plugin dev servers to start..."));
237
+ await new Promise((resolve) => setTimeout(resolve, 8000));
238
+
239
+ // Verify plugin servers
240
+ console.log(chalk.yellow("Verifying plugin dev servers..."));
241
+ let allReady = true;
242
+
243
+ for (const plugin of plugins) {
244
+ const isReady = await checkPortHealth(plugin.port);
245
+ if (isReady) {
246
+ console.log(chalk.green(` ✓ ${plugin.project} ready on port ${plugin.port}`));
247
+ } else {
248
+ console.log(chalk.red(` ✗ ${plugin.project} NOT ready on port ${plugin.port}`));
249
+ allReady = false;
250
+ }
251
+ }
252
+
253
+ if (!allReady) {
254
+ console.log(chalk.red(`\nSome plugin servers failed to start. Check logs in ${logDir}/`));
255
+ process.exit(1);
256
+ }
257
+
258
+ // Print third-party instructions
259
+ printThirdPartyInstructions(plugins, hostConfig, logDir);
260
+ }
261
+
262
+ /**
263
+ * Print summary for internal development
264
+ */
265
+ function printSummary(hostConfig, plugins, logDir) {
266
+ console.log("");
267
+ console.log(chalk.green("=================================================================="));
268
+ console.log(chalk.green("Development Environment Ready!"));
269
+ console.log(chalk.green("=================================================================="));
270
+ console.log("");
271
+ console.log(chalk.blue("Plugin Dev Servers:"));
272
+
273
+ for (const plugin of plugins) {
274
+ console.log(` - ${plugin.project}: http://localhost:${plugin.port}`);
275
+ }
276
+
277
+ console.log("");
278
+ console.log(chalk.blue("Host App:"));
279
+ console.log(` - ${hostConfig.project}: http://localhost:${hostConfig.port}`);
280
+ console.log("");
281
+ console.log(chalk.blue("Logs:"));
282
+ console.log(` - ${logDir}/`);
283
+ console.log("");
284
+ console.log(chalk.yellow("Press Ctrl+C to stop all servers"));
285
+ console.log("");
286
+ }
287
+
288
+ /**
289
+ * Print third-party connection instructions
290
+ */
291
+ function printThirdPartyInstructions(plugins, hostConfig, logDir) {
292
+ console.log("");
293
+ console.log(chalk.green("=================================================================="));
294
+ console.log(chalk.green("Plugin Dev Servers Running"));
295
+ console.log(chalk.green("=================================================================="));
296
+ console.log("");
297
+ console.log(chalk.blue("Plugin Dev Servers:"));
298
+
299
+ for (const plugin of plugins) {
300
+ console.log(` - ${plugin.project}: http://localhost:${plugin.port}`);
301
+ }
302
+
303
+ console.log("");
304
+ console.log(chalk.yellow("Connection Instructions:"));
305
+ console.log("");
306
+ console.log(chalk.cyan("Connect to your deployed device using query parameters:"));
307
+ console.log("");
308
+
309
+ const yourIp = "<your-development-machine-ip>";
310
+ const deviceUrl = "<device-ip:port>";
311
+ const pluginServers = plugins.map((p) => `http://${yourIp}:${p.port}`).join(",");
312
+
313
+ console.log(chalk.green(` ${deviceUrl}/?pluginDevServers=${pluginServers}`));
314
+ console.log("");
315
+ console.log(chalk.yellow("Replace:"));
316
+ console.log(` - ${chalk.cyan("<your-development-machine-ip>")} with your machine's IP address`);
317
+ console.log(` - ${chalk.cyan("<device-ip:port>")} with your device's address`);
318
+ console.log("");
319
+ console.log(chalk.yellow("Example:"));
320
+ const examplePluginServers = plugins.map((p) => `http://localhost:${p.port}`).join(",");
321
+ console.log(
322
+ chalk.green(
323
+ ` http://192.168.1.100:8080/?pluginDevServers=${examplePluginServers}`
324
+ )
325
+ );
326
+ console.log("");
327
+ console.log(chalk.blue("Logs:"));
328
+ console.log(` - ${logDir}/`);
329
+ console.log("");
330
+ console.log(chalk.yellow("Press Ctrl+C to stop all servers"));
331
+ console.log("");
332
+ }
333
+
334
+ export default async function (plop) {
335
+ plop.setActionType("runDev", async function (answers) {
336
+ // Validate configuration
337
+ const validation = validateDevConfig();
338
+ if (!validation.isValid) {
339
+ console.log(chalk.red("\nError: Invalid development configuration"));
340
+ validation.errors.forEach((err) => console.log(chalk.red(` - ${err}`)));
341
+ console.log("");
342
+ console.log(chalk.yellow("This workspace does not support metadata-driven development."));
343
+ console.log(chalk.yellow("Run the migration generator or create a new workspace with the latest preset."));
344
+ return "Failed: Invalid configuration";
345
+ }
346
+
347
+ const { host, plugins: pluginsArg, disable, pluginsOnly } = answers;
348
+
349
+ // Get host configuration
350
+ const hostConfig = getHost(host);
351
+ if (!hostConfig) {
352
+ console.log(chalk.red(`\nError: Host '${host}' not found in .kos.json`));
353
+ return `Failed: Host not found`;
354
+ }
355
+
356
+ // Parse plugin filters
357
+ const pluginFilter = pluginsArg ? pluginsArg.split(",") : null;
358
+ const disableFilter = disable ? disable.split(",") : null;
359
+
360
+ // Filter plugins
361
+ const plugins = filterPlugins(hostConfig.plugins, {
362
+ plugins: pluginFilter,
363
+ disable: disableFilter,
364
+ });
365
+
366
+ if (plugins.length === 0) {
367
+ console.log(chalk.yellow("Warning: No plugins selected"));
368
+ console.log("");
369
+ }
370
+
371
+ // Determine log directory
372
+ const logDir = process.env.DEV_LOG_DIR || "/tmp/kos-dev-logs";
373
+
374
+ // Determine mode
375
+ const hostProjectExists = checkProjectExists(hostConfig.project);
376
+
377
+ if (hostProjectExists && !pluginsOnly) {
378
+ // Internal Development Mode
379
+ await startInternalDevelopment(hostConfig, plugins, logDir);
380
+ } else {
381
+ // Third-Party Development Mode
382
+ if (!pluginsOnly && !hostProjectExists) {
383
+ console.log(chalk.yellow(`Note: Host project '${hostConfig.project}' not found in workspace.`));
384
+ console.log(chalk.yellow("Starting in third-party development mode (plugins only)."));
385
+ console.log("");
386
+ }
387
+ await startThirdPartyDevelopment(hostConfig, plugins, logDir);
388
+ }
389
+
390
+ // Keep process running
391
+ await new Promise(() => {});
392
+ });
393
+
394
+ // Get available hosts for prompts
395
+ const hosts = getAllHosts();
396
+
397
+ if (hosts.length === 0) {
398
+ console.warn("[kos-cli] No hosts found in .kos.json development configuration");
399
+ }
400
+
401
+ plop.setGenerator("dev", {
402
+ description: "Start development servers for host applications and plugins",
403
+ prompts: [
404
+ {
405
+ type: "list",
406
+ name: "host",
407
+ message: "Select a host application:",
408
+ choices: hosts.map((h) => ({
409
+ name: `${h.name} (${h.type}) - ${h.plugins.length} plugin(s)`,
410
+ value: h.name,
411
+ })),
412
+ },
413
+ {
414
+ type: "input",
415
+ name: "plugins",
416
+ message: "Specific plugins to start (comma-separated aliases, leave empty for all enabled):",
417
+ default: "",
418
+ },
419
+ {
420
+ type: "input",
421
+ name: "disable",
422
+ message: "Plugins to disable (comma-separated aliases, leave empty for none):",
423
+ default: "",
424
+ },
425
+ {
426
+ type: "confirm",
427
+ name: "pluginsOnly",
428
+ message: "Start plugins only (third-party mode)?",
429
+ default: false,
430
+ },
431
+ ],
432
+ actions: () => [{ type: "runDev" }],
433
+ });
434
+ }
@@ -23,12 +23,31 @@
23
23
  }
24
24
  }
25
25
  },
26
+ {
27
+ "category": "dev",
28
+ "file": "index.mjs",
29
+ "metadata": {
30
+ "key": "dev",
31
+ "name": "Development Server",
32
+ "description": "Start development servers for host applications and plugins",
33
+ "namedArguments": {
34
+ "host": "host",
35
+ "plugins": "plugins",
36
+ "disable": "disable",
37
+ "pluginsOnly": "pluginsOnly",
38
+ "interactive": "interactive"
39
+ }
40
+ }
41
+ },
26
42
  {
27
43
  "category": "env",
28
44
  "file": "index.mjs",
29
45
  "metadata": {
30
46
  "key": "env",
31
- "name": "Discover and Set Studio Environment Variables"
47
+ "name": "Discover and Set Studio Environment Variables",
48
+ "namedArguments": {
49
+ "interactive": "interactive"
50
+ }
32
51
  }
33
52
  },
34
53
  {
@@ -44,6 +63,18 @@
44
63
  }
45
64
  }
46
65
  },
66
+ {
67
+ "category": "kab",
68
+ "file": "index.mjs",
69
+ "metadata": {
70
+ "key": "kab",
71
+ "name": "Run Kab Target",
72
+ "namedArguments": {
73
+ "project": "project",
74
+ "interactive": "interactive"
75
+ }
76
+ }
77
+ },
47
78
  {
48
79
  "category": "model",
49
80
  "file": "add-future.mjs",
@@ -79,6 +110,7 @@
79
110
  "parentAware": "parentAware",
80
111
  "singleton": "singleton",
81
112
  "dataServices": "dataServices",
113
+ "autoRegister": "autoRegister",
82
114
  "dryRun": "dryRun",
83
115
  "interactive": "interactive"
84
116
  }
@@ -97,6 +129,7 @@
97
129
  "parentAware": "parentAware",
98
130
  "singleton": "singleton",
99
131
  "dataServices": "dataServices",
132
+ "autoRegister": "autoRegister",
100
133
  "dryRun": "dryRun",
101
134
  "interactive": "interactive"
102
135
  }
@@ -145,6 +178,7 @@
145
178
  "singleton": "singleton",
146
179
  "dataServices": "dataServices",
147
180
  "futureAware": "futureAware",
181
+ "autoRegister": "autoRegister",
148
182
  "dryRun": "dryRun",
149
183
  "interactive": "interactive"
150
184
  }
@@ -225,6 +259,17 @@
225
259
  "project": "componentProject",
226
260
  "componentProject": "componentProject"
227
261
  }
262
+ },
263
+ {
264
+ "key": "plugin:custom",
265
+ "name": "KOS UI Plugin Custom (User-Specified Contribution)",
266
+ "namedArguments": {
267
+ "name": "componentName",
268
+ "componentName": "componentName",
269
+ "project": "componentProject",
270
+ "componentProject": "componentProject",
271
+ "contributionKey": "contributionKey"
272
+ }
228
273
  }
229
274
  ]
230
275
  },
@@ -300,6 +345,29 @@
300
345
  }
301
346
  }
302
347
  },
348
+ {
349
+ "category": "serve",
350
+ "file": "index.mjs",
351
+ "metadata": {
352
+ "key": "serve",
353
+ "name": "Run Serve Target",
354
+ "namedArguments": {
355
+ "project": "project",
356
+ "interactive": "interactive"
357
+ }
358
+ }
359
+ },
360
+ {
361
+ "category": "version",
362
+ "file": "index.mjs",
363
+ "metadata": {
364
+ "key": "version",
365
+ "name": "Display CLI and SDK Version Information",
366
+ "namedArguments": {
367
+ "interactive": "interactive"
368
+ }
369
+ }
370
+ },
303
371
  {
304
372
  "category": "workspace",
305
373
  "file": "index.mjs",
@@ -308,7 +376,8 @@
308
376
  "name": "Create a new KOS UI Workspace",
309
377
  "namedArguments": {
310
378
  "name": "workspaceName",
311
- "workspaceName": "workspaceName"
379
+ "workspaceName": "workspaceName",
380
+ "enablePlugins": "enablePlugins"
312
381
  }
313
382
  }
314
383
  },
@@ -9,7 +9,8 @@ export const metadata = {
9
9
  name: "Create a new KOS UI Workspace",
10
10
  namedArguments: {
11
11
  name: "workspaceName",
12
- workspaceName: "workspaceName"
12
+ workspaceName: "workspaceName",
13
+ enablePlugins: "enablePlugins"
13
14
  }
14
15
  };
15
16
  export default async function (plop) {
@@ -17,6 +18,7 @@ export default async function (plop) {
17
18
  await createWorkspace("@kosdev-code/kos-nx-plugin", {
18
19
  nxCloud: "skip",
19
20
  name: answers.workspaceName,
21
+ enablePlugins: answers.enablePlugins,
20
22
  });
21
23
  return `Workspace ${answers.workspaceName} created.`;
22
24
  });
@@ -30,10 +32,16 @@ export default async function (plop) {
30
32
  message: "Enter the workspace name",
31
33
  validate: required,
32
34
  },
35
+ {
36
+ type: "confirm",
37
+ name: "enablePlugins",
38
+ message: "Do you want to enable plugin support?",
39
+ default: true,
40
+ },
33
41
  ],
34
42
  actions: () => [{ type: "createWorkspace" }],
35
43
  });
36
-
44
+
37
45
  // Register additional workspace generators
38
46
  await registerListModels(plop);
39
47
  await registerListProjects(plop);
@@ -13,6 +13,7 @@ import registerComponent from "./generators/component/index.mjs";
13
13
  import registerPluginComponent from "./generators/plugin/index.mjs";
14
14
 
15
15
  import registerCacheGenerators from "./generators/cache/index.mjs";
16
+ import registerDev from "./generators/dev/index.mjs";
16
17
  import registerEnv from "./generators/env/index.mjs";
17
18
  import registerKab from "./generators/kab/index.mjs";
18
19
  import registerServe from "./generators/serve/index.mjs";
@@ -59,6 +60,7 @@ export default async function (plop) {
59
60
  await registerSplashProject(plop);
60
61
  await registerI18n(plop);
61
62
  await registerI18nNamespace(plop);
63
+ await registerDev(plop);
62
64
  await registerEnv(plop);
63
65
  await registerKab(plop);
64
66
  await registerServe(plop);
@@ -0,0 +1,150 @@
1
+ // utils/dev-config.mjs
2
+ import { existsSync, readFileSync } from "fs";
3
+ import path from "path";
4
+
5
+ /**
6
+ * Read the development configuration from root .kos.json
7
+ *
8
+ * @returns {Object|null} Development config or null if not found
9
+ */
10
+ export function getDevConfig() {
11
+ const workspaceRoot = process.cwd();
12
+ const kosJsonPath = path.join(workspaceRoot, '.kos.json');
13
+
14
+ if (!existsSync(kosJsonPath)) {
15
+ return null;
16
+ }
17
+
18
+ try {
19
+ const content = readFileSync(kosJsonPath, 'utf-8');
20
+ const config = JSON.parse(content);
21
+
22
+ if (config.type !== 'root') {
23
+ return null;
24
+ }
25
+
26
+ if (!config.development || !config.development.hosts) {
27
+ return null;
28
+ }
29
+
30
+ return config.development;
31
+ } catch (error) {
32
+ console.error(`[kos-cli] Error reading .kos.json: ${error.message}`);
33
+ return null;
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Find a host configuration by name
39
+ *
40
+ * @param {string} hostName - Name of the host to find
41
+ * @returns {Object|null} Host config or null if not found
42
+ */
43
+ export function getHost(hostName) {
44
+ const devConfig = getDevConfig();
45
+
46
+ if (!devConfig) {
47
+ return null;
48
+ }
49
+
50
+ return devConfig.hosts.find(h => h.name === hostName) || null;
51
+ }
52
+
53
+ /**
54
+ * Get all available hosts
55
+ *
56
+ * @returns {Array} Array of host configurations
57
+ */
58
+ export function getAllHosts() {
59
+ const devConfig = getDevConfig();
60
+
61
+ if (!devConfig) {
62
+ return [];
63
+ }
64
+
65
+ return devConfig.hosts || [];
66
+ }
67
+
68
+ /**
69
+ * Filter plugins based on options
70
+ *
71
+ * @param {Array} hostPlugins - Array of plugin configs from host
72
+ * @param {Object} options - Filter options
73
+ * @param {Array} options.plugins - Specific plugin aliases to include
74
+ * @param {Array} options.disable - Plugin aliases to exclude
75
+ * @returns {Array} Filtered array of plugin configurations
76
+ */
77
+ export function filterPlugins(hostPlugins, options = {}) {
78
+ let plugins = hostPlugins.filter(p => p.enabled);
79
+
80
+ // If specific plugins requested, override enabled flag
81
+ if (options.plugins && options.plugins.length > 0) {
82
+ plugins = hostPlugins.filter(p => options.plugins.includes(p.alias));
83
+ }
84
+
85
+ // Apply disable list
86
+ if (options.disable && options.disable.length > 0) {
87
+ plugins = plugins.filter(p => !options.disable.includes(p.alias));
88
+ }
89
+
90
+ return plugins;
91
+ }
92
+
93
+ /**
94
+ * Check if a project exists in the workspace
95
+ *
96
+ * @param {string} projectName - Name of the project
97
+ * @returns {boolean} True if project exists
98
+ */
99
+ export function checkProjectExists(projectName) {
100
+ const workspaceRoot = process.cwd();
101
+
102
+ // Check common project locations
103
+ const appPath = path.join(workspaceRoot, 'apps', projectName, 'project.json');
104
+ const libPath = path.join(workspaceRoot, 'libs', projectName, 'project.json');
105
+ const pluginPath = path.join(workspaceRoot, 'plugins', projectName, 'project.json');
106
+ const packagesPath = path.join(workspaceRoot, 'packages', projectName, 'project.json');
107
+
108
+ return existsSync(appPath) ||
109
+ existsSync(libPath) ||
110
+ existsSync(pluginPath) ||
111
+ existsSync(packagesPath);
112
+ }
113
+
114
+ /**
115
+ * Validate development configuration
116
+ *
117
+ * @returns {Object} Validation result with isValid and errors
118
+ */
119
+ export function validateDevConfig() {
120
+ const devConfig = getDevConfig();
121
+ const errors = [];
122
+
123
+ if (!devConfig) {
124
+ errors.push('.kos.json missing or does not contain development configuration');
125
+ return { isValid: false, errors };
126
+ }
127
+
128
+ if (!Array.isArray(devConfig.hosts) || devConfig.hosts.length === 0) {
129
+ errors.push('No hosts defined in development configuration');
130
+ return { isValid: false, errors };
131
+ }
132
+
133
+ // Validate each host
134
+ devConfig.hosts.forEach((host, idx) => {
135
+ if (!host.name) {
136
+ errors.push(`Host at index ${idx} missing required 'name' field`);
137
+ }
138
+ if (!host.project) {
139
+ errors.push(`Host at index ${idx} missing required 'project' field`);
140
+ }
141
+ if (!host.port) {
142
+ errors.push(`Host at index ${idx} missing required 'port' field`);
143
+ }
144
+ if (!Array.isArray(host.plugins)) {
145
+ errors.push(`Host '${host.name}' missing 'plugins' array`);
146
+ }
147
+ });
148
+
149
+ return { isValid: errors.length === 0, errors };
150
+ }