Skip to content

Commit 19c51d4

Browse files
author
bigmacfive
committed
Add agent prompt workflow
1 parent 2f866b5 commit 19c51d4

7 files changed

Lines changed: 215 additions & 4 deletions

File tree

README.ko.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,14 @@ appbun https://www.notion.so --package-manager npm
8989
appbun https://github.com --name "GitHub" --out-dir ./github --yes
9090
```
9191

92+
개발 중인 웹서비스 repo에서 에이전트에게 그대로 붙여넣을 프롬프트를 만들려면:
93+
94+
```bash
95+
appbun prompt http://localhost:3000 --name "My App"
96+
```
97+
98+
그러면 에이전트가 현재 웹앱을 `./desktop/my-app` 아래에 `appbun@latest`로 패키징하고 빌드하게 만드는 지시문이 출력됩니다.
99+
92100
## Showcase
93101

94102
로그인 없이 바로 동작하는 공개 웹앱을 Playwright로 캡처하고, 생성되는 shell 느낌에 맞춰 프레임한 예시입니다.

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,14 @@ Skip confirmation prompts in scripted runs:
8989
appbun https://github.com --name "GitHub" --out-dir ./github --yes
9090
```
9191

92+
Generate a copy-paste prompt for a coding agent that is working inside your web app repo:
93+
94+
```bash
95+
appbun prompt http://localhost:3000 --name "My App"
96+
```
97+
98+
That outputs an instruction block telling the agent to package the current web app into `./desktop/my-app` with `appbun@latest`, then build it.
99+
92100
## Showcase
93101

94102
Public no-login web apps captured with Playwright and framed to match the generated shell:

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "appbun",
3-
"version": "0.5.1",
3+
"version": "0.5.2",
44
"description": "Turn any webpage into a desktop app with one command using Electrobun.",
55
"license": "MIT",
66
"type": "module",

src/__tests__/generator.test.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { tmpdir } from "node:os";
44

55
import { afterEach, describe, expect, test } from "bun:test";
66

7+
import { buildAgentPrompt } from "../lib/agent-prompt.js";
78
import { renderTemplateFiles, resolveAppConfig, writeProject } from "../lib/generator.js";
89
import { createFallbackSiteMetadata } from "../lib/metadata.js";
910
import { deriveIdentifier, isDirectoryEmpty, normalizeHexColor, slugify, suggestAlternativeOutputDirectory } from "../lib/utils.js";
@@ -56,6 +57,44 @@ describe("utils", () => {
5657
});
5758

5859
describe("generator", () => {
60+
test("buildAgentPrompt targets an existing web app repo workflow", () => {
61+
const config = resolveAppConfig(
62+
"http://localhost:3000",
63+
{
64+
width: 1440,
65+
height: 900,
66+
packageManager: "bun",
67+
install: false,
68+
dmg: false,
69+
yes: false,
70+
showConfig: false,
71+
quiet: true,
72+
name: "My App",
73+
outDir: "./desktop/my-app",
74+
},
75+
{
76+
title: "My App",
77+
description: "Internal dashboard",
78+
themeColor: "#2255aa",
79+
sourceUrl: "http://localhost:3000",
80+
iconCandidates: [],
81+
},
82+
);
83+
84+
const prompt = buildAgentPrompt({ ...config, outDir: "./desktop/my-app" }, {
85+
title: "My App",
86+
description: "Internal dashboard",
87+
themeColor: "#2255aa",
88+
sourceUrl: "http://localhost:3000",
89+
iconCandidates: [],
90+
});
91+
92+
expect(prompt).toContain("You are working inside the repository of an existing web app.");
93+
expect(prompt).toContain("http://localhost:3000/");
94+
expect(prompt).toContain("./desktop/my-app");
95+
expect(prompt).toContain("npx -y appbun@latest");
96+
});
97+
5998
test("createFallbackSiteMetadata derives defaults from url", () => {
6099
const metadata = createFallbackSiteMetadata("https://chat.openai.com");
61100
expect(metadata.title).toBe("Chat");
@@ -173,5 +212,5 @@ describe("generator", () => {
173212
expect(readFileSync(join(config.outDir, "src", "mainview", "index.ts"), "utf8")).toContain("example.com");
174213
expect(readFileSync(join(config.outDir, "package.json"), "utf8")).toContain("\"build:dmg\"");
175214
expect(icons.sourceUrl).toBe(svgIconDataUrl);
176-
});
215+
}, 10000);
177216
});

