@poncho-ai/cli 0.8.2 → 0.9.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.
Files changed (50) hide show
  1. package/.turbo/turbo-build.log +6 -6
  2. package/CHANGELOG.md +20 -0
  3. package/dist/chunk-42U2R3FH.js +5752 -0
  4. package/dist/chunk-4UDNQZ3G.js +5752 -0
  5. package/dist/chunk-5NHWU4QU.js +5752 -0
  6. package/dist/chunk-6CDE6R7D.js +5752 -0
  7. package/dist/chunk-74HD63WM.js +5819 -0
  8. package/dist/chunk-7TRWWFGI.js +5752 -0
  9. package/dist/chunk-G67AWHXV.js +5752 -0
  10. package/dist/chunk-GFGEMANG.js +5820 -0
  11. package/dist/chunk-J2MTY7EY.js +5780 -0
  12. package/dist/chunk-L65TFTEI.js +5752 -0
  13. package/dist/chunk-O5NLOW2I.js +5752 -0
  14. package/dist/chunk-OGTT4YJG.js +5752 -0
  15. package/dist/chunk-OTOMFL3L.js +5773 -0
  16. package/dist/chunk-PHVOJ2R5.js +5781 -0
  17. package/dist/chunk-Q3WHF2FP.js +5752 -0
  18. package/dist/chunk-RN7FDRZH.js +5752 -0
  19. package/dist/chunk-SWPCETEB.js +5772 -0
  20. package/dist/chunk-VP4ABFQK.js +5795 -0
  21. package/dist/chunk-ZCLLCLRR.js +5752 -0
  22. package/dist/chunk-ZHHKZDHY.js +5795 -0
  23. package/dist/cli.js +1 -1
  24. package/dist/index.d.ts +3 -1
  25. package/dist/index.js +1 -1
  26. package/dist/run-interactive-ink-2CVZHZLL.js +535 -0
  27. package/dist/run-interactive-ink-3TNAVPQ7.js +534 -0
  28. package/dist/run-interactive-ink-54UJ6WGA.js +535 -0
  29. package/dist/run-interactive-ink-64XY2KJD.js +535 -0
  30. package/dist/run-interactive-ink-7EB3ZX6P.js +535 -0
  31. package/dist/run-interactive-ink-7OSESHKH.js +535 -0
  32. package/dist/run-interactive-ink-BU4ZKI3Z.js +535 -0
  33. package/dist/run-interactive-ink-DORF57NC.js +535 -0
  34. package/dist/run-interactive-ink-EOW4MLEH.js +535 -0
  35. package/dist/run-interactive-ink-EU3DN4MJ.js +535 -0
  36. package/dist/run-interactive-ink-HMVUIZRO.js +533 -0
  37. package/dist/run-interactive-ink-MQTTMSSO.js +535 -0
  38. package/dist/run-interactive-ink-NT66KRS5.js +535 -0
  39. package/dist/run-interactive-ink-O5AV46ZE.js +535 -0
  40. package/dist/run-interactive-ink-OC57VVOY.js +535 -0
  41. package/dist/run-interactive-ink-PTUDJKT5.js +535 -0
  42. package/dist/run-interactive-ink-PYQFHCNW.js +537 -0
  43. package/dist/run-interactive-ink-QXIIUBIC.js +534 -0
  44. package/dist/run-interactive-ink-XEK5ZPSU.js +535 -0
  45. package/dist/run-interactive-ink-YWJ5OBNI.js +535 -0
  46. package/package.json +3 -4
  47. package/src/index.ts +295 -243
  48. package/src/init-onboarding.ts +12 -0
  49. package/src/run-interactive-ink.ts +18 -4
  50. package/test/cli.test.ts +129 -84
