@byfungsi/funforge 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.
Files changed (59) hide show
  1. package/README.md +273 -0
  2. package/dist/__tests__/api.test.d.ts +5 -0
  3. package/dist/__tests__/api.test.d.ts.map +1 -0
  4. package/dist/__tests__/api.test.js +177 -0
  5. package/dist/__tests__/config.test.d.ts +5 -0
  6. package/dist/__tests__/config.test.d.ts.map +1 -0
  7. package/dist/__tests__/config.test.js +58 -0
  8. package/dist/__tests__/mcp.test.d.ts +7 -0
  9. package/dist/__tests__/mcp.test.d.ts.map +1 -0
  10. package/dist/__tests__/mcp.test.js +142 -0
  11. package/dist/__tests__/project-config.test.d.ts +5 -0
  12. package/dist/__tests__/project-config.test.d.ts.map +1 -0
  13. package/dist/__tests__/project-config.test.js +122 -0
  14. package/dist/__tests__/tarball.test.d.ts +5 -0
  15. package/dist/__tests__/tarball.test.d.ts.map +1 -0
  16. package/dist/__tests__/tarball.test.js +113 -0
  17. package/dist/api.d.ts +157 -0
  18. package/dist/api.d.ts.map +1 -0
  19. package/dist/api.js +165 -0
  20. package/dist/cli.d.ts +8 -0
  21. package/dist/cli.d.ts.map +1 -0
  22. package/dist/cli.js +129 -0
  23. package/dist/commands/apps.d.ts +29 -0
  24. package/dist/commands/apps.d.ts.map +1 -0
  25. package/dist/commands/apps.js +151 -0
  26. package/dist/commands/auth.d.ts +27 -0
  27. package/dist/commands/auth.d.ts.map +1 -0
  28. package/dist/commands/auth.js +127 -0
  29. package/dist/commands/config.d.ts +31 -0
  30. package/dist/commands/config.d.ts.map +1 -0
  31. package/dist/commands/config.js +287 -0
  32. package/dist/commands/deploy.d.ts +24 -0
  33. package/dist/commands/deploy.d.ts.map +1 -0
  34. package/dist/commands/deploy.js +196 -0
  35. package/dist/commands/domains.d.ts +35 -0
  36. package/dist/commands/domains.d.ts.map +1 -0
  37. package/dist/commands/domains.js +217 -0
  38. package/dist/commands/env.d.ts +26 -0
  39. package/dist/commands/env.d.ts.map +1 -0
  40. package/dist/commands/env.js +183 -0
  41. package/dist/config.d.ts +22 -0
  42. package/dist/config.d.ts.map +1 -0
  43. package/dist/config.js +23 -0
  44. package/dist/credentials.d.ts +46 -0
  45. package/dist/credentials.d.ts.map +1 -0
  46. package/dist/credentials.js +60 -0
  47. package/dist/index.d.ts +14 -0
  48. package/dist/index.d.ts.map +1 -0
  49. package/dist/index.js +18 -0
  50. package/dist/mcp.d.ts +19 -0
  51. package/dist/mcp.d.ts.map +1 -0
  52. package/dist/mcp.js +480 -0
  53. package/dist/project-config.d.ts +47 -0
  54. package/dist/project-config.d.ts.map +1 -0
  55. package/dist/project-config.js +55 -0
  56. package/dist/tarball.d.ts +29 -0
  57. package/dist/tarball.d.ts.map +1 -0
  58. package/dist/tarball.js +148 -0
  59. package/package.json +45 -0
