create-nevex-agent 0.1.2 → 0.2.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/README.md +12 -0
- package/bin/create-nevex-agent.mjs +164 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,6 +6,18 @@ Genera un proyecto independiente a partir del template de Nevex:
|
|
|
6
6
|
npx create-nevex-agent coagua --display-name "Coagua"
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
+
Por defecto crea la instalación en modo Cloudflare (Worker + D1 + KV). Para
|
|
10
|
+
VPS/Node:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npx create-nevex-agent coagua --mode node
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
El generador deja solo el modo elegido: borra los adapters, migraciones, tests y
|
|
17
|
+
docs de deploy del otro (ADR-0005) y lo registra en `TEMPLATE.md`. Las
|
|
18
|
+
dependencias del `pnpm-lock.yaml` se conservan completas para no romper
|
|
19
|
+
`--frozen-lockfile`; el código que no corre ya no está.
|
|
20
|
+
|
|
9
21
|
El CLI se publica en npm. El repositorio template que clona permanece privado
|
|
10
22
|
y requiere acceso SSH a `nevex-labs/nevex-agent-template`.
|
|
11
23
|
|
|
@@ -25,6 +25,8 @@ if (!projectName || args.help) {
|
|
|
25
25
|
|
|
26
26
|
const slug = normalizeSlug(projectName);
|
|
27
27
|
|
|
28
|
+
const mode = normalizeMode(args.mode);
|
|
29
|
+
|
|
28
30
|
const target = resolve(args.output ?? projectName);
|
|
29
31
|
|
|
30
32
|
assertTargetIsSafe(target);
|
|
@@ -45,6 +47,7 @@ cpSync(source, target, {
|
|
|
45
47
|
|
|
46
48
|
customizeProject(target, {
|
|
47
49
|
slug,
|
|
50
|
+
mode,
|
|
48
51
|
displayName: args["display-name"] ?? titleCase(slug),
|
|
49
52
|
templateRepo:
|
|
50
53
|
args["template-path"] ?? args["template-repo"] ?? process.env.NEVEX_AGENT_TEMPLATE_REPO ?? null,
|
|
@@ -89,33 +92,83 @@ function readTemplateCommit(directory) {
|
|
|
89
92
|
}
|
|
90
93
|
|
|
91
94
|
function customizeProject(directory, values) {
|
|
95
|
+
applyMode(directory, values.mode);
|
|
96
|
+
|
|
92
97
|
const packagePath = join(directory, "package.json");
|
|
93
98
|
const packageJson = JSON.parse(readFileSync(packagePath, "utf8"));
|
|
94
99
|
packageJson.name = values.slug;
|
|
95
100
|
packageJson.private = true;
|
|
101
|
+
// The CLI smoke lives in the template only; installations must not keep
|
|
102
|
+
// a script pointing at the package the generator just excluded.
|
|
103
|
+
delete packageJson.scripts?.["test:cli"];
|
|
104
|
+
|
|
105
|
+
if (values.mode === "cloudflare") {
|
|
106
|
+
delete packageJson.scripts?.["build:node"];
|
|
107
|
+
delete packageJson.scripts?.start;
|
|
108
|
+
} else {
|
|
109
|
+
delete packageJson.scripts?.deploy;
|
|
110
|
+
delete packageJson.scripts?.["cf-typegen"];
|
|
111
|
+
packageJson.scripts.dev = "DEPLOY_TARGET=node dotenv -e .env.local -- vite dev";
|
|
112
|
+
packageJson.scripts.build = "DEPLOY_TARGET=node vite build";
|
|
113
|
+
packageJson.scripts.preview = "DEPLOY_TARGET=node vite preview";
|
|
114
|
+
}
|
|
115
|
+
|
|
96
116
|
writeFileSync(packagePath, `${JSON.stringify(packageJson, null, 2)}\n`);
|
|
97
117
|
|
|
118
|
+
replaceInFile(join(directory, ".github/workflows/ci.yml"), /^ - run: pnpm test:cli\n/m, "");
|
|
119
|
+
|
|
120
|
+
replaceInFile(join(directory, "pnpm-workspace.yaml"), /^\npackages:\n - packages\/\*\n/m, "\n");
|
|
121
|
+
|
|
122
|
+
for (const file of ["README.md", "docs/workflow.md", "docs/README.md"]) {
|
|
123
|
+
stripTemplateOnly(join(directory, file));
|
|
124
|
+
}
|
|
125
|
+
|
|
98
126
|
replaceInFile(
|
|
99
127
|
join(directory, "src/lib/config.ts"),
|
|
100
128
|
/^export const APP_NAME = .*;$/m,
|
|
101
129
|
`export const APP_NAME = ${JSON.stringify(values.displayName)};`,
|
|
102
130
|
);
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
131
|
+
|
|
132
|
+
if (values.mode === "cloudflare") {
|
|
133
|
+
replaceInFile(
|
|
134
|
+
join(directory, "wrangler.jsonc"),
|
|
135
|
+
/"name": "[^"]*"/,
|
|
136
|
+
`"name": ${JSON.stringify(values.slug)}`,
|
|
137
|
+
);
|
|
138
|
+
replaceInFile(
|
|
139
|
+
join(directory, "wrangler.jsonc"),
|
|
140
|
+
/"database_name": "[^"]*"/,
|
|
141
|
+
`"database_name": ${JSON.stringify(values.slug)}`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
113
145
|
replaceInFile(
|
|
114
146
|
join(directory, "README.md"),
|
|
115
147
|
/^# Nevex Agent Template/m,
|
|
116
148
|
`# ${values.displayName}`,
|
|
117
149
|
);
|
|
118
150
|
|
|
151
|
+
replaceInFile(
|
|
152
|
+
join(directory, "AGENTS.md"),
|
|
153
|
+
/^This repo is a template\..*$/m,
|
|
154
|
+
"This repo is a Nevex agent installation: its own repo, database, secrets, and WhatsApp number.",
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
if (values.mode === "node") {
|
|
158
|
+
replaceInFile(
|
|
159
|
+
join(directory, "AGENTS.md"),
|
|
160
|
+
/## Cloudflare resources[\s\S]*$/m,
|
|
161
|
+
`## Node resources
|
|
162
|
+
|
|
163
|
+
- **Host**: VPS or container, Node 22+.
|
|
164
|
+
- **Postgres**: database owned by the installation.
|
|
165
|
+
- **Redis**: claims and pauses with TTL.
|
|
166
|
+
- **Cron triggers**: systemd timer or the host scheduler; Cloudflare \`scheduled\` does not exist in this mode.
|
|
167
|
+
- **Backups**: owned by the host; document the policy once provisioned.
|
|
168
|
+
`,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
119
172
|
const lines = [
|
|
120
173
|
"# Template origin",
|
|
121
174
|
"",
|
|
@@ -125,10 +178,92 @@ function customizeProject(directory, values) {
|
|
|
125
178
|
if (values.templateRef) lines.push(`- Ref: ${values.templateRef}`);
|
|
126
179
|
|
|
127
180
|
if (values.templateCommit) lines.push(`- Commit: ${values.templateCommit}`);
|
|
128
|
-
lines.push(
|
|
181
|
+
lines.push(
|
|
182
|
+
`- Mode: ${values.mode}`,
|
|
183
|
+
`- Infrastructure: ${heldInfrastructure(values.mode)}`,
|
|
184
|
+
`- Generated: ${new Date().toISOString().slice(0, 10)}`,
|
|
185
|
+
"",
|
|
186
|
+
);
|
|
129
187
|
writeFileSync(join(directory, "TEMPLATE.md"), `${lines.join("\n")}`);
|
|
130
188
|
}
|
|
131
189
|
|
|
190
|
+
function heldInfrastructure(mode) {
|
|
191
|
+
return mode === "cloudflare" ? "Cloudflare Worker + D1 + KV" : "Node + Postgres + Redis";
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function normalizeMode(value) {
|
|
195
|
+
if (value === undefined) return "cloudflare";
|
|
196
|
+
|
|
197
|
+
if (value !== "cloudflare" && value !== "node") {
|
|
198
|
+
throw new Error(`Modo inválido: ${value}. Usá "cloudflare" o "node".`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return value;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function modeDrop(mode) {
|
|
205
|
+
return {
|
|
206
|
+
cloudflare: [
|
|
207
|
+
"compose.yaml",
|
|
208
|
+
"docs/node-deploy.md",
|
|
209
|
+
"migrations/postgres",
|
|
210
|
+
"src/adapters/storage/postgres-conversations.ts",
|
|
211
|
+
"src/adapters/storage/postgres-conversations.test.ts",
|
|
212
|
+
"src/adapters/storage/postgres-conversations.integration.test.ts",
|
|
213
|
+
"src/adapters/storage/redis.ts",
|
|
214
|
+
"src/adapters/storage/redis.test.ts",
|
|
215
|
+
"src/infrastructure/migrations.postgres.test.ts",
|
|
216
|
+
"src/infrastructure/node",
|
|
217
|
+
"src/infrastructure/runtime.node.ts",
|
|
218
|
+
"src/infrastructure/runtime.node.test.ts",
|
|
219
|
+
],
|
|
220
|
+
node: [
|
|
221
|
+
"docs/cloudflare-deploy.md",
|
|
222
|
+
"wrangler.jsonc",
|
|
223
|
+
"worker-configuration.d.ts",
|
|
224
|
+
"src/adapters/storage/cloudflare-kv.test.ts",
|
|
225
|
+
"src/adapters/storage/cloudflare-kv.ts",
|
|
226
|
+
"src/adapters/storage/d1.ts",
|
|
227
|
+
"src/adapters/storage/d1-conversations.test.ts",
|
|
228
|
+
"src/adapters/storage/d1-conversations.ts",
|
|
229
|
+
"src/infrastructure/cloudflare",
|
|
230
|
+
"src/infrastructure/migrations.d1.test.ts",
|
|
231
|
+
"src/infrastructure/runtime.resolution.test.ts",
|
|
232
|
+
"src/lib/env.test.ts",
|
|
233
|
+
"src/test/helpers/local-d1.ts",
|
|
234
|
+
],
|
|
235
|
+
}[mode];
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function applyMode(directory, mode) {
|
|
239
|
+
for (const relative of modeDrop(mode)) {
|
|
240
|
+
rmSync(join(directory, relative), { recursive: true, force: true });
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (mode !== "node") return;
|
|
244
|
+
|
|
245
|
+
for (const file of readdirSync(join(directory, "migrations"))) {
|
|
246
|
+
if (file.endsWith(".sql")) rmSync(join(directory, "migrations", file), { force: true });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
writeFileSync(
|
|
250
|
+
join(directory, "src/infrastructure/runtime.ts"),
|
|
251
|
+
'export * from "./runtime.node";\n',
|
|
252
|
+
);
|
|
253
|
+
writeFileSync(
|
|
254
|
+
join(directory, "src/server.ts"),
|
|
255
|
+
`import handler from "@tanstack/react-start/server-entry";
|
|
256
|
+
import { withSecurityHeaders } from "#/lib/security-headers";
|
|
257
|
+
|
|
258
|
+
export default {
|
|
259
|
+
async fetch(request: Request) {
|
|
260
|
+
return withSecurityHeaders(request, await handler.fetch(request));
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
`,
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
132
267
|
function replaceInFile(path, pattern, replacement) {
|
|
133
268
|
const content = readFileSync(path, "utf8");
|
|
134
269
|
|
|
@@ -139,6 +274,23 @@ function replaceInFile(path, pattern, replacement) {
|
|
|
139
274
|
writeFileSync(path, content.replace(pattern, replacement));
|
|
140
275
|
}
|
|
141
276
|
|
|
277
|
+
// Sections wrapped in template-only markers describe the generator itself.
|
|
278
|
+
// They make no sense in an installation, so the generator cuts them out.
|
|
279
|
+
function stripTemplateOnly(path) {
|
|
280
|
+
const content = readFileSync(path, "utf8");
|
|
281
|
+
|
|
282
|
+
const stripped = content.replace(
|
|
283
|
+
/<!-- template-only:start -->[\s\S]*?<!-- template-only:end -->\n?/g,
|
|
284
|
+
"",
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
if (stripped.includes("template-only:")) {
|
|
288
|
+
throw new Error(`Bloque template-only sin cerrar en ${path}.`);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
writeFileSync(path, stripped);
|
|
292
|
+
}
|
|
293
|
+
|
|
142
294
|
function shouldSkip(entry, source) {
|
|
143
295
|
const relative = entry.slice(source.length + 1);
|
|
144
296
|
|
|
@@ -199,6 +351,7 @@ function printHelp() {
|
|
|
199
351
|
|
|
200
352
|
Opciones:
|
|
201
353
|
--display-name <nombre> Nombre visible del agente
|
|
354
|
+
--mode <cloudflare|node> Infraestructura de la instalación (default: cloudflare)
|
|
202
355
|
--output <directorio> Directorio de destino
|
|
203
356
|
--template-path <path> Usar un template local, útil para desarrollo
|
|
204
357
|
--template-repo <repo> URL o ruta del repositorio template
|