create-caspian-app 0.3.0-rc.2 → 0.3.0-rc.21

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.
@@ -1,5 +1,5 @@
1
1
  [project]
2
- name = "my-app"
2
+ name = "caspian-app"
3
3
  version = "1.0.0"
4
4
  requires-python = ">=3.14"
5
5
  dependencies = []
@@ -1,7 +1,7 @@
1
- {
2
- "local": "http://localhost:5090",
3
- "external": "http://172.18.64.1:5090",
4
- "ui": "http://localhost:3001",
5
- "uiExternal": "http://172.18.64.1:3001",
6
- "backend": "http://localhost:5091"
1
+ {
2
+ "local": "http://localhost:5091",
3
+ "external": "http://192.168.1.109:5091",
4
+ "ui": "http://localhost:3003",
5
+ "uiExternal": "http://192.168.1.109:3003",
6
+ "backend": "http://localhost:5201"
7
7
  }
@@ -8,6 +8,7 @@ import { DebouncedWorker, createSrcWatcher, DEFAULT_AWF } from "./utils.js";
8
8
  import {
9
9
  startPythonServer,
10
10
  restartPythonServer,
11
+ stopPythonServer,
11
12
  waitForPort,
12
13
  } from "./python-server.js";
13
14
  import { componentMap } from "./component-map.js";
@@ -25,6 +26,7 @@ const PUBLIC_ROOT = join(WORKSPACE_ROOT, PUBLIC_DIR);
25
26
  const PUBLIC_IGNORE_DIRS = ["uploads"];
26
27
  let previousRouteFiles: string[] = [];
27
28
  let lastChangedFile: string | null = null;
29
+ let shuttingDown = false;
28
30
 
29
31
  let pythonPort = 0;
30
32
  let bsPort = 0;
@@ -173,7 +175,9 @@ const pipeline = new DebouncedWorker(
173
175
  if (isReady && bs.active) {
174
176
  bs.reload();
175
177
  } else {
176
- console.error(chalk.red("⚠ Server failed to start or timed out."));
178
+ console.error(
179
+ chalk.red("Warning: Server failed to start or timed out."),
180
+ );
177
181
  }
178
182
  return;
179
183
  }
@@ -199,7 +203,7 @@ const pipeline = new DebouncedWorker(
199
203
  if (previousRouteFiles.length > 0 && routesChanged) {
200
204
  console.log(
201
205
  chalk.yellow(
202
- " Structure changed (New/Deleted file), restarting Python server...",
206
+ "-> Structure changed (New/Deleted file), restarting Python server...",
203
207
  ),
204
208
  );
205
209
  await restartPythonServer(pythonPort, bsPort);
@@ -238,13 +242,48 @@ function isIgnoredPublicPath(absPath: string): boolean {
238
242
 
239
243
  const publicPipeline = new DebouncedWorker(
240
244
  async () => {
241
- console.log(chalk.cyan("→ Public directory changed, reloading browser..."));
245
+ console.log(
246
+ chalk.cyan("-> Public directory changed, reloading browser..."),
247
+ );
242
248
  if (bs.active) bs.reload();
243
249
  },
244
250
  350,
245
251
  "bs-public-pipeline",
246
252
  );
247
253
 
254
+ type ClosableWatcher = {
255
+ close: () => Promise<void>;
256
+ };
257
+
258
+ const sourceWatchers: ClosableWatcher[] = [];
259
+
260
+ async function closeWatchers(): Promise<void> {
261
+ await Promise.all(
262
+ sourceWatchers.splice(0).map(async (watcher) => {
263
+ try {
264
+ await watcher.close();
265
+ } catch {}
266
+ }),
267
+ );
268
+ }
269
+
270
+ async function shutdown(exitCode: number): Promise<void> {
271
+ if (shuttingDown) {
272
+ return;
273
+ }
274
+
275
+ shuttingDown = true;
276
+
277
+ await closeWatchers();
278
+
279
+ if (bs.active) {
280
+ bs.exit();
281
+ }
282
+
283
+ stopPythonServer();
284
+ process.exit(exitCode);
285
+ }
286
+
248
287
  (async () => {
249
288
  const reservedPorts = getReservedPorts();
250
289
 
@@ -253,77 +292,90 @@ const publicPipeline = new DebouncedWorker(
253
292
 
254
293
  updateRouteFilesCache();
255
294
 
256
- createSrcWatcher(join(SRC_DIR, "**", "*"), {
257
- onEvent: (_ev, _abs, rel) => {
258
- if (rel.includes("__pycache__") || rel.endsWith(".pyc")) return;
259
- lastChangedFile = rel;
260
- pipeline.schedule(rel);
261
- },
262
- awaitWriteFinish: DEFAULT_AWF,
263
- logPrefix: "watch-src",
264
- usePolling: true,
265
- interval: 1000,
266
- });
295
+ sourceWatchers.push(
296
+ createSrcWatcher(join(SRC_DIR, "**", "*"), {
297
+ onEvent: (_ev, _abs, rel) => {
298
+ if (rel.includes("__pycache__") || rel.endsWith(".pyc")) return;
299
+ lastChangedFile = rel;
300
+ pipeline.schedule(rel);
301
+ },
302
+ awaitWriteFinish: DEFAULT_AWF,
303
+ logPrefix: "watch-src",
304
+ usePolling: true,
305
+ interval: 1000,
306
+ }),
307
+ );
267
308
 
268
- createSrcWatcher(join(PUBLIC_DIR, "**", "*"), {
269
- onEvent: (_ev, abs, _) => {
270
- const relFromPublic = relative(PUBLIC_ROOT, abs).replace(/\\/g, "/");
271
- if (isIgnoredPublicPath(abs)) return;
272
- publicPipeline.schedule(relFromPublic);
273
- },
274
- awaitWriteFinish: DEFAULT_AWF,
275
- logPrefix: "watch-public",
276
- usePolling: true,
277
- interval: 1000,
278
- });
309
+ sourceWatchers.push(
310
+ createSrcWatcher(join(PUBLIC_DIR, "**", "*"), {
311
+ onEvent: (_ev, abs, _) => {
312
+ const relFromPublic = relative(PUBLIC_ROOT, abs).replace(/\\/g, "/");
313
+ if (isIgnoredPublicPath(abs)) return;
314
+ publicPipeline.schedule(relFromPublic);
315
+ },
316
+ awaitWriteFinish: DEFAULT_AWF,
317
+ logPrefix: "watch-public",
318
+ usePolling: true,
319
+ interval: 1000,
320
+ }),
321
+ );
279
322
 
280
323
  const viteFlagFile = join(__dirname, "..", ".casp", ".vite-build-complete");
281
324
  mkdirSync(dirname(viteFlagFile), { recursive: true });
282
325
  if (!existsSync(viteFlagFile)) writeFileSync(viteFlagFile, "0");
283
326
 
284
- createSrcWatcher(viteFlagFile, {
285
- onEvent: (ev) => {
286
- if (ev === "change" && bs.active) {
287
- bs.reload();
288
- }
289
- },
290
- awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 50 },
291
- logPrefix: "watch-vite",
292
- usePolling: true,
293
- interval: 500,
294
- });
327
+ sourceWatchers.push(
328
+ createSrcWatcher(viteFlagFile, {
329
+ onEvent: (ev) => {
330
+ if (ev === "change" && bs.active) {
331
+ bs.reload();
332
+ }
333
+ },
334
+ awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 50 },
335
+ logPrefix: "watch-vite",
336
+ usePolling: true,
337
+ interval: 500,
338
+ }),
339
+ );
295
340
 
296
- createSrcWatcher(join(__dirname, "..", "utils", "**", "*.py"), {
297
- onEvent: async (_ev, _abs, _) => {
298
- if (_abs.includes("__pycache__")) return;
299
- await restartPythonServer(pythonPort, bsPort);
300
- const isReady = await waitForPort(pythonPort);
301
- if (isReady && bs.active) bs.reload();
302
- },
303
- awaitWriteFinish: DEFAULT_AWF,
304
- logPrefix: "watch-utils",
305
- usePolling: true,
306
- interval: 1000,
307
- });
341
+ sourceWatchers.push(
342
+ createSrcWatcher(join(__dirname, "..", "utils", "**", "*.py"), {
343
+ onEvent: async (_ev, _abs, _) => {
344
+ if (_abs.includes("__pycache__")) return;
345
+ await restartPythonServer(pythonPort, bsPort);
346
+ const isReady = await waitForPort(pythonPort);
347
+ if (isReady && bs.active) bs.reload();
348
+ },
349
+ awaitWriteFinish: DEFAULT_AWF,
350
+ logPrefix: "watch-utils",
351
+ usePolling: true,
352
+ interval: 1000,
353
+ }),
354
+ );
308
355
 
309
- createSrcWatcher(join(__dirname, "..", "main.py"), {
310
- onEvent: async (_ev, _abs, _) => {
311
- if (_abs.includes("__pycache__")) return;
312
- await restartPythonServer(pythonPort, bsPort);
313
- const isReady = await waitForPort(pythonPort);
314
- if (isReady && bs.active) bs.reload();
315
- },
316
- awaitWriteFinish: DEFAULT_AWF,
317
- logPrefix: "watch-main",
318
- usePolling: true,
319
- interval: 1000,
320
- });
356
+ sourceWatchers.push(
357
+ createSrcWatcher(join(__dirname, "..", "main.py"), {
358
+ onEvent: async (_ev, _abs, _) => {
359
+ if (_abs.includes("__pycache__")) return;
360
+ await restartPythonServer(pythonPort, bsPort);
361
+ const isReady = await waitForPort(pythonPort);
362
+ if (isReady && bs.active) bs.reload();
363
+ },
364
+ awaitWriteFinish: DEFAULT_AWF,
365
+ logPrefix: "watch-main",
366
+ usePolling: true,
367
+ interval: 1000,
368
+ }),
369
+ );
321
370
 
322
371
  startPythonServer(pythonPort, bsPort);
323
372
 
324
373
  bs.init(
325
374
  {
326
- proxy: `http://localhost:${pythonPort}`,
375
+ proxy: {
376
+ target: `http://localhost:${pythonPort}`,
377
+ ws: true,
378
+ },
327
379
  port: bsPort,
328
380
  online: true,
329
381
  middleware: [
@@ -356,7 +408,7 @@ const publicPipeline = new DebouncedWorker(
356
408
  const uiExtUrl = urls.get("ui-external");
357
409
 
358
410
  console.log("");
359
- console.log(chalk.green.bold(" Ports Configured:"));
411
+ console.log(chalk.green.bold("OK Ports Configured:"));
360
412
  console.log(
361
413
  ` ${chalk.blue.bold("Frontend (BrowserSync):")} ${chalk.magenta(localUrl)}`,
362
414
  );
@@ -394,3 +446,21 @@ const publicPipeline = new DebouncedWorker(
394
446
  },
395
447
  );
396
448
  })();
449
+
450
+ process.once("SIGINT", () => {
451
+ void shutdown(0);
452
+ });
453
+
454
+ process.once("SIGTERM", () => {
455
+ void shutdown(0);
456
+ });
457
+
458
+ process.once("uncaughtException", (error) => {
459
+ console.error(error);
460
+ void shutdown(1);
461
+ });
462
+
463
+ process.once("unhandledRejection", (reason) => {
464
+ console.error(reason);
465
+ void shutdown(1);
466
+ });
@@ -981,7 +981,7 @@ function analyzeFile(filePath: string, rootDir: string): ComponentMetadata[] {
981
981
 
982
982
  function loadConfig(): CaspianConfig {
983
983
  if (!fs.existsSync(CONFIG_PATH)) {
984
- console.error(`❌ Configuration file not found at: ${CONFIG_PATH}`);
984
+ console.error(`Error: Configuration file not found at: ${CONFIG_PATH}`);
985
985
  process.exit(1);
986
986
  }
987
987
  return JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
@@ -1024,7 +1024,7 @@ function walkDirectory(
1024
1024
  */
1025
1025
 
1026
1026
  export async function componentMap() {
1027
- console.log(`🔍 Starting Component Analysis (Lezer AST-first)...`);
1027
+ console.log("Starting Component Analysis (Lezer AST-first)...");
1028
1028
  const config = loadConfig();
1029
1029
  let allFiles: string[] = [];
1030
1030
 
@@ -1036,7 +1036,7 @@ export async function componentMap() {
1036
1036
  );
1037
1037
  });
1038
1038
 
1039
- console.log(`📂 Found ${allFiles.length} Python files.`);
1039
+ console.log(`Found ${allFiles.length} Python files.`);
1040
1040
  const componentRegistry: ComponentMetadata[] = [];
1041
1041
 
1042
1042
  allFiles.forEach((file) => {
@@ -1044,15 +1044,15 @@ export async function componentMap() {
1044
1044
  const foundComponents = analyzeFile(file, PROJECT_ROOT);
1045
1045
  componentRegistry.push(...foundComponents);
1046
1046
  } catch (e) {
1047
- console.warn(`⚠️ Failed to parse ${file}:`, e);
1047
+ console.warn(`Warning: Failed to parse ${file}:`, e);
1048
1048
  }
1049
1049
  });
1050
1050
 
1051
- console.log(`✅ Discovered ${componentRegistry.length} Components.`);
1051
+ console.log(`Discovered ${componentRegistry.length} Components.`);
1052
1052
 
1053
1053
  const outputPath = path.join(__dirname, "component-map.json");
1054
1054
  if (componentRegistry.length > 0 || !fs.existsSync(outputPath)) {
1055
1055
  fs.writeFileSync(outputPath, JSON.stringify(componentRegistry, null, 2));
1056
- console.log(`📝 Component map written to: ${outputPath}`);
1056
+ console.log(`Component map written to: ${outputPath}`);
1057
1057
  }
1058
1058
  }
@@ -17,7 +17,7 @@ function getVenvPythonPath(): string {
17
17
  : join(".venv", "bin", "python");
18
18
 
19
19
  if (!existsSync(venvPython)) {
20
- console.warn(`⚠ Virtual environment not found, using system python`);
20
+ console.warn("Warning: Virtual environment not found, using system python");
21
21
  return isWindows() ? "python" : "python3";
22
22
  }
23
23
  return venvPython;
@@ -118,7 +118,7 @@ function spawnPython(port: number, browserSyncPort?: number): ChildProcess {
118
118
  const pythonPath = getVenvPythonPath();
119
119
  const args = ["-u", "main.py"];
120
120
 
121
- console.log(`→ Starting Python server on port ${port}...`);
121
+ console.log(`-> Starting Python server on port ${port}...`);
122
122
 
123
123
  const env = {
124
124
  ...process.env,
@@ -156,7 +156,7 @@ export async function restartPythonServer(
156
156
  isRestarting = true;
157
157
 
158
158
  try {
159
- console.log(" Restarting Python server...");
159
+ console.log("-> Restarting Python server...");
160
160
  const prev = pythonProcess;
161
161
  pythonProcess = null;
162
162
 
@@ -171,18 +171,8 @@ export async function restartPythonServer(
171
171
  }
172
172
  }
173
173
 
174
- export function stopPythonServer(): void {
175
- const prev = pythonProcess;
176
- pythonProcess = null;
177
- if (prev) killProcessTree(prev);
178
- }
179
-
180
- process.on("exit", () => stopPythonServer());
181
- process.on("SIGINT", () => {
182
- stopPythonServer();
183
- process.exit(0);
184
- });
185
- process.on("SIGTERM", () => {
186
- stopPythonServer();
187
- process.exit(0);
188
- });
174
+ export function stopPythonServer(): void {
175
+ const prev = pythonProcess;
176
+ pythonProcess = null;
177
+ if (prev) killProcessTree(prev);
178
+ }
@@ -109,7 +109,7 @@ export class DebouncedWorker {
109
109
  ) {}
110
110
 
111
111
  schedule(reason?: string) {
112
- if (reason) console.log(`[${this.name}] ${reason} scheduled`);
112
+ if (reason) console.log(`[${this.name}] ${reason} -> scheduled`);
113
113
  if (this.timer) clearTimeout(this.timer);
114
114
  this.timer = setTimeout(() => {
115
115
  this.timer = null;
@@ -203,7 +203,7 @@ export function createRestartableProcess(spec: {
203
203
  async function stop(): Promise<void> {
204
204
  if (!child || child.killed) return;
205
205
  const pid = child.pid!;
206
- console.log(`[${name}] Stopping…`);
206
+ console.log(`[${name}] Stopping...`);
207
207
 
208
208
  if (process.platform === "win32" && windowsKillTree) {
209
209
  await killOnWindows(pid);
@@ -245,7 +245,7 @@ export function createRestartableProcess(spec: {
245
245
 
246
246
  export function onExit(fn: () => Promise<void> | void) {
247
247
  const wrap = (sig: string) => async () => {
248
- console.log(`[proc] Received ${sig}, shutting down…`);
248
+ console.log(`[proc] Received ${sig}, shutting down...`);
249
249
  try {
250
250
  await fn();
251
251
  } finally {
@@ -26,7 +26,7 @@ def build_auth_settings() -> AuthSettings:
26
26
  # This app-owned starter config begins public-first; switch to True only when most routes require auth.
27
27
  # Use all-private mode when only a few routes should stay public.
28
28
  is_all_routes_private=False,
29
- public_routes=["/"],
29
+ public_routes=["/", "/health"],
30
30
  # Sign-in and signup stay public by default; only change this when the app explicitly needs it.
31
31
  auth_routes=["/signin", "/signup"],
32
32
  private_routes=[], # unused when all-routes-private is True
@@ -14,13 +14,13 @@ FILES_LIST_PATH = PROJECT_ROOT / "settings" / "files-list.json"
14
14
  COMPONENT_MAP_PATH = PROJECT_ROOT / "settings" / "component-map.json"
15
15
 
16
16
 
17
- mcp = FastMCP(
18
- name="Mapka MCP",
19
- instructions=(
20
- "Read-only workspace metadata for the Mapka Caspian application. "
21
- "Use these tools for project configuration, generated file inventory, and component discovery."
22
- ),
23
- )
17
+ mcp = FastMCP(
18
+ name="Caspian MCP",
19
+ instructions=(
20
+ "Read-only workspace metadata for the Caspian application. "
21
+ "Use these tools for project configuration, generated file inventory, and component discovery."
22
+ ),
23
+ )
24
24
 
25
25
 
26
26
  def _load_json(path: Path, fallback: Any) -> Any:
@@ -88,4 +88,4 @@ def component_inventory() -> dict[str, Any]:
88
88
  return {
89
89
  "count": len(components),
90
90
  "components": components,
91
- }
91
+ }
@@ -0,0 +1,84 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import MutableMapping
4
+ from datetime import datetime, timezone
5
+ from typing import Any
6
+
7
+ from fastapi import WebSocket
8
+
9
+
10
+ def get_websocket_session(websocket: WebSocket) -> MutableMapping[str, Any]:
11
+ session = getattr(websocket, "session", None)
12
+ if isinstance(session, MutableMapping):
13
+ return session
14
+
15
+ scope_session = websocket.scope.get("session")
16
+ if isinstance(scope_session, MutableMapping):
17
+ return scope_session
18
+
19
+ return {}
20
+
21
+
22
+ def get_authenticated_payload_from_session(
23
+ session: MutableMapping[str, Any],
24
+ auth_instance: Any,
25
+ ) -> dict[str, Any] | None:
26
+ payload_key = getattr(auth_instance, "PAYLOAD_SESSION_KEY", "")
27
+ payload_name = getattr(auth_instance, "PAYLOAD_NAME", "")
28
+
29
+ payload_wrapper = session.get(payload_key)
30
+ if not isinstance(payload_wrapper, dict):
31
+ return None
32
+
33
+ exp = payload_wrapper.get("exp")
34
+ if exp is not None:
35
+ try:
36
+ if float(exp) < datetime.now(timezone.utc).timestamp():
37
+ session.pop(payload_key, None)
38
+ return None
39
+ except Exception:
40
+ session.pop(payload_key, None)
41
+ return None
42
+
43
+ payload = payload_wrapper.get(payload_name)
44
+ if payload is None:
45
+ session.pop(payload_key, None)
46
+ return None
47
+
48
+ if isinstance(payload, dict):
49
+ if getattr(getattr(auth_instance, "settings", None), "token_auto_refresh", False):
50
+ refreshed_wrapper = dict(payload_wrapper)
51
+ refreshed_wrapper["exp"] = auth_instance._calculate_expiration(
52
+ auth_instance.settings.default_token_validity
53
+ )
54
+ session[payload_key] = refreshed_wrapper
55
+ return payload
56
+
57
+ return {"value": payload}
58
+
59
+
60
+ class WebSocketConnectionManager:
61
+ def __init__(self) -> None:
62
+ self._connections: set[WebSocket] = set()
63
+
64
+ async def connect(self, websocket: WebSocket) -> None:
65
+ await websocket.accept()
66
+ self._connections.add(websocket)
67
+
68
+ def disconnect(self, websocket: WebSocket) -> None:
69
+ self._connections.discard(websocket)
70
+
71
+ async def broadcast_json(self, payload: dict[str, Any]) -> None:
72
+ stale: list[WebSocket] = []
73
+ for websocket in list(self._connections):
74
+ try:
75
+ await websocket.send_json(payload)
76
+ except RuntimeError:
77
+ stale.append(websocket)
78
+
79
+ for websocket in stale:
80
+ self.disconnect(websocket)
81
+
82
+
83
+ websocket_connections = WebSocketConnectionManager()
84
+ public_websocket_connections = WebSocketConnectionManager()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-caspian-app",
3
- "version": "0.3.0-rc.2",
3
+ "version": "0.3.0-rc.21",
4
4
  "description": "Scaffold a new Caspian project (FastAPI-powered reactive Python framework).",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",