@poncho-ai/cli 0.36.9 → 0.38.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/.turbo/turbo-build.log +7 -7
- package/CHANGELOG.md +244 -0
- package/dist/{chunk-IZLDZWPA.js → chunk-U643TWFX.js} +6206 -4729
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +186 -128
- package/dist/index.js +111 -5
- package/dist/run-interactive-ink-CE7U47S5.js +679 -0
- package/package.json +4 -4
- package/src/cron-helpers.ts +174 -0
- package/src/http-utils.ts +220 -0
- package/src/index.ts +1019 -4736
- package/src/logger.ts +9 -0
- package/src/mcp-commands.ts +283 -0
- package/src/project-init.ts +150 -0
- package/src/run-commands.ts +145 -0
- package/src/scaffolding.ts +528 -0
- package/src/skills.ts +372 -0
- package/src/templates.ts +563 -0
- package/src/testing.ts +108 -0
- package/src/web-ui-client.ts +865 -94
- package/src/web-ui-styles.ts +278 -1
- package/src/web-ui.ts +24 -0
- package/test/cli.test.ts +52 -1
- package/dist/run-interactive-ink-KDWRD7FT.js +0 -2115
- package/test/run-orchestration.test.ts +0 -171
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, relative, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import {
|
|
5
|
+
parseAgentMarkdown,
|
|
6
|
+
loadPonchoConfig,
|
|
7
|
+
type CronJobConfig,
|
|
8
|
+
type PonchoConfig,
|
|
9
|
+
} from "@poncho-ai/harness";
|
|
10
|
+
import type { DeployTarget } from "./init-onboarding.js";
|
|
11
|
+
|
|
12
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
export const packageRoot = resolve(__dirname, "..");
|
|
14
|
+
|
|
15
|
+
export const ensureFile = async (path: string, content: string): Promise<void> => {
|
|
16
|
+
await mkdir(dirname(path), { recursive: true });
|
|
17
|
+
await writeFile(path, content, { encoding: "utf8", flag: "wx" });
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type DeployScaffoldTarget = Exclude<DeployTarget, "none">;
|
|
21
|
+
|
|
22
|
+
export const normalizeDeployTarget = (target: string): DeployScaffoldTarget => {
|
|
23
|
+
const normalized = target.toLowerCase();
|
|
24
|
+
if (
|
|
25
|
+
normalized === "vercel" ||
|
|
26
|
+
normalized === "docker" ||
|
|
27
|
+
normalized === "lambda" ||
|
|
28
|
+
normalized === "fly"
|
|
29
|
+
) {
|
|
30
|
+
return normalized;
|
|
31
|
+
}
|
|
32
|
+
throw new Error(`Unsupported build target: ${target}`);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const readCliVersion = async (): Promise<string> => {
|
|
36
|
+
const fallback = "0.1.0";
|
|
37
|
+
try {
|
|
38
|
+
const packageJsonPath = resolve(packageRoot, "package.json");
|
|
39
|
+
const content = await readFile(packageJsonPath, "utf8");
|
|
40
|
+
const parsed = JSON.parse(content) as { version?: unknown };
|
|
41
|
+
if (typeof parsed.version === "string" && parsed.version.trim().length > 0) {
|
|
42
|
+
return parsed.version;
|
|
43
|
+
}
|
|
44
|
+
} catch {
|
|
45
|
+
// Use fallback when package metadata cannot be read.
|
|
46
|
+
}
|
|
47
|
+
return fallback;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const readCliDependencyVersion = async (
|
|
51
|
+
dependencyName: string,
|
|
52
|
+
fallback: string,
|
|
53
|
+
): Promise<string> => {
|
|
54
|
+
try {
|
|
55
|
+
const packageJsonPath = resolve(packageRoot, "package.json");
|
|
56
|
+
const content = await readFile(packageJsonPath, "utf8");
|
|
57
|
+
const parsed = JSON.parse(content) as { dependencies?: Record<string, unknown> };
|
|
58
|
+
const value = parsed.dependencies?.[dependencyName];
|
|
59
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
} catch {
|
|
63
|
+
// Use fallback when package metadata cannot be read.
|
|
64
|
+
}
|
|
65
|
+
return fallback;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const writeScaffoldFile = async (
|
|
69
|
+
filePath: string,
|
|
70
|
+
content: string,
|
|
71
|
+
options: { force?: boolean; writtenPaths: string[]; baseDir: string },
|
|
72
|
+
): Promise<void> => {
|
|
73
|
+
if (!options.force) {
|
|
74
|
+
try {
|
|
75
|
+
await access(filePath);
|
|
76
|
+
throw new Error(
|
|
77
|
+
`Refusing to overwrite existing file: ${relative(options.baseDir, filePath)}. Re-run with --force to overwrite.`,
|
|
78
|
+
);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if (!(error instanceof Error) || !error.message.includes("Refusing to overwrite")) {
|
|
81
|
+
// File does not exist, safe to continue.
|
|
82
|
+
} else {
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
88
|
+
await writeFile(filePath, content, "utf8");
|
|
89
|
+
options.writtenPaths.push(relative(options.baseDir, filePath));
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export const UPLOAD_PROVIDER_DEPS: Record<string, Array<{ name: string; fallback: string }>> = {
|
|
93
|
+
"vercel-blob": [{ name: "@vercel/blob", fallback: "^2.3.0" }],
|
|
94
|
+
s3: [
|
|
95
|
+
{ name: "@aws-sdk/client-s3", fallback: "^3.700.0" },
|
|
96
|
+
{ name: "@aws-sdk/s3-request-presigner", fallback: "^3.700.0" },
|
|
97
|
+
],
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const ensureRuntimeCliDependency = async (
|
|
101
|
+
projectDir: string,
|
|
102
|
+
cliVersion: string,
|
|
103
|
+
config?: PonchoConfig,
|
|
104
|
+
target?: string,
|
|
105
|
+
): Promise<{ paths: string[]; addedDeps: string[] }> => {
|
|
106
|
+
const packageJsonPath = resolve(projectDir, "package.json");
|
|
107
|
+
const content = await readFile(packageJsonPath, "utf8");
|
|
108
|
+
const parsed = JSON.parse(content) as {
|
|
109
|
+
dependencies?: Record<string, string>;
|
|
110
|
+
devDependencies?: Record<string, string>;
|
|
111
|
+
};
|
|
112
|
+
const dependencies = { ...(parsed.dependencies ?? {}) };
|
|
113
|
+
const isLocalOnlySpecifier = (value: string | undefined): boolean =>
|
|
114
|
+
typeof value === "string" &&
|
|
115
|
+
(value.startsWith("link:") || value.startsWith("workspace:") || value.startsWith("file:"));
|
|
116
|
+
|
|
117
|
+
// Deployment projects should not depend on local monorepo paths.
|
|
118
|
+
if (isLocalOnlySpecifier(dependencies["@poncho-ai/harness"])) {
|
|
119
|
+
delete dependencies["@poncho-ai/harness"];
|
|
120
|
+
}
|
|
121
|
+
if (isLocalOnlySpecifier(dependencies["@poncho-ai/sdk"])) {
|
|
122
|
+
delete dependencies["@poncho-ai/sdk"];
|
|
123
|
+
}
|
|
124
|
+
dependencies.marked = await readCliDependencyVersion("marked", "^17.0.2");
|
|
125
|
+
dependencies["@poncho-ai/cli"] = `^${cliVersion}`;
|
|
126
|
+
|
|
127
|
+
const addedDeps: string[] = [];
|
|
128
|
+
const uploadsProvider = config?.uploads?.provider;
|
|
129
|
+
if (uploadsProvider && UPLOAD_PROVIDER_DEPS[uploadsProvider]) {
|
|
130
|
+
for (const dep of UPLOAD_PROVIDER_DEPS[uploadsProvider]) {
|
|
131
|
+
if (!dependencies[dep.name]) {
|
|
132
|
+
dependencies[dep.name] = dep.fallback;
|
|
133
|
+
addedDeps.push(dep.name);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (target === "vercel" && !dependencies["@vercel/functions"]) {
|
|
139
|
+
dependencies["@vercel/functions"] = "^1.0.0";
|
|
140
|
+
addedDeps.push("@vercel/functions");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
parsed.dependencies = dependencies;
|
|
144
|
+
await writeFile(packageJsonPath, `${JSON.stringify(parsed, null, 2)}\n`, "utf8");
|
|
145
|
+
return { paths: [relative(projectDir, packageJsonPath)], addedDeps };
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export const checkVercelCronDrift = async (projectDir: string): Promise<void> => {
|
|
149
|
+
const vercelJsonPath = resolve(projectDir, "vercel.json");
|
|
150
|
+
try {
|
|
151
|
+
await access(vercelJsonPath);
|
|
152
|
+
} catch {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
let agentCrons: Record<string, CronJobConfig> = {};
|
|
156
|
+
try {
|
|
157
|
+
const agentMd = await readFile(resolve(projectDir, "AGENT.md"), "utf8");
|
|
158
|
+
const parsed = parseAgentMarkdown(agentMd);
|
|
159
|
+
agentCrons = parsed.frontmatter.cron ?? {};
|
|
160
|
+
} catch {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
let vercelCrons: Array<{ path: string; schedule: string }> = [];
|
|
164
|
+
try {
|
|
165
|
+
const raw = await readFile(vercelJsonPath, "utf8");
|
|
166
|
+
const vercelConfig = JSON.parse(raw) as { crons?: Array<{ path: string; schedule: string }> };
|
|
167
|
+
vercelCrons = vercelConfig.crons ?? [];
|
|
168
|
+
} catch {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const vercelCronMap = new Map(
|
|
172
|
+
vercelCrons
|
|
173
|
+
.filter((c) => c.path.startsWith("/api/cron/"))
|
|
174
|
+
.map((c) => [decodeURIComponent(c.path.replace("/api/cron/", "")), c.schedule]),
|
|
175
|
+
);
|
|
176
|
+
const diffs: string[] = [];
|
|
177
|
+
for (const [jobName, job] of Object.entries(agentCrons)) {
|
|
178
|
+
const existing = vercelCronMap.get(jobName);
|
|
179
|
+
if (!existing) {
|
|
180
|
+
diffs.push(` + missing job "${jobName}" (${job.schedule})`);
|
|
181
|
+
} else if (existing !== job.schedule) {
|
|
182
|
+
diffs.push(` ~ "${jobName}" schedule changed: "${existing}" → "${job.schedule}"`);
|
|
183
|
+
}
|
|
184
|
+
vercelCronMap.delete(jobName);
|
|
185
|
+
}
|
|
186
|
+
for (const [jobName, schedule] of vercelCronMap) {
|
|
187
|
+
diffs.push(` - removed job "${jobName}" (${schedule})`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Check reminder polling cron
|
|
191
|
+
try {
|
|
192
|
+
const cfg = await loadPonchoConfig(projectDir);
|
|
193
|
+
const reminderCron = vercelCrons.find((c) => c.path === "/api/reminders/check");
|
|
194
|
+
if (cfg?.reminders?.enabled && !reminderCron) {
|
|
195
|
+
diffs.push(` + missing reminders polling cron`);
|
|
196
|
+
} else if (!cfg?.reminders?.enabled && reminderCron) {
|
|
197
|
+
diffs.push(` - reminders polling cron present but reminders disabled`);
|
|
198
|
+
} else if (cfg?.reminders?.enabled && reminderCron) {
|
|
199
|
+
const expected = cfg.reminders.pollSchedule ?? "*/10 * * * *";
|
|
200
|
+
if (reminderCron.schedule !== expected) {
|
|
201
|
+
diffs.push(` ~ reminders poll schedule changed: "${reminderCron.schedule}" → "${expected}"`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
} catch { /* best-effort */ }
|
|
205
|
+
|
|
206
|
+
if (diffs.length > 0) {
|
|
207
|
+
process.stderr.write(
|
|
208
|
+
`\u26A0 vercel.json crons are out of sync with AGENT.md / poncho.config.js:\n${diffs.join("\n")}\n Run \`poncho build vercel --force\` to update.\n\n`,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
export const scaffoldDeployTarget = async (
|
|
214
|
+
projectDir: string,
|
|
215
|
+
target: DeployScaffoldTarget,
|
|
216
|
+
options?: { force?: boolean },
|
|
217
|
+
): Promise<string[]> => {
|
|
218
|
+
const writtenPaths: string[] = [];
|
|
219
|
+
const cliVersion = await readCliVersion();
|
|
220
|
+
const sharedServerEntrypoint = `import { startDevServer } from "@poncho-ai/cli";
|
|
221
|
+
|
|
222
|
+
const port = Number.parseInt(process.env.PORT ?? "3000", 10);
|
|
223
|
+
await startDevServer(Number.isNaN(port) ? 3000 : port, { workingDir: process.cwd() });
|
|
224
|
+
`;
|
|
225
|
+
|
|
226
|
+
if (target === "vercel") {
|
|
227
|
+
// Build @vercel/nft trace hints for packages that are dynamically loaded
|
|
228
|
+
// at runtime. Bare `import("pkg")` with a string literal is enough for
|
|
229
|
+
// nft to include the package in the bundle. Using async import() avoids
|
|
230
|
+
// blocking the module graph at cold start; .catch() prevents errors when
|
|
231
|
+
// an optional package isn't installed.
|
|
232
|
+
const traceHints: string[] = [];
|
|
233
|
+
|
|
234
|
+
let browserEnabled = false;
|
|
235
|
+
try {
|
|
236
|
+
const cfg = await loadPonchoConfig(projectDir);
|
|
237
|
+
browserEnabled = !!cfg?.browser;
|
|
238
|
+
} catch { /* best-effort */ }
|
|
239
|
+
|
|
240
|
+
if (browserEnabled) {
|
|
241
|
+
traceHints.push(`import("@poncho-ai/browser").catch(() => {});`);
|
|
242
|
+
|
|
243
|
+
const projectPkgPath = resolve(projectDir, "package.json");
|
|
244
|
+
try {
|
|
245
|
+
const raw = await readFile(projectPkgPath, "utf8");
|
|
246
|
+
const pkg = JSON.parse(raw) as { dependencies?: Record<string, string> };
|
|
247
|
+
if (pkg.dependencies?.["@sparticuz/chromium"]) {
|
|
248
|
+
traceHints.push(`import("@sparticuz/chromium").catch(() => {});`);
|
|
249
|
+
}
|
|
250
|
+
} catch { /* best-effort */ }
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const traceBlock = traceHints.length > 0
|
|
254
|
+
? `\n${traceHints.join("\n")}\n`
|
|
255
|
+
: "";
|
|
256
|
+
|
|
257
|
+
const entryPath = resolve(projectDir, "api", "index.mjs");
|
|
258
|
+
await writeScaffoldFile(
|
|
259
|
+
entryPath,
|
|
260
|
+
`import "marked";${traceBlock}
|
|
261
|
+
import { createRequestHandler } from "@poncho-ai/cli";
|
|
262
|
+
let handlerPromise;
|
|
263
|
+
export default async function handler(req, res) {
|
|
264
|
+
try {
|
|
265
|
+
if (!handlerPromise) {
|
|
266
|
+
handlerPromise = createRequestHandler({ workingDir: process.cwd() });
|
|
267
|
+
}
|
|
268
|
+
const requestHandler = await handlerPromise;
|
|
269
|
+
await requestHandler(req, res);
|
|
270
|
+
} catch (error) {
|
|
271
|
+
console.error("Handler error:", error);
|
|
272
|
+
if (!res.headersSent) {
|
|
273
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
274
|
+
res.end(JSON.stringify({ error: "Internal server error", message: error?.message || "Unknown error" }));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
`,
|
|
279
|
+
{ force: options?.force, writtenPaths, baseDir: projectDir },
|
|
280
|
+
);
|
|
281
|
+
const vercelConfigPath = resolve(projectDir, "vercel.json");
|
|
282
|
+
let vercelCrons: Array<{ path: string; schedule: string }> | undefined;
|
|
283
|
+
try {
|
|
284
|
+
const agentMd = await readFile(resolve(projectDir, "AGENT.md"), "utf8");
|
|
285
|
+
const parsed = parseAgentMarkdown(agentMd);
|
|
286
|
+
if (parsed.frontmatter.cron) {
|
|
287
|
+
vercelCrons = Object.entries(parsed.frontmatter.cron).map(
|
|
288
|
+
([jobName, job]) => ({
|
|
289
|
+
path: `/api/cron/${encodeURIComponent(jobName)}`,
|
|
290
|
+
schedule: job.schedule,
|
|
291
|
+
}),
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
} catch {
|
|
295
|
+
// AGENT.md may not exist yet during init; skip cron generation
|
|
296
|
+
}
|
|
297
|
+
let existingVercelConfig: Record<string, unknown> = {};
|
|
298
|
+
try {
|
|
299
|
+
const raw = await readFile(vercelConfigPath, "utf8");
|
|
300
|
+
existingVercelConfig = JSON.parse(raw) as Record<string, unknown>;
|
|
301
|
+
} catch {
|
|
302
|
+
// No existing vercel.json or invalid JSON — start fresh
|
|
303
|
+
}
|
|
304
|
+
const existingFunctions = (existingVercelConfig.functions ?? {}) as Record<string, Record<string, unknown>>;
|
|
305
|
+
const existingApiEntry = existingFunctions["api/index.mjs"] ?? {};
|
|
306
|
+
const vercelConfig: Record<string, unknown> = {
|
|
307
|
+
...existingVercelConfig,
|
|
308
|
+
version: 2,
|
|
309
|
+
functions: {
|
|
310
|
+
...existingFunctions,
|
|
311
|
+
"api/index.mjs": {
|
|
312
|
+
...existingApiEntry,
|
|
313
|
+
includeFiles:
|
|
314
|
+
"{AGENT.md,poncho.config.js,skills/**,tests/**,node_modules/.pnpm/marked@*/node_modules/marked/lib/marked.umd.js}",
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
headers: [
|
|
318
|
+
{
|
|
319
|
+
source: "/api/(.*)",
|
|
320
|
+
headers: [
|
|
321
|
+
{ key: "Cache-Control", value: "private, no-cache, no-store, must-revalidate" },
|
|
322
|
+
],
|
|
323
|
+
},
|
|
324
|
+
],
|
|
325
|
+
routes: [{ src: "/(.*)", dest: "/api/index.mjs" }],
|
|
326
|
+
};
|
|
327
|
+
// Add reminder polling cron if reminders are enabled
|
|
328
|
+
try {
|
|
329
|
+
const cfg = await loadPonchoConfig(projectDir);
|
|
330
|
+
if (cfg?.reminders?.enabled) {
|
|
331
|
+
const schedule = cfg.reminders.pollSchedule ?? "*/10 * * * *";
|
|
332
|
+
if (!vercelCrons) vercelCrons = [];
|
|
333
|
+
vercelCrons.push({ path: "/api/reminders/check", schedule });
|
|
334
|
+
}
|
|
335
|
+
} catch { /* best-effort */ }
|
|
336
|
+
|
|
337
|
+
if (vercelCrons && vercelCrons.length > 0) {
|
|
338
|
+
vercelConfig.crons = vercelCrons;
|
|
339
|
+
}
|
|
340
|
+
await writeScaffoldFile(
|
|
341
|
+
vercelConfigPath,
|
|
342
|
+
`${JSON.stringify(vercelConfig, null, 2)}\n`,
|
|
343
|
+
{ force: options?.force, writtenPaths, baseDir: projectDir },
|
|
344
|
+
);
|
|
345
|
+
} else if (target === "docker") {
|
|
346
|
+
const dockerfilePath = resolve(projectDir, "Dockerfile");
|
|
347
|
+
await writeScaffoldFile(
|
|
348
|
+
dockerfilePath,
|
|
349
|
+
`FROM node:20-slim
|
|
350
|
+
WORKDIR /app
|
|
351
|
+
COPY package.json package.json
|
|
352
|
+
COPY AGENT.md AGENT.md
|
|
353
|
+
COPY poncho.config.js poncho.config.js
|
|
354
|
+
COPY skills skills
|
|
355
|
+
COPY tests tests
|
|
356
|
+
COPY .env.example .env.example
|
|
357
|
+
RUN corepack enable && npm install -g @poncho-ai/cli@^${cliVersion}
|
|
358
|
+
COPY server.js server.js
|
|
359
|
+
EXPOSE 3000
|
|
360
|
+
CMD ["node","server.js"]
|
|
361
|
+
`,
|
|
362
|
+
{ force: options?.force, writtenPaths, baseDir: projectDir },
|
|
363
|
+
);
|
|
364
|
+
await writeScaffoldFile(resolve(projectDir, "server.js"), sharedServerEntrypoint, {
|
|
365
|
+
force: options?.force,
|
|
366
|
+
writtenPaths,
|
|
367
|
+
baseDir: projectDir,
|
|
368
|
+
});
|
|
369
|
+
} else if (target === "lambda") {
|
|
370
|
+
await writeScaffoldFile(
|
|
371
|
+
resolve(projectDir, "lambda-handler.js"),
|
|
372
|
+
`import { startDevServer } from "@poncho-ai/cli";
|
|
373
|
+
let serverPromise;
|
|
374
|
+
export const handler = async (event = {}) => {
|
|
375
|
+
if (!serverPromise) {
|
|
376
|
+
serverPromise = startDevServer(0, { workingDir: process.cwd() });
|
|
377
|
+
}
|
|
378
|
+
const body = JSON.stringify({
|
|
379
|
+
status: "ready",
|
|
380
|
+
route: event.rawPath ?? event.path ?? "/",
|
|
381
|
+
});
|
|
382
|
+
return { statusCode: 200, headers: { "content-type": "application/json" }, body };
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
// Cron jobs: use AWS EventBridge (CloudWatch Events) to trigger scheduled invocations.
|
|
386
|
+
// Create a rule for each cron job defined in AGENT.md that sends a GET request to:
|
|
387
|
+
// /api/cron/<jobName>
|
|
388
|
+
// Include the Authorization header with your PONCHO_AUTH_TOKEN as a Bearer token.
|
|
389
|
+
//
|
|
390
|
+
// Reminders: Create a CloudWatch Events rule that triggers GET /api/reminders/check
|
|
391
|
+
// every 10 minutes (or your preferred interval) with Authorization: Bearer <PONCHO_AUTH_TOKEN>.
|
|
392
|
+
`,
|
|
393
|
+
{ force: options?.force, writtenPaths, baseDir: projectDir },
|
|
394
|
+
);
|
|
395
|
+
} else if (target === "fly") {
|
|
396
|
+
await writeScaffoldFile(
|
|
397
|
+
resolve(projectDir, "fly.toml"),
|
|
398
|
+
`app = "poncho-app"
|
|
399
|
+
[env]
|
|
400
|
+
PORT = "3000"
|
|
401
|
+
[http_service]
|
|
402
|
+
internal_port = 3000
|
|
403
|
+
force_https = true
|
|
404
|
+
auto_start_machines = true
|
|
405
|
+
auto_stop_machines = "stop"
|
|
406
|
+
min_machines_running = 0
|
|
407
|
+
`,
|
|
408
|
+
{ force: options?.force, writtenPaths, baseDir: projectDir },
|
|
409
|
+
);
|
|
410
|
+
await writeScaffoldFile(
|
|
411
|
+
resolve(projectDir, "Dockerfile"),
|
|
412
|
+
`FROM node:20-slim
|
|
413
|
+
WORKDIR /app
|
|
414
|
+
COPY package.json package.json
|
|
415
|
+
COPY AGENT.md AGENT.md
|
|
416
|
+
COPY poncho.config.js poncho.config.js
|
|
417
|
+
COPY skills skills
|
|
418
|
+
COPY tests tests
|
|
419
|
+
RUN npm install -g @poncho-ai/cli@^${cliVersion}
|
|
420
|
+
COPY server.js server.js
|
|
421
|
+
EXPOSE 3000
|
|
422
|
+
CMD ["node","server.js"]
|
|
423
|
+
`,
|
|
424
|
+
{ force: options?.force, writtenPaths, baseDir: projectDir },
|
|
425
|
+
);
|
|
426
|
+
await writeScaffoldFile(resolve(projectDir, "server.js"), sharedServerEntrypoint, {
|
|
427
|
+
force: options?.force,
|
|
428
|
+
writtenPaths,
|
|
429
|
+
baseDir: projectDir,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const config = await loadPonchoConfig(projectDir);
|
|
434
|
+
const { paths: packagePaths, addedDeps } = await ensureRuntimeCliDependency(
|
|
435
|
+
projectDir,
|
|
436
|
+
cliVersion,
|
|
437
|
+
config,
|
|
438
|
+
target,
|
|
439
|
+
);
|
|
440
|
+
const depNote = addedDeps.length > 0 ? ` (added ${addedDeps.join(", ")})` : "";
|
|
441
|
+
for (const p of packagePaths) {
|
|
442
|
+
if (!writtenPaths.includes(p)) {
|
|
443
|
+
writtenPaths.push(depNote ? `${p}${depNote}` : p);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return writtenPaths;
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
export const serializeJs = (value: unknown, indent = 0): string => {
|
|
451
|
+
const pad = " ".repeat(indent);
|
|
452
|
+
const padInner = " ".repeat(indent + 1);
|
|
453
|
+
if (value === null || value === undefined) return String(value);
|
|
454
|
+
if (typeof value === "boolean" || typeof value === "number") return String(value);
|
|
455
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
456
|
+
if (Array.isArray(value)) {
|
|
457
|
+
if (value.length === 0) return "[]";
|
|
458
|
+
const items = value.map((v) => `${padInner}${serializeJs(v, indent + 1)}`);
|
|
459
|
+
return `[\n${items.join(",\n")},\n${pad}]`;
|
|
460
|
+
}
|
|
461
|
+
if (typeof value === "object") {
|
|
462
|
+
const entries = Object.entries(value as Record<string, unknown>).filter(
|
|
463
|
+
([, v]) => v !== undefined,
|
|
464
|
+
);
|
|
465
|
+
if (entries.length === 0) return "{}";
|
|
466
|
+
const safeKey = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
|
|
467
|
+
const lines = entries.map(([k, v]) => {
|
|
468
|
+
const key = safeKey.test(k) ? k : JSON.stringify(k);
|
|
469
|
+
return `${padInner}${key}: ${serializeJs(v, indent + 1)}`;
|
|
470
|
+
});
|
|
471
|
+
return `{\n${lines.join(",\n")},\n${pad}}`;
|
|
472
|
+
}
|
|
473
|
+
return String(value);
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
export const renderConfigFile = (config: PonchoConfig): string =>
|
|
477
|
+
`export default ${serializeJs(config)}\n`;
|
|
478
|
+
|
|
479
|
+
export const writeConfigFile = async (workingDir: string, config: PonchoConfig): Promise<void> => {
|
|
480
|
+
const serialized = renderConfigFile(config);
|
|
481
|
+
await writeFile(resolve(workingDir, "poncho.config.js"), serialized, "utf8");
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
export const ensureEnvPlaceholder = async (filePath: string, key: string): Promise<boolean> => {
|
|
485
|
+
const normalizedKey = key.trim();
|
|
486
|
+
if (!normalizedKey) {
|
|
487
|
+
return false;
|
|
488
|
+
}
|
|
489
|
+
let content = "";
|
|
490
|
+
try {
|
|
491
|
+
content = await readFile(filePath, "utf8");
|
|
492
|
+
} catch {
|
|
493
|
+
await writeFile(filePath, `${normalizedKey}=\n`, "utf8");
|
|
494
|
+
return true;
|
|
495
|
+
}
|
|
496
|
+
const present = content
|
|
497
|
+
.split(/\r?\n/)
|
|
498
|
+
.some((line) => line.trimStart().startsWith(`${normalizedKey}=`));
|
|
499
|
+
if (present) {
|
|
500
|
+
return false;
|
|
501
|
+
}
|
|
502
|
+
const withTrailingNewline = content.length === 0 || content.endsWith("\n")
|
|
503
|
+
? content
|
|
504
|
+
: `${content}\n`;
|
|
505
|
+
await writeFile(filePath, `${withTrailingNewline}${normalizedKey}=\n`, "utf8");
|
|
506
|
+
return true;
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
export const removeEnvPlaceholder = async (filePath: string, key: string): Promise<boolean> => {
|
|
510
|
+
const normalizedKey = key.trim();
|
|
511
|
+
if (!normalizedKey) {
|
|
512
|
+
return false;
|
|
513
|
+
}
|
|
514
|
+
let content = "";
|
|
515
|
+
try {
|
|
516
|
+
content = await readFile(filePath, "utf8");
|
|
517
|
+
} catch {
|
|
518
|
+
return false;
|
|
519
|
+
}
|
|
520
|
+
const lines = content.split(/\r?\n/);
|
|
521
|
+
const filtered = lines.filter((line) => !line.trimStart().startsWith(`${normalizedKey}=`));
|
|
522
|
+
if (filtered.length === lines.length) {
|
|
523
|
+
return false;
|
|
524
|
+
}
|
|
525
|
+
const nextContent = filtered.join("\n").replace(/\n+$/, "");
|
|
526
|
+
await writeFile(filePath, nextContent.length > 0 ? `${nextContent}\n` : "", "utf8");
|
|
527
|
+
return true;
|
|
528
|
+
};
|