@ghosty-ai/cli 0.1.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/dist/cli.js ADDED
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { readFileSync } from "fs";
5
+ import { fileURLToPath } from "url";
6
+ import { dirname, join } from "path";
7
+ var __dirname = dirname(fileURLToPath(import.meta.url));
8
+ function getVersion() {
9
+ try {
10
+ const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf8"));
11
+ return pkg.version ?? "0.0.0";
12
+ } catch {
13
+ return "0.0.0";
14
+ }
15
+ }
16
+ function printHelp() {
17
+ console.log(`
18
+ ghosty \u2014 Ghosty Plugin Development CLI
19
+
20
+ Usage:
21
+ ghosty create [name] Scaffold a new plugin project
22
+ ghosty dev Start dev server with hot reload
23
+ ghosty --help Show this help
24
+ ghosty --version Show version
25
+
26
+ Docs: https://dev.ghosty.to
27
+ `);
28
+ }
29
+ async function main() {
30
+ const [, , command, ...args] = process.argv;
31
+ switch (command) {
32
+ case "create": {
33
+ const { runCreate } = await import("./create-MLDT2ZRH.js");
34
+ await runCreate(args[0]);
35
+ break;
36
+ }
37
+ case "dev": {
38
+ const { runDev } = await import("./dev-WPS3QYGP.js");
39
+ await runDev();
40
+ break;
41
+ }
42
+ case "--version":
43
+ case "-v":
44
+ console.log(getVersion());
45
+ break;
46
+ case "--help":
47
+ case "-h":
48
+ case void 0:
49
+ printHelp();
50
+ break;
51
+ default:
52
+ console.error(` Unknown command: ${command}
53
+ `);
54
+ printHelp();
55
+ process.exit(1);
56
+ }
57
+ }
58
+ main().catch((err) => {
59
+ console.error(err instanceof Error ? err.message : String(err));
60
+ process.exit(1);
61
+ });
@@ -0,0 +1,153 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/create.ts
4
+ import { mkdirSync, writeFileSync, existsSync } from "fs";
5
+ import { join } from "path";
6
+ import { createInterface } from "readline";
7
+ function prompt(question) {
8
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
9
+ return new Promise((resolve) => {
10
+ rl.question(question, (answer) => {
11
+ rl.close();
12
+ resolve(answer.trim());
13
+ });
14
+ });
15
+ }
16
+ function validateName(name) {
17
+ if (name.length < 1 || name.length > 50) return "Name must be 1-50 characters";
18
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(name)) return "Name must be lowercase alphanumeric with hyphens (e.g. my-plugin)";
19
+ return null;
20
+ }
21
+ var MANIFEST_TEMPLATE = `{
22
+ "version": "1.0.0",
23
+ "entry": "main.ts",
24
+ "permissions": []
25
+ }
26
+ `;
27
+ function mainTsTemplate(name) {
28
+ return `import { createPlugin } from '@ghosty-ai/sdk';
29
+
30
+ const plugin = createPlugin();
31
+
32
+ // Set context for the AI chat overlay
33
+ plugin.setContext({
34
+ title: '${name}',
35
+ data: { message: 'Hello from ${name}!' },
36
+ summary: 'This plugin is running.',
37
+ });
38
+
39
+ // Listen for theme changes
40
+ plugin.onThemeChange((theme) => {
41
+ document.documentElement.setAttribute('data-theme', theme);
42
+ });
43
+
44
+ // Signal that the plugin is ready
45
+ plugin.ready();
46
+
47
+ // Your plugin code goes here
48
+ const app = document.getElementById('app');
49
+ if (app) {
50
+ app.innerHTML = \`
51
+ <div class="container">
52
+ <h1>${name}</h1>
53
+ <p>Edit <code>main.ts</code> and save to see changes.</p>
54
+ </div>
55
+ \`;
56
+ }
57
+ `;
58
+ }
59
+ var STYLES_TEMPLATE = `/* Ghosty Plugin Styles */
60
+
61
+ :root {
62
+ --bg: #ffffff;
63
+ --text: #23272a;
64
+ --text-secondary: #6b7280;
65
+ --surface: #f9fafb;
66
+ --border: #e5e7eb;
67
+ --primary: #5865f2;
68
+ }
69
+
70
+ [data-theme='dark'] {
71
+ --bg: #171717;
72
+ --text: #ffffff;
73
+ --text-secondary: #8b8b8b;
74
+ --surface: #1f1f1f;
75
+ --border: #292929;
76
+ --primary: #7983f5;
77
+ }
78
+
79
+ *, *::before, *::after {
80
+ margin: 0;
81
+ padding: 0;
82
+ box-sizing: border-box;
83
+ }
84
+
85
+ body {
86
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
87
+ background: var(--bg);
88
+ color: var(--text);
89
+ line-height: 1.5;
90
+ transition: background 300ms, color 300ms;
91
+ }
92
+
93
+ .container {
94
+ max-width: 600px;
95
+ margin: 2rem auto;
96
+ padding: 0 1rem;
97
+ }
98
+
99
+ h1 {
100
+ font-size: 1.5rem;
101
+ font-weight: 700;
102
+ margin-bottom: 0.5rem;
103
+ }
104
+
105
+ p {
106
+ color: var(--text-secondary);
107
+ }
108
+
109
+ code {
110
+ background: var(--surface);
111
+ border: 1px solid var(--border);
112
+ border-radius: 4px;
113
+ padding: 0.125rem 0.375rem;
114
+ font-size: 0.875em;
115
+ }
116
+ `;
117
+ async function runCreate(nameArg) {
118
+ let name = nameArg;
119
+ if (!name) {
120
+ name = await prompt(" Plugin name: ");
121
+ }
122
+ if (!name) {
123
+ console.error(" Plugin name is required.");
124
+ process.exit(1);
125
+ }
126
+ const error = validateName(name);
127
+ if (error) {
128
+ console.error(` ${error}`);
129
+ process.exit(1);
130
+ }
131
+ const dir = join(process.cwd(), name);
132
+ if (existsSync(dir)) {
133
+ console.error(` Directory "${name}" already exists.`);
134
+ process.exit(1);
135
+ }
136
+ mkdirSync(dir, { recursive: true });
137
+ writeFileSync(join(dir, "ghosty.manifest.json"), MANIFEST_TEMPLATE);
138
+ writeFileSync(join(dir, "main.ts"), mainTsTemplate(name));
139
+ writeFileSync(join(dir, "styles.css"), STYLES_TEMPLATE);
140
+ console.log(`
141
+ Created plugin project: ${name}/
142
+ ghosty.manifest.json
143
+ main.ts
144
+ styles.css
145
+
146
+ Next steps:
147
+ cd ${name}
148
+ ghosty dev
149
+ `);
150
+ }
151
+ export {
152
+ runCreate
153
+ };
@@ -0,0 +1,608 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/dev.ts
4
+ import { watch } from "chokidar";
5
+ import { exec } from "child_process";
6
+
7
+ // src/build.ts
8
+ import * as esbuild from "esbuild";
9
+ import { readFileSync, existsSync, readdirSync, statSync } from "fs";
10
+ import { join, extname } from "path";
11
+ import { createRequire } from "module";
12
+ var esmRequire = createRequire(import.meta.url);
13
+ var _sdkSource = null;
14
+ function getSdkSource() {
15
+ if (_sdkSource !== null) return _sdkSource;
16
+ try {
17
+ const sdkPath = esmRequire.resolve("@ghosty-ai/sdk/dist/umd/index.global.js");
18
+ _sdkSource = readFileSync(sdkPath, "utf8");
19
+ } catch {
20
+ throw new Error(
21
+ "Could not load @ghosty-ai/sdk UMD bundle.\nIf developing locally: npm run -w @ghosty-ai/sdk build\nIf installed globally: npm install -g @ghosty-ai/sdk"
22
+ );
23
+ }
24
+ return _sdkSource;
25
+ }
26
+ var SDK_SHIM = `
27
+ var __sdk = globalThis.GhostySDK;
28
+ export default __sdk;
29
+ export var createPlugin = __sdk.createPlugin;
30
+ export var isBridgeMessage = __sdk.isBridgeMessage;
31
+ export var sendMessage = __sdk.sendMessage;
32
+ export var sendRequest = __sdk.sendRequest;
33
+ export var sendResponse = __sdk.sendResponse;
34
+ export var onMessage = __sdk.onMessage;
35
+ `.trim();
36
+ function createSdkShimPlugin() {
37
+ return {
38
+ name: "ghosty-sdk-shim",
39
+ setup(build2) {
40
+ build2.onResolve({ filter: /^(@ghosty-ai\/sdk|ghosty-sdk)/ }, () => ({
41
+ path: "ghosty-sdk-shim",
42
+ namespace: "ghosty-shim"
43
+ }));
44
+ build2.onResolve({ filter: /ghosty-sdk(\.min)?\.js$/ }, () => ({
45
+ path: "ghosty-sdk-shim",
46
+ namespace: "ghosty-shim"
47
+ }));
48
+ build2.onLoad({ filter: /.*/, namespace: "ghosty-shim" }, () => ({
49
+ contents: SDK_SHIM,
50
+ loader: "js"
51
+ }));
52
+ build2.onResolve({ filter: /\.css$/ }, (args) => ({
53
+ path: args.path,
54
+ external: true
55
+ }));
56
+ }
57
+ };
58
+ }
59
+ function collectFiles(dir, base = "") {
60
+ const files = /* @__PURE__ */ new Map();
61
+ for (const entry of readdirSync(dir)) {
62
+ if (entry === "node_modules" || entry === ".git" || entry === "dist") continue;
63
+ const fullPath = join(dir, entry);
64
+ const relativePath = base ? `${base}/${entry}` : entry;
65
+ if (statSync(fullPath).isDirectory()) {
66
+ for (const [k, v] of collectFiles(fullPath, relativePath)) {
67
+ files.set(k, v);
68
+ }
69
+ } else {
70
+ files.set(relativePath, fullPath);
71
+ }
72
+ }
73
+ return files;
74
+ }
75
+ function generateHtml(entry, cssFiles) {
76
+ const cssLinks = cssFiles.map((f) => ` <link rel="stylesheet" href="${f}" />`).join("\n");
77
+ return `<!DOCTYPE html>
78
+ <html lang="en">
79
+ <head>
80
+ <meta charset="UTF-8" />
81
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
82
+ <title>Plugin</title>
83
+ ${cssLinks ? cssLinks + "\n" : ""}</head>
84
+ <body>
85
+ <div id="app"></div>
86
+ <script src="${entry}"></script>
87
+ </body>
88
+ </html>`;
89
+ }
90
+ function injectSdk(html) {
91
+ if (/ghosty-sdk(\.min)?\.js/.test(html)) return html;
92
+ const sdkSource = getSdkSource().replace(/<\/script>/gi, "<\\/script>");
93
+ const sdkTag = ` <script>/* Ghosty SDK */${sdkSource}</script>`;
94
+ if (html.includes("</head>")) {
95
+ return html.replace("</head>", `${sdkTag}
96
+ </head>`);
97
+ }
98
+ if (html.includes("<script")) {
99
+ return html.replace("<script", `${sdkTag}
100
+ <script`);
101
+ }
102
+ return html.replace("<body>", `<body>
103
+ ${sdkTag}`);
104
+ }
105
+ async function buildPlugin(cwd) {
106
+ const manifestPath = join(cwd, "ghosty.manifest.json");
107
+ if (!existsSync(manifestPath)) {
108
+ throw new Error("No ghosty.manifest.json found. Run from a plugin directory or use `ghosty create`.");
109
+ }
110
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
111
+ const output = /* @__PURE__ */ new Map();
112
+ const sourceFiles = collectFiles(cwd);
113
+ let html;
114
+ const hasIndex = sourceFiles.has("index.html");
115
+ if (hasIndex) {
116
+ html = readFileSync(sourceFiles.get("index.html"), "utf8");
117
+ } else if (manifest.entry) {
118
+ const cssFiles = [];
119
+ for (const name of sourceFiles.keys()) {
120
+ if (name.endsWith(".css")) cssFiles.push(name);
121
+ }
122
+ cssFiles.sort();
123
+ html = generateHtml(manifest.entry, cssFiles);
124
+ } else {
125
+ throw new Error('Bundle must have index.html or specify "entry" in the manifest.');
126
+ }
127
+ html = injectSdk(html);
128
+ const scriptRegex = /<script\b([^>]*)>/gi;
129
+ const entryPoints = [];
130
+ let scriptMatch;
131
+ while ((scriptMatch = scriptRegex.exec(html)) !== null) {
132
+ const attrs = scriptMatch[1] ?? "";
133
+ const srcMatch = attrs.match(/\bsrc\s*=\s*["']([^"']+)["']/);
134
+ if (!srcMatch) continue;
135
+ const src = srcMatch[1];
136
+ const ext = extname(src);
137
+ const needsBuild = [".ts", ".tsx", ".jsx"].includes(ext) || /\btype\s*=\s*["']module["']/i.test(attrs);
138
+ if (needsBuild) {
139
+ entryPoints.push({ src, original: src });
140
+ }
141
+ }
142
+ if (entryPoints.length === 0) {
143
+ const jsMatch = html.match(/src\s*=\s*["']([^"']+\.m?js)["']/);
144
+ if (jsMatch) {
145
+ const src = jsMatch[1];
146
+ const fullPath = sourceFiles.get(src);
147
+ if (fullPath) {
148
+ const content = readFileSync(fullPath, "utf8");
149
+ if (/\b(import\s+|import\s*\{|export\s+|export\s*\{|export\s+default)\b/.test(content)) {
150
+ entryPoints.push({ src, original: src });
151
+ }
152
+ }
153
+ }
154
+ }
155
+ const consumedFiles = /* @__PURE__ */ new Set();
156
+ for (const ep of entryPoints) {
157
+ const fullPath = sourceFiles.get(ep.src);
158
+ if (!fullPath) {
159
+ throw new Error(`Script "${ep.src}" referenced in HTML not found in bundle.`);
160
+ }
161
+ const result = await esbuild.build({
162
+ entryPoints: [fullPath],
163
+ bundle: true,
164
+ write: false,
165
+ format: "iife",
166
+ target: "es2020",
167
+ platform: "browser",
168
+ minify: false,
169
+ sourcemap: false,
170
+ metafile: true,
171
+ plugins: [createSdkShimPlugin()],
172
+ logLevel: "silent"
173
+ });
174
+ if (result.errors.length > 0) {
175
+ const messages = result.errors.map((e) => {
176
+ const loc = e.location ? `${e.location.file}:${e.location.line}:${e.location.column}` : ep.src;
177
+ return ` ${loc}: ${e.text}`;
178
+ });
179
+ throw new Error(`Build failed:
180
+ ${messages.join("\n")}`);
181
+ }
182
+ const outputFile = result.outputFiles?.[0];
183
+ if (outputFile) {
184
+ const outName = ep.src.replace(/\.(ts|tsx|jsx)$/, ".js");
185
+ output.set(outName, Buffer.from(outputFile.contents));
186
+ if (ep.src !== outName) {
187
+ html = html.split(ep.src).join(outName);
188
+ }
189
+ html = html.replace(
190
+ new RegExp(`(<script[^>]*?)\\s*type\\s*=\\s*["']module["']([^>]*src\\s*=\\s*["']${outName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}["'])`, "i"),
191
+ "$1$2"
192
+ );
193
+ }
194
+ if (result.metafile) {
195
+ for (const inputPath of Object.keys(result.metafile.inputs)) {
196
+ for (const [relPath, absPath] of sourceFiles) {
197
+ if (inputPath === absPath || inputPath.endsWith(relPath)) {
198
+ consumedFiles.add(relPath);
199
+ }
200
+ }
201
+ }
202
+ }
203
+ }
204
+ for (const [relPath, absPath] of sourceFiles) {
205
+ if (consumedFiles.has(relPath)) continue;
206
+ if (relPath === "index.html") continue;
207
+ if (!output.has(relPath)) {
208
+ output.set(relPath, readFileSync(absPath));
209
+ }
210
+ }
211
+ output.set("index.html", Buffer.from(html, "utf8"));
212
+ return { files: output, manifest };
213
+ }
214
+
215
+ // src/shell.ts
216
+ function generateShellHtml(options) {
217
+ const { pluginName, manifest, port } = options;
218
+ const manifestJson = JSON.stringify(manifest);
219
+ return `<!DOCTYPE html>
220
+ <html lang="en">
221
+ <head>
222
+ <meta charset="UTF-8" />
223
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
224
+ <title>Ghosty Dev \u2014 ${pluginName}</title>
225
+ <style>
226
+ * { margin: 0; padding: 0; box-sizing: border-box; }
227
+ body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0d0d0d; color: #fff; height: 100vh; display: flex; flex-direction: column; }
228
+
229
+ .toolbar { display: flex; align-items: center; justify-content: space-between; padding: 0 16px; height: 40px; background: #171717; border-bottom: 1px solid #292929; flex-shrink: 0; }
230
+ .toolbar-left { display: flex; align-items: center; gap: 10px; }
231
+ .toolbar-dot { width: 8px; height: 8px; border-radius: 50%; background: #22c55e; }
232
+ .toolbar-title { font-size: 12px; font-weight: 500; color: rgba(255,255,255,0.5); }
233
+ .toolbar-name { color: rgba(255,255,255,0.8); font-weight: 600; }
234
+ .toolbar-actions { display: flex; gap: 6px; align-items: center; }
235
+ .theme-btn { background: #292929; border: 1px solid #3a3a3a; border-radius: 6px; padding: 4px 12px; color: #ccc; cursor: pointer; font-size: 11px; font-weight: 500; font-family: inherit; transition: all 150ms; }
236
+ .theme-btn:hover { background: #3a3a3a; color: #fff; }
237
+ .theme-btn.light { background: #5865f2; border-color: #5865f2; color: #fff; }
238
+
239
+ .iframe-wrap { flex: 1; position: relative; background: #000; }
240
+ .iframe-wrap iframe { width: 100%; height: 100%; border: 0; }
241
+
242
+ .debug { flex-shrink: 0; background: #111; border-top: 1px solid #292929; display: flex; flex-direction: column; max-height: 220px; transition: max-height 200ms; }
243
+ .debug.collapsed { max-height: 32px; }
244
+ .debug-header { display: flex; align-items: center; justify-content: space-between; padding: 0 16px; height: 32px; background: #171717; cursor: pointer; user-select: none; flex-shrink: 0; }
245
+ .debug-header span { font-size: 11px; font-weight: 600; color: rgba(255,255,255,0.4); }
246
+ .debug-count { background: #292929; padding: 1px 7px; border-radius: 9999px; font-size: 10px; color: rgba(255,255,255,0.5); margin-left: 8px; }
247
+ .debug-log { flex: 1; overflow-y: auto; padding: 6px 16px; font-family: 'SF Mono', 'Cascadia Code', Consolas, monospace; font-size: 11px; line-height: 1.7; }
248
+ .debug-log::-webkit-scrollbar { width: 5px; }
249
+ .debug-log::-webkit-scrollbar-thumb { background: #333; border-radius: 3px; }
250
+ .msg { padding: 1px 0; opacity: 0.8; }
251
+ .msg:hover { opacity: 1; }
252
+ .msg-time { color: rgba(255,255,255,0.2); margin-right: 8px; }
253
+ .msg-in { color: #4ade80; }
254
+ .msg-out { color: #facc15; }
255
+ .msg-sys { color: #7983f5; }
256
+ .msg-type { font-weight: 600; }
257
+ .msg-payload { color: rgba(255,255,255,0.3); margin-left: 6px; }
258
+ </style>
259
+ </head>
260
+ <body>
261
+ <div class="toolbar">
262
+ <div class="toolbar-left">
263
+ <div class="toolbar-dot"></div>
264
+ <span class="toolbar-title"><span class="toolbar-name">${pluginName}</span> \u2014 localhost:${port}</span>
265
+ </div>
266
+ <div class="toolbar-actions">
267
+ <button class="theme-btn" id="themeBtn">Dark</button>
268
+ </div>
269
+ </div>
270
+
271
+ <div class="iframe-wrap">
272
+ <iframe id="frame" src="/plugin/index.html" sandbox="allow-scripts"></iframe>
273
+ </div>
274
+
275
+ <div class="debug" id="debug">
276
+ <div class="debug-header" id="debugToggle">
277
+ <div><span>Bridge</span><span class="debug-count" id="msgCount">0</span></div>
278
+ <span id="arrow">&#9660;</span>
279
+ </div>
280
+ <div class="debug-log" id="log"></div>
281
+ </div>
282
+
283
+ <script>
284
+ var theme = 'dark';
285
+ var manifest = ${manifestJson};
286
+ var frame = document.getElementById('frame');
287
+ var log = document.getElementById('log');
288
+ var msgTotal = 0;
289
+
290
+ function ts() {
291
+ return new Date().toLocaleTimeString('en-US', { hour12: false, fractionalSecondDigits: 3 });
292
+ }
293
+
294
+ function logMsg(dir, type, payload) {
295
+ var el = document.createElement('div');
296
+ el.className = 'msg';
297
+ var cls = dir === 'IN' ? 'msg-in' : dir === 'OUT' ? 'msg-out' : 'msg-sys';
298
+ var pl = payload !== undefined ? JSON.stringify(payload).slice(0, 300) : '';
299
+ el.innerHTML = '<span class="msg-time">' + ts() + '</span><span class="' + cls + ' msg-type">' + dir + ' ' + type + '</span>' + (pl ? '<span class="msg-payload">' + pl + '</span>' : '');
300
+ log.appendChild(el);
301
+ log.scrollTop = log.scrollHeight;
302
+ msgTotal++;
303
+ document.getElementById('msgCount').textContent = msgTotal;
304
+ }
305
+
306
+ function send(type, messageId, payload) {
307
+ if (!frame.contentWindow) return;
308
+ frame.contentWindow.postMessage({ protocol: 'ghosty-bridge', version: 1, type: type, messageId: messageId, payload: payload }, '*');
309
+ logMsg('OUT', type, payload);
310
+ }
311
+
312
+ window.addEventListener('message', function(e) {
313
+ var msg = e.data;
314
+ if (!msg || msg.protocol !== 'ghosty-bridge' || msg.version !== 1) return;
315
+ if (e.source !== frame.contentWindow) return;
316
+
317
+ logMsg('IN', msg.type, msg.payload);
318
+
319
+ switch (msg.type) {
320
+ case 'GHOSTY_READY':
321
+ send('GHOSTY_THEME_CHANGE', 'init-' + Date.now(), { theme: theme });
322
+ break;
323
+
324
+ case 'GHOSTY_SET_CONTEXT':
325
+ break;
326
+
327
+ case 'GHOSTY_GET_USER':
328
+ send('GHOSTY_GET_USER_RESPONSE', msg.messageId, { displayName: 'Developer', avatarUrl: null, theme: theme });
329
+ break;
330
+
331
+ case 'GHOSTY_GET_MANIFEST':
332
+ send('GHOSTY_GET_MANIFEST_RESPONSE', msg.messageId, manifest);
333
+ break;
334
+
335
+ case 'GHOSTY_GET_MEDIA_BASE':
336
+ send('GHOSTY_GET_MEDIA_BASE_RESPONSE', msg.messageId, { apiBaseUrl: 'http://localhost:${port}', pluginId: 'dev' });
337
+ break;
338
+
339
+ case 'GHOSTY_FETCH':
340
+ var url = (msg.payload && msg.payload.url) || '';
341
+ var opts = (msg.payload && msg.payload.options) || {};
342
+ if (!url) {
343
+ send('GHOSTY_FETCH_RESPONSE', msg.messageId, { ok: false, status: 0, data: 'No URL', contentType: '' });
344
+ break;
345
+ }
346
+ fetch('/proxy?url=' + encodeURIComponent(url), {
347
+ method: opts.method || 'GET',
348
+ headers: Object.assign({ 'x-proxy-method': opts.method || 'GET' }, opts.headers || {}),
349
+ body: opts.body ? JSON.stringify(opts.body) : undefined
350
+ })
351
+ .then(function(res) { return res.json(); })
352
+ .then(function(data) { send('GHOSTY_FETCH_RESPONSE', msg.messageId, data); })
353
+ .catch(function(err) {
354
+ send('GHOSTY_FETCH_RESPONSE', msg.messageId, { ok: false, status: 0, data: err.message, contentType: '' });
355
+ });
356
+ break;
357
+
358
+ case 'GHOSTY_REGISTER_TOOLS':
359
+ break;
360
+
361
+ case 'GHOSTY_TOOL_CALL_RESPONSE':
362
+ break;
363
+ }
364
+ });
365
+
366
+ // Theme toggle
367
+ var themeBtn = document.getElementById('themeBtn');
368
+ themeBtn.addEventListener('click', function() {
369
+ theme = theme === 'dark' ? 'light' : 'dark';
370
+ themeBtn.textContent = theme === 'dark' ? 'Dark' : 'Light';
371
+ themeBtn.classList.toggle('light', theme === 'light');
372
+ send('GHOSTY_THEME_CHANGE', 'theme-' + Date.now(), { theme: theme });
373
+ });
374
+
375
+ // Debug toggle
376
+ var debugPanel = document.getElementById('debug');
377
+ document.getElementById('debugToggle').addEventListener('click', function() {
378
+ debugPanel.classList.toggle('collapsed');
379
+ document.getElementById('arrow').innerHTML = debugPanel.classList.contains('collapsed') ? '&#9650;' : '&#9660;';
380
+ });
381
+
382
+ // Hot reload
383
+ (function connect() {
384
+ var ws = new WebSocket('ws://localhost:${port}/__ws');
385
+ ws.onmessage = function(e) {
386
+ if (e.data === 'reload') {
387
+ frame.src = frame.src;
388
+ logMsg('SYS', 'HOT_RELOAD', { message: 'Rebuilt, reloading...' });
389
+ }
390
+ if (e.data.startsWith('error:')) {
391
+ logMsg('SYS', 'BUILD_ERROR', { message: e.data.slice(6) });
392
+ }
393
+ };
394
+ ws.onclose = function() { setTimeout(connect, 1000); };
395
+ })();
396
+ </script>
397
+ </body>
398
+ </html>`;
399
+ }
400
+
401
+ // src/serve.ts
402
+ import { createServer } from "http";
403
+ import { WebSocketServer, WebSocket } from "ws";
404
+ var MIME = {
405
+ ".html": "text/html; charset=utf-8",
406
+ ".css": "text/css; charset=utf-8",
407
+ ".js": "application/javascript; charset=utf-8",
408
+ ".mjs": "application/javascript; charset=utf-8",
409
+ ".json": "application/json; charset=utf-8",
410
+ ".png": "image/png",
411
+ ".jpg": "image/jpeg",
412
+ ".jpeg": "image/jpeg",
413
+ ".svg": "image/svg+xml",
414
+ ".gif": "image/gif",
415
+ ".webp": "image/webp",
416
+ ".ico": "image/x-icon",
417
+ ".woff": "font/woff",
418
+ ".woff2": "font/woff2",
419
+ ".ttf": "font/ttf"
420
+ };
421
+ function mimeType(path) {
422
+ const ext = path.slice(path.lastIndexOf("."));
423
+ return MIME[ext] ?? "application/octet-stream";
424
+ }
425
+ function startServer(options) {
426
+ const { shellHtml, getPluginFiles } = options;
427
+ let resolvedPort = options.port;
428
+ const wss = new WebSocketServer({ noServer: true });
429
+ const server = createServer(async (req, res) => {
430
+ const url = new URL(req.url ?? "/", `http://localhost:${resolvedPort}`);
431
+ const pathname = url.pathname;
432
+ if (pathname === "/" || pathname === "/index.html") {
433
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
434
+ res.end(shellHtml);
435
+ return;
436
+ }
437
+ if (pathname.startsWith("/plugin/")) {
438
+ const filePath = pathname.slice("/plugin/".length) || "index.html";
439
+ const files = getPluginFiles();
440
+ const data = files.get(filePath);
441
+ if (data) {
442
+ res.writeHead(200, {
443
+ "Content-Type": mimeType(filePath),
444
+ "Cache-Control": "no-cache"
445
+ });
446
+ res.end(data);
447
+ } else {
448
+ res.writeHead(404);
449
+ res.end("Not found");
450
+ }
451
+ return;
452
+ }
453
+ if (pathname === "/proxy") {
454
+ const targetUrl = url.searchParams.get("url");
455
+ if (!targetUrl) {
456
+ res.writeHead(400, { "Content-Type": "application/json" });
457
+ res.end(JSON.stringify({ ok: false, status: 0, data: "Missing url param", contentType: "" }));
458
+ return;
459
+ }
460
+ try {
461
+ const method = req.headers["x-proxy-method"] || "GET";
462
+ const proxyRes = await fetch(targetUrl, { method });
463
+ const contentType = proxyRes.headers.get("content-type") ?? "";
464
+ let data;
465
+ if (contentType.includes("json")) {
466
+ data = await proxyRes.json();
467
+ } else {
468
+ data = await proxyRes.text();
469
+ }
470
+ res.writeHead(200, { "Content-Type": "application/json" });
471
+ res.end(JSON.stringify({ ok: proxyRes.ok, status: proxyRes.status, data, contentType }));
472
+ } catch (err) {
473
+ const message = err instanceof Error ? err.message : String(err);
474
+ res.writeHead(200, { "Content-Type": "application/json" });
475
+ res.end(JSON.stringify({ ok: false, status: 0, data: message, contentType: "" }));
476
+ }
477
+ return;
478
+ }
479
+ res.writeHead(404);
480
+ res.end("Not found");
481
+ });
482
+ server.on("upgrade", (req, socket, head) => {
483
+ const pathname = new URL(req.url ?? "/", `http://localhost:${resolvedPort}`).pathname;
484
+ if (pathname === "/__ws") {
485
+ wss.handleUpgrade(req, socket, head, (ws) => {
486
+ wss.emit("connection", ws, req);
487
+ });
488
+ } else {
489
+ socket.destroy();
490
+ }
491
+ });
492
+ function broadcastReload() {
493
+ for (const client of wss.clients) {
494
+ if (client.readyState === WebSocket.OPEN) {
495
+ client.send("reload");
496
+ }
497
+ }
498
+ }
499
+ function broadcastError(message) {
500
+ for (const client of wss.clients) {
501
+ if (client.readyState === WebSocket.OPEN) {
502
+ client.send(`error:${message}`);
503
+ }
504
+ }
505
+ }
506
+ return new Promise((resolve, reject) => {
507
+ function tryListen(port, attempts) {
508
+ server.listen(port, () => {
509
+ resolvedPort = port;
510
+ resolve({
511
+ port,
512
+ broadcastReload,
513
+ broadcastError,
514
+ close: () => {
515
+ wss.close();
516
+ server.close();
517
+ }
518
+ });
519
+ });
520
+ server.once("error", (err) => {
521
+ if (err.code === "EADDRINUSE" && attempts < 10) {
522
+ server.removeAllListeners("error");
523
+ tryListen(port + 1, attempts + 1);
524
+ } else {
525
+ reject(err);
526
+ }
527
+ });
528
+ }
529
+ tryListen(options.port, 0);
530
+ });
531
+ }
532
+
533
+ // src/dev.ts
534
+ var DEFAULT_PORT = 3100;
535
+ function openBrowser(url) {
536
+ const cmd = process.platform === "win32" ? `start "" "${url}"` : process.platform === "darwin" ? `open "${url}"` : `xdg-open "${url}"`;
537
+ exec(cmd, () => {
538
+ });
539
+ }
540
+ async function runDev() {
541
+ const cwd = process.cwd();
542
+ console.log("\n Building plugin...\n");
543
+ let buildResult;
544
+ try {
545
+ buildResult = await buildPlugin(cwd);
546
+ } catch (err) {
547
+ console.error(` ${err instanceof Error ? err.message : String(err)}`);
548
+ process.exit(1);
549
+ }
550
+ const pluginName = buildResult.manifest.entry ?? "Plugin";
551
+ const shell = generateShellHtml({
552
+ pluginName,
553
+ manifest: buildResult.manifest,
554
+ port: DEFAULT_PORT
555
+ });
556
+ const server = await startServer({
557
+ port: DEFAULT_PORT,
558
+ shellHtml: shell,
559
+ getPluginFiles: () => buildResult.files
560
+ });
561
+ console.log(` Ghosty Dev Server
562
+ `);
563
+ console.log(` Preview: http://localhost:${server.port}`);
564
+ console.log(` Plugin: http://localhost:${server.port}/plugin/index.html
565
+ `);
566
+ console.log(` Watching for changes...
567
+ `);
568
+ openBrowser(`http://localhost:${server.port}`);
569
+ let rebuildTimer = null;
570
+ const watcher = watch(
571
+ ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "**/*.css", "**/*.html", "**/*.json"],
572
+ {
573
+ cwd,
574
+ ignored: ["**/node_modules/**", "**/dist/**", "**/.git/**"],
575
+ ignoreInitial: true
576
+ }
577
+ );
578
+ watcher.on("all", (event, path) => {
579
+ if (rebuildTimer) clearTimeout(rebuildTimer);
580
+ rebuildTimer = setTimeout(async () => {
581
+ const time = (/* @__PURE__ */ new Date()).toLocaleTimeString("en-US", { hour12: false });
582
+ console.log(` [${time}] Change detected: ${path}`);
583
+ try {
584
+ buildResult = await buildPlugin(cwd);
585
+ server.broadcastReload();
586
+ console.log(` [${time}] Rebuilt successfully`);
587
+ } catch (err) {
588
+ const message = err instanceof Error ? err.message : String(err);
589
+ console.error(` [${time}] Build error: ${message}`);
590
+ server.broadcastError(message);
591
+ }
592
+ }, 100);
593
+ });
594
+ process.on("SIGINT", () => {
595
+ console.log("\n Shutting down...\n");
596
+ watcher.close();
597
+ server.close();
598
+ process.exit(0);
599
+ });
600
+ process.on("SIGTERM", () => {
601
+ watcher.close();
602
+ server.close();
603
+ process.exit(0);
604
+ });
605
+ }
606
+ export {
607
+ runDev
608
+ };
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@ghosty-ai/cli",
3
+ "version": "0.1.0",
4
+ "description": "CLI tools for Ghosty plugin development — scaffold, preview, and test plugins locally.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "ghosty": "dist/cli.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "README.md"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsup",
16
+ "typecheck": "tsc --noEmit"
17
+ },
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "dependencies": {
22
+ "@ghosty-ai/sdk": "*",
23
+ "chokidar": "^4.0.3",
24
+ "esbuild": "^0.25.0",
25
+ "ws": "^8.18.0"
26
+ },
27
+ "devDependencies": {
28
+ "@types/ws": "^8.18.1",
29
+ "tsup": "^8.5.1",
30
+ "typescript": "^5.7.3"
31
+ }
32
+ }