@byfungsi/funforge 0.2.0 → 0.2.3

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.
@@ -130,11 +130,17 @@ describe("api", () => {
130
130
  app: { id: "new-app", name: "New App", slug: "new-app" },
131
131
  }),
132
132
  });
133
- const result = await createApp({ name: "New App" });
133
+ const result = await createApp({
134
+ name: "New App",
135
+ tierKey: "cost-optimized.small",
136
+ });
134
137
  expect(result.app.name).toBe("New App");
135
138
  expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining("/api/cli/apps"), expect.objectContaining({
136
139
  method: "POST",
137
- body: JSON.stringify({ name: "New App" }),
140
+ body: JSON.stringify({
141
+ name: "New App",
142
+ tierKey: "cost-optimized.small",
143
+ }),
138
144
  }));
139
145
  });
140
146
  it("should create app with name and slug", async () => {
@@ -144,7 +150,11 @@ describe("api", () => {
144
150
  app: { id: "new-app", name: "New App", slug: "custom-slug" },
145
151
  }),
146
152
  });
147
- const result = await createApp({ name: "New App", slug: "custom-slug" });
153
+ const result = await createApp({
154
+ name: "New App",
155
+ slug: "custom-slug",
156
+ tierKey: "cost-optimized.small",
157
+ });
148
158
  expect(result.app.slug).toBe("custom-slug");
149
159
  });
150
160
  });
@@ -18,7 +18,7 @@ describe("config", () => {
18
18
  expect(config.apiUrl).toBe("https://api.fungsi.app");
19
19
  });
20
20
  it("should have production auth URL", () => {
21
- expect(config.authUrl).toBe("https://cloud.fungsi.app");
21
+ expect(config.authUrl).toBe("https://funforge.fungsi.app");
22
22
  });