package/dist/mcp.js ADDED
@@ -0,0 +1,480 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * FunForge MCP Server
4
+ *
5
+ * Model Context Protocol server for editor integration.
6
+ * Exposes FunForge CLI functionality as MCP tools.
7
+ *
8
+ * Usage in claude_desktop_config.json:
9
+ * {
10
+ * "mcpServers": {
11
+ * "funforge": {
12
+ * "command": "npx",
13
+ * "args": ["@byfungsi/funforge-mcp"]
14
+ * }
15
+ * }
16
+ * }
17
+ */
18
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
19
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
20
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
21
+ import { ApiError, apiRequest, createApp, createDeployment, getUploadUrl, listApps, uploadTarball, } from "./api.js";
22
+ import { getCredentials, isAuthenticated } from "./credentials.js";
23
+ import { getLinkedAppId, readConfig } from "./project-config.js";
24
+ import { createTarball, formatSize, readTarball } from "./tarball.js";
25
+ // ============================================
26
+ // TOOL DEFINITIONS
27
+ // ============================================
28
+ const TOOLS = [
29
+ {
30
+ name: "funforge_whoami",
31
+ description: "Show the currently authenticated FunForge user. Returns email and user ID.",
32
+ inputSchema: {
33
+ type: "object",
34
+ properties: {},
35
+ required: [],
36
+ },
37
+ },
38
+ {
39
+ name: "funforge_apps_list",
40
+ description: "List all apps owned by the authenticated user. Returns app names, IDs, and slugs.",
41
+ inputSchema: {
42
+ type: "object",
43
+ properties: {},
44
+ required: [],
45
+ },
46
+ },
47
+ {
48
+ name: "funforge_apps_create",
49
+ description: "Create a new FunForge app.",
50
+ inputSchema: {
51
+ type: "object",
52
+ properties: {
53
+ name: {
54
+ type: "string",
55
+ description: "Name of the app",
56
+ },
57
+ slug: {
58
+ type: "string",
59
+ description: "Custom slug/subdomain (optional, auto-generated if not provided)",
60
+ },
61
+ },
62
+ required: ["name"],
63
+ },
64
+ },
65
+ {
66
+ name: "funforge_status",
67
+ description: "Get the deployment status for the linked app in the current directory. Shows current and active deployments.",
68
+ inputSchema: {
69
+ type: "object",
70
+ properties: {
71
+ directory: {
72
+ type: "string",
73
+ description: "Directory to check (defaults to current working directory)",
74
+ },
75
+ },
76
+ required: [],
77
+ },
78
+ },
79
+ {
80
+ name: "funforge_deploy",
81
+ description: "Deploy the current directory to FunForge. Creates a tarball of project files, uploads to R2, and triggers deployment. The directory must be linked to an app via funforge.json.",
82
+ inputSchema: {
83
+ type: "object",
84
+ properties: {
85
+ directory: {
86
+ type: "string",
87
+ description: "Directory to deploy (defaults to current working directory)",
88
+ },
89
+ },
90
+ required: [],
91
+ },
92
+ },
93
+ {
94
+ name: "funforge_env_list",
95
+ description: "List environment variables for the linked app. Shows both app-level and user-level (inherited) env vars.",
96
+ inputSchema: {
97
+ type: "object",
98
+ properties: {
99
+ directory: {
100
+ type: "string",
101
+ description: "Directory with funforge.json (defaults to cwd)",
102
+ },
103
+ },
104
+ required: [],
105
+ },
106
+ },
107
+ {
108
+ name: "funforge_env_set",
109
+ description: "Set environment variables for the linked app. Changes take effect on next deployment.",
110
+ inputSchema: {
111
+ type: "object",
112
+ properties: {
113
+ directory: {
114
+ type: "string",
115
+ description: "Directory with funforge.json (defaults to cwd)",
116
+ },
117
+ envVars: {
118
+ type: "object",
119
+ description: "Key-value pairs of environment variables to set",
120
+ additionalProperties: { type: "string" },
121
+ },
122
+ },
123
+ required: ["envVars"],
124
+ },
125
+ },
126
+ {
127
+ name: "funforge_env_unset",
128
+ description: "Remove environment variables from the linked app.",
129
+ inputSchema: {
130
+ type: "object",
131
+ properties: {
132
+ directory: {
133
+ type: "string",
134
+ description: "Directory with funforge.json (defaults to cwd)",
135
+ },
136
+ keys: {
137
+ type: "array",
138
+ items: { type: "string" },
139
+ description: "List of environment variable keys to remove",
140
+ },
141
+ },
142
+ required: ["keys"],
143
+ },
144
+ },
145
+ {
146
+ name: "funforge_domains_list",
147
+ description: "List custom domains configured for the linked app.",
148
+ inputSchema: {
149
+ type: "object",
150
+ properties: {
151
+ directory: {
152
+ type: "string",
153
+ description: "Directory with funforge.json (defaults to cwd)",
154
+ },
155
+ },
156
+ required: [],
157
+ },
158
+ },
159
+ {
160
+ name: "funforge_domains_add",
161
+ description: "Add a custom domain to the linked app. Returns DNS configuration instructions.",
162
+ inputSchema: {
163
+ type: "object",
164
+ properties: {
165
+ directory: {
166
+ type: "string",
167
+ description: "Directory with funforge.json (defaults to cwd)",
168
+ },
169
+ domain: {
170
+ type: "string",
171
+ description: "Domain name to add (e.g., example.com)",
172
+ },
173
+ verificationMethod: {
174
+ type: "string",
175
+ enum: ["cname", "txt"],
176
+ description: "DNS verification method (default: cname)",
177
+ },
178
+ },
179
+ required: ["domain"],
180
+ },
181
+ },
182
+ ];
183
+ async function handleToolCall(name, args) {
184
+ try {
185
+ switch (name) {
186
+ case "funforge_whoami":
187
+ return await handleWhoami();
188
+ case "funforge_apps_list":
189
+ return await handleAppsList();
190
+ case "funforge_apps_create":
191
+ return await handleAppsCreate(args.name, args.slug);
192
+ case "funforge_status":
193
+ return await handleStatus(args.directory);
194
+ case "funforge_deploy":
195
+ return await handleDeploy(args.directory);
196
+ case "funforge_env_list":
197
+ return await handleEnvList(args.directory);
198
+ case "funforge_env_set":
199
+ return await handleEnvSet(args.directory, args.envVars);
200
+ case "funforge_env_unset":
201
+ return await handleEnvUnset(args.directory, args.keys);
202
+ case "funforge_domains_list":
203
+ return await handleDomainsList(args.directory);
204
+ case "funforge_domains_add":
205
+ return await handleDomainsAdd(args.directory, args.domain, args.verificationMethod);
206
+ default:
207
+ return errorResult(`Unknown tool: ${name}`);
208
+ }
209
+ }
210
+ catch (error) {
211
+ return errorResult(formatError(error));
212
+ }
213
+ }
214
+ // ============================================
215
+ // HANDLER IMPLEMENTATIONS
216
+ // ============================================
217
+ async function handleWhoami() {
218
+ if (!isAuthenticated()) {
219
+ return errorResult("Not authenticated. User needs to run `funforge login` in their terminal first.");
220
+ }
221
+ const creds = getCredentials();
222
+ return successResult({
223
+ authenticated: true,
224
+ email: creds.email,
225
+ userId: creds.userId,
226
+ authenticatedAt: creds.savedAt,
227
+ });
228
+ }
229
+ async function handleAppsList() {
230
+ requireAuth();
231
+ const { apps } = await listApps();
232
+ return successResult({
233
+ count: apps.length,
234
+ apps: apps.map((app) => ({
235
+ id: app.id,
236
+ name: app.name,
237
+ slug: app.slug,
238
+ sourceType: app.sourceType,
239
+ createdAt: app.createdAt,
240
+ })),
241
+ });
242
+ }
243
+ async function handleAppsCreate(name, slug) {
244
+ requireAuth();
245
+ const { app } = await createApp({ name, slug });
246
+ return successResult({
247
+ message: `App "${app.name}" created successfully`,
248
+ app: {
249
+ id: app.id,
250
+ name: app.name,
251
+ slug: app.slug,
252
+ },
253
+ });
254
+ }
255
+ async function handleStatus(directory) {
256
+ requireAuth();
257
+ const dir = directory || process.cwd();
258
+ const appId = await getLinkedAppId(dir);
259
+ if (!appId) {
260
+ return errorResult(`No app linked in ${dir}. Create funforge.json with an appId or run \`funforge link\` command.`);
261
+ }
262
+ const status = await apiRequest(`/api/cli/apps/${appId}/status`);
263
+ return successResult({
264
+ app: status.app,
265
+ currentDeployment: status.currentDeployment,
266
+ activeDeployment: status.activeDeployment,
267
+ url: status.app.subdomain
268
+ ? `https://${status.app.subdomain}.funforge.app`
269
+ : null,
270
+ });
271
+ }
272
+ async function handleDeploy(directory) {
273
+ requireAuth();
274
+ const dir = directory || process.cwd();
275
+ const config = await readConfig(dir);
276
+ if (!config?.appId) {
277
+ return errorResult(`No app linked in ${dir}. Create funforge.json with an appId or run \`funforge link\` command.`);
278
+ }
279
+ // 1. Create tarball
280
+ const tarballResult = await createTarball(dir);
281
+ // Check size limit (50MB)
282
+ const MAX_SIZE = 50 * 1024 * 1024;
283
+ if (tarballResult.size > MAX_SIZE) {
284
+ return errorResult(`Package too large: ${formatSize(tarballResult.size)} (max ${formatSize(MAX_SIZE)}). Add patterns to .funforgeignore to reduce size.`);
285
+ }
286
+ // 2. Get upload URL
287
+ const uploadInfo = await getUploadUrl(config.appId, {
288
+ filename: "context.tar.gz",
289
+ contentLength: tarballResult.size,
290
+ sha256: tarballResult.sha256,
291
+ });
292
+ // 3. Upload tarball
293
+ const tarballBuffer = await readTarball(tarballResult.path);
294
+ await uploadTarball(uploadInfo.uploadUrl, tarballBuffer);
295
+ // 4. Trigger deployment
296
+ const deployResult = await createDeployment(config.appId, {
297
+ sourceId: uploadInfo.sourceId,
298
+ });
299
+ return successResult({
300
+ message: "Deployment started successfully",
301
+ deployment: {
302
+ id: deployResult.deployment.id,
303
+ status: deployResult.deployment.status,
304
+ createdAt: deployResult.deployment.createdAt,
305
+ },
306
+ package: {
307
+ files: tarballResult.fileCount,
308
+ size: formatSize(tarballResult.size),
309
+ },
310
+ tip: "Use funforge_status to check deployment progress",
311
+ });
312
+ }
313
+ async function handleEnvList(directory) {
314
+ requireAuth();
315
+ const dir = directory || process.cwd();
316
+ const appId = await getLinkedAppId(dir);
317
+ if (!appId) {
318
+ return errorResult(`No app linked in ${dir}.`);
319
+ }
320
+ const { appEnvVars, userEnvVars } = await apiRequest(`/api/cli/apps/${appId}/env`);
321
+ return successResult({
322
+ appEnvVars,
323
+ userEnvVars,
324
+ note: "User env vars are inherited but can be overridden by app env vars",
325
+ });
326
+ }
327
+ async function handleEnvSet(directory, envVars) {
328
+ requireAuth();
329
+ const dir = directory || process.cwd();
330
+ const appId = await getLinkedAppId(dir);
331
+ if (!appId) {
332
+ return errorResult(`No app linked in ${dir}.`);
333
+ }
334
+ await apiRequest(`/api/cli/apps/${appId}/env`, {
335
+ method: "PATCH",
336
+ body: { envVars },
337
+ });
338
+ return successResult({
339
+ message: `Set ${Object.keys(envVars).length} environment variable(s)`,
340
+ keys: Object.keys(envVars),
341
+ note: "Changes take effect on next deployment",
342
+ });
343
+ }
344
+ async function handleEnvUnset(directory, keys) {
345
+ requireAuth();
346
+ const dir = directory || process.cwd();
347
+ const appId = await getLinkedAppId(dir);
348
+ if (!appId) {
349
+ return errorResult(`No app linked in ${dir}.`);
350
+ }
351
+ const envVars = {};
352
+ for (const key of keys) {
353
+ envVars[key] = null;
354
+ }
355
+ await apiRequest(`/api/cli/apps/${appId}/env`, {
356
+ method: "PATCH",
357
+ body: { envVars },
358
+ });
359
+ return successResult({
360
+ message: `Removed ${keys.length} environment variable(s)`,
361
+ keys,
362
+ note: "Changes take effect on next deployment",
363
+ });
364
+ }
365
+ async function handleDomainsList(directory) {
366
+ requireAuth();
367
+ const dir = directory || process.cwd();
368
+ const appId = await getLinkedAppId(dir);
369
+ if (!appId) {
370
+ return errorResult(`No app linked in ${dir}.`);
371
+ }
372
+ const { domains } = await apiRequest(`/api/cli/apps/${appId}/domains`);
373
+ return successResult({
374
+ count: domains.length,
375
+ domains: domains.map((d) => ({
376
+ id: d.id,
377
+ domain: d.domain,
378
+ verified: d.verified,
379
+ sslStatus: d.sslStatus,
380
+ })),
381
+ });
382
+ }
383
+ async function handleDomainsAdd(directory, domain, verificationMethod) {
384
+ requireAuth();
385
+ const dir = directory || process.cwd();
386
+ const appId = await getLinkedAppId(dir);
387
+ if (!appId) {
388
+ return errorResult(`No app linked in ${dir}.`);
389
+ }
390
+ // Normalize domain
391
+ const cleanDomain = domain
392
+ .replace(/^https?:\/\//, "")
393
+ .replace(/\/.*$/, "")
394
+ .toLowerCase();
395
+ const result = await apiRequest(`/api/cli/apps/${appId}/domains`, {
396
+ method: "POST",
397
+ body: {
398
+ domain: cleanDomain,
399
+ verificationMethod: verificationMethod || "cname",
400
+ },
401
+ });
402
+ return successResult({
403
+ message: `Domain ${cleanDomain} added`,
404
+ domain: result.domain,
405
+ dnsConfiguration: {
406
+ recordType: result.verification.type.toUpperCase(),
407
+ name: result.verification.name,
408
+ value: result.verification.value,
409
+ },
410
+ nextStep: "Add the DNS record above, then verify with funforge domains verify command",
411
+ });
412
+ }
413
+ // ============================================
414
+ // HELPERS
415
+ // ============================================
416
+ function requireAuth() {
417
+ if (!isAuthenticated()) {
418
+ throw new Error("Not authenticated. User needs to run `funforge login` in their terminal first.");
419
+ }
420
+ }
421
+ function successResult(data) {
422
+ return {
423
+ content: [
424
+ {
425
+ type: "text",
426
+ text: JSON.stringify(data, null, 2),
427
+ },
428
+ ],
429
+ };
430
+ }
431
+ function errorResult(message) {
432
+ return {
433
+ content: [
434
+ {
435
+ type: "text",
436
+ text: JSON.stringify({ error: message }, null, 2),
437
+ },
438
+ ],
439
+ isError: true,
440
+ };
441
+ }
442
+ function formatError(error) {
443
+ if (error instanceof ApiError) {
444
+ const body = error.body;
445
+ const message = body?.message || body?.error?.message || error.message;
446
+ return `API Error ${error.statusCode}: ${message}`;
447
+ }
448
+ return error instanceof Error ? error.message : String(error);
449
+ }
450
+ // ============================================
451
+ // SERVER SETUP
452
+ // ============================================
453
+ async function main() {
454
+ const server = new Server({
455
+ name: "funforge",
456
+ version: "0.1.0",
457
+ }, {
458
+ capabilities: {
459
+ tools: {},
460
+ },
461
+ });
462
+ // Register tool listing handler
463
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
464
+ return { tools: TOOLS };
465
+ });
466
+ // Register tool call handler
467
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
468
+ const { name, arguments: args } = request.params;
469
+ return handleToolCall(name, args ?? {});
470
+ });
471
+ // Connect via stdio
472
+ const transport = new StdioServerTransport();
473
+ await server.connect(transport);
474
+ // Log to stderr (stdout is for MCP protocol)
475
+ console.error("FunForge MCP server started");
476
+ }
477
+ main().catch((error) => {
478
+ console.error("Failed to start MCP server:", error);
479
+ process.exit(1);
480
+ });
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Project Configuration (funforge.json)
3
+ *
4
+ * Handles reading and writing funforge.json config files.
5
+ */
6
+ export interface FunForgeConfig {
7
+ /** App ID this directory is linked to */
8
+ appId?: string;
9
+ /** App name (for display) */
10
+ appName?: string;
11
+ /** App slug (subdomain) */
12
+ appSlug?: string;
13
+ /** Build settings */
14
+ build?: {
15
+ /** Custom build command */
16
+ buildCommand?: string;
17
+ /** Custom install command */
18
+ installCommand?: string;
19
+ /** Custom start command */
20
+ startCommand?: string;
21
+ /** Node.js version */
22
+ nodeVersion?: string;
23
+ };
24
+ /** Port the app listens on */
25
+ port?: number;
26
+ }
27
+ /**
28
+ * Check if funforge.json exists in the directory
29
+ */
30
+ export declare function configExists(dir?: string): Promise<boolean>;
31
+ /**
32
+ * Read funforge.json from directory
33
+ */
34
+ export declare function readConfig(dir?: string): Promise<FunForgeConfig | null>;
35
+ /**
36
+ * Write funforge.json to directory
37
+ */
38
+ export declare function writeConfig(config: FunForgeConfig, dir?: string): Promise<void>;
39
+ /**
40
+ * Update funforge.json (merge with existing)
41
+ */
42
+ export declare function updateConfig(updates: Partial<FunForgeConfig>, dir?: string): Promise<FunForgeConfig>;
43
+ /**
44
+ * Get linked app ID from config
45
+ */
46
+ export declare function getLinkedAppId(dir?: string): Promise<string | null>;
47
+ //# sourceMappingURL=project-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project-config.d.ts","sourceRoot":"","sources":["../src/project-config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,MAAM,WAAW,cAAc;IAC7B,yCAAyC;IACzC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6BAA6B;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,2BAA2B;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qBAAqB;IACrB,KAAK,CAAC,EAAE;QACN,2BAA2B;QAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,6BAA6B;QAC7B,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,2BAA2B;QAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,sBAAsB;QACtB,WAAW,CAAC,EAAE,MAAM,CAAC;KACtB,CAAC;IACF,8BAA8B;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAID;;GAEG;AACH,wBAAsB,YAAY,CAAC,GAAG,GAAE,MAAY,GAAG,OAAO,CAAC,OAAO,CAAC,CAOtE;AAED;;GAEG;AACH,wBAAsB,UAAU,CAC9B,GAAG,GAAE,MAAY,GAChB,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAOhC;AAED;;GAEG;AACH,wBAAsB,WAAW,CAC/B,MAAM,EAAE,cAAc,EACtB,GAAG,GAAE,MAAY,GAChB,OAAO,CAAC,IAAI,CAAC,CAGf;AAED;;GAEG;AACH,wBAAsB,YAAY,CAChC,OAAO,EAAE,OAAO,CAAC,cAAc,CAAC,EAChC,GAAG,GAAE,MAAY,GAChB,OAAO,CAAC,cAAc,CAAC,CAKzB;AAED;;GAEG;AACH,wBAAsB,cAAc,CAClC,GAAG,GAAE,MAAY,GAChB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAGxB"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Project Configuration (funforge.json)
3
+ *
4
+ * Handles reading and writing funforge.json config files.
5
+ */
6
+ import { access, readFile, writeFile } from "node:fs/promises";
7
+ import { join } from "node:path";
8
+ const CONFIG_FILENAME = "funforge.json";
9
+ /**
10
+ * Check if funforge.json exists in the directory
11
+ */
12
+ export async function configExists(dir = ".") {
13
+ try {
14
+ await access(join(dir, CONFIG_FILENAME));
15
+ return true;
16
+ }
17
+ catch {
18
+ return false;
19
+ }
20
+ }
21
+ /**
22
+ * Read funforge.json from directory
23
+ */
24
+ export async function readConfig(dir = ".") {
25
+ try {
26
+ const content = await readFile(join(dir, CONFIG_FILENAME), "utf-8");
27
+ return JSON.parse(content);
28
+ }
29
+ catch {
30
+ return null;
31
+ }
32
+ }
33
+ /**
34
+ * Write funforge.json to directory
35
+ */
36
+ export async function writeConfig(config, dir = ".") {
37
+ const content = JSON.stringify(config, null, 2) + "\n";
38
+ await writeFile(join(dir, CONFIG_FILENAME), content, "utf-8");
39
+ }
40
+ /**
41
+ * Update funforge.json (merge with existing)
42
+ */
43
+ export async function updateConfig(updates, dir = ".") {
44
+ const existing = (await readConfig(dir)) ?? {};
45
+ const updated = { ...existing, ...updates };
46
+ await writeConfig(updated, dir);
47
+ return updated;
48
+ }
49
+ /**
50
+ * Get linked app ID from config
51
+ */
52
+ export async function getLinkedAppId(dir = ".") {
53
+ const config = await readConfig(dir);
54
+ return config?.appId ?? null;
55
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Tarball Utilities
3
+ *
4
+ * Creates gzipped tarballs from project directories,
5
+ * respecting .gitignore and .funforgeignore patterns.
6
+ */
7
+ export interface TarballResult {
8
+ /** Path to the created tarball */
9
+ path: string;
10
+ /** Size in bytes */
11
+ size: number;
12
+ /** SHA-256 hash */
13
+ sha256: string;
14
+ /** Number of files included */
15
+ fileCount: number;
16
+ }
17
+ /**
18
+ * Create a gzipped tarball from a project directory
19
+ */
20
+ export declare function createTarball(projectDir: string): Promise<TarballResult>;
21
+ /**
22
+ * Read tarball as buffer
23
+ */
24
+ export declare function readTarball(tarballPath: string): Promise<Buffer>;
25
+ /**
26
+ * Format file size for display
27
+ */
28
+ export declare function formatSize(bytes: number): string;
29
+ //# sourceMappingURL=tarball.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tarball.d.ts","sourceRoot":"","sources":["../src/tarball.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AA4FH,MAAM,WAAW,aAAa;IAC5B,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,oBAAoB;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,mBAAmB;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,+BAA+B;IAC/B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,wBAAsB,aAAa,CACjC,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,aAAa,CAAC,CA8CxB;AAgBD;;GAEG;AACH,wBAAsB,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAStE;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIhD"}