@poncho-ai/cli 0.8.3 → 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.
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 {
@@ -493,7 +494,7 @@ ${name}/
493
494
  \`\`\`bash
494
495
  # Build for Vercel
495
496
  poncho build vercel
496
- cd .poncho-build/vercel && vercel deploy --prod
497
+ vercel deploy --prod
497
498
 
498
499
  # Build for Docker
499
500
  poncho build docker
@@ -506,19 +507,7 @@ https://github.com/cesr/poncho-ai
506
507
 
507
508
  const ENV_TEMPLATE = "ANTHROPIC_API_KEY=sk-ant-...\n";
508
509
  const GITIGNORE_TEMPLATE =
509
- ".env\nnode_modules\ndist\n.poncho-build\n.poncho/\ninteractive-session.json\n";
510
- const VERCEL_RUNTIME_DEPENDENCIES: Record<string, string> = {
511
- "@anthropic-ai/sdk": "^0.74.0",
512
- "@aws-sdk/client-dynamodb": "^3.988.0",
513
- "@latitude-data/telemetry": "^2.0.2",
514
- commander: "^12.0.0",
515
- dotenv: "^16.4.0",
516
- jiti: "^2.6.1",
517
- mustache: "^4.2.0",
518
- openai: "^6.3.0",
519
- redis: "^5.10.0",
520
- yaml: "^2.8.1",
521
- };
510
+ ".env\nnode_modules\ndist\n.poncho/\ninteractive-session.json\n.vercel\n";
522
511
  const TEST_TEMPLATE = `tests:
523
512
  - name: "Basic sanity"
524
513
  task: "What is 2 + 2?"
@@ -617,35 +606,106 @@ const ensureFile = async (path: string, content: string): Promise<void> => {
617
606
  await writeFile(path, content, { encoding: "utf8", flag: "wx" });
618
607
  };
619
608
 
620
- const copyIfExists = async (sourcePath: string, destinationPath: string): Promise<void> => {
621
- try {
622
- await access(sourcePath);
623
- } catch {
624
- 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;
625
620
  }
626
- await mkdir(dirname(destinationPath), { recursive: true });
627
- // Build outputs should contain materialized files, not symlinks to paths
628
- // that may not exist inside deployment artifacts (e.g. .agents/skills/*).
629
- await cp(sourcePath, destinationPath, { recursive: true, dereference: true });
621
+ throw new Error(`Unsupported build target: ${target}`);
630
622
  };
631
623
 
632
- const resolveCliEntrypoint = async (): Promise<string> => {
633
- const sourceEntrypoint = resolve(packageRoot, "src", "index.ts");
624
+ const readCliVersion = async (): Promise<string> => {
625
+ const fallback = "0.1.0";
634
626
  try {
635
- await access(sourceEntrypoint);
636
- 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
+ }
637
633
  } catch {
638
- return resolve(packageRoot, "dist", "index.js");
634
+ // Use fallback when package metadata cannot be read.
639
635
  }
636
+ return fallback;
640
637
  };
641
638
 
642
- const buildVercelHandlerBundle = async (outDir: string): Promise<void> => {
643
- const { build: esbuild } = await import("esbuild");
644
- const cliEntrypoint = await resolveCliEntrypoint();
645
- const tempEntry = resolve(outDir, "api", "_entry.js");
646
- await writeFile(
647
- tempEntry,
648
- `import { createRequestHandler } from ${JSON.stringify(cliEntrypoint)};
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
+ }
657
+ }
658
+ await mkdir(dirname(filePath), { recursive: true });
659
+ await writeFile(filePath, content, "utf8");
660
+ options.writtenPaths.push(relative(options.baseDir, filePath));
661
+ };
662
+
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";
649
709
  let handlerPromise;
650
710
  export default async function handler(req, res) {
651
711
  try {
@@ -663,68 +723,114 @@ export default async function handler(req, res) {
663
723
  }
664
724
  }
665
725
  `,
666
- "utf8",
667
- );
668
- await esbuild({
669
- entryPoints: [tempEntry],
670
- bundle: true,
671
- platform: "node",
672
- format: "esm",
673
- target: "node20",
674
- outfile: resolve(outDir, "api", "index.js"),
675
- sourcemap: false,
676
- legalComments: "none",
677
- external: [
678
- ...Object.keys(VERCEL_RUNTIME_DEPENDENCIES),
679
- "@anthropic-ai/sdk/*",
680
- "child_process",
681
- "fs",
682
- "fs/promises",
683
- "http",
684
- "https",
685
- "path",
686
- "module",
687
- "url",
688
- "readline",
689
- "readline/promises",
690
- "crypto",
691
- "stream",
692
- "events",
693
- "util",
694
- "os",
695
- "zlib",
696
- "net",
697
- "tls",
698
- "dns",
699
- "assert",
700
- "buffer",
701
- "timers",
702
- "timers/promises",
703
- "node:child_process",
704
- "node:fs",
705
- "node:fs/promises",
706
- "node:http",
707
- "node:https",
708
- "node:path",
709
- "node:module",
710
- "node:url",
711
- "node:readline",
712
- "node:readline/promises",
713
- "node:crypto",
714
- "node:stream",
715
- "node:events",
716
- "node:util",
717
- "node:os",
718
- "node:zlib",
719
- "node:net",
720
- "node:tls",
721
- "node:dns",
722
- "node:assert",
723
- "node:buffer",
724
- "node:timers",
725
- "node:timers/promises",
726
- ],
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 ?? "/",
727
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;
728
834
  };
729
835
 
730
836
  const renderConfigFile = (config: PonchoConfig): string =>
@@ -844,6 +950,13 @@ export const initProject = async (
844
950
  process.stdout.write(` ${D}+${R} ${D}${file.path}${R}\n`);
845
951
  }
846
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
+
847
960
  await initializeOnboardingMarker(projectDir, {
848
961
  allowIntro: !(onboardingOptions.yes ?? false),
849
962
  });
@@ -2415,150 +2528,19 @@ export const runTests = async (
2415
2528
  return { passed, failed };
2416
2529
  };
2417
2530
 
2418
- export const buildTarget = async (workingDir: string, target: string): Promise<void> => {
2419
- const outDir = resolve(workingDir, ".poncho-build", target);
2420
- await mkdir(outDir, { recursive: true });
2421
- const serverEntrypoint = `import { startDevServer } from "@poncho-ai/cli";
2422
-
2423
- const port = Number.parseInt(process.env.PORT ?? "3000", 10);
2424
- await startDevServer(Number.isNaN(port) ? 3000 : port, { workingDir: process.cwd() });
2425
- `;
2426
- const runtimePackageJson = JSON.stringify(
2427
- {
2428
- name: "poncho-runtime-bundle",
2429
- private: true,
2430
- type: "module",
2431
- scripts: {
2432
- start: "node server.js",
2433
- },
2434
- dependencies: {
2435
- "@poncho-ai/cli": "^0.1.0",
2436
- },
2437
- },
2438
- null,
2439
- 2,
2440
- );
2441
-
2442
- if (target === "vercel") {
2443
- await mkdir(resolve(outDir, "api"), { recursive: true });
2444
- await copyIfExists(resolve(workingDir, "AGENT.md"), resolve(outDir, "AGENT.md"));
2445
- await copyIfExists(
2446
- resolve(workingDir, "poncho.config.js"),
2447
- resolve(outDir, "poncho.config.js"),
2448
- );
2449
- await copyIfExists(resolve(workingDir, "skills"), resolve(outDir, "skills"));
2450
- await copyIfExists(resolve(workingDir, "tests"), resolve(outDir, "tests"));
2451
- await writeFile(
2452
- resolve(outDir, "vercel.json"),
2453
- JSON.stringify(
2454
- {
2455
- version: 2,
2456
- functions: {
2457
- "api/index.js": {
2458
- includeFiles: "{AGENT.md,poncho.config.js,skills/**,tests/**}",
2459
- },
2460
- },
2461
- routes: [{ src: "/(.*)", dest: "/api/index.js" }],
2462
- },
2463
- null,
2464
- 2,
2465
- ),
2466
- "utf8",
2467
- );
2468
- await buildVercelHandlerBundle(outDir);
2469
- await writeFile(
2470
- resolve(outDir, "package.json"),
2471
- JSON.stringify(
2472
- {
2473
- private: true,
2474
- type: "module",
2475
- engines: {
2476
- node: "20.x",
2477
- },
2478
- dependencies: VERCEL_RUNTIME_DEPENDENCIES,
2479
- },
2480
- null,
2481
- 2,
2482
- ),
2483
- "utf8",
2484
- );
2485
- } else if (target === "docker") {
2486
- await writeFile(
2487
- resolve(outDir, "Dockerfile"),
2488
- `FROM node:20-slim
2489
- WORKDIR /app
2490
- COPY package.json package.json
2491
- COPY AGENT.md AGENT.md
2492
- COPY poncho.config.js poncho.config.js
2493
- COPY skills skills
2494
- COPY tests tests
2495
- COPY .env.example .env.example
2496
- RUN corepack enable && npm install -g @poncho-ai/cli
2497
- COPY server.js server.js
2498
- EXPOSE 3000
2499
- CMD ["node","server.js"]
2500
- `,
2501
- "utf8",
2502
- );
2503
- await writeFile(resolve(outDir, "server.js"), serverEntrypoint, "utf8");
2504
- await writeFile(resolve(outDir, "package.json"), runtimePackageJson, "utf8");
2505
- } else if (target === "lambda") {
2506
- await writeFile(
2507
- resolve(outDir, "lambda-handler.js"),
2508
- `import { startDevServer } from "@poncho-ai/cli";
2509
- let serverPromise;
2510
- export const handler = async (event = {}) => {
2511
- if (!serverPromise) {
2512
- serverPromise = startDevServer(0, { workingDir: process.cwd() });
2513
- }
2514
- const body = JSON.stringify({
2515
- status: "ready",
2516
- 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,
2517
2539
  });
2518
- return { statusCode: 200, headers: { "content-type": "application/json" }, body };
2519
- };
2520
- `,
2521
- "utf8",
2522
- );
2523
- await writeFile(resolve(outDir, "package.json"), runtimePackageJson, "utf8");
2524
- } else if (target === "fly") {
2525
- await writeFile(
2526
- resolve(outDir, "fly.toml"),
2527
- `app = "poncho-app"
2528
- [env]
2529
- PORT = "3000"
2530
- [http_service]
2531
- internal_port = 3000
2532
- force_https = true
2533
- auto_start_machines = true
2534
- auto_stop_machines = "stop"
2535
- min_machines_running = 0
2536
- `,
2537
- "utf8",
2538
- );
2539
- await writeFile(
2540
- resolve(outDir, "Dockerfile"),
2541
- `FROM node:20-slim
2542
- WORKDIR /app
2543
- COPY package.json package.json
2544
- COPY AGENT.md AGENT.md
2545
- COPY poncho.config.js poncho.config.js
2546
- COPY skills skills
2547
- COPY tests tests
2548
- RUN npm install -g @poncho-ai/cli
2549
- COPY server.js server.js
2550
- EXPOSE 3000
2551
- CMD ["node","server.js"]
2552
- `,
2553
- "utf8",
2554
- );
2555
- await writeFile(resolve(outDir, "server.js"), serverEntrypoint, "utf8");
2556
- await writeFile(resolve(outDir, "package.json"), runtimePackageJson, "utf8");
2557
- } else {
2558
- 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`);
2559
2543
  }
2560
-
2561
- process.stdout.write(`Build artifacts generated at ${outDir}\n`);
2562
2544
  };
2563
2545
 
2564
2546
  const normalizeMcpName = (entry: { url?: string; name?: string }): string =>
@@ -2950,9 +2932,10 @@ export const buildCli = (): Command => {
2950
2932
  program
2951
2933
  .command("build")
2952
2934
  .argument("<target>", "vercel|docker|lambda|fly")
2953
- .description("Generate build artifacts for deployment target")
2954
- .action(async (target: string) => {
2955
- 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 });
2956
2939
  });
2957
2940
 
2958
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),