src/cli.ts

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
#!/usr/bin/env node
22
import process from "node:process";
33
import { createInterface } from "node:readline/promises";
4+
import { spawnSync } from "node:child_process";
45

56
import pc from "picocolors";
67
import { Command } from "commander";
78

9+
import { buildAgentPrompt } from "./lib/agent-prompt.js";
810
import {
911
findLatestDmg,
1012
installDependencies,
@@ -36,7 +38,7 @@ const program = new Command();
3638
program
3739
.name("appbun")
3840
.description("Generate an Electrobun desktop wrapper from any web app URL.")
39-
.version("0.5.1");
41+
.version("0.5.2");
4042

4143
program
4244
.command("create")
@@ -164,6 +166,76 @@ program
164166
}
165167
});
166168

169+
program
170+
.command("prompt")
171+
.argument("<url>", "web app URL to wrap")
172+
.option("-n, --name <name>", "app display name")
173+
.option("-o, --out-dir <dir>", "desktop wrapper output directory inside the repo")
174+
.option("--title <title>", "desktop window title")
175+
.option("--description <description>", "package description")
176+
.option("--identifier <identifier>", "bundle identifier, for example com.example.app")
177+
.option("--theme-color <hex>", "shell accent color, for example #2563eb")
178+
.option("--width <number>", "window width", parseInteger, defaultOptions.width)
179+
.option("--height <number>", "window height", parseInteger, defaultOptions.height)
180+
.option("--copy", "copy the generated prompt to the clipboard when supported")
181+
.option("--quiet", "reduce metadata logs")
182+
.action(async (url: string, options: CreateCommandOptions & { copy?: boolean }) => {
183+
try {
184+
validatePackageManager(defaultOptions.packageManager);
185+
let usedFallbackMetadata = false;
186+
const metadata = await fetchSiteMetadata(url).catch((error) => {
187+
usedFallbackMetadata = true;
188+
if (!options.quiet) {
189+
const message = error instanceof Error ? error.message : String(error);
190+
console.log(pc.bold(pc.yellow("warning")), `metadata fetch failed, continuing with URL defaults`);
191+
console.log(` reason: ${message}`);
192+
}
193+
return createFallbackSiteMetadata(url);
194+
});
195+
196+
const resolvedOptions = {
197+
...defaultOptions,
198+
...options,
199+
packageManager: defaultOptions.packageManager,
200+
install: false,
201+
dmg: false,
202+
yes: false,
203+
showConfig: false,
204+
};
205+
let config = resolveAppConfig(url, resolvedOptions, metadata);
206+
if (!options.outDir) {
207+
config = {
208+
...config,
209+
outDir: `./desktop/${config.slug}`,
210+
};
211+
}
212+
const prompt = buildAgentPrompt(config, metadata);
213+
214+
if (!options.quiet) {
215+
console.log(pc.bold(pc.cyan("appbun")), "agent prompt");
216+
console.log(` title: ${metadata.title ?? "(not found)"}`);
217+
console.log(` description: ${metadata.description ?? "(not found)"}`);
218+
console.log(` theme color: ${metadata.themeColor ?? "(not found)"}`);
219+
console.log(` icon candidates: ${metadata.iconCandidates.length}`);
220+
console.log(` metadata mode: ${usedFallbackMetadata ? "fallback" : "fetched"}`);
221+
}
222+
223+
if (options.copy) {
224+
copyToClipboard(prompt);
225+
if (!options.quiet) {
226+
console.log(pc.bold(pc.green("copied")), "agent prompt copied to clipboard");
227+
console.log("");
228+
}
229+
}
230+
231+
console.log(prompt);
232+
} catch (error) {
233+
const message = error instanceof Error ? error.message : String(error);
234+
console.error(pc.bold(pc.red("error")), message);
235+
process.exitCode = 1;
236+
}
237+
});
238+
167239
program.addHelpText(
168240
"after",
169241
`
@@ -173,6 +245,7 @@ Examples:
173245
$ appbun create https://chat.openai.com --theme-color #10a37f --width 1600 --height 1000
174246
$ appbun https://chat.openai.com --name ChatGPT --dmg
175247
$ appbun https://github.com --name GitHub --out-dir ./github --yes
248+
$ appbun prompt https://myapp.com --name "My App" --copy
176249
`,
177250
);
178251

