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 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
- replaceInFile(
104
- join(directory, "wrangler.jsonc"),
105
- /"name": "[^"]*"/,
106
- `"name": ${JSON.stringify(values.slug)}`,
107
- );
108
- replaceInFile(
109
- join(directory, "wrangler.jsonc"),
110
- /"database_name": "[^"]*"/,
111
- `"database_name": ${JSON.stringify(values.slug)}`,
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(`- Generated: ${new Date().toISOString().slice(0, 10)}`, "");
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-nevex-agent",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "Create a custom Nevex WhatsApp agent project",
5
5
  "bin": {
6
6
  "create-nevex-agent": "bin/create-nevex-agent.mjs"