23
23
  it("should have 30s timeout", () => {
24
24
  expect(config.timeout).toBe(30000);
@@ -31,7 +31,7 @@ describe("config", () => {
31
31
  delete process.env.FUNFORGE_TIMEOUT;
32
32
  const result = getConfig();
33
33
  expect(result.apiUrl).toBe("https://api.fungsi.app");
34
- expect(result.authUrl).toBe("https://cloud.fungsi.app");
34
+ expect(result.authUrl).toBe("https://funforge.fungsi.app");
35
35
  expect(result.timeout).toBe(30000);
36
36
  });
37
37
  it("should override apiUrl from environment", () => {
package/dist/api.d.ts CHANGED
@@ -105,6 +105,7 @@ export declare function updateApp(appId: string, settings: UpdateAppSettings): P
105
105
  export declare function createApp(data: {
106
106
  name: string;
107
107
  slug?: string;
108
+ tierKey: string;
108
109
  }): Promise<{
109
110
  app: App;
110
111
  }>;
package/dist/api.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
package/dist/api.js CHANGED
@@ -78,14 +78,28 @@ export async function initDeviceAuth() {
78
78
  */
79
79
  export async function pollDeviceAuth(deviceCode) {
80
80
  const config = getConfig();
81
- const response = await fetch(`${config.apiUrl}/api/cli/auth/device/poll`, {
82
- method: "POST",
83
- headers: { "Content-Type": "application/json" },
84
- body: JSON.stringify({ device_code: deviceCode }),
85
- });
81
+ let response;
82
+ try {
83
+ response = await fetch(`${config.apiUrl}/api/cli/auth/device/poll`, {
84
+ method: "POST",
85
+ headers: { "Content-Type": "application/json" },
86
+ body: JSON.stringify({ device_code: deviceCode }),
87
+ });
88
+ }
89
+ catch (fetchError) {
90
+ // Network error - throw with details
91
+ const err = fetchError;
92
+ throw new ApiError(`Network error: ${err.message}`, 0);
93
+ }
86
94
  // Handle non-200 responses that contain error info
87
95
  if (!response.ok) {
88
- const data = (await response.json());
96
+ let data = {};
97
+ try {
98
+ data = (await response.json());
99
+ }
100
+ catch {
101
+ // couldn't parse JSON
102
+ }
89
103
  // Map OAuth error codes to our status
90
104
  if (data.error === "access_denied") {
91
105
  return { status: "denied" };
@@ -93,7 +107,7 @@ export async function pollDeviceAuth(deviceCode) {
93
107
  if (data.error === "expired_token") {
94
108
  return { status: "expired" };
95
109
  }
96
- throw new ApiError("Failed to poll auth status", response.status);
110
+ throw new ApiError(`Failed to poll auth status: ${data.error_description || data.error || "unknown"}`, response.status, data);
97
111
  }
98
112
  // API returns snake_case, map to camelCase
99
113
  const data = (await response.json());
package/dist/cli.js CHANGED
@@ -4,8 +4,12 @@
4
4
  *
5
5
  * Deploy without git, without leaving your editor.
6
6
  */
7
+ import { createRequire } from "node:module";
7
8
  import chalk from "chalk";
8
9
  import { Command } from "commander";
10
+ // Load version from package.json
11
+ const require = createRequire(import.meta.url);
12
+ const pkg = require("../package.json");
9
13
  import { appsCreateCommand, appsListCommand, linkCommand, } from "./commands/apps.js";
10
14
  // Import commands
11
15
  import { loginCommand, logoutCommand, whoamiCommand } from "./commands/auth.js";
@@ -18,7 +22,7 @@ const program = new Command();
18
22
  program
19
23
  .name("funforge")
20
24
  .description("Deploy without git, without leaving your editor")
21
- .version("0.1.0");
25
+ .version(pkg.version);
22
26
  // ============================================
23
27
  // AUTH COMMANDS
24
28
  // ============================================
@@ -43,6 +47,7 @@ appsCmd
43
47
  .command("create <name>")
44
48
  .description("Create a new app")
45
49
  .option("-s, --slug <slug>", "Custom slug (subdomain)")
50
+ .option("-t, --tier <tier>", "Tier key (e.g., cost-optimized.small)")
46
51
  .action(appsCreateCommand);
47
52
  program
48
53
  .command("link [appId]")
@@ -2,7 +2,7 @@
2
2
  * Apps Commands
3
3
  *
4
4
  * - apps list: List all apps
5
- * - apps create: Create a new app
5
+ * - apps create: Create a new app (with interactive tier selection)
6
6
  * - link: Link current directory to an app
7
7
  */
8
8
  /**
@@ -14,10 +14,11 @@ export declare function appsListCommand(): Promise<void>;
14
14
  /**
15
15
  * funforge apps create <name>
16
16
  *
17
- * Create a new app.
17
+ * Create a new app with interactive tier selection.
18
18
  */
19
19
  export declare function appsCreateCommand(name: string, options: {
20
20
  slug?: string;
21
+ tier?: string;
21
22
  }): Promise<void>;
22
23
  /**
23
24
  * funforge link [appId]
@@ -1 +1 @@
1
- {"version":3,"file":"apps.d.ts","sourceRoot":"","sources":["../../src/commands/apps.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAQH;;;;GAIG;AACH,wBAAsB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAgCrD;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GACzB,OAAO,CAAC,IAAI,CAAC,CAoBf;AAED;;;;;GAKG;AACH,wBAAsB,WAAW,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA8D/D"}
1
+ {"version":3,"file":"apps.d.ts","sourceRoot":"","sources":["../../src/commands/apps.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAeH;;;;GAIG;AACH,wBAAsB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAgCrD;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GACxC,OAAO,CAAC,IAAI,CAAC,CAyCf;AAED;;;;;GAKG;AACH,wBAAsB,WAAW,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA8D/D"}
@@ -2,14 +2,16 @@
2
2
  * Apps Commands
3
3
  *
4
4
  * - apps list: List all apps
5
- * - apps create: Create a new app
5
+ * - apps create: Create a new app (with interactive tier selection)
6
6
  * - link: Link current directory to an app
7
7
  */
8
8
  import chalk from "chalk";
9
9
  import ora from "ora";
10
- import { ApiError, createApp, getApp, listApps } from "../api.js";
10
+ import { createApp, getApp, listApps } from "../api.js";
11
11
  import { isAuthenticated } from "../credentials.js";
12
+ import { handleError } from "../errors.js";
12
13
  import { readConfig, updateConfig } from "../project-config.js";
14
+ import { VALID_TIER_KEYS, getTierInfo, isValidTierKey, selectTier, } from "../tiers.js";
13
15
  /**
14
16
  * funforge apps list
15
17
  *
@@ -43,17 +45,34 @@ export async function appsListCommand() {
43
45
  /**
44
46
  * funforge apps create <name>
45
47
  *
46
- * Create a new app.
48
+ * Create a new app with interactive tier selection.
47
49
  */
48
50
  export async function appsCreateCommand(name, options) {
49
51
  requireAuth();
50
- const spinner = ora(`Creating app "${name}"...`).start();
52
+ // Get tier - from flag or interactive prompt
53
+ let tierKey;
54
+ if (options.tier) {
55
+ if (!isValidTierKey(options.tier)) {
56
+ console.log(chalk.red(`Invalid tier: ${options.tier}`));
57
+ console.log(chalk.gray(`Valid tiers: ${VALID_TIER_KEYS.join(", ")}`));
58
+ process.exit(1);
59
+ }
60
+ tierKey = options.tier;
61
+ }
62
+ else {
63
+ tierKey = await selectTier();
64
+ }
65
+ const tierInfo = getTierInfo(tierKey);
66
+ const spinner = ora(`Creating app "${name}" with ${tierInfo?.name ?? tierKey} tier...`).start();
51
67
  try {
52
- const { app } = await createApp({ name, slug: options.slug });
68
+ const { app } = await createApp({ name, slug: options.slug, tierKey });
53
69
  spinner.succeed(`App created!`);
54
70
  console.log();
55
71
  console.log(` Name: ${app.name}`);
56
72
  console.log(` Slug: ${chalk.cyan(app.slug)}`);
73
+ if (tierInfo) {
74
+ console.log(` Tier: ${tierInfo.category}/${tierInfo.name} (${tierInfo.credits} credits/mo)`);
75
+ }
57
76
  console.log(` ID: ${app.id}`);
58
77
  console.log();
59
78
  console.log(chalk.gray("Link this directory with:"));
@@ -132,20 +151,3 @@ function requireAuth() {
132
151
  process.exit(1);
133
152
  }
134
153
  }
135
- /**
136
- * Handle API errors
137
- */
138
- function handleError(error) {
139
- if (error instanceof ApiError) {
140
- console.error(chalk.red(`Error ${error.statusCode}: ${error.message}`));
141
- if (error.body &&
142
- typeof error.body === "object" &&
143
- "message" in error.body) {
144
- console.error(chalk.gray(error.body.message));
145
- }
146
- }
147
- else {
148
- console.error(chalk.red(error instanceof Error ? error.message : String(error)));
149
- }
150
- process.exit(1);
151
- }
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/commands/auth.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAcH;;;;;GAKG;AACH,wBAAsB,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CA+FlD;AAED;;;;GAIG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CASnD;AAED;;;;GAIG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAcnD"}
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/commands/auth.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAcH;;;;;GAKG;AACH,wBAAsB,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAsGlD;AAED;;;;GAIG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CASnD;AAED;;;;GAIG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAcnD"}
@@ -54,7 +54,16 @@ export async function loginCommand() {
54
54
  const expiresAt = startTime + auth.expiresIn * 1000;
55
55
  while (Date.now() < expiresAt) {
56
56
  await sleep(auth.interval * 1000);
57
- const result = await pollDeviceAuth(auth.deviceCode);
57
+ let result;
58
+ try {
59
+ result = await pollDeviceAuth(auth.deviceCode);
60
+ }
61
+ catch (pollError) {
62
+ const err = pollError;
63
+ pollSpinner.fail("Poll failed");
64
+ console.error(chalk.red(err.message));
65
+ process.exit(1);
66
+ }
58
67
  if (result.status === "authorized") {
59
68
  pollSpinner.succeed("Authenticated!");
60
69
  // Save credentials
@@ -85,7 +94,8 @@ export async function loginCommand() {
85
94
  }
86
95
  catch (error) {
87
96
  spinner.fail("Authentication failed");
88
- console.error(chalk.red(error instanceof Error ? error.message : String(error)));
97
+ const err = error;
98
+ console.error(chalk.red(err.message));
89
99
  process.exit(1);
90
100
  }
91
101
  }
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/commands/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAwEH;;;;;GAKG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CA+EvD;AAED;;;;;GAKG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CA4EvD;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CAgFvD"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/commands/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAoEH;;;;;GAKG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CA+EvD;AAED;;;;;GAKG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CA4EvD;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CAgFvD"}
@@ -10,8 +10,9 @@
10
10
  */
11
11
  import chalk from "chalk";
12
12
  import ora from "ora";
13
- import { ApiError, getAppDetails, updateApp, } from "../api.js";
13
+ import { getAppDetails, updateApp } from "../api.js";
14
14
  import { isAuthenticated } from "../credentials.js";
15
+ import { handleError } from "../errors.js";
15
16
  import { readConfig, updateConfig, } from "../project-config.js";
16
17
  /**
17
18
  * Extract build settings from funforge.json config
@@ -268,20 +269,3 @@ function requireAuth() {
268
269
  process.exit(1);
269
270
  }
270
271
  }
271
- /**
272
- * Handle API errors
273
- */
274
- function handleError(error) {
275
- if (error instanceof ApiError) {
276
- console.error(chalk.red(`Error ${error.statusCode}: ${error.message}`));
277
- if (error.body &&
278
- typeof error.body === "object" &&
279
- "message" in error.body) {
280
- console.error(chalk.gray(error.body.message));
281
- }
282
- }
283
- else {
284
- console.error(chalk.red(error instanceof Error ? error.message : String(error)));
285
- }
286
- process.exit(1);
287
- }
@@ -10,8 +10,9 @@
10
10
  */
11
11
  import chalk from "chalk";
12
12
  import ora from "ora";
13
- import { ApiError, createDeployment, getDeployment, getUploadUrl, uploadTarball, } from "../api.js";
13
+ import { createDeployment, getDeployment, getUploadUrl, uploadTarball, } from "../api.js";
14
14
  import { isAuthenticated } from "../credentials.js";
15
+ import { handleError } from "../errors.js";
15
16
  import { readConfig } from "../project-config.js";
16
17
  import { createTarball, formatSize, readTarball } from "../tarball.js";
17
18
  /**
@@ -174,23 +175,6 @@ function requireAuth() {
174
175
  process.exit(1);
175
176
  }
176
177
  }
177
- /**
178
- * Handle API errors
179
- */
180
- function handleError(error) {
181
- if (error instanceof ApiError) {
182
- console.error(chalk.red(`Error ${error.statusCode}: ${error.message}`));
183
- if (error.body &&
184
- typeof error.body === "object" &&
185
- "message" in error.body) {
186
- console.error(chalk.gray(error.body.message));
187
- }
188
- }
189
- else {
190
- console.error(chalk.red(error instanceof Error ? error.message : String(error)));
191
- }
192
- process.exit(1);
193
- }
194
178
  function sleep(ms) {
195
179
  return new Promise((resolve) => setTimeout(resolve, ms));
196
180
  }
@@ -1 +1 @@
1
- {"version":3,"file":"domains.d.ts","sourceRoot":"","sources":["../../src/commands/domains.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAqCH;;;;GAIG;AACH,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC,CA6CxD;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,MAAM,EACd,OAAO,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAC3B,OAAO,CAAC,IAAI,CAAC,CAkDf;AAED;;;;GAIG;AACH,wBAAsB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAoCxE;AAED;;;;GAIG;AACH,wBAAsB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA6DxE"}
1
+ {"version":3,"file":"domains.d.ts","sourceRoot":"","sources":["../../src/commands/domains.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAsCH;;;;GAIG;AACH,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC,CA6CxD;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,MAAM,EACd,OAAO,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAC3B,OAAO,CAAC,IAAI,CAAC,CAkDf;AAED;;;;GAIG;AACH,wBAAsB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAoCxE;AAED;;;;GAIG;AACH,wBAAsB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA6DxE"}
@@ -8,8 +8,9 @@
8
8
  */
9
9
  import chalk from "chalk";
10
10
  import ora from "ora";
11
- import { ApiError, apiRequest } from "../api.js";
11
+ import { apiRequest } from "../api.js";
12
12
  import { isAuthenticated } from "../credentials.js";
13
+ import { handleError } from "../errors.js";
13
14
  import { readConfig } from "../project-config.js";
14
15
  /**
15
16
  * funforge domains list
@@ -198,20 +199,3 @@ function requireAuth() {
198
199
  process.exit(1);
199
200
  }
200
201
  }
201
- /**
202
- * Handle API errors
203
- */
204
- function handleError(error) {
205
- if (error instanceof ApiError) {
206
- console.error(chalk.red(`Error ${error.statusCode}: ${error.message}`));
207
- if (error.body &&
208
- typeof error.body === "object" &&
209
- "message" in error.body) {
210
- console.error(chalk.gray(error.body.message));
211
- }
212
- }
213
- else {
214
- console.error(chalk.red(error instanceof Error ? error.message : String(error)));
215
- }
216
- process.exit(1);
217
- }
@@ -1 +1 @@
1
- {"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../../src/commands/env.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAaH;;;;GAIG;AACH,wBAAsB,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CAqDpD;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAqDlE;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAkCnE"}
1
+ {"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../../src/commands/env.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAcH;;;;GAIG;AACH,wBAAsB,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CAqDpD;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAqDlE;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAkCnE"}
@@ -7,8 +7,9 @@
7
7
  */
8
8
  import chalk from "chalk";
9
9
  import ora from "ora";
10
- import { ApiError, apiRequest } from "../api.js";
10
+ import { apiRequest } from "../api.js";
11
11
  import { isAuthenticated } from "../credentials.js";
12
+ import { handleError } from "../errors.js";
12
13
  import { readConfig } from "../project-config.js";
13
14
  /**
14
15
  * funforge env list
@@ -164,20 +165,3 @@ function requireAuth() {
164
165
  process.exit(1);
165
166
  }
166
167
  }
167
- /**
168
- * Handle API errors
169
- */
170
- function handleError(error) {
171
- if (error instanceof ApiError) {
172
- console.error(chalk.red(`Error ${error.statusCode}: ${error.message}`));
173
- if (error.body &&
174
- typeof error.body === "object" &&
175
- "message" in error.body) {
176
- console.error(chalk.gray(error.body.message));
177
- }
178
- }
179
- else {
180
- console.error(chalk.red(error instanceof Error ? error.message : String(error)));
181
- }
182
- process.exit(1);
183
- }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Error Handling Utilities
3
+ *
4
+ * Shared error formatting for user-friendly CLI output.
5
+ */
6
+ import { ApiError } from "./api.js";
7
+ /**
8
+ * Format an API error for user-friendly display.
9
+ *
10
+ * Handles known error codes with actionable hints:
11
+ * - 402: Billing/credits issues
12
+ * - 403: Plan limits
13
+ * - 409: Conflicts (deployment in progress)
14
+ * - 404: Not found
15
+ */
16
+ export declare function formatApiError(error: ApiError): string;
17
+ /**
18
+ * Handle any error and exit.
19
+ *
20
+ * Use this in catch blocks for consistent error output.
21
+ */
22
+ export declare function handleError(error: unknown): never;
23
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAmBpC;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,QAAQ,GAAG,MAAM,CA0EtD;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,CASjD"}
package/dist/errors.js ADDED
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Error Handling Utilities
3
+ *
4
+ * Shared error formatting for user-friendly CLI output.
5
+ */
6
+ import chalk from "chalk";
7
+ import { ApiError } from "./api.js";
8
+ /**
9
+ * Format an API error for user-friendly display.
10
+ *
11
+ * Handles known error codes with actionable hints:
12
+ * - 402: Billing/credits issues
13
+ * - 403: Plan limits
14
+ * - 409: Conflicts (deployment in progress)
15
+ * - 404: Not found
16
+ */
17
+ export function formatApiError(error) {
18
+ const body = error.body;
19
+ const code = body?.error?.code;
20
+ const message = body?.error?.message || body?.message || error.message;
21
+ // 402 - Payment Required (billing/credits)
22
+ if (error.statusCode === 402 || code === "BILLING_ERROR") {
23
+ return [
24
+ chalk.red(message),
25
+ "",
26
+ chalk.yellow("Add credits at: https://funforge.fungsi.app/balance"),
27
+ ].join("\n");
28
+ }
29
+ // 403 - Plan limits exceeded
30
+ if (code === "PLAN_LIMIT_EXCEEDED" || code === "APP_LIMIT_EXCEEDED") {
31
+ return [
32
+ chalk.red(message),
33
+ "",
34
+ chalk.yellow("Delete unused apps or upgrade your plan to continue."),
35
+ ].join("\n");
36
+ }
37
+ // 409 - Conflict (deployment in progress, etc.)
38
+ if (error.statusCode === 409 || code === "DEPLOYMENT_IN_PROGRESS") {
39
+ return [
40
+ chalk.red(message),
41
+ "",
42
+ chalk.gray("Wait for the current deployment to complete and try again."),
43
+ ].join("\n");
44
+ }
45
+ // 404 - Not found
46
+ if (error.statusCode === 404) {
47
+ return chalk.red(message || "Resource not found.");
48
+ }
49
+ // 401 - Unauthorized
50
+ if (error.statusCode === 401) {
51
+ return [
52
+ chalk.red(message || "Not authenticated."),
53
+ "",
54
+ chalk.gray("Run `funforge login` to authenticate."),
55
+ ].join("\n");
56
+ }
57
+ // 400 - Bad request / validation
58
+ if (error.statusCode === 400) {
59
+ const details = body?.error?.details;
60
+ if (details && Array.isArray(details) && details.length > 0) {
61
+ // Format Zod validation issues nicely
62
+ const issues = details
63
+ .map((d) => {
64
+ const field = d.path?.join(".") || "input";
65
+ return ` - ${field}: ${d.message}`;
66
+ })
67
+ .join("\n");
68
+ return [chalk.red(message || "Invalid input:"), chalk.gray(issues)].join("\n");
69
+ }
70
+ return chalk.red(message || "Invalid request.");
71
+ }
72
+ // 500+ - Server errors
73
+ if (error.statusCode >= 500) {
74
+ return [
75
+ chalk.red("Server error. Please try again later."),
76
+ chalk.gray(message),
77
+ ].join("\n");
78
+ }
79
+ // Default: show the message
80
+ return chalk.red(message);
81
+ }
82
+ /**
83
+ * Handle any error and exit.
84
+ *
85
+ * Use this in catch blocks for consistent error output.
86
+ */
87
+ export function handleError(error) {
88
+ if (error instanceof ApiError) {
89
+ console.error(formatApiError(error));
90
+ }
91
+ else if (error instanceof Error) {
92
+ console.error(chalk.red(error.message));
93
+ }
94
+ else {
95
+ console.error(chalk.red(String(error)));
96
+ }
97
+ process.exit(1);
98
+ }
package/dist/mcp.js CHANGED
@@ -58,8 +58,24 @@ const TOOLS = [
58
58
  type: "string",
59
59
  description: "Custom slug/subdomain (optional, auto-generated if not provided)",
60
60
  },
61
+ tierKey: {
62
+ type: "string",
63
+ description: "Tier key for the app. Cost-optimized tiers auto-sleep after 3h idle. Standard tiers are always-on. Options: cost-optimized.micro (10 credits/mo), cost-optimized.small (25 credits/mo), cost-optimized.medium (50 credits/mo), cost-optimized.large (100 credits/mo), cost-optimized.xlarge (125 credits/mo), standard.micro (50 credits/mo), standard.small (75 credits/mo), standard.medium (125 credits/mo), standard.large (200 credits/mo), standard.xlarge (250 credits/mo)",
64
+ enum: [
65
+ "cost-optimized.micro",
66
+ "cost-optimized.small",
67
+ "cost-optimized.medium",
68
+ "cost-optimized.large",
69
+ "cost-optimized.xlarge",
70
+ "standard.micro",
71
+ "standard.small",
72
+ "standard.medium",
73
+ "standard.large",
74
+ "standard.xlarge",
75
+ ],
76
+ },
61
77
  },
62
- required: ["name"],
78
+ required: ["name", "tierKey"],
63
79
  },
64
80
  },
65
81
  {
@@ -188,7 +204,7 @@ async function handleToolCall(name, args) {
188
204
  case "funforge_apps_list":
189
205
  return await handleAppsList();
190
206
  case "funforge_apps_create":
191
- return await handleAppsCreate(args.name, args.slug);
207
+ return await handleAppsCreate(args.name, args.tierKey, args.slug);
192
208
  case "funforge_status":
193
209
  return await handleStatus(args.directory);
194
210
  case "funforge_deploy":
@@ -240,15 +256,16 @@ async function handleAppsList() {
240
256
  })),
241
257
  });
242
258
  }
243
- async function handleAppsCreate(name, slug) {
259
+ async function handleAppsCreate(name, tierKey, slug) {
244
260
  requireAuth();
245
- const { app } = await createApp({ name, slug });
261
+ const { app } = await createApp({ name, slug, tierKey });
246
262
  return successResult({
247
263
  message: `App "${app.name}" created successfully`,
248
264
  app: {
249
265
  id: app.id,
250
266
  name: app.name,
251
267
  slug: app.slug,
268
+ tierKey,
252
269
  },
253
270
  });
254
271
  }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * FunForge Tier Selection
3
+ *
4
+ * Interactive tier picker for app creation.
5
+ */
6
+ /** Tier configuration for display */
7
+ interface TierOption {
8
+ key: string;
9
+ name: string;
10
+ credits: number;
11
+ priceIdr: string;
12
+ memory: string;
13
+ cpu: string;
14
+ category: "cost-optimized" | "standard";
15
+ }
16
+ /**
17
+ * All available tiers grouped by category
18
+ *
19
+ * Cost-Optimized: Auto-sleeps after 3h idle (cheaper)
20
+ * Standard: Always-on (higher availability)
21
+ */
22
+ export declare const TIER_OPTIONS: TierOption[];
23
+ export declare const VALID_TIER_KEYS: string[];
24
+ export declare const DEFAULT_TIER = "cost-optimized.small";
25
+ /**
26
+ * Interactive tier selection prompt
27
+ */
28
+ export declare function selectTier(): Promise<string>;
29
+ /**
30
+ * Check if a string is a valid tier key
31
+ */
32
+ export declare function isValidTierKey(key: string): boolean;
33
+ /**
34
+ * Get tier display info by key
35
+ */
36
+ export declare function getTierInfo(key: string): TierOption | undefined;
37
+ export {};
38
+ //# sourceMappingURL=tiers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tiers.d.ts","sourceRoot":"","sources":["../src/tiers.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,qCAAqC;AACrC,UAAU,UAAU;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,gBAAgB,GAAG,UAAU,CAAC;CACzC;AAED;;;;;GAKG;AACH,eAAO,MAAM,YAAY,EAAE,UAAU,EA6FpC,CAAC;AAEF,eAAO,MAAM,eAAe,UAAiC,CAAC;AAC9D,eAAO,MAAM,YAAY,yBAAyB,CAAC;AAanD;;GAEG;AACH,wBAAsB,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC,CAsClD;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAEnD;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS,CAE/D"}
package/dist/tiers.js ADDED
@@ -0,0 +1,168 @@
1
+ /**
2
+ * FunForge Tier Selection
3
+ *
4
+ * Interactive tier picker for app creation.
5
+ */
6
+ import { select } from "@inquirer/prompts";
7
+ import chalk from "chalk";
8
+ /**
9
+ * All available tiers grouped by category
10
+ *
11
+ * Cost-Optimized: Auto-sleeps after 3h idle (cheaper)
12
+ * Standard: Always-on (higher availability)
13
+ */
14
+ export const TIER_OPTIONS = [
15
+ // Cost-Optimized (auto-sleep after 3h idle)
16
+ {
17
+ key: "cost-optimized.micro",
18
+ name: "Micro",
19
+ credits: 10,
20
+ priceIdr: "Rp 10.000",
21
+ memory: "256MB",
22
+ cpu: "0.1",
23
+ category: "cost-optimized",
24
+ },
25
+ {
26
+ key: "cost-optimized.small",
27
+ name: "Small",
28
+ credits: 25,
29
+ priceIdr: "Rp 25.000",
30
+ memory: "512MB",
31
+ cpu: "0.25",
32
+ category: "cost-optimized",
33
+ },
34
+ {
35
+ key: "cost-optimized.medium",
36
+ name: "Medium",
37
+ credits: 50,
38
+ priceIdr: "Rp 50.000",
39
+ memory: "1GB",
40
+ cpu: "0.5",
41
+ category: "cost-optimized",
42
+ },
43
+ {
44
+ key: "cost-optimized.large",
45
+ name: "Large",
46
+ credits: 100,
47
+ priceIdr: "Rp 100.000",
48
+ memory: "2GB",
49
+ cpu: "1.0",
50
+ category: "cost-optimized",
51
+ },
52
+ {
53
+ key: "cost-optimized.xlarge",
54
+ name: "X-Large",
55
+ credits: 125,
56
+ priceIdr: "Rp 125.000",
57
+ memory: "4GB",
58
+ cpu: "2.0",
59
+ category: "cost-optimized",
60
+ },
61
+ // Standard (always-on)
62
+ {
63
+ key: "standard.micro",
64
+ name: "Micro",
65
+ credits: 50,
66
+ priceIdr: "Rp 50.000",
67
+ memory: "256MB",
68
+ cpu: "0.1",
69
+ category: "standard",
70
+ },
71
+ {
72
+ key: "standard.small",
73
+ name: "Small",
74
+ credits: 75,
75
+ priceIdr: "Rp 75.000",
76
+ memory: "512MB",
77
+ cpu: "0.25",
78
+ category: "standard",
79
+ },
80
+ {
81
+ key: "standard.medium",
82
+ name: "Medium",
83
+ credits: 125,
84
+ priceIdr: "Rp 125.000",
85
+ memory: "1GB",
86
+ cpu: "0.5",
87
+ category: "standard",
88
+ },
89
+ {
90
+ key: "standard.large",
91
+ name: "Large",
92
+ credits: 200,
93
+ priceIdr: "Rp 200.000",
94
+ memory: "2GB",
95
+ cpu: "1.0",
96
+ category: "standard",
97
+ },
98
+ {
99
+ key: "standard.xlarge",
100
+ name: "X-Large",
101
+ credits: 250,
102
+ priceIdr: "Rp 250.000",
103
+ memory: "4GB",
104
+ cpu: "2.0",
105
+ category: "standard",
106
+ },
107
+ ];
108
+ export const VALID_TIER_KEYS = TIER_OPTIONS.map((t) => t.key);
109
+ export const DEFAULT_TIER = "cost-optimized.small";
110
+ /**
111
+ * Format tier for display in dropdown
112
+ */
113
+ function formatTier(t) {
114
+ const nameCol = t.name.padEnd(8);
115
+ const creditsCol = `${t.credits.toString().padStart(3)} credits`;
116
+ const priceCol = `(${t.priceIdr})`;
117
+ const resourcesCol = `${t.memory}, ${t.cpu} CPU`;
118
+ return `${nameCol} ${creditsCol} ${priceCol.padEnd(14)} - ${resourcesCol}`;
119
+ }
120
+ /**
121
+ * Interactive tier selection prompt
122
+ */
123
+ export async function selectTier() {
124
+ console.log();
125
+ console.log(chalk.bold("Select a tier for your app:"));
126
+ console.log();
127
+ const costOptimized = TIER_OPTIONS.filter((t) => t.category === "cost-optimized");
128
+ const standard = TIER_OPTIONS.filter((t) => t.category === "standard");
129
+ const tierKey = await select({
130
+ message: "Tier",
131
+ choices: [
132
+ // Cost-Optimized group header
133
+ {
134
+ value: "_header_cost",
135
+ name: chalk.dim("── Cost-Optimized (auto-sleep after 3h idle) ──"),
136
+ disabled: true,
137
+ },
138
+ ...costOptimized.map((t) => ({
139
+ value: t.key,
140
+ name: ` ${formatTier(t)}`,
141
+ })),
142
+ // Standard group header
143
+ {
144
+ value: "_header_standard",
145
+ name: chalk.dim("── Standard (always-on) ──"),
146
+ disabled: true,
147
+ },
148
+ ...standard.map((t) => ({
149
+ value: t.key,
150
+ name: ` ${formatTier(t)}`,
151
+ })),
152
+ ],
153
+ default: DEFAULT_TIER,
154
+ });
155
+ return tierKey;
156
+ }
157
+ /**
158
+ * Check if a string is a valid tier key
159
+ */
160
+ export function isValidTierKey(key) {
161
+ return VALID_TIER_KEYS.includes(key);
162
+ }
163
+ /**
164
+ * Get tier display info by key
165
+ */
166
+ export function getTierInfo(key) {
167
+ return TIER_OPTIONS.find((t) => t.key === key);
168
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@byfungsi/funforge",
3
- "version": "0.2.0",
3
+ "version": "0.2.3",
4
4
  "description": "Deploy without git, without leaving your editor",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -9,22 +9,37 @@
9
9
  "funforge": "./dist/cli.js",
10
10
  "funforge-mcp": "./dist/mcp.js"
11
11
  },
12
- "files": ["dist", "README.md"],
12
+ "files": [
13
+ "dist",
14
+ "README.md"
15
+ ],
13
16
  "scripts": {
14
17
  "build": "tsc",
15
18
  "dev": "tsc --watch",
16
19
  "typecheck": "tsc --noEmit",
17
20
  "test": "vitest run",
18
21
  "start": "node dist/cli.js",
19
- "mcp": "node dist/mcp.js"
22
+ "mcp": "node dist/mcp.js",
23
+ "release:patch": "npm version patch --no-git-tag-version && pnpm run release:publish",
24
+ "release:minor": "npm version minor --no-git-tag-version && pnpm run release:publish",
25
+ "release:major": "npm version major --no-git-tag-version && pnpm run release:publish",
26
+ "release:publish": "pnpm run build && pnpm run test && npm publish --access public",
27
+ "release:dry": "pnpm run build && pnpm run test && npm publish --access public --dry-run"
20
28
  },
21
- "keywords": ["cli", "deploy", "funforge", "fungsi", "mcp"],
29
+ "keywords": [
30
+ "cli",
31
+ "deploy",
32
+ "funforge",
33
+ "fungsi",
34
+ "mcp"
35
+ ],
22
36
  "author": "Fungsi",
23
37
  "license": "MIT",
24
38
  "engines": {
25
39
  "node": ">=18.0.0"
26
40
  },
27
41
  "dependencies": {
42
+ "@inquirer/prompts": "^8.2.0",
28
43
  "@modelcontextprotocol/sdk": "^1.12.1",
29
44
  "chalk": "^5.4.1",
30
45
  "commander": "^13.1.0",