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
|
|
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
|
-
|
|
138
|
-
|
|
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
|
|
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),
|