@@ -276,3 +349,26 @@ async function askForConfirmation(message: string, defaultYes: boolean): Promise
276349
function isInteractiveSession(): boolean {
277350
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
278351
}
352+
353+
function copyToClipboard(value: string): void {
354+
const command =
355+
process.platform === "darwin"
356+
? { bin: "pbcopy", args: [] }
357+
: process.platform === "win32"
358+
? { bin: "clip", args: [] }
359+
: { bin: "xclip", args: ["-selection", "clipboard"] };
360+
361+
const result = spawnSync(command.bin, command.args, {
362+
input: value,
363+
encoding: "utf8",
364+
stdio: ["pipe", "ignore", "pipe"],
365+
shell: process.platform === "win32",
366+
});
367+
368+
if (result.status !== 0) {
369+
const error = result.stderr?.toString().trim();
370+
throw new Error(
371+
`Clipboard copy failed${error ? `: ${error}` : ""}. Re-run without --copy if clipboard access is unavailable.`,
372+
);
373+
}
374+
}

src/lib/agent-prompt.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import type { ResolvedAppConfig, SiteMetadata } from './types.js';
2+
3+
export function buildAgentPrompt(config: ResolvedAppConfig, metadata: SiteMetadata): string {
4+
const lines = [
5+
'You are working inside the repository of an existing web app.',
6+
'Create a desktop app wrapper for that app using appbun.',
7+
'',
8+
'Project inputs:',
9+
`- Web app URL to package: ${config.url}`,
10+
`- App name: ${config.name}`,
11+
`- Window title: ${config.title}`,
12+
`- Description: ${config.description}`,
13+
`- Package name: ${config.packageName}`,
14+
`- Bundle identifier: ${config.identifier}`,
15+
`- Default window size: ${config.width}x${config.height}`,
16+
`- Theme color: ${config.themeColor}`,
17+
`- Desktop wrapper output directory: ${config.outDir}`,
18+
`- Source metadata title: ${metadata.title ?? config.name}`,
19+
`- Source metadata description: ${metadata.description ?? config.description}`,
20+
'',
21+
'Rules:',
22+
'- Treat the current repository as the source web app project.',
23+
'- Use `appbun@latest`; do not hand-roll the wrapper unless appbun output needs a specific fix.',
24+
'- Keep the generated project inspectable and editable.',
25+
'- Preserve site branding and icon metadata when available.',
26+
'- If the URL points to a local dev server, make sure the dev server is running and reachable before packaging.',
27+
'- Put the generated desktop wrapper inside this repo so the team can version it together.',
28+
'- If the output directory already exists, prefer a safe non-destructive path or explicit confirmation.',
29+
'- On macOS, include the DMG packaging step.',
30+
'',
31+
'Execution plan:',
32+
'1. If needed, start the current repo web app and verify the target URL responds in a browser.',
33+
`2. Run: npx -y appbun@latest ${shellQuote(config.url)} --name ${shellQuote(config.name)} --out-dir ${shellQuote(config.outDir)} --width ${config.width} --height ${config.height} --theme-color ${shellQuote(config.themeColor)} --yes`,
34+
`3. Change into the generated wrapper directory: cd ${shellQuote(config.outDir)}`,
35+
'4. Run: bun install',
36+
'5. Run: bun run build',
37+
'6. On macOS, run: bun run build:dmg',
38+
'7. If the repo needs it, add a short README section explaining how to rebuild the desktop wrapper.',
39+
'',
40+
'Expected result:',
41+
`- A generated Electrobun project in ${config.outDir}`,
42+
'- Buildable macOS, Windows, and Linux wrapper project output',
43+
'- macOS DMG output when packaging is requested',
44+
'',
45+
'When you reply, include:',
46+
'- what command you ran',
47+
'- where the generated project was written',
48+
'- what metadata or icons were detected',
49+
'- what still needs manual attention, if anything',
50+
];
51+
52+
return `${lines.join('\n')}\n`;
53+
}
54+
55+
function shellQuote(value: string): string {
56+
if (/^[a-zA-Z0-9_./:-]+$/.test(value)) {
57+
return value;
58+
}
59+
return JSON.stringify(value);
60+
}

src/lib/metadata.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const USER_AGENT = [
77
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)",
88
"AppleWebKit/537.36 (KHTML, like Gecko)",
99
"Chrome/133.0.0.0 Safari/537.36",
10-
"appbun/0.5.1 (+https://github.com/bigmacfive/appbun)"
10+
"appbun/0.5.2 (+https://github.com/bigmacfive/appbun)"
1111
].join(" ");
1212

1313
export async function fetchSiteMetadata(rawUrl: string): Promise<SiteMetadata> {

0 commit comments

Comments
 (0)