@spawn-dock/create 0.2.0-canary.20260321111558.f5e5655 → 0.2.3
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/README.md +10 -2
- package/package.json +1 -1
- package/packages/app/dist/src/app/program.js +1 -0
- package/packages/app/dist/src/core/bootstrap.js +13 -17
- package/packages/app/dist/src/shell/bootstrap.js +37 -1
- package/packages/app/template-nextjs-overlay/.mcp.json +11 -0
- package/packages/app/template-nextjs-overlay/opencode.json +13 -0
- package/packages/app/template-nextjs-overlay/spawndock/config.mjs +15 -0
- package/packages/app/template-nextjs-overlay/spawndock/mcp.mjs +24 -0
package/README.md
CHANGED
|
@@ -29,7 +29,6 @@ npx --yes github:SpawnDock/create#main --token <pairing-token> [project-dir]
|
|
|
29
29
|
- `spawndock.config.json`
|
|
30
30
|
- `.env.local`
|
|
31
31
|
- `spawndock.dev-tunnel.json`
|
|
32
|
-
- `opencode.json`
|
|
33
32
|
- `public/tonconnect-manifest.json`
|
|
34
33
|
|
|
35
34
|
## Built-in Overlay
|
|
@@ -40,11 +39,20 @@ The package also ships a built-in TMA overlay and applies it after cloning
|
|
|
40
39
|
- `spawndock/dev.mjs`
|
|
41
40
|
- `spawndock/next.mjs`
|
|
42
41
|
- `spawndock/tunnel.mjs`
|
|
42
|
+
- `spawndock/mcp.mjs`
|
|
43
|
+
- `opencode.json`
|
|
44
|
+
- `.mcp.json`
|
|
43
45
|
- `next.config.ts`
|
|
44
46
|
- `public/tonconnect-manifest.json`
|
|
45
47
|
- patching project scripts and `@spawn-dock/dev-tunnel`
|
|
46
48
|
|
|
47
|
-
|
|
49
|
+
`spawndock/mcp.mjs` resolves `<controlPlaneUrl>/mcp/sse` from
|
|
50
|
+
`spawndock.config.json`.
|
|
51
|
+
|
|
52
|
+
- `opencode.json` is shipped by the template for OpenCode.
|
|
53
|
+
- `.mcp.json` is shipped by the template for Claude Code.
|
|
54
|
+
- if `codex` is installed locally, bootstrap also registers the same MCP server in
|
|
55
|
+
the global Codex MCP config automatically.
|
|
48
56
|
|
|
49
57
|
## Development
|
|
50
58
|
|
package/package.json
CHANGED
|
@@ -6,6 +6,7 @@ const formatSuccess = (summary) => [
|
|
|
6
6
|
`SpawnDock project created at ${summary.projectDir}`,
|
|
7
7
|
`Project: ${summary.projectName}`,
|
|
8
8
|
`Preview URL: ${summary.previewOrigin}`,
|
|
9
|
+
`MCP ready for: ${summary.mcpAgents.join(", ")}`,
|
|
9
10
|
`Run: cd "${summary.projectDir}" && npm run dev`,
|
|
10
11
|
].join("\n");
|
|
11
12
|
const cliProgram = pipe(readCliOptions, Effect.flatMap((options) => options.token.length > 0
|
|
@@ -4,6 +4,7 @@ export const DEFAULT_CLAIM_PATH = "/v1/bootstrap/claim";
|
|
|
4
4
|
export const DEFAULT_TEMPLATE_REPO = "https://github.com/SpawnDock/tma-project.git";
|
|
5
5
|
export const DEFAULT_TEMPLATE_BRANCH = "master";
|
|
6
6
|
export const TEMPLATE_ID = "nextjs-template";
|
|
7
|
+
export const DEFAULT_MCP_AGENTS = ["OpenCode", "Claude Code"];
|
|
7
8
|
export const normalizeDisplayName = (value) => value
|
|
8
9
|
.split(/[^a-zA-Z0-9]+/g)
|
|
9
10
|
.filter(Boolean)
|
|
@@ -30,6 +31,17 @@ export const buildMcpServerUrl = (controlPlaneUrl) => {
|
|
|
30
31
|
url.pathname = normalizedPath.length > 0 ? `${normalizedPath}/mcp/sse` : "/mcp/sse";
|
|
31
32
|
return url.toString();
|
|
32
33
|
};
|
|
34
|
+
export const buildCodexMcpCommandArgs = (mcpServerUrl) => [
|
|
35
|
+
"mcp",
|
|
36
|
+
"add",
|
|
37
|
+
"spawndock",
|
|
38
|
+
"--env",
|
|
39
|
+
`MCP_SERVER_URL=${mcpServerUrl}`,
|
|
40
|
+
"--",
|
|
41
|
+
"npx",
|
|
42
|
+
"-y",
|
|
43
|
+
"@spawn-dock/mcp",
|
|
44
|
+
];
|
|
33
45
|
export const buildTonConnectManifest = (context, claim) => `${JSON.stringify({
|
|
34
46
|
url: claim.previewOrigin,
|
|
35
47
|
name: context.projectName,
|
|
@@ -64,19 +76,6 @@ export const buildGeneratedFiles = (context, claim) => {
|
|
|
64
76
|
SPAWNDOCK_PROJECT_SLUG: claim.projectSlug,
|
|
65
77
|
SPAWNDOCK_ALLOWED_DEV_ORIGINS: claim.previewOrigin,
|
|
66
78
|
};
|
|
67
|
-
const opencode = {
|
|
68
|
-
$schema: "https://opencode.ai/config.json",
|
|
69
|
-
mcp: {
|
|
70
|
-
spawndock: {
|
|
71
|
-
type: "local",
|
|
72
|
-
command: ["npx", "-y", "@spawn-dock/mcp"],
|
|
73
|
-
enabled: true,
|
|
74
|
-
environment: {
|
|
75
|
-
MCP_SERVER_URL: mcpServerUrl,
|
|
76
|
-
},
|
|
77
|
-
},
|
|
78
|
-
},
|
|
79
|
-
};
|
|
80
79
|
return [
|
|
81
80
|
{
|
|
82
81
|
path: "spawndock.config.json",
|
|
@@ -97,10 +96,6 @@ export const buildGeneratedFiles = (context, claim) => {
|
|
|
97
96
|
port: claim.localPort,
|
|
98
97
|
}, null, 2)}\n`,
|
|
99
98
|
},
|
|
100
|
-
{
|
|
101
|
-
path: "opencode.json",
|
|
102
|
-
content: `${JSON.stringify(opencode, null, 2)}\n`,
|
|
103
|
-
},
|
|
104
99
|
{
|
|
105
100
|
path: "public/tonconnect-manifest.json",
|
|
106
101
|
content: buildTonConnectManifest(context, claim),
|
|
@@ -118,6 +113,7 @@ export const patchPackageJsonContent = (input) => {
|
|
|
118
113
|
packageJson.devDependencies = {
|
|
119
114
|
...(packageJson.devDependencies ?? {}),
|
|
120
115
|
"@spawn-dock/dev-tunnel": "latest",
|
|
116
|
+
"@spawn-dock/mcp": "latest",
|
|
121
117
|
};
|
|
122
118
|
return `${JSON.stringify(packageJson, null, 2)}\n`;
|
|
123
119
|
};
|
|
@@ -3,7 +3,7 @@ import { spawnSync } from "node:child_process";
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { Effect } from "effect";
|
|
6
|
-
import { buildGeneratedFiles, patchPackageJsonContent, resolveProjectContext, } from "../core/bootstrap.js";
|
|
6
|
+
import { buildCodexMcpCommandArgs, buildGeneratedFiles, buildMcpServerUrl, DEFAULT_MCP_AGENTS, patchPackageJsonContent, resolveProjectContext, } from "../core/bootstrap.js";
|
|
7
7
|
const TEMPLATE_OVERLAY_DIR = resolve(fileURLToPath(new URL("../../../template-nextjs-overlay", import.meta.url)));
|
|
8
8
|
export const bootstrapProject = (options) => Effect.gen(function* () {
|
|
9
9
|
if (options.token.length === 0) {
|
|
@@ -24,10 +24,12 @@ export const bootstrapProject = (options) => Effect.gen(function* () {
|
|
|
24
24
|
});
|
|
25
25
|
yield* writeGeneratedFilesToProject(projectDir, context, claim);
|
|
26
26
|
yield* installDependencies(projectDir);
|
|
27
|
+
const mcpAgents = yield* registerAgentIntegrations(projectDir, claim);
|
|
27
28
|
return {
|
|
28
29
|
projectDir,
|
|
29
30
|
projectName: context.projectName,
|
|
30
31
|
previewOrigin: claim.previewOrigin,
|
|
32
|
+
mcpAgents,
|
|
31
33
|
};
|
|
32
34
|
});
|
|
33
35
|
const ensureEmptyProjectDir = (projectDir) => Effect.try({
|
|
@@ -120,6 +122,23 @@ const installDependencies = (projectDir) => Effect.gen(function* () {
|
|
|
120
122
|
}
|
|
121
123
|
yield* runCommand("pnpm", ["install"], projectDir);
|
|
122
124
|
});
|
|
125
|
+
const registerAgentIntegrations = (projectDir, claim) => Effect.gen(function* () {
|
|
126
|
+
const integrations = [...DEFAULT_MCP_AGENTS];
|
|
127
|
+
const mcpServerUrl = buildMcpServerUrl(claim.controlPlaneUrl);
|
|
128
|
+
const codexRegistered = yield* registerCodexIntegration(projectDir, mcpServerUrl);
|
|
129
|
+
if (codexRegistered) {
|
|
130
|
+
integrations.push("Codex");
|
|
131
|
+
}
|
|
132
|
+
return integrations;
|
|
133
|
+
});
|
|
134
|
+
const registerCodexIntegration = (projectDir, mcpServerUrl) => Effect.gen(function* () {
|
|
135
|
+
const codexAvailable = yield* commandExists("codex");
|
|
136
|
+
if (!codexAvailable) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
const result = yield* runCommand("codex", buildCodexMcpCommandArgs(mcpServerUrl), projectDir, false);
|
|
140
|
+
return result.status === 0;
|
|
141
|
+
});
|
|
123
142
|
const runCommand = (command, args, cwd = process.cwd(), failOnNonZero = true) => Effect.try({
|
|
124
143
|
try: () => {
|
|
125
144
|
const result = spawnSync(command, [...args], {
|
|
@@ -136,6 +155,23 @@ const runCommand = (command, args, cwd = process.cwd(), failOnNonZero = true) =>
|
|
|
136
155
|
},
|
|
137
156
|
catch: toError,
|
|
138
157
|
});
|
|
158
|
+
const commandExists = (command) => Effect.try({
|
|
159
|
+
try: () => {
|
|
160
|
+
const result = spawnSync(command, ["--help"], {
|
|
161
|
+
cwd: process.cwd(),
|
|
162
|
+
encoding: "utf8",
|
|
163
|
+
stdio: "ignore",
|
|
164
|
+
});
|
|
165
|
+
if (result.error) {
|
|
166
|
+
if (isNodeError(result.error) && result.error.code === "ENOENT") {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
throw toError(result.error);
|
|
170
|
+
}
|
|
171
|
+
return true;
|
|
172
|
+
},
|
|
173
|
+
catch: toError,
|
|
174
|
+
});
|
|
139
175
|
const parseClaimResponse = (input, fallbackProjectSlug, fallbackControlPlaneUrl, fallbackLocalPort) => {
|
|
140
176
|
if (!isRecord(input)) {
|
|
141
177
|
return null;
|
|
@@ -12,3 +12,18 @@ export const resolvePreviewOrigin = (config) =>
|
|
|
12
12
|
|
|
13
13
|
export const resolveAllowedDevOrigins = (config) =>
|
|
14
14
|
[config.previewOrigin].filter(Boolean)
|
|
15
|
+
|
|
16
|
+
export const resolveMcpServerUrl = (config) => {
|
|
17
|
+
if (typeof config.mcpServerUrl === "string" && config.mcpServerUrl.length > 0) {
|
|
18
|
+
return config.mcpServerUrl
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (typeof config.controlPlaneUrl === "string" && config.controlPlaneUrl.length > 0) {
|
|
22
|
+
const url = new URL(config.controlPlaneUrl)
|
|
23
|
+
const normalizedPath = url.pathname.replace(/\/$/, "")
|
|
24
|
+
url.pathname = normalizedPath.length > 0 ? `${normalizedPath}/mcp/sse` : "/mcp/sse"
|
|
25
|
+
return url.toString()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
throw new Error("Missing mcpServerUrl or controlPlaneUrl in spawndock.config.json")
|
|
29
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { spawn } from "node:child_process"
|
|
2
|
+
|
|
3
|
+
import { readSpawndockConfig, resolveMcpServerUrl } from "./config.mjs"
|
|
4
|
+
|
|
5
|
+
const config = readSpawndockConfig()
|
|
6
|
+
const mcpServerUrl = process.env.MCP_SERVER_URL ?? resolveMcpServerUrl(config)
|
|
7
|
+
|
|
8
|
+
const child = spawn("pnpm", ["exec", "spawn-dock-mcp"], {
|
|
9
|
+
cwd: process.cwd(),
|
|
10
|
+
env: {
|
|
11
|
+
...process.env,
|
|
12
|
+
MCP_SERVER_URL: mcpServerUrl,
|
|
13
|
+
},
|
|
14
|
+
stdio: "inherit",
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
child.on("exit", (code) => {
|
|
18
|
+
process.exit(typeof code === "number" ? code : 1)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
child.on("error", (error) => {
|
|
22
|
+
console.error(error)
|
|
23
|
+
process.exit(1)
|
|
24
|
+
})
|