create-caspian-app 0.2.0-beta.35 → 0.2.0-beta.37

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.
@@ -6,6 +6,7 @@
6
6
  ## Global Rules
7
7
 
8
8
  - Use this source-of-truth order: app runtime and app-owned code first, installed `casp` runtime second, packaged markdown docs third.
9
+ - As the app grows, prefer `src/components/` for reusable application UI and reserve `src/lib/` for reusable non-UI code such as services, validators, adapters, and shared helpers.
9
10
  - Read `./caspian.config.json` almost immediately before making feature, tooling, scaffolding, or file-placement decisions. Treat it as the workspace feature gate for flags such as `backendOnly`, `tailwindcss`, `mcp`, `prisma`, `typescript`, and `componentScanDirs`.
10
11
  - Treat `caspian.config.json` as the single source of truth for whether optional Caspian features are enabled in the current workspace. Use feature-specific docs, files, and commands only after the matching flag is confirmed as enabled.
11
12
  - If a feature is disabled and the user wants it, ask whether they want to enable it first, then update `caspian.config.json` and follow `npx casp update project` so framework-managed files align with the new feature set.
@@ -46,11 +47,18 @@
46
47
 
47
48
  ### `src/lib/**/*.py`
48
49
 
49
- - Keep `src/lib/` for app-owned shared code, service wrappers, and reusable helpers.
50
+ - Keep `src/lib/` for app-owned shared non-UI code, service wrappers, validators, adapters, and reusable helpers.
51
+ - Prefer `src/components/` for reusable rendered UI instead of placing component modules in `src/lib/`.
50
52
  - Reuse the generated `src/lib/prisma/` package for Python database access, but do not hand-edit files under `src/lib/prisma/`; regenerate them with `npx ppy generate` after schema changes.
51
53
  - When `caspian.config.json` has `mcp: true`, keep app-owned MCP tools in `src/lib/mcp/mcp_server.py` and keep the default FastMCP config in `src/lib/mcp/fastmcp.json`. If those locations change, update `settings/restart-mcp.ts` and the MCP docs together.
52
54
  - Keep auth policy in `src/lib/auth/auth_config.py`. Keep auth bootstrap and middleware order changes in `main.py`.
53
55
 
56
+ ### `src/components/**/*.py`
57
+
58
+ - Keep `src/components/` as the default home for reusable application UI components.
59
+ - Move shared cards, forms, shells, navigation, and other reusable rendered building blocks here once they are used across routes or features.
60
+ - Keep route-owned markup in `src/app/**`, and keep non-UI helpers or services in `src/lib/**`.
61
+
54
62
  ### `public/js/main.js`
55
63
 
56
64
  - Treat `public/js/main.js` as the thin browser bootstrap entry point.
package/dist/AGENTS.md CHANGED
@@ -54,6 +54,7 @@ Treat `caspian.config.json` as the single source of truth for whether an optiona
54
54
  - The schema-change workflow in this workspace is: `npx prisma migrate dev`; if seed flow or `prisma/seed.ts` is involved, run `npx prisma generate` and then `npx prisma db seed`; then run `npx ppy generate`.
55
55
  - `npx ppy generate` owns `src/lib/prisma/__init__.py`, `src/lib/prisma/db.py`, `src/lib/prisma/models.py`, and `settings/prisma-schema.json`; do not hand-edit those generated files.
56
56
  - `caspian.config.json` is the first config file to check for enabled workspace features. In the current workspace it sets `backendOnly: false`, `tailwindcss: true`, `mcp: false`, `prisma: true`, `typescript: false`, and `componentScanDirs: ["src"]`.
57
+ - As the app grows, prefer `src/components/` for reusable rendered UI and `src/lib/` for reusable non-UI support code.
57
58
  - PulsePoint runtime code is shipped in `public/js/pp-reactive-v2.js` and loaded from `public/js/main.js`.
58
59
  - `pp-component` is injected by the Python render pipeline onto page, layout, and component roots; authored route and component templates should not add it manually.
59
60
  - `main.py` runs `transform_scripts(...)`, so authored body `<script>` tags are rewritten to `<script type="text/pp">` in rendered HTML; route, layout, and component templates should write plain `<script>` in source.
@@ -88,7 +89,9 @@ Use this map before making changes.
88
89
  ## Editing Rules
89
90
 
90
91
  - Keep app-owned shared code in `src/lib/**`.