package/src/index.ts CHANGED
@@ -42,6 +42,7 @@ import {
42
42
  import { createInterface } from "node:readline/promises";
43
43
  import {
44
44
  runInitOnboarding,
45
+ type DeployTarget,
45
46
  type InitOnboardingOptions,
46
47
  } from "./init-onboarding.js";
47
48
  import {
@@ -478,10 +479,14 @@ ${name}/
478
479
  ├── tests/
479
480
  │ └── basic.yaml # Test suite
480
481
  └── skills/
481
- └── starter/
482
+ ├── starter/
483
+ │ ├── SKILL.md
484
+ │ └── scripts/
485
+ │ └── starter-echo.ts
486
+ └── fetch-page/
482
487
  ├── SKILL.md
483
488
  └── scripts/
484
- └── starter-echo.ts
489
+ └── fetch-page.ts
485
490
  \`\`\`
486
491
 
487
492
  ## Deployment
@@ -489,7 +494,7 @@ ${name}/
489
494
  \`\`\`bash
490
495
  # Build for Vercel
491
496
  poncho build vercel
492
- cd .poncho-build/vercel && vercel deploy --prod
497
+ vercel deploy --prod
493
498
 
494
499
  # Build for Docker
495
500
  poncho build docker
@@ -502,19 +507,7 @@ https://github.com/cesr/poncho-ai
502
507
 
503
508
  const ENV_TEMPLATE = "ANTHROPIC_API_KEY=sk-ant-...\n";
504
509
  const GITIGNORE_TEMPLATE =
505
- ".env\nnode_modules\ndist\n.poncho-build\n.poncho/\ninteractive-session.json\n";
506
- const VERCEL_RUNTIME_DEPENDENCIES: Record<string, string> = {
507
- "@anthropic-ai/sdk": "^0.74.0",
508
- "@aws-sdk/client-dynamodb": "^3.988.0",
509
- "@latitude-data/telemetry": "^2.0.2",
510
- commander: "^12.0.0",
511
- dotenv: "^16.4.0",
512
- jiti: "^2.6.1",
513
- mustache: "^4.2.0",
514
- openai: "^6.3.0",
515
- redis: "^5.10.0",
516
- yaml: "^2.8.1",
517
- };
510
+ ".env\nnode_modules\ndist\n.poncho/\ninteractive-session.json\n.vercel\n";
518
511
  const TEST_TEMPLATE = `tests:
519
512
  - name: "Basic sanity"
520
513
  task: "What is 2 + 2?"
@@ -545,40 +538,174 @@ const SKILL_TOOL_TEMPLATE = `export default async function run(input) {
545
538
  }
546
539
  `;
547
540
 
541
+ const FETCH_PAGE_SKILL_TEMPLATE = `---
542
+ name: fetch-page
543
+ description: Fetch a web page and return its text content
544
+ allowed-tools:
545
+ - ./scripts/fetch-page.ts
546
+ ---
547
+
548
+ # Fetch Page
549
+
550
+ Fetches a URL and returns the page body as plain text (HTML tags stripped).
551
+
552
+ ## Usage
553
+
554
+ Call \`run_skill_script\` with:
555
+ - **skill**: \`fetch-page\`
556
+ - **script**: \`./scripts/fetch-page.ts\`
557
+ - **input**: \`{ "url": "https://example.com" }\`
558
+
559
+ The script returns \`{ url, status, content }\` where \`content\` is the
560
+ text-only body (capped at ~32 000 chars to stay context-friendly).
561
+ `;
562
+
563
+ const FETCH_PAGE_SCRIPT_TEMPLATE = `export default async function run(input) {
564
+ const url = typeof input?.url === "string" ? input.url.trim() : "";
565
+ if (!url) {
566
+ return { error: "A \\"url\\" string is required." };
567
+ }
568
+
569
+ const MAX_LENGTH = 32_000;
570
+
571
+ const response = await fetch(url, {
572
+ headers: { "User-Agent": "poncho-fetch-page/1.0" },
573
+ redirect: "follow",
574
+ });
575
+
576
+ if (!response.ok) {
577
+ return { url, status: response.status, error: response.statusText };
578
+ }
579
+
580
+ const html = await response.text();
581
+
582
+ // Lightweight HTML-to-text: strip tags, collapse whitespace.
583
+ const text = html
584
+ .replace(/<script[\\s\\S]*?<\\/script>/gi, "")
585
+ .replace(/<style[\\s\\S]*?<\\/style>/gi, "")
586
+ .replace(/<[^>]+>/g, " ")
587
+ .replace(/&nbsp;/gi, " ")
588
+ .replace(/&amp;/gi, "&")
589
+ .replace(/&lt;/gi, "<")
590
+ .replace(/&gt;/gi, ">")
591
+ .replace(/&quot;/gi, '"')
592
+ .replace(/&#39;/gi, "'")
593
+ .replace(/\\s+/g, " ")
594
+ .trim();
595
+
596
+ const content = text.length > MAX_LENGTH
597
+ ? text.slice(0, MAX_LENGTH) + "… (truncated)"
598
+ : text;
599
+
600
+ return { url, status: response.status, content };
601
+ }
602
+ `;
603
+
548
604
  const ensureFile = async (path: string, content: string): Promise<void> => {
549
605
  await mkdir(dirname(path), { recursive: true });
550
606
  await writeFile(path, content, { encoding: "utf8", flag: "wx" });
551
607
  };
552
608
 
553
- const copyIfExists = async (sourcePath: string, destinationPath: string): Promise<void> => {
554
- try {
555
- await access(sourcePath);
556
- } catch {
557
- return;
609
+ type DeployScaffoldTarget = Exclude<DeployTarget, "none">;
610
+
611
+ const normalizeDeployTarget = (target: string): DeployScaffoldTarget => {
612
+ const normalized = target.toLowerCase();
613
+ if (
614
+ normalized === "vercel" ||
615
+ normalized === "docker" ||
616
+ normalized === "lambda" ||
617
+ normalized === "fly"
618
+ ) {
619
+ return normalized;
558
620
  }
559
- await mkdir(dirname(destinationPath), { recursive: true });
560
- // Build outputs should contain materialized files, not symlinks to paths
561
- // that may not exist inside deployment artifacts (e.g. .agents/skills/*).
562
- await cp(sourcePath, destinationPath, { recursive: true, dereference: true });
621
+ throw new Error(`Unsupported build target: ${target}`);
563
622
  };
564
623
 
565
- const resolveCliEntrypoint = async (): Promise<string> => {
566
- const sourceEntrypoint = resolve(packageRoot, "src", "index.ts");
624
+ const readCliVersion = async (): Promise<string> => {
625
+ const fallback = "0.1.0";
567
626
  try {
568
- await access(sourceEntrypoint);
569
- return sourceEntrypoint;
627
+ const packageJsonPath = resolve(packageRoot, "package.json");
628
+ const content = await readFile(packageJsonPath, "utf8");
629
+ const parsed = JSON.parse(content) as { version?: unknown };
630
+ if (typeof parsed.version === "string" && parsed.version.trim().length > 0) {
631
+ return parsed.version;
632
+ }
570
633
  } catch {
571
- return resolve(packageRoot, "dist", "index.js");
634
+ // Use fallback when package metadata cannot be read.
635
+ }
636
+ return fallback;
637
+ };
638
+
639
+ const writeScaffoldFile = async (
640
+ filePath: string,
641
+ content: string,
642
+ options: { force?: boolean; writtenPaths: string[]; baseDir: string },
643
+ ): Promise<void> => {
644
+ if (!options.force) {
645
+ try {
646
+ await access(filePath);
647
+ throw new Error(
648
+ `Refusing to overwrite existing file: ${relative(options.baseDir, filePath)}. Re-run with --force to overwrite.`,
649
+ );
650
+ } catch (error) {
651
+ if (!(error instanceof Error) || !error.message.includes("Refusing to overwrite")) {
652
+ // File does not exist, safe to continue.
653
+ } else {
654
+ throw error;
655
+ }
656
+ }
572
657
  }
658
+ await mkdir(dirname(filePath), { recursive: true });
659
+ await writeFile(filePath, content, "utf8");
660
+ options.writtenPaths.push(relative(options.baseDir, filePath));
573
661
  };
574
662
 
575
- const buildVercelHandlerBundle = async (outDir: string): Promise<void> => {
576
- const { build: esbuild } = await import("esbuild");
577
- const cliEntrypoint = await resolveCliEntrypoint();
578
- const tempEntry = resolve(outDir, "api", "_entry.js");
579
- await writeFile(
580
- tempEntry,
581
- `import { createRequestHandler } from ${JSON.stringify(cliEntrypoint)};
663
+ const ensureRuntimeCliDependency = async (
664
+ projectDir: string,
665
+ cliVersion: string,
666
+ ): Promise<string[]> => {
667
+ const packageJsonPath = resolve(projectDir, "package.json");
668
+ const content = await readFile(packageJsonPath, "utf8");
669
+ const parsed = JSON.parse(content) as {
670
+ dependencies?: Record<string, string>;
671
+ devDependencies?: Record<string, string>;
672
+ };
673
+ const dependencies = { ...(parsed.dependencies ?? {}) };
674
+ const isLocalOnlySpecifier = (value: string | undefined): boolean =>
675
+ typeof value === "string" &&
676
+ (value.startsWith("link:") || value.startsWith("workspace:") || value.startsWith("file:"));
677
+
678
+ // Deployment projects should not depend on local monorepo paths.
679
+ if (isLocalOnlySpecifier(dependencies["@poncho-ai/harness"])) {
680
+ delete dependencies["@poncho-ai/harness"];
681
+ }
682
+ if (isLocalOnlySpecifier(dependencies["@poncho-ai/sdk"])) {
683
+ delete dependencies["@poncho-ai/sdk"];
684
+ }
685
+ dependencies["@poncho-ai/cli"] = `^${cliVersion}`;
686
+ parsed.dependencies = dependencies;
687
+ await writeFile(packageJsonPath, `${JSON.stringify(parsed, null, 2)}\n`, "utf8");
688
+ return [relative(projectDir, packageJsonPath)];
689
+ };
690
+
691
+ const scaffoldDeployTarget = async (
692
+ projectDir: string,
693
+ target: DeployScaffoldTarget,
694
+ options?: { force?: boolean },
695
+ ): Promise<string[]> => {
696
+ const writtenPaths: string[] = [];
697
+ const cliVersion = await readCliVersion();
698
+ const sharedServerEntrypoint = `import { startDevServer } from "@poncho-ai/cli";
699
+
700
+ const port = Number.parseInt(process.env.PORT ?? "3000", 10);
701
+ await startDevServer(Number.isNaN(port) ? 3000 : port, { workingDir: process.cwd() });
702
+ `;
703
+
704
+ if (target === "vercel") {
705
+ const entryPath = resolve(projectDir, "api", "index.mjs");
706
+ await writeScaffoldFile(
707
+ entryPath,
708
+ `import { createRequestHandler } from "@poncho-ai/cli";
582
709
  let handlerPromise;
583
710
  export default async function handler(req, res) {
584
711
  try {
@@ -596,68 +723,114 @@ export default async function handler(req, res) {
596
723
  }
597
724
  }
598
725
  `,
599
- "utf8",
600
- );
601
- await esbuild({
602
- entryPoints: [tempEntry],
603
- bundle: true,
604
- platform: "node",
605
- format: "esm",
606
- target: "node20",
607
- outfile: resolve(outDir, "api", "index.js"),
608
- sourcemap: false,
609
- legalComments: "none",
610
- external: [
611
- ...Object.keys(VERCEL_RUNTIME_DEPENDENCIES),
612
- "@anthropic-ai/sdk/*",
613
- "child_process",
614
- "fs",
615
- "fs/promises",
616
- "http",
617
- "https",
618
- "path",
619
- "module",
620
- "url",
621
- "readline",
622
- "readline/promises",
623
- "crypto",
624
- "stream",
625
- "events",
626
- "util",
627
- "os",
628
- "zlib",
629
- "net",
630
- "tls",
631
- "dns",
632
- "assert",
633
- "buffer",
634
- "timers",
635
- "timers/promises",
636
- "node:child_process",
637
- "node:fs",
638
- "node:fs/promises",
639
- "node:http",
640
- "node:https",
641
- "node:path",
642
- "node:module",
643
- "node:url",
644
- "node:readline",
645
- "node:readline/promises",
646
- "node:crypto",
647
- "node:stream",
648
- "node:events",
649
- "node:util",
650
- "node:os",
651
- "node:zlib",
652
- "node:net",
653
- "node:tls",
654
- "node:dns",
655
- "node:assert",
656
- "node:buffer",
657
- "node:timers",
658
- "node:timers/promises",
659
- ],
726
+ { force: options?.force, writtenPaths, baseDir: projectDir },
727
+ );
728
+ const vercelConfigPath = resolve(projectDir, "vercel.json");
729
+ await writeScaffoldFile(
730
+ vercelConfigPath,
731
+ `${JSON.stringify(
732
+ {
733
+ version: 2,
734
+ functions: {
735
+ "api/index.mjs": {
736
+ includeFiles: "{AGENT.md,poncho.config.js,skills/**,tests/**}",
737
+ },
738
+ },
739
+ routes: [{ src: "/(.*)", dest: "/api/index.mjs" }],
740
+ },
741
+ null,
742
+ 2,
743
+ )}\n`,
744
+ { force: options?.force, writtenPaths, baseDir: projectDir },
745
+ );
746
+ } else if (target === "docker") {
747
+ const dockerfilePath = resolve(projectDir, "Dockerfile");
748
+ await writeScaffoldFile(
749
+ dockerfilePath,
750
+ `FROM node:20-slim
751
+ WORKDIR /app
752
+ COPY package.json package.json
753
+ COPY AGENT.md AGENT.md
754
+ COPY poncho.config.js poncho.config.js
755
+ COPY skills skills
756
+ COPY tests tests
757
+ COPY .env.example .env.example
758
+ RUN corepack enable && npm install -g @poncho-ai/cli@^${cliVersion}
759
+ COPY server.js server.js
760
+ EXPOSE 3000
761
+ CMD ["node","server.js"]
762
+ `,
763
+ { force: options?.force, writtenPaths, baseDir: projectDir },
764
+ );
765
+ await writeScaffoldFile(resolve(projectDir, "server.js"), sharedServerEntrypoint, {
766
+ force: options?.force,
767
+ writtenPaths,
768
+ baseDir: projectDir,
769
+ });
770
+ } else if (target === "lambda") {
771
+ await writeScaffoldFile(
772
+ resolve(projectDir, "lambda-handler.js"),
773
+ `import { startDevServer } from "@poncho-ai/cli";
774
+ let serverPromise;
775
+ export const handler = async (event = {}) => {
776
+ if (!serverPromise) {
777
+ serverPromise = startDevServer(0, { workingDir: process.cwd() });
778
+ }
779
+ const body = JSON.stringify({
780
+ status: "ready",
781
+ route: event.rawPath ?? event.path ?? "/",
660
782
  });
783
+ return { statusCode: 200, headers: { "content-type": "application/json" }, body };
784
+ };
785
+ `,
786
+ { force: options?.force, writtenPaths, baseDir: projectDir },
787
+ );
788
+ } else if (target === "fly") {
789
+ await writeScaffoldFile(
790
+ resolve(projectDir, "fly.toml"),
791
+ `app = "poncho-app"
792
+ [env]
793
+ PORT = "3000"
794
+ [http_service]
795
+ internal_port = 3000
796
+ force_https = true
797
+ auto_start_machines = true
798
+ auto_stop_machines = "stop"
799
+ min_machines_running = 0
800
+ `,
801
+ { force: options?.force, writtenPaths, baseDir: projectDir },
802
+ );
803
+ await writeScaffoldFile(
804
+ resolve(projectDir, "Dockerfile"),
805
+ `FROM node:20-slim
806
+ WORKDIR /app
807
+ COPY package.json package.json
808
+ COPY AGENT.md AGENT.md
809
+ COPY poncho.config.js poncho.config.js
810
+ COPY skills skills
811
+ COPY tests tests
812
+ RUN npm install -g @poncho-ai/cli@^${cliVersion}
813
+ COPY server.js server.js
814
+ EXPOSE 3000
815
+ CMD ["node","server.js"]
816
+ `,
817
+ { force: options?.force, writtenPaths, baseDir: projectDir },
818
+ );
819
+ await writeScaffoldFile(resolve(projectDir, "server.js"), sharedServerEntrypoint, {
820
+ force: options?.force,
821
+ writtenPaths,
822
+ baseDir: projectDir,
823
+ });
824
+ }
825
+
826
+ const packagePaths = await ensureRuntimeCliDependency(projectDir, cliVersion);
827
+ for (const path of packagePaths) {
828
+ if (!writtenPaths.includes(path)) {
829
+ writtenPaths.push(path);
830
+ }
831
+ }
832
+
833
+ return writtenPaths;
661
834
  };
662
835
 
663
836
  const renderConfigFile = (config: PonchoConfig): string =>
@@ -765,6 +938,8 @@ export const initProject = async (
765
938
  { path: "tests/basic.yaml", content: TEST_TEMPLATE },
766
939
  { path: "skills/starter/SKILL.md", content: SKILL_TEMPLATE },
767
940
  { path: "skills/starter/scripts/starter-echo.ts", content: SKILL_TOOL_TEMPLATE },
941
+ { path: "skills/fetch-page/SKILL.md", content: FETCH_PAGE_SKILL_TEMPLATE },
942
+ { path: "skills/fetch-page/scripts/fetch-page.ts", content: FETCH_PAGE_SCRIPT_TEMPLATE },
768
943
  ];
769
944
  if (onboarding.envFile) {
770
945
  scaffoldFiles.push({ path: ".env", content: onboarding.envFile });
@@ -775,6 +950,13 @@ export const initProject = async (
775
950
  process.stdout.write(` ${D}+${R} ${D}${file.path}${R}\n`);
776
951
  }
777
952
 
953
+ if (onboarding.deployTarget !== "none") {
954
+ const deployFiles = await scaffoldDeployTarget(projectDir, onboarding.deployTarget);
955
+ for (const filePath of deployFiles) {
956
+ process.stdout.write(` ${D}+${R} ${D}${filePath}${R}\n`);
957
+ }
958
+ }
959
+
778
960
  await initializeOnboardingMarker(projectDir, {
779
961
  allowIntro: !(onboardingOptions.yes ?? false),
780
962
  });
@@ -2346,150 +2528,19 @@ export const runTests = async (
2346
2528
  return { passed, failed };
2347
2529
  };
2348
2530
 
2349
- export const buildTarget = async (workingDir: string, target: string): Promise<void> => {
2350
- const outDir = resolve(workingDir, ".poncho-build", target);
2351
- await mkdir(outDir, { recursive: true });
2352
- const serverEntrypoint = `import { startDevServer } from "@poncho-ai/cli";
2353
-
2354
- const port = Number.parseInt(process.env.PORT ?? "3000", 10);
2355
- await startDevServer(Number.isNaN(port) ? 3000 : port, { workingDir: process.cwd() });
2356
- `;
2357
- const runtimePackageJson = JSON.stringify(
2358
- {
2359
- name: "poncho-runtime-bundle",
2360
- private: true,
2361
- type: "module",
2362
- scripts: {
2363
- start: "node server.js",
2364
- },
2365
- dependencies: {
2366
- "@poncho-ai/cli": "^0.1.0",
2367
- },
2368
- },
2369
- null,
2370
- 2,
2371
- );
2372
-
2373
- if (target === "vercel") {
2374
- await mkdir(resolve(outDir, "api"), { recursive: true });
2375
- await copyIfExists(resolve(workingDir, "AGENT.md"), resolve(outDir, "AGENT.md"));
2376
- await copyIfExists(
2377
- resolve(workingDir, "poncho.config.js"),
2378
- resolve(outDir, "poncho.config.js"),
2379
- );
2380
- await copyIfExists(resolve(workingDir, "skills"), resolve(outDir, "skills"));
2381
- await copyIfExists(resolve(workingDir, "tests"), resolve(outDir, "tests"));
2382
- await writeFile(
2383
- resolve(outDir, "vercel.json"),
2384
- JSON.stringify(
2385
- {
2386
- version: 2,
2387
- functions: {
2388
- "api/index.js": {
2389
- includeFiles: "{AGENT.md,poncho.config.js,skills/**,tests/**}",
2390
- },
2391
- },
2392
- routes: [{ src: "/(.*)", dest: "/api/index.js" }],
2393
- },
2394
- null,
2395
- 2,
2396
- ),
2397
- "utf8",
2398
- );
2399
- await buildVercelHandlerBundle(outDir);
2400
- await writeFile(
2401
- resolve(outDir, "package.json"),
2402
- JSON.stringify(
2403
- {
2404
- private: true,
2405
- type: "module",
2406
- engines: {
2407
- node: "20.x",
2408
- },
2409
- dependencies: VERCEL_RUNTIME_DEPENDENCIES,
2410
- },
2411
- null,
2412
- 2,
2413
- ),
2414
- "utf8",
2415
- );
2416
- } else if (target === "docker") {
2417
- await writeFile(
2418
- resolve(outDir, "Dockerfile"),
2419
- `FROM node:20-slim
2420
- WORKDIR /app
2421
- COPY package.json package.json
2422
- COPY AGENT.md AGENT.md
2423
- COPY poncho.config.js poncho.config.js
2424
- COPY skills skills
2425
- COPY tests tests
2426
- COPY .env.example .env.example
2427
- RUN corepack enable && npm install -g @poncho-ai/cli
2428
- COPY server.js server.js
2429
- EXPOSE 3000
2430
- CMD ["node","server.js"]
2431
- `,
2432
- "utf8",
2433
- );
2434
- await writeFile(resolve(outDir, "server.js"), serverEntrypoint, "utf8");
2435
- await writeFile(resolve(outDir, "package.json"), runtimePackageJson, "utf8");
2436
- } else if (target === "lambda") {
2437
- await writeFile(
2438
- resolve(outDir, "lambda-handler.js"),
2439
- `import { startDevServer } from "@poncho-ai/cli";
2440
- let serverPromise;
2441
- export const handler = async (event = {}) => {
2442
- if (!serverPromise) {
2443
- serverPromise = startDevServer(0, { workingDir: process.cwd() });
2444
- }
2445
- const body = JSON.stringify({
2446
- status: "ready",
2447
- route: event.rawPath ?? event.path ?? "/",
2531
+ export const buildTarget = async (
2532
+ workingDir: string,
2533
+ target: string,
2534
+ options?: { force?: boolean },
2535
+ ): Promise<void> => {
2536
+ const normalizedTarget = normalizeDeployTarget(target);
2537
+ const writtenPaths = await scaffoldDeployTarget(workingDir, normalizedTarget, {
2538
+ force: options?.force,
2448
2539
  });
2449
- return { statusCode: 200, headers: { "content-type": "application/json" }, body };
2450
- };
2451
- `,
2452
- "utf8",
2453
- );
2454
- await writeFile(resolve(outDir, "package.json"), runtimePackageJson, "utf8");
2455
- } else if (target === "fly") {
2456
- await writeFile(
2457
- resolve(outDir, "fly.toml"),
2458
- `app = "poncho-app"
2459
- [env]
2460
- PORT = "3000"
2461
- [http_service]
2462
- internal_port = 3000
2463
- force_https = true
2464
- auto_start_machines = true
2465
- auto_stop_machines = "stop"
2466
- min_machines_running = 0
2467
- `,
2468
- "utf8",
2469
- );
2470
- await writeFile(
2471
- resolve(outDir, "Dockerfile"),
2472
- `FROM node:20-slim
2473
- WORKDIR /app
2474
- COPY package.json package.json
2475
- COPY AGENT.md AGENT.md
2476
- COPY poncho.config.js poncho.config.js
2477
- COPY skills skills
2478
- COPY tests tests
2479
- RUN npm install -g @poncho-ai/cli
2480
- COPY server.js server.js
2481
- EXPOSE 3000
2482
- CMD ["node","server.js"]
2483
- `,
2484
- "utf8",
2485
- );
2486
- await writeFile(resolve(outDir, "server.js"), serverEntrypoint, "utf8");
2487
- await writeFile(resolve(outDir, "package.json"), runtimePackageJson, "utf8");
2488
- } else {
2489
- throw new Error(`Unsupported build target: ${target}`);
2540
+ process.stdout.write(`Scaffolded deploy files for ${normalizedTarget}:\n`);
2541
+ for (const filePath of writtenPaths) {
2542
+ process.stdout.write(` - ${filePath}\n`);
2490
2543
  }
2491
-
2492
- process.stdout.write(`Build artifacts generated at ${outDir}\n`);
2493
2544
  };
2494
2545
 
2495
2546
  const normalizeMcpName = (entry: { url?: string; name?: string }): string =>
@@ -2881,9 +2932,10 @@ export const buildCli = (): Command => {
2881
2932
  program
2882
2933
  .command("build")
2883
2934
  .argument("<target>", "vercel|docker|lambda|fly")
2884
- .description("Generate build artifacts for deployment target")
2885
- .action(async (target: string) => {
2886
- await buildTarget(process.cwd(), target);
2935
+ .option("--force", "overwrite existing deployment files")
2936
+ .description("Scaffold deployment files for a target")
2937
+ .action(async (target: string, options: { force?: boolean }) => {
2938
+ await buildTarget(process.cwd(), target, { force: options.force });
2887
2939
  });
2888
2940
 
2889
2941
  const mcpCommand = program.command("mcp").description("Manage MCP servers");
@@ -21,6 +21,7 @@ const bold = (s: string): string => `${C.bold}${s}${C.reset}`;
21
21
  const INPUT_CARET = "»";
22
22
 
23
23
  type OnboardingAnswers = Record<string, string | number | boolean>;
24
+ export type DeployTarget = "none" | "vercel" | "docker" | "fly" | "lambda";
24
25
 
25
26
  export type InitOnboardingOptions = {
26
27
  yes?: boolean;
@@ -33,6 +34,7 @@ export type InitOnboardingResult = {
33
34
  envExample: string;
34
35
  envFile: string;
35
36
  envNeedsUserInput: boolean;
37
+ deployTarget: DeployTarget;
36
38
  agentModel: {
37
39
  provider: "anthropic" | "openai";
38
40
  name: string;
@@ -276,6 +278,14 @@ const askOnboardingQuestions = async (options: InitOnboardingOptions): Promise<O
276
278
  const getProviderModelName = (provider: string): string =>
277
279
  provider === "openai" ? "gpt-4.1" : "claude-opus-4-5";
278
280
 
281
+ const normalizeDeployTarget = (value: unknown): DeployTarget => {
282
+ const target = typeof value === "string" ? value.toLowerCase() : "";
283
+ if (target === "vercel" || target === "docker" || target === "fly" || target === "lambda") {
284
+ return target;
285
+ }
286
+ return "none";
287
+ };
288
+
279
289
  const maybeSet = (
280
290
  target: object,
281
291
  key: string,
@@ -506,6 +516,7 @@ export const runInitOnboarding = async (
506
516
  ): Promise<InitOnboardingResult> => {
507
517
  const answers = await askOnboardingQuestions(options);
508
518
  const provider = String(answers["model.provider"] ?? "anthropic");
519
+ const deployTarget = normalizeDeployTarget(answers["deploy.target"]);
509
520
  const config = buildConfigFromOnboardingAnswers(answers);
510
521
  const envExampleLines = collectEnvVars(answers);
511
522
  const envFileLines = collectEnvFileLines(answers);
@@ -522,6 +533,7 @@ export const runInitOnboarding = async (
522
533
  envExample: `${envExampleLines.join("\n")}\n`,
523
534
  envFile: envFileLines.length > 0 ? `${envFileLines.join("\n")}\n` : "",
524
535
  envNeedsUserInput,
536
+ deployTarget,
525
537
  agentModel: {
526
538
  provider: provider === "openai" ? "openai" : "anthropic",
527
539
  name: getProviderModelName(provider),