@clubedge/create-clubedge-app 0.36.0 → 0.37.0
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/CHANGELOG.md +5 -0
- package/README.md +58 -2
- package/bin/mcp.mjs +2 -0
- package/dist/mcp-server.js +253 -0
- package/dist/steps/git.js +4 -4
- package/dist/steps/install.js +2 -2
- package/dist/utils/process.js +2 -2
- package/package.json +12 -5
- package/skills/clubedge-starter/SKILL.md +56 -0
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -27,6 +27,61 @@ Add, remove, or upgrade them later without losing your own changes.
|
|
|
27
27
|
pnpm dlx @clubedge/create-clubedge-app my-app
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
+
## MCP server
|
|
31
|
+
|
|
32
|
+
Coding agents can connect to a local MCP server to inspect the capabilities of this CLI's pinned Starter release, validate stack and module choices, preview a complete scaffold plan, and generate a project in the server's working directory. The server exposes `get_starter_capabilities`, `validate_project_config`, `plan_project`, and `create_project`. Planning and validation do not write files. Project creation does not initialize Git or install dependencies unless those options are requested.
|
|
33
|
+
|
|
34
|
+
After publishing a release that includes the MCP server, run it from the workspace where projects should be created:
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
npx -y --package @clubedge/create-clubedge-app create-clubedge-app-mcp
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Configure an MCP client to launch `npx -y --package @clubedge/create-clubedge-app create-clubedge-app-mcp` with its working directory set to the workspace. In Codex, add this to `~/.codex/config.toml` and replace the example with the absolute path of the workspace where projects should be created:
|
|
41
|
+
|
|
42
|
+
```toml
|
|
43
|
+
[mcp_servers.clubedge-starter]
|
|
44
|
+
command = "npx"
|
|
45
|
+
args = ["--yes", "--package", "@clubedge/create-clubedge-app", "create-clubedge-app-mcp"]
|
|
46
|
+
cwd = "D:/path/to/workspace"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The MCP server is included in the same package and uses the CLI's bundled Starter and compatibility rules. Its create tool only writes beneath the configured working directory and refuses a non-empty destination.
|
|
50
|
+
|
|
51
|
+
### Agent skill
|
|
52
|
+
|
|
53
|
+
The npm package includes the Agent Skill at [`skills/clubedge-starter/SKILL.md`](skills/clubedge-starter/SKILL.md). After the release is published, download and extract the package in PowerShell:
|
|
54
|
+
|
|
55
|
+
```powershell
|
|
56
|
+
$skillTarball = npm pack @clubedge/create-clubedge-app@latest
|
|
57
|
+
tar -xzf $skillTarball
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Then install it for the current user:
|
|
61
|
+
|
|
62
|
+
```powershell
|
|
63
|
+
# Codex
|
|
64
|
+
New-Item -ItemType Directory -Force "$HOME/.agents/skills/clubedge-starter" | Out-Null
|
|
65
|
+
Copy-Item .\package\skills\clubedge-starter\SKILL.md "$HOME/.agents/skills/clubedge-starter/SKILL.md"
|
|
66
|
+
|
|
67
|
+
# Claude Code
|
|
68
|
+
New-Item -ItemType Directory -Force "$HOME/.claude/skills/clubedge-starter" | Out-Null
|
|
69
|
+
Copy-Item .\package\skills\clubedge-starter\SKILL.md "$HOME/.claude/skills/clubedge-starter/SKILL.md"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
These commands create `package/` in the current directory. Remove that extracted folder and tarball when the copy is done. Before publication, the same copy commands work from the repository root by replacing `package/skills/...` with `skills/...`.
|
|
73
|
+
|
|
74
|
+
For a repository-only install, use `.agents/skills/clubedge-starter/` for Codex or `.claude/skills/clubedge-starter/` for Claude Code instead of the `$HOME` paths. Restart the agent session after installing. In Codex, ask it to `Use the clubedge-starter skill to inspect the available stacks and modules. Do not create a project.` In Claude Code, invoke `/clubedge-starter` and give it the same request. The skill uses live MCP capabilities when available, so its choices follow the Starter release installed with the server.
|
|
75
|
+
|
|
76
|
+
To connect the MCP server in Claude Code, run this from the workspace where projects should be created:
|
|
77
|
+
|
|
78
|
+
```sh
|
|
79
|
+
claude mcp add --scope local --transport stdio clubedge-starter -- npx -y --package @clubedge/create-clubedge-app create-clubedge-app-mcp
|
|
80
|
+
claude mcp get clubedge-starter
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Then open Claude Code in that workspace, invoke `/clubedge-starter`, and ask it to inspect the available stacks and modules without creating a project. In Codex, use the `cwd` in `~/.codex/config.toml` above, restart Codex, and ask the same prompt. To exercise project creation, ask the agent to create a named project in a new child folder with Git and dependency installation disabled; verify the folder and report before enabling those optional actions.
|
|
84
|
+
|
|
30
85
|
## Why create-clubedge-app
|
|
31
86
|
|
|
32
87
|
Copying a starter is easy; living with it is not. You delete the services you don't use by hand, and once you've changed the code, every later improvement to the starter has to be copied over by hand too.
|
|
@@ -282,7 +337,7 @@ Projects created with `--infra neon` also have `.github/workflows/preview.yml`,
|
|
|
282
337
|
Each CLI release is tested against one specific Starter release:
|
|
283
338
|
|
|
284
339
|
```text
|
|
285
|
-
create-clubedge-app v0.
|
|
340
|
+
create-clubedge-app v0.37.0
|
|
286
341
|
|
|
|
287
342
|
v
|
|
288
343
|
Clubedge Starter v0.24.1
|
|
@@ -293,7 +348,7 @@ The current relationship is:
|
|
|
293
348
|
|
|
294
349
|
| CLI release | Default Starter release | Meaning |
|
|
295
350
|
| --- | --- | --- |
|
|
296
|
-
| `create-clubedge-app@0.
|
|
351
|
+
| `create-clubedge-app@0.37.0` | `clubedge-starter@v0.24.1` | The CLI's required, tested default |
|
|
297
352
|
|
|
298
353
|
This means the same CLI version always scaffolds the same Starter revision by default. The CLI does not silently follow Starter `main`.
|
|
299
354
|
|
|
@@ -404,6 +459,7 @@ Each phase ends with a Starter release and a CLI release pinned to it. The full
|
|
|
404
459
|
| 0.34.0 | v0.23.0 | Starter v0.23.0: a FastAPI API in Python (`--framework vite --api fastapi`), generated with no CLI change |
|
|
405
460
|
| 0.35.0 | v0.24.0 | Deploy frontend and API stack packs; Default to Starter v0.24.0 |
|
|
406
461
|
| 0.36.0 | v0.24.1 | Bump vitest from 5.0.3 to 5.0.5 in the minor-and-patch group; Bump giget from 2.0.0 to 3.3.1; Bump @types/node from 22.20.5 to 26.6.5; Default to Starter v0.24.1 |
|
|
462
|
+
| 0.37.0 | v0.24.1 | Add MCP server and agent skill |
|
|
407
463
|
| next | | Show the Starter's version of a conflicted file next to yours |
|
|
408
464
|
|
|
409
465
|
## Community
|
package/bin/mcp.mjs
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { realpath } from "node:fs/promises";
|
|
2
|
+
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/server";
|
|
5
|
+
import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
6
|
+
import { z } from "zod/v4";
|
|
7
|
+
import { executePlan } from "./core/execute.js";
|
|
8
|
+
import { readTemplateManifest } from "./core/manifest.js";
|
|
9
|
+
import { createPlan, describePlan } from "./core/plan.js";
|
|
10
|
+
import { resolveTemplateSource } from "./core/source.js";
|
|
11
|
+
import { cliVersion, cliUrl, siteUrl } from "./package-info.js";
|
|
12
|
+
import { initializeGit } from "./steps/git.js";
|
|
13
|
+
import { installDependencies, resolvePnpm } from "./steps/install.js";
|
|
14
|
+
import { commandSucceeds, run } from "./utils/process.js";
|
|
15
|
+
import { displayNameFromPackageName, packageNameFromDirectory } from "./utils/names.js";
|
|
16
|
+
// Keep subprocess output off the MCP JSON-RPC stdout stream, and don't let children read MCP input.
|
|
17
|
+
const mcpStdio = ["ignore", process.stderr, process.stderr];
|
|
18
|
+
const selectionSchema = {
|
|
19
|
+
stacks: z.record(z.string(), z.string()).optional().describe("Stack id by slot, such as { framework: 'next', api: 'hono' }."),
|
|
20
|
+
modules: z.record(z.string(), z.string()).optional().describe("Module option by module id, such as { auth: 'supabase', storage: 's3' }."),
|
|
21
|
+
};
|
|
22
|
+
function isWithin(directory, candidate) {
|
|
23
|
+
const path = relative(directory, candidate);
|
|
24
|
+
return path === "" || (!isAbsolute(path) && path !== ".." && !path.startsWith(`..${sep}`));
|
|
25
|
+
}
|
|
26
|
+
async function workspaceDestination(directory) {
|
|
27
|
+
if (!directory.trim())
|
|
28
|
+
throw new Error("Choose a project directory inside the MCP server's working directory.");
|
|
29
|
+
if (isAbsolute(directory))
|
|
30
|
+
throw new Error("Use a relative project directory beneath the MCP server's working directory.");
|
|
31
|
+
const workspace = await realpath(process.cwd());
|
|
32
|
+
const target = resolve(workspace, directory);
|
|
33
|
+
if (!isWithin(workspace, target) || target === workspace) {
|
|
34
|
+
throw new Error("The project directory must be a child of the MCP server's working directory.");
|
|
35
|
+
}
|
|
36
|
+
// Resolve the nearest existing ancestor so a symlink inside the workspace cannot redirect
|
|
37
|
+
// project writes to an unrelated directory.
|
|
38
|
+
let ancestor = dirname(target);
|
|
39
|
+
let resolvedAncestor;
|
|
40
|
+
while (resolvedAncestor === undefined) {
|
|
41
|
+
try {
|
|
42
|
+
resolvedAncestor = await realpath(ancestor);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
if (!(error && typeof error === "object" && "code" in error && error.code === "ENOENT"))
|
|
46
|
+
throw error;
|
|
47
|
+
const parent = dirname(ancestor);
|
|
48
|
+
if (parent === ancestor)
|
|
49
|
+
throw error;
|
|
50
|
+
ancestor = parent;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (resolvedAncestor === undefined || !isWithin(workspace, resolvedAncestor)) {
|
|
54
|
+
throw new Error("The project directory resolves outside the MCP server's working directory.");
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const resolvedTarget = await realpath(target);
|
|
58
|
+
if (!isWithin(workspace, resolvedTarget)) {
|
|
59
|
+
throw new Error("The project directory resolves outside the MCP server's working directory.");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (!(error && typeof error === "object" && "code" in error && error.code === "ENOENT"))
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
return target;
|
|
67
|
+
}
|
|
68
|
+
async function withTemplate(fn) {
|
|
69
|
+
const source = await resolveTemplateSource({});
|
|
70
|
+
try {
|
|
71
|
+
return await fn(source);
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
await source.cleanup();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function planProject(source, options, directory, sideEffects = {}) {
|
|
78
|
+
const manifest = await readTemplateManifest(source.root);
|
|
79
|
+
return createPlan({
|
|
80
|
+
targetDirectory: directory,
|
|
81
|
+
identity: {
|
|
82
|
+
packageName: packageNameFromDirectory(directory),
|
|
83
|
+
displayName: displayNameFromPackageName(packageNameFromDirectory(directory)),
|
|
84
|
+
},
|
|
85
|
+
source,
|
|
86
|
+
manifest,
|
|
87
|
+
stacks: options.stacks,
|
|
88
|
+
modules: options.modules,
|
|
89
|
+
cliVersion,
|
|
90
|
+
git: sideEffects.git ?? false,
|
|
91
|
+
install: sideEffects.install ?? false,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
function capabilities(manifest, source) {
|
|
95
|
+
return {
|
|
96
|
+
cliVersion,
|
|
97
|
+
starter: {
|
|
98
|
+
repository: source.repository,
|
|
99
|
+
ref: source.ref,
|
|
100
|
+
commit: source.commit,
|
|
101
|
+
docs: cliUrl,
|
|
102
|
+
website: siteUrl,
|
|
103
|
+
},
|
|
104
|
+
slots: Object.entries(manifest.slots).map(([id, slot]) => ({
|
|
105
|
+
id,
|
|
106
|
+
name: slot.name,
|
|
107
|
+
default: slot.default ?? "none",
|
|
108
|
+
choices: Object.entries(manifest.stacks)
|
|
109
|
+
.filter(([, stack]) => stack.slot === id)
|
|
110
|
+
.map(([stackId, stack]) => ({ id: stackId, name: stack.name, role: stack.role, modules: stack.modules })),
|
|
111
|
+
})),
|
|
112
|
+
modules: Object.entries(manifest.modules).map(([id, module]) => ({
|
|
113
|
+
id,
|
|
114
|
+
name: module.name,
|
|
115
|
+
scope: module.scope,
|
|
116
|
+
default: module.default,
|
|
117
|
+
options: Object.entries(module.options).map(([optionId, option]) => ({
|
|
118
|
+
id: optionId,
|
|
119
|
+
name: option.name,
|
|
120
|
+
requires: option.requires,
|
|
121
|
+
})),
|
|
122
|
+
})),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function result(value) {
|
|
126
|
+
return { content: [{ type: "text", text: JSON.stringify(value, null, 2) }] };
|
|
127
|
+
}
|
|
128
|
+
function createServer() {
|
|
129
|
+
const server = new McpServer({ name: "clubedge-starter", version: cliVersion });
|
|
130
|
+
server.registerTool("get_starter_capabilities", {
|
|
131
|
+
description: "Discover the frameworks, API stacks, modules, defaults, and compatibility choices supported by this CLI's pinned Clubedge Starter release.",
|
|
132
|
+
inputSchema: z.object({}),
|
|
133
|
+
annotations: { readOnlyHint: true },
|
|
134
|
+
}, async () => withTemplate(async (source) => result(capabilities(await readTemplateManifest(source.root), source))));
|
|
135
|
+
server.registerTool("validate_project_config", {
|
|
136
|
+
description: "Check requested stack and module choices against the pinned Starter. This does not write files.",
|
|
137
|
+
inputSchema: z.object(selectionSchema),
|
|
138
|
+
annotations: { readOnlyHint: true },
|
|
139
|
+
}, async (options) => withTemplate(async (source) => {
|
|
140
|
+
try {
|
|
141
|
+
const plan = await planProject(source, options, resolve(process.cwd(), ".clubedge-validation"));
|
|
142
|
+
return result({
|
|
143
|
+
valid: true,
|
|
144
|
+
stacks: plan.apps.map(({ slot, id, name }) => ({ slot, id, name })),
|
|
145
|
+
modules: plan.modules.map(({ id, option, optionName }) => ({ id, option, name: optionName })),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
return result({ valid: false, error: error instanceof Error ? error.message : String(error) });
|
|
150
|
+
}
|
|
151
|
+
}));
|
|
152
|
+
server.registerTool("plan_project", {
|
|
153
|
+
description: "Preview a complete scaffold plan using the pinned Starter. Validates stack and module compatibility and lists planned files and setup actions without writing to disk.",
|
|
154
|
+
inputSchema: z.object({
|
|
155
|
+
directory: z.string().min(1).describe("Destination path relative to the MCP server's working directory."),
|
|
156
|
+
...selectionSchema,
|
|
157
|
+
initialize_git: z.boolean().default(false).describe("Include Git initialization in the plan."),
|
|
158
|
+
install_dependencies: z.boolean().default(false).describe("Include dependency installation in the plan."),
|
|
159
|
+
}),
|
|
160
|
+
annotations: { readOnlyHint: true },
|
|
161
|
+
}, async ({ directory, initialize_git, install_dependencies, ...options }) => withTemplate(async (source) => {
|
|
162
|
+
try {
|
|
163
|
+
const target = await workspaceDestination(directory);
|
|
164
|
+
const plan = await planProject(source, options, target, {
|
|
165
|
+
git: initialize_git,
|
|
166
|
+
install: install_dependencies,
|
|
167
|
+
});
|
|
168
|
+
return result({
|
|
169
|
+
valid: true,
|
|
170
|
+
directory: target,
|
|
171
|
+
destinationMustBeEmpty: true,
|
|
172
|
+
starterRef: plan.provenance.starterRef,
|
|
173
|
+
starterCommit: plan.provenance.starterCommit,
|
|
174
|
+
stacks: plan.apps.map(({ slot, id, name }) => ({ slot, id, name })),
|
|
175
|
+
modules: plan.modules.map(({ id, option, optionName }) => ({ id, option, name: optionName })),
|
|
176
|
+
fileCount: plan.files.length,
|
|
177
|
+
editCount: plan.edits.length,
|
|
178
|
+
environmentFiles: plan.envFiles.map(({ from, to }) => ({ source: from, destination: to })),
|
|
179
|
+
toolchains: plan.toolchains.map(({ directory: appDirectory, app, toolchain }) => ({
|
|
180
|
+
directory: appDirectory,
|
|
181
|
+
app,
|
|
182
|
+
name: toolchain.name,
|
|
183
|
+
install: toolchain.install,
|
|
184
|
+
})),
|
|
185
|
+
actions: describePlan(plan),
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
return result({ valid: false, error: error instanceof Error ? error.message : String(error) });
|
|
190
|
+
}
|
|
191
|
+
}));
|
|
192
|
+
server.registerTool("create_project", {
|
|
193
|
+
description: "Generate a Clubedge Starter project under the MCP server's working directory. The destination must be empty or absent. Git initialization and dependency installation are opt-in.",
|
|
194
|
+
inputSchema: z.object({
|
|
195
|
+
directory: z.string().min(1).describe("Destination path relative to the MCP server's working directory."),
|
|
196
|
+
...selectionSchema,
|
|
197
|
+
initialize_git: z.boolean().default(false).describe("Initialize a Git repository after generating files."),
|
|
198
|
+
install_dependencies: z.boolean().default(false).describe("Run pnpm install and declared secondary toolchain installs after generating files."),
|
|
199
|
+
}),
|
|
200
|
+
}, async ({ directory, initialize_git, install_dependencies, ...options }) => withTemplate(async (source) => {
|
|
201
|
+
const target = await workspaceDestination(directory);
|
|
202
|
+
const plan = await planProject(source, options, target, { git: initialize_git, install: install_dependencies });
|
|
203
|
+
await executePlan(plan, source);
|
|
204
|
+
const warnings = [];
|
|
205
|
+
if (initialize_git) {
|
|
206
|
+
try {
|
|
207
|
+
await initializeGit(target, mcpStdio);
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
warnings.push(`Project created, but Git initialization failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (install_dependencies) {
|
|
214
|
+
const pnpm = await resolvePnpm();
|
|
215
|
+
if (!pnpm) {
|
|
216
|
+
warnings.push("Project created, but pnpm was not found. Install pnpm and run it in the project directory.");
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
try {
|
|
220
|
+
await installDependencies(target, pnpm, [], mcpStdio);
|
|
221
|
+
for (const { directory: appDirectory, app, toolchain } of plan.toolchains) {
|
|
222
|
+
const [check, ...checkArgs] = toolchain.check;
|
|
223
|
+
if (!(await commandSucceeds(check, checkArgs))) {
|
|
224
|
+
warnings.push(`${toolchain.name} is required to install ${app} dependencies: ${toolchain.url}`);
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const [command, ...args] = toolchain.install;
|
|
228
|
+
try {
|
|
229
|
+
await run(command, args, resolve(target, appDirectory), mcpStdio);
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
warnings.push(`${app} dependencies did not install: ${error instanceof Error ? error.message : String(error)}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
warnings.push(`Project created, but dependency installation did not complete: ${error instanceof Error ? error.message : String(error)}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return result({
|
|
242
|
+
created: true,
|
|
243
|
+
directory: target,
|
|
244
|
+
starterRef: plan.provenance.starterRef,
|
|
245
|
+
stacks: plan.apps.map(({ slot, id, name }) => ({ slot, id, name })),
|
|
246
|
+
modules: plan.modules.map(({ id, option, optionName }) => ({ id, option, name: optionName })),
|
|
247
|
+
actions: describePlan(plan),
|
|
248
|
+
warnings,
|
|
249
|
+
});
|
|
250
|
+
}));
|
|
251
|
+
return server;
|
|
252
|
+
}
|
|
253
|
+
serveStdio(createServer);
|
package/dist/steps/git.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { run } from "../utils/process.js";
|
|
2
|
-
export async function initializeGit(directory) {
|
|
2
|
+
export async function initializeGit(directory, stdio) {
|
|
3
3
|
try {
|
|
4
|
-
await run("git", ["init", "-b", "main"], directory);
|
|
4
|
+
await run("git", ["init", "-b", "main"], directory, stdio);
|
|
5
5
|
}
|
|
6
6
|
catch {
|
|
7
7
|
// Git before 2.28 has no -b option.
|
|
8
|
-
await run("git", ["init"], directory);
|
|
9
|
-
await run("git", ["branch", "-M", "main"], directory);
|
|
8
|
+
await run("git", ["init"], directory, stdio);
|
|
9
|
+
await run("git", ["branch", "-M", "main"], directory, stdio);
|
|
10
10
|
}
|
|
11
11
|
}
|
package/dist/steps/install.js
CHANGED
|
@@ -13,7 +13,7 @@ export function detectLauncher(userAgent = process.env.npm_config_user_agent) {
|
|
|
13
13
|
const name = userAgent?.split("/")[0];
|
|
14
14
|
return name && ["npm", "pnpm", "yarn", "bun"].includes(name) ? name : null;
|
|
15
15
|
}
|
|
16
|
-
export async function installDependencies(directory, pnpm, args = []) {
|
|
16
|
+
export async function installDependencies(directory, pnpm, args = [], stdio) {
|
|
17
17
|
const [command, ...prefix] = pnpm;
|
|
18
|
-
await run(command, [...prefix, "install", ...args], directory);
|
|
18
|
+
await run(command, [...prefix, "install", ...args], directory, stdio);
|
|
19
19
|
}
|
package/dist/utils/process.js
CHANGED
|
@@ -16,9 +16,9 @@ export function spawnCommand(command, args, options, shell = useShell) {
|
|
|
16
16
|
return spawn([command, ...args].map(quoteForCmd).join(" "), { ...options, shell: true });
|
|
17
17
|
}
|
|
18
18
|
/** Runs a command with inherited output. Rejects on a non-zero exit. */
|
|
19
|
-
export async function run(command, args, cwd) {
|
|
19
|
+
export async function run(command, args, cwd, stdio = "inherit") {
|
|
20
20
|
await new Promise((resolve, reject) => {
|
|
21
|
-
const child = spawnCommand(command, args, { cwd, stdio
|
|
21
|
+
const child = spawnCommand(command, args, { cwd, stdio });
|
|
22
22
|
child.once("error", reject);
|
|
23
23
|
child.once("exit", (code, signal) => {
|
|
24
24
|
if (code === 0)
|
package/package.json
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clubedge/create-clubedge-app",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Create
|
|
3
|
+
"version": "0.37.0",
|
|
4
|
+
"description": "Create React apps from Clubedge Starter with a CLI or local MCP server.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@10.9.0",
|
|
7
7
|
"bin": {
|
|
8
|
-
"create-clubedge-app": "./bin/create.mjs"
|
|
8
|
+
"create-clubedge-app": "./bin/create.mjs",
|
|
9
|
+
"create-clubedge-app-mcp": "./bin/mcp.mjs"
|
|
9
10
|
},
|
|
10
11
|
"files": [
|
|
11
12
|
"bin/",
|
|
12
13
|
"dist/",
|
|
13
14
|
"template/",
|
|
14
15
|
"template.lock.json",
|
|
16
|
+
"skills/",
|
|
15
17
|
"README.md",
|
|
16
18
|
"CHANGELOG.md",
|
|
17
19
|
"LICENSE",
|
|
@@ -40,7 +42,10 @@
|
|
|
40
42
|
"nextjs",
|
|
41
43
|
"tanstack-start",
|
|
42
44
|
"starter",
|
|
43
|
-
"scaffolding"
|
|
45
|
+
"scaffolding",
|
|
46
|
+
"mcp",
|
|
47
|
+
"model-context-protocol",
|
|
48
|
+
"ai-agent"
|
|
44
49
|
],
|
|
45
50
|
"scripts": {
|
|
46
51
|
"build": "tsc -p tsconfig.build.json",
|
|
@@ -52,10 +57,12 @@
|
|
|
52
57
|
"release": "node scripts/release.mjs"
|
|
53
58
|
},
|
|
54
59
|
"dependencies": {
|
|
60
|
+
"@modelcontextprotocol/server": "^2.0.0",
|
|
55
61
|
"@clack/prompts": "^1.8.1",
|
|
56
62
|
"giget": "^3.3.1",
|
|
57
63
|
"node-diff3": "^3.2.1",
|
|
58
|
-
"ssh2": "^1.17.0"
|
|
64
|
+
"ssh2": "^1.17.0",
|
|
65
|
+
"zod": "^4.2.0"
|
|
59
66
|
},
|
|
60
67
|
"devDependencies": {
|
|
61
68
|
"@types/node": "^26.6.5",
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: clubedge-starter
|
|
3
|
+
description: Create and evolve applications based on Clubedge Starter with the create-clubedge-app CLI or MCP server. Use when asked to scaffold a Clubedge app, choose its framework or modules, validate a configuration, or change Starter modules in an existing generated project.
|
|
4
|
+
compatibility: Requires Node.js 22.12+ and pnpm 10.9+ to create projects. The MCP workflow requires the Clubedge Starter MCP server to be configured.
|
|
5
|
+
metadata:
|
|
6
|
+
author: Clubedge
|
|
7
|
+
repository: https://github.com/Clubedge/create-clubedge-app
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Clubedge Starter
|
|
11
|
+
|
|
12
|
+
Create and manage Clubedge Starter projects through the supported CLI or MCP tools. Follow the user's requested stack and services; do not add unrequested modules.
|
|
13
|
+
|
|
14
|
+
## Choose the interface
|
|
15
|
+
|
|
16
|
+
- If the `clubedge-starter` MCP server is available, call `get_starter_capabilities` first. Treat its response as authoritative for framework/API choices, module options, defaults, and requirements.
|
|
17
|
+
- Before scaffolding through MCP, call `plan_project` with the destination `directory` and requested `stacks` and `modules`. It validates compatibility and previews the files and setup actions without writing to disk. Resolve invalid combinations and any open choices before creating the project. Use `validate_project_config` when only a compatibility check is needed and no destination plan is useful.
|
|
18
|
+
- Use `create_project` to scaffold through MCP. Give `directory` as a relative path beneath the MCP server's working directory. Set `initialize_git` and `install_dependencies` to `true` only when appropriate for the user's request; both default to `false`.
|
|
19
|
+
- If MCP is unavailable, use the CLI. Prefer the installed project-local executable when present; otherwise use `pnpm dlx @clubedge/create-clubedge-app <directory> --yes` and provide the requested options as CLI flags.
|
|
20
|
+
- Use the CLI's `add`, `remove`, or `upgrade` commands for changes to an existing generated project. Do not reproduce those changes by manually copying Starter files.
|
|
21
|
+
|
|
22
|
+
## Scaffold a project
|
|
23
|
+
|
|
24
|
+
For a new app:
|
|
25
|
+
|
|
26
|
+
1. Identify the requested framework and API shape. Full-stack choices such as Next.js and TanStack Start do not use a separate API; Vite can be paired with Hono or FastAPI.
|
|
27
|
+
2. Choose only the requested modules. Check the live capabilities or CLI `--help` rather than relying on a hard-coded list, since each CLI release is pinned to a specific Starter release.
|
|
28
|
+
3. Validate that module requirements are met. For example, module compatibility can depend on the selected framework or another module.
|
|
29
|
+
4. Preview with the CLI's `--dry-run` when the requested changes or destination are unclear. For MCP, describe the exact target directory and options before creating if the user has not made them clear.
|
|
30
|
+
5. Create only in a new or empty directory. The MCP tool only accepts paths under its configured working directory. The CLI also refuses to overwrite an existing project.
|
|
31
|
+
6. Report the target path, selected stacks and modules, and any actions that still need to be run, such as dependency installation or starting local infrastructure.
|
|
32
|
+
|
|
33
|
+
CLI example:
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
pnpm dlx @clubedge/create-clubedge-app my-app --yes --framework next --auth better-auth --infra local
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Change an existing project
|
|
40
|
+
|
|
41
|
+
Run the CLI from the generated project directory:
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
pnpm clubedge add payments stripe
|
|
45
|
+
pnpm clubedge remove storage
|
|
46
|
+
pnpm clubedge upgrade
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Check the project's Git state first. These commands preserve user edits and can report merge conflicts that need review. After a change, follow the CLI's instructions and run relevant checks, migrations, or service setup for the modules involved.
|
|
50
|
+
|
|
51
|
+
## Project guidance
|
|
52
|
+
|
|
53
|
+
- Keep secrets in the project's environment file and never print or commit secret values.
|
|
54
|
+
- Do not claim a project was created, installed, migrated, or deployed until the corresponding action succeeded.
|
|
55
|
+
- If an install or provider setup fails, report the exact failed step and the next command needed to resume; do not silently present a partially configured project as ready.
|
|
56
|
+
- Consult the generated `SETUP.md` and the official docs at https://starter.clubedge.live/docs for provider-specific configuration.
|