92
+ - Keep reusable application UI components in `src/components/**`.
91
93
  - Keep route-specific logic in `src/app/**`.
94
+ - When deciding between `src/components/**` and `src/lib/**`, put reusable rendered UI in `src/components/**` and put services, validators, adapters, database helpers, and other non-UI support code in `src/lib/**`.
92
95
  - Read `caspian.config.json` before deciding whether a Caspian feature should be used, documented, scaffolded, or avoided in the current workspace.
93
96
  - Treat `caspian.config.json` as the single source of truth for optional features. Do not use feature-specific files, commands, or docs until the corresponding flag is enabled.
94
97
  - If a feature flag is false and the user wants that feature, ask for confirmation first, then update `caspian.config.json` and run `npx casp update project` so framework-managed files align with the new feature set.
@@ -13,6 +13,7 @@ import { componentMap } from "./component-map.js";
13
13
  import { createServer } from "net";
14
14
  import chalk from "chalk";
15
15
  import { networkInterfaces } from "os";
16
+ import caspianConfig from "../caspian.config.json";
16
17
 
17
18
  const { __dirname } = getFileMeta();
18
19
  const bs: BrowserSyncInstance = browserSync.create();
@@ -24,14 +25,48 @@ let lastChangedFile: string | null = null;
24
25
  let pythonPort = 0;
25
26
  let bsPort = 0;
26
27
 
27
- function getAvailablePort(startPort: number): Promise<number> {
28
+ function getReservedPorts(): Set<number> {
29
+ const reservedPorts = new Set<number>();
30
+
31
+ if (!caspianConfig.mcp) {
32
+ return reservedPorts;
33
+ }
34
+
35
+ const mcpSpecPath = join(__dirname, "..", "src", "lib", "mcp", "fastmcp.json");
36
+ if (!existsSync(mcpSpecPath)) {
37
+ return reservedPorts;
38
+ }
39
+
40
+ try {
41
+ const mcpSpec = JSON.parse(readFileSync(mcpSpecPath, "utf-8"));
42
+ const reservedPort = Number(mcpSpec?.deployment?.port);
43
+
44
+ if (Number.isInteger(reservedPort) && reservedPort > 0) {
45
+ reservedPorts.add(reservedPort);
46
+ }
47
+ } catch {
48
+ // Ignore malformed local MCP config and fall back to dynamic allocation.
49
+ }
50
+
51
+ return reservedPorts;
52
+ }
53
+
54
+ function getAvailablePort(
55
+ startPort: number,
56
+ reservedPorts: Set<number> = new Set(),
57
+ ): Promise<number> {
28
58
  return new Promise((resolve) => {
59
+ if (reservedPorts.has(startPort)) {
60
+ resolve(getAvailablePort(startPort + 1, reservedPorts));
61
+ return;
62
+ }
63
+
29
64
  const server = createServer();
30
65
  server.listen(startPort, () => {
31
66
  server.close(() => resolve(startPort));
32
67
  });
33
68
  server.on("error", () => {
34
- resolve(getAvailablePort(startPort + 1));
69
+ resolve(getAvailablePort(startPort + 1, reservedPorts));
35
70
  });
36
71
  });
37
72
  }
@@ -134,8 +169,10 @@ const publicPipeline = new DebouncedWorker(
134
169
  );
135
170
 
136
171
  (async () => {
137
- bsPort = await getAvailablePort(5090);
138
- pythonPort = await getAvailablePort(bsPort + 10);
172
+ const reservedPorts = getReservedPorts();
173
+
174
+ bsPort = await getAvailablePort(5090, reservedPorts);
175
+ pythonPort = await getAvailablePort(5200, reservedPorts);
139
176
 
140
177
  updateRouteFilesCache();
141
178
 
@@ -1,5 +1,6 @@
1
- import { existsSync } from "fs";
2
- import { join } from "path";
1
+ import { existsSync, readFileSync } from "fs";
2
+ import { isAbsolute, join } from "path";
3
+ import { createServer } from "net";
3
4
  import caspianConfig from "../caspian.config.json";
4
5
  import { createRestartableProcess, onExit } from "./utils.js";
5
6
 
@@ -36,11 +37,79 @@ function getServerSpec(): string | null {
36
37
  return null;
37
38
  }
38
39
 
39
- function buildArgs(serverSpec: string): string[] {
40
+ function getServerSpecPath(serverSpec: string): string | null {
41
+ if (serverSpec.startsWith("http://") || serverSpec.startsWith("https://")) {
42
+ return null;
43
+ }
44
+
45
+ return isAbsolute(serverSpec) ? serverSpec : join(projectRoot, serverSpec);
46
+ }
47
+
48
+ function readDeploymentConfig(serverSpec: string): Record<string, unknown> {
49
+ const serverSpecPath = getServerSpecPath(serverSpec);
50
+ if (!serverSpecPath || !existsSync(serverSpecPath)) {
51
+ return {};
52
+ }
53
+
54
+ try {
55
+ const rawConfig = JSON.parse(readFileSync(serverSpecPath, "utf-8"));
56
+ if (rawConfig && typeof rawConfig === "object") {
57
+ const deployment = rawConfig.deployment;
58
+ if (deployment && typeof deployment === "object") {
59
+ return deployment as Record<string, unknown>;
60
+ }
61
+ }
62
+ } catch {
63
+ // Ignore malformed specs and allow FastMCP to validate them.
64
+ }
65
+
66
+ return {};
67
+ }
68
+
69
+ function getPreferredHost(serverSpec: string): string {
70
+ return (
71
+ process.env.MCP_HOST?.trim() ||
72
+ String(readDeploymentConfig(serverSpec).host || "").trim() ||
73
+ "127.0.0.1"
74
+ );
75
+ }
76
+
77
+ function getPreferredPort(serverSpec: string): number | null {
78
+ const rawPort =
79
+ process.env.MCP_PORT?.trim() ||
80
+ String(readDeploymentConfig(serverSpec).port || "").trim();
81
+
82
+ if (!rawPort) {
83
+ return null;
84
+ }
85
+
86
+ const port = Number(rawPort);
87
+ if (!Number.isInteger(port) || port <= 0) {
88
+ return null;
89
+ }
90
+
91
+ return port;
92
+ }
93
+
94
+ function findAvailablePort(startPort: number, host: string): Promise<number> {
95
+ return new Promise((resolve) => {
96
+ const server = createServer();
97
+
98
+ server.listen(startPort, host, () => {
99
+ server.close(() => resolve(startPort));
100
+ });
101
+
102
+ server.on("error", () => {
103
+ resolve(findAvailablePort(startPort + 1, host));
104
+ });
105
+ });
106
+ }
107
+
108
+ function buildArgs(serverSpec: string, portOverride?: string): string[] {
40
109
  const args = ["run", serverSpec, "--no-banner"];
41
110
  const transport = process.env.MCP_TRANSPORT?.trim();
42
111
  const host = process.env.MCP_HOST?.trim();
43
- const port = process.env.MCP_PORT?.trim();
112
+ const port = portOverride || process.env.MCP_PORT?.trim();
44
113
  const path = process.env.MCP_PATH?.trim();
45
114
  const logLevel = process.env.MCP_LOG_LEVEL?.trim();
46
115
 
@@ -133,12 +202,24 @@ if (!serverSpec) {
133
202
  process.exit(0);
134
203
  }
135
204
 
205
+ const preferredHost = getPreferredHost(serverSpec);
206
+ const preferredPort = getPreferredPort(serverSpec);
207
+ const resolvedPort = preferredPort
208
+ ? await findAvailablePort(preferredPort, preferredHost)
209
+ : null;
210
+
211
+ if (preferredPort && resolvedPort && preferredPort !== resolvedPort) {
212
+ console.log(
213
+ `[mcp] Port ${preferredPort} is unavailable on ${preferredHost}, using ${resolvedPort} instead.`,
214
+ );
215
+ }
216
+
136
217
  const handleMcpOutput = createMcpOutputHandler();
137
218
 
138
219
  const runner = createRestartableProcess({
139
220
  name: "mcp",
140
221
  cmd: fastMcpCommand,
141
- args: buildArgs(serverSpec),
222
+ args: buildArgs(serverSpec, resolvedPort ? String(resolvedPort) : undefined),
142
223
  startMessage: "[mcp] Starting MCP server...",
143
224
  onStdout: createLineHandler(handleMcpOutput),
144
225
  onStderr: createLineHandler(handleMcpOutput),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-caspian-app",
3
- "version": "0.2.0-beta.35",
3
+ "version": "0.2.0-beta.37",
4
4
  "description": "Scaffold a new Caspian project (FastAPI-powered reactive Python framework).",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",