@byfungsi/funforge 0.2.2 → 0.4.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.
@@ -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
  });
@@ -124,19 +124,31 @@ describe("MCP Integration", () => {
124
124
  "funforge_whoami",
125
125
  "funforge_apps_list",
126
126
  "funforge_apps_create",
127
+ "funforge_apps_delete",
127
128
  "funforge_status",
128
129
  "funforge_deploy",
130
+ "funforge_stop",
131
+ "funforge_start",
132
+ "funforge_cancel",
129
133
  "funforge_env_list",
130
134
  "funforge_env_set",
131
135
  "funforge_env_unset",
132
136
  "funforge_domains_list",
133
137
  "funforge_domains_add",
138
+ "funforge_domains_remove",
139
+ "funforge_domains_verify",
134
140
  ];
135
141
  it("should define all expected tools", () => {
136
142
  // This is a documentation test to ensure we have all tools
137
- expect(expectedTools).toHaveLength(10);
143
+ expect(expectedTools).toHaveLength(16);
138
144
  expect(expectedTools).toContain("funforge_deploy");
139
145
  expect(expectedTools).toContain("funforge_whoami");
146
+ expect(expectedTools).toContain("funforge_stop");
147
+ expect(expectedTools).toContain("funforge_start");
148
+ expect(expectedTools).toContain("funforge_cancel");
149
+ expect(expectedTools).toContain("funforge_apps_delete");
150
+ expect(expectedTools).toContain("funforge_domains_remove");
151
+ expect(expectedTools).toContain("funforge_domains_verify");
140
152
  });
141
153
  });
142
154
  });
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
  }>;
@@ -153,5 +154,43 @@ export declare function getDeployment(deploymentId: string): Promise<{
153
154
  * Stream deployment logs (returns EventSource URL)
154
155
  */
155
156
  export declare function getDeploymentLogsUrl(deploymentId: string): string;
157
+ /**
158
+ * Stop the current deployment for an app
159
+ */
160
+ export declare function stopDeployment(appId: string): Promise<{
161
+ success: boolean;
162
+ }>;
163
+ /**
164
+ * Start a stopped deployment for an app
165
+ */
166
+ export declare function startDeployment(appId: string): Promise<{
167
+ success: boolean;
168
+ }>;
169
+ /**
170
+ * Cancel an in-progress deployment
171
+ */
172
+ export declare function cancelDeployment(appId: string): Promise<{
173
+ success: boolean;
174
+ }>;
175
+ /**
176
+ * Delete an app
177
+ */
178
+ export declare function deleteApp(appId: string): Promise<{
179
+ success: boolean;
180
+ }>;
181
+ /**
182
+ * Remove a custom domain from an app
183
+ */
184
+ export declare function removeDomain(appId: string, domainId: string): Promise<{
185
+ success: boolean;
186
+ }>;
187
+ /**
188
+ * Verify DNS and provision SSL for a domain
189
+ */
190
+ export declare function verifyDomain(appId: string, domainId: string): Promise<{
191
+ verified: boolean;
192
+ sslStatus?: string;
193
+ message?: string;
194
+ }>;
156
195
  export {};
157
196
  //# sourceMappingURL=api.d.ts.map
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
@@ -208,3 +208,57 @@ export function getDeploymentLogsUrl(deploymentId) {
208
208
  const apiKey = getApiKey();
209
209
  return `${config.apiUrl}/api/cli/deployments/${deploymentId}/logs?token=${apiKey}`;
210
210
  }
211
+ // ============================================
212
+ // DEPLOYMENT CONTROL ENDPOINTS
213
+ // ============================================
214
+ /**
215
+ * Stop the current deployment for an app
216
+ */
217
+ export async function stopDeployment(appId) {
218
+ return apiRequest(`/api/cli/apps/${appId}/stop`, {
219
+ method: "POST",
220
+ });
221
+ }
222
+ /**
223
+ * Start a stopped deployment for an app
224
+ */
225
+ export async function startDeployment(appId) {
226
+ return apiRequest(`/api/cli/apps/${appId}/start`, {
227
+ method: "POST",
228
+ });
229
+ }
230
+ /**
231
+ * Cancel an in-progress deployment
232
+ */
233
+ export async function cancelDeployment(appId) {
234
+ return apiRequest(`/api/cli/apps/${appId}/cancel`, {
235
+ method: "POST",
236
+ });
237
+ }
238
+ /**
239
+ * Delete an app
240
+ */
241
+ export async function deleteApp(appId) {
242
+ return apiRequest(`/api/cli/apps/${appId}`, {
243
+ method: "DELETE",
244
+ });
245
+ }
246
+ // ============================================
247
+ // DOMAIN ENDPOINTS
248
+ // ============================================
249
+ /**
250
+ * Remove a custom domain from an app
251
+ */
252
+ export async function removeDomain(appId, domainId) {
253
+ return apiRequest(`/api/cli/apps/${appId}/domains/${domainId}`, {
254
+ method: "DELETE",
255
+ });
256
+ }
257
+ /**
258
+ * Verify DNS and provision SSL for a domain
259
+ */
260
+ export async function verifyDomain(appId, domainId) {
261
+ return apiRequest(`/api/cli/apps/${appId}/domains/${domainId}/verify`, {
262
+ method: "POST",
263
+ });
264
+ }
package/dist/cli.js CHANGED
@@ -10,10 +10,11 @@ import { Command } from "commander";
10
10
  // Load version from package.json
11
11
  const require = createRequire(import.meta.url);
12
12
  const pkg = require("../package.json");
13
- import { appsCreateCommand, appsListCommand, linkCommand, } from "./commands/apps.js";
13
+ import { appsCreateCommand, appsDeleteCommand, appsListCommand, linkCommand, } from "./commands/apps.js";
14
14
  // Import commands
15
15
  import { loginCommand, logoutCommand, whoamiCommand } from "./commands/auth.js";
16
16
  import { configPullCommand, configPushCommand, configShowCommand, } from "./commands/config.js";
17
+ import { cancelCommand, startCommand, stopCommand, } from "./commands/control.js";
17
18
  import { deployCommand } from "./commands/deploy.js";
18
19
  import { domainsAddCommand, domainsListCommand, domainsRemoveCommand, domainsVerifyCommand, } from "./commands/domains.js";
19
20
  import { envListCommand, envSetCommand, envUnsetCommand, } from "./commands/env.js";
@@ -47,7 +48,13 @@ appsCmd
47
48
  .command("create <name>")
48
49
  .description("Create a new app")
49
50
  .option("-s, --slug <slug>", "Custom slug (subdomain)")
51
+ .option("-t, --tier <tier>", "Tier key (e.g., cost-optimized.small)")
50
52
  .action(appsCreateCommand);
53
+ appsCmd
54
+ .command("delete <appId>")
55
+ .description("Delete an app")
56
+ .option("-y, --yes", "Skip confirmation prompt")
57
+ .action(appsDeleteCommand);
51
58
  program
52
59
  .command("link [appId]")
53
60
  .description("Link current directory to an app")
@@ -62,6 +69,21 @@ program
62
69
  .option("--no-watch", "Don't watch deployment logs")
63
70
  .action(deployCommand);
64
71
  // ============================================
72
+ // DEPLOYMENT CONTROL COMMANDS
73
+ // ============================================
74
+ program
75
+ .command("stop")
76
+ .description("Stop the current deployment")
77
+ .action(stopCommand);
78
+ program
79
+ .command("start")
80
+ .description("Start a stopped deployment")
81
+ .action(startCommand);
82
+ program
83
+ .command("cancel")
84
+ .description("Cancel an in-progress deployment")
85
+ .action(cancelCommand);
86
+ // ============================================
65
87
  // ENVIRONMENT VARIABLES
66
88
  // ============================================
67
89
  const envCmd = program
@@ -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]
@@ -26,4 +27,12 @@ export declare function appsCreateCommand(name: string, options: {
26
27
  * If no appId provided, shows interactive selector.
27
28
  */
28
29
  export declare function linkCommand(appId?: string): Promise<void>;
30
+ /**
31
+ * funforge apps delete <appId>
32
+ *
33
+ * Delete an app (requires confirmation).
34
+ */
35
+ export declare function appsDeleteCommand(appId: string, options: {
36
+ yes?: boolean;
37
+ }): Promise<void>;
29
38
  //# sourceMappingURL=apps.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"apps.d.ts","sourceRoot":"","sources":["../../src/commands/apps.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AASH;;;;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;AAgBH;;;;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;AAaD;;;;GAIG;AACH,wBAAsB,iBAAiB,CACrC,KAAK,EAAE,MAAM,EACb,OAAO,EAAE;IAAE,GAAG,CAAC,EAAE,OAAO,CAAA;CAAE,GACzB,OAAO,CAAC,IAAI,CAAC,CA8Cf"}
@@ -2,15 +2,17 @@
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
+ import { confirm } from "@inquirer/prompts";
8
9
  import chalk from "chalk";
9
10
  import ora from "ora";
10
- import { createApp, getApp, listApps } from "../api.js";
11
+ import { createApp, deleteApp, getApp, listApps } from "../api.js";
11
12
  import { isAuthenticated } from "../credentials.js";
12
13
  import { handleError } from "../errors.js";
13
14
  import { readConfig, updateConfig } from "../project-config.js";
15
+ import { VALID_TIER_KEYS, getTierInfo, isValidTierKey, selectTier, } from "../tiers.js";
14
16
  /**
15
17
  * funforge apps list
16
18
  *
@@ -44,17 +46,34 @@ export async function appsListCommand() {
44
46
  /**
45
47
  * funforge apps create <name>
46
48
  *
47
- * Create a new app.
49
+ * Create a new app with interactive tier selection.
48
50
  */
49
51
  export async function appsCreateCommand(name, options) {
50
52
  requireAuth();
51
- const spinner = ora(`Creating app "${name}"...`).start();
53
+ // Get tier - from flag or interactive prompt
54
+ let tierKey;
55
+ if (options.tier) {
56
+ if (!isValidTierKey(options.tier)) {
57
+ console.log(chalk.red(`Invalid tier: ${options.tier}`));
58
+ console.log(chalk.gray(`Valid tiers: ${VALID_TIER_KEYS.join(", ")}`));
59
+ process.exit(1);
60
+ }
61
+ tierKey = options.tier;
62
+ }
63
+ else {
64
+ tierKey = await selectTier();
65
+ }
66
+ const tierInfo = getTierInfo(tierKey);
67
+ const spinner = ora(`Creating app "${name}" with ${tierInfo?.name ?? tierKey} tier...`).start();
52
68
  try {
53
- const { app } = await createApp({ name, slug: options.slug });
69
+ const { app } = await createApp({ name, slug: options.slug, tierKey });
54
70
  spinner.succeed(`App created!`);
55
71
  console.log();
56
72
  console.log(` Name: ${app.name}`);
57
73
  console.log(` Slug: ${chalk.cyan(app.slug)}`);
74
+ if (tierInfo) {
75
+ console.log(` Tier: ${tierInfo.category}/${tierInfo.name} (${tierInfo.credits} credits/mo)`);
76
+ }
58
77
  console.log(` ID: ${app.id}`);
59
78
  console.log();
60
79
  console.log(chalk.gray("Link this directory with:"));
@@ -133,3 +152,50 @@ function requireAuth() {
133
152
  process.exit(1);
134
153
  }
135
154
  }
155
+ /**
156
+ * funforge apps delete <appId>
157
+ *
158
+ * Delete an app (requires confirmation).
159
+ */
160
+ export async function appsDeleteCommand(appId, options) {
161
+ requireAuth();
162
+ // Get app info first
163
+ const infoSpinner = ora("Fetching app info...").start();
164
+ let app;
165
+ try {
166
+ const result = await getApp(appId);
167
+ app = result.app;
168
+ infoSpinner.stop();
169
+ }
170
+ catch (error) {
171
+ infoSpinner.fail("Failed to fetch app");
172
+ handleError(error);
173
+ return;
174
+ }
175
+ console.log();
176
+ console.log(chalk.bold("App to delete:"));
177
+ console.log(` Name: ${app.name}`);
178
+ console.log(` Slug: ${chalk.cyan(app.slug)}`);
179
+ console.log(` ID: ${app.id}`);
180
+ console.log();
181
+ // Confirm deletion
182
+ if (!options.yes) {
183
+ const confirmed = await confirm({
184
+ message: `Are you sure you want to delete "${app.name}"? This cannot be undone.`,
185
+ default: false,
186
+ });
187
+ if (!confirmed) {
188
+ console.log(chalk.gray("Deletion canceled."));
189
+ return;
190
+ }
191
+ }
192
+ const deleteSpinner = ora(`Deleting ${app.name}...`).start();
193
+ try {
194
+ await deleteApp(appId);
195
+ deleteSpinner.succeed(`App "${app.name}" deleted`);
196
+ }
197
+ catch (error) {
198
+ deleteSpinner.fail("Failed to delete app");
199
+ handleError(error);
200
+ }
201
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Deployment Control Commands
3
+ *
4
+ * - stop: Stop the current deployment
5
+ * - start: Start a stopped deployment
6
+ * - cancel: Cancel an in-progress deployment
7
+ */
8
+ /**
9
+ * funforge stop
10
+ *
11
+ * Stop the current deployment for the linked app.
12
+ */
13
+ export declare function stopCommand(): Promise<void>;
14
+ /**
15
+ * funforge start
16
+ *
17
+ * Start a stopped deployment for the linked app.
18
+ */
19
+ export declare function startCommand(): Promise<void>;
20
+ /**
21
+ * funforge cancel
22
+ *
23
+ * Cancel an in-progress deployment for the linked app.
24
+ */
25
+ export declare function cancelCommand(): Promise<void>;
26
+ //# sourceMappingURL=control.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"control.d.ts","sourceRoot":"","sources":["../../src/commands/control.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AASH;;;;GAIG;AACH,wBAAsB,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC,CAejD;AAED;;;;GAIG;AACH,wBAAsB,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAalD;AAED;;;;GAIG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAanD"}
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Deployment Control Commands
3
+ *
4
+ * - stop: Stop the current deployment
5
+ * - start: Start a stopped deployment
6
+ * - cancel: Cancel an in-progress deployment
7
+ */
8
+ import chalk from "chalk";
9
+ import ora from "ora";
10
+ import { cancelDeployment, startDeployment, stopDeployment } from "../api.js";
11
+ import { isAuthenticated } from "../credentials.js";
12
+ import { handleError } from "../errors.js";
13
+ import { readConfig } from "../project-config.js";
14
+ /**
15
+ * funforge stop
16
+ *
17
+ * Stop the current deployment for the linked app.
18
+ */
19
+ export async function stopCommand() {
20
+ requireAuth();
21
+ const appId = await getLinkedAppId();
22
+ const spinner = ora("Stopping deployment...").start();
23
+ try {
24
+ await stopDeployment(appId);
25
+ spinner.succeed("Deployment stopped");
26
+ console.log();
27
+ console.log(chalk.gray("Use `funforge start` to restart the deployment"));
28
+ }
29
+ catch (error) {
30
+ spinner.fail("Failed to stop deployment");
31
+ handleError(error);
32
+ }
33
+ }
34
+ /**
35
+ * funforge start
36
+ *
37
+ * Start a stopped deployment for the linked app.
38
+ */
39
+ export async function startCommand() {
40
+ requireAuth();
41
+ const appId = await getLinkedAppId();
42
+ const spinner = ora("Starting deployment...").start();
43
+ try {
44
+ await startDeployment(appId);
45
+ spinner.succeed("Deployment started");
46
+ }
47
+ catch (error) {
48
+ spinner.fail("Failed to start deployment");
49
+ handleError(error);
50
+ }
51
+ }
52
+ /**
53
+ * funforge cancel
54
+ *
55
+ * Cancel an in-progress deployment for the linked app.
56
+ */
57
+ export async function cancelCommand() {
58
+ requireAuth();
59
+ const appId = await getLinkedAppId();
60
+ const spinner = ora("Canceling deployment...").start();
61
+ try {
62
+ await cancelDeployment(appId);
63
+ spinner.succeed("Deployment canceled");
64
+ }
65
+ catch (error) {
66
+ spinner.fail("Failed to cancel deployment");
67
+ handleError(error);
68
+ }
69
+ }
70
+ /**
71
+ * Get the linked app ID or exit
72
+ */
73
+ async function getLinkedAppId() {
74
+ const config = await readConfig();
75
+ if (!config?.appId) {
76
+ console.log(chalk.red("Not linked to an app."));
77
+ console.log(chalk.gray("Run `funforge link <app-id>` first."));
78
+ process.exit(1);
79
+ }
80
+ return config.appId;
81
+ }
82
+ /**
83
+ * Check if user is authenticated, exit if not
84
+ */
85
+ function requireAuth() {
86
+ if (!isAuthenticated()) {
87
+ console.log(chalk.red("Not authenticated."));
88
+ console.log(chalk.gray("Run `funforge login` first."));
89
+ process.exit(1);
90
+ }
91
+ }
@@ -99,7 +99,7 @@ export async function deployCommand(options = {}) {
99
99
  console.log();
100
100
  // 6. Watch logs if requested
101
101
  if (options.watch !== false) {
102
- await watchDeployment(deployResult.deployment.id);
102
+ await watchDeployment(deployResult.deployment.id, config.appSlug);
103
103
  }
104
104
  else {
105
105
  console.log(chalk.gray("Run with --watch to follow logs"));
@@ -108,7 +108,7 @@ export async function deployCommand(options = {}) {
108
108
  /**
109
109
  * Watch deployment status until completion
110
110
  */
111
- async function watchDeployment(deploymentId) {
111
+ async function watchDeployment(deploymentId, appSlug) {
112
112
  console.log(chalk.gray("Watching deployment..."));
113
113
  console.log();
114
114
  const spinner = ora("Building...").start();
@@ -123,7 +123,12 @@ async function watchDeployment(deploymentId) {
123
123
  if (deployment.status === "live") {
124
124
  spinner.succeed(chalk.green("Deployment live!"));
125
125
  console.log();
126
- // TODO: Show deployment URL
126
+ // Show deployment URL
127
+ if (appSlug) {
128
+ console.log(chalk.bold("Your app is live at:"));
129
+ console.log(` ${chalk.cyan(`https://sarana-${appSlug}.funforge.app`)}`);
130
+ console.log();
131
+ }
127
132
  return;
128
133
  }
129
134
  if (deployment.status === "failed" || deployment.status === "canceled") {
@@ -1 +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;AAWpC;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,QAAQ,GAAG,MAAM,CA6DtD;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,CASjD"}
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 CHANGED
@@ -56,6 +56,17 @@ export function formatApiError(error) {
56
56
  }
57
57
  // 400 - Bad request / validation
58
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
+ }
59
70
  return chalk.red(message || "Invalid request.");
60
71
  }
61
72
  // 500+ - Server errors
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
  {
@@ -179,6 +195,98 @@ const TOOLS = [
179
195
  required: ["domain"],
180
196
  },
181
197
  },
198
+ {
199
+ name: "funforge_domains_remove",
200
+ description: "Remove a custom domain from the linked app.",
201
+ inputSchema: {
202
+ type: "object",
203
+ properties: {
204
+ directory: {
205
+ type: "string",
206
+ description: "Directory with funforge.json (defaults to cwd)",
207
+ },
208
+ domain: {
209
+ type: "string",
210
+ description: "Domain name or domain ID to remove",
211
+ },
212
+ },
213
+ required: ["domain"],
214
+ },
215
+ },
216
+ {
217
+ name: "funforge_domains_verify",
218
+ description: "Verify DNS configuration and provision SSL for a custom domain.",
219
+ inputSchema: {
220
+ type: "object",
221
+ properties: {
222
+ directory: {
223
+ type: "string",
224
+ description: "Directory with funforge.json (defaults to cwd)",
225
+ },
226
+ domain: {
227
+ type: "string",
228
+ description: "Domain name or domain ID to verify",
229
+ },
230
+ },
231
+ required: ["domain"],
232
+ },
233
+ },
234
+ {
235
+ name: "funforge_stop",
236
+ description: "Stop the current deployment for the linked app. The app will be scaled down to 0 replicas.",
237
+ inputSchema: {
238
+ type: "object",
239
+ properties: {
240
+ directory: {
241
+ type: "string",
242
+ description: "Directory with funforge.json (defaults to cwd)",
243
+ },
244
+ },
245
+ required: [],
246
+ },
247
+ },
248
+ {
249
+ name: "funforge_start",
250
+ description: "Start a stopped deployment for the linked app. Scales the app back up.",
251
+ inputSchema: {
252
+ type: "object",
253
+ properties: {
254
+ directory: {
255
+ type: "string",
256
+ description: "Directory with funforge.json (defaults to cwd)",
257
+ },
258
+ },
259
+ required: [],
260
+ },
261
+ },
262
+ {
263
+ name: "funforge_cancel",
264
+ description: "Cancel an in-progress deployment for the linked app. Use this to abort a build or deployment that is stuck.",
265
+ inputSchema: {
266
+ type: "object",
267
+ properties: {
268
+ directory: {
269
+ type: "string",
270
+ description: "Directory with funforge.json (defaults to cwd)",
271
+ },
272
+ },
273
+ required: [],
274
+ },
275
+ },
276
+ {
277
+ name: "funforge_apps_delete",
278
+ description: "Delete an app permanently. This will stop all deployments and remove all associated resources.",
279
+ inputSchema: {
280
+ type: "object",
281
+ properties: {
282
+ appId: {
283
+ type: "string",
284
+ description: "The app ID to delete",
285
+ },
286
+ },
287
+ required: ["appId"],
288
+ },
289
+ },
182
290
  ];
183
291
  async function handleToolCall(name, args) {
184
292
  try {
@@ -188,7 +296,7 @@ async function handleToolCall(name, args) {
188
296
  case "funforge_apps_list":
189
297
  return await handleAppsList();
190
298
  case "funforge_apps_create":
191
- return await handleAppsCreate(args.name, args.slug);
299
+ return await handleAppsCreate(args.name, args.tierKey, args.slug);
192
300
  case "funforge_status":
193
301
  return await handleStatus(args.directory);
194
302
  case "funforge_deploy":
@@ -203,6 +311,18 @@ async function handleToolCall(name, args) {
203
311
  return await handleDomainsList(args.directory);
204
312
  case "funforge_domains_add":
205
313
  return await handleDomainsAdd(args.directory, args.domain, args.verificationMethod);
314
+ case "funforge_domains_remove":
315
+ return await handleDomainsRemove(args.directory, args.domain);
316
+ case "funforge_domains_verify":
317
+ return await handleDomainsVerify(args.directory, args.domain);
318
+ case "funforge_stop":
319
+ return await handleStop(args.directory);
320
+ case "funforge_start":
321
+ return await handleStart(args.directory);
322
+ case "funforge_cancel":
323
+ return await handleCancel(args.directory);
324
+ case "funforge_apps_delete":
325
+ return await handleAppsDelete(args.appId);
206
326
  default:
207
327
  return errorResult(`Unknown tool: ${name}`);
208
328
  }
@@ -240,15 +360,16 @@ async function handleAppsList() {
240
360
  })),
241
361
  });
242
362
  }
243
- async function handleAppsCreate(name, slug) {
363
+ async function handleAppsCreate(name, tierKey, slug) {
244
364
  requireAuth();
245
- const { app } = await createApp({ name, slug });
365
+ const { app } = await createApp({ name, slug, tierKey });
246
366
  return successResult({
247
367
  message: `App "${app.name}" created successfully`,
248
368
  app: {
249
369
  id: app.id,
250
370
  name: app.name,
251
371
  slug: app.slug,
372
+ tierKey,
252
373
  },
253
374
  });
254
375
  }
@@ -410,6 +531,126 @@ async function handleDomainsAdd(directory, domain, verificationMethod) {
410
531
  nextStep: "Add the DNS record above, then verify with funforge domains verify command",
411
532
  });
412
533
  }
534
+ async function handleDomainsRemove(directory, domain) {
535
+ requireAuth();
536
+ const dir = directory || process.cwd();
537
+ const appId = await getLinkedAppId(dir);
538
+ if (!appId) {
539
+ return errorResult(`No app linked in ${dir}.`);
540
+ }
541
+ const { domains } = await apiRequest(`/api/cli/apps/${appId}/domains`);
542
+ const cleanDomain = domain.toLowerCase();
543
+ const targetDomain = domains.find((d) => d.domain.toLowerCase() === cleanDomain || d.id === domain);
544
+ if (!targetDomain) {
545
+ return errorResult(`Domain not found: ${domain}. Use funforge_domains_list to see available domains.`);
546
+ }
547
+ await apiRequest(`/api/cli/apps/${appId}/domains/${targetDomain.id}`, {
548
+ method: "DELETE",
549
+ });
550
+ return successResult({
551
+ message: `Domain ${targetDomain.domain} removed`,
552
+ domainId: targetDomain.id,
553
+ });
554
+ }
555
+ async function handleDomainsVerify(directory, domain) {
556
+ requireAuth();
557
+ const dir = directory || process.cwd();
558
+ const appId = await getLinkedAppId(dir);
559
+ if (!appId) {
560
+ return errorResult(`No app linked in ${dir}.`);
561
+ }
562
+ const { domains } = await apiRequest(`/api/cli/apps/${appId}/domains`);
563
+ const cleanDomain = domain.toLowerCase();
564
+ const targetDomain = domains.find((d) => d.domain.toLowerCase() === cleanDomain || d.id === domain);
565
+ if (!targetDomain) {
566
+ return errorResult(`Domain not found: ${domain}. Use funforge_domains_list to see available domains.`);
567
+ }
568
+ if (targetDomain.verified) {
569
+ return successResult({
570
+ message: `Domain ${targetDomain.domain} is already verified`,
571
+ domain: targetDomain.domain,
572
+ verified: true,
573
+ });
574
+ }
575
+ const result = await apiRequest(`/api/cli/apps/${appId}/domains/${targetDomain.id}/verify`, { method: "POST" });
576
+ if (result.verified) {
577
+ return successResult({
578
+ message: `Domain ${targetDomain.domain} verified successfully`,
579
+ domain: targetDomain.domain,
580
+ verified: true,
581
+ sslStatus: result.sslStatus,
582
+ });
583
+ }
584
+ return successResult({
585
+ message: result.message || "Domain verification failed",
586
+ domain: targetDomain.domain,
587
+ verified: false,
588
+ hint: "Make sure your DNS record is correctly configured. DNS changes can take up to 48 hours to propagate.",
589
+ });
590
+ }
591
+ async function handleStop(directory) {
592
+ requireAuth();
593
+ const dir = directory || process.cwd();
594
+ const appId = await getLinkedAppId(dir);
595
+ if (!appId) {
596
+ return errorResult(`No app linked in ${dir}.`);
597
+ }
598
+ await apiRequest(`/api/cli/apps/${appId}/stop`, {
599
+ method: "POST",
600
+ });
601
+ return successResult({
602
+ message: "Deployment stopped",
603
+ appId,
604
+ note: "Use funforge_start to restart the deployment",
605
+ });
606
+ }
607
+ async function handleStart(directory) {
608
+ requireAuth();
609
+ const dir = directory || process.cwd();
610
+ const appId = await getLinkedAppId(dir);
611
+ if (!appId) {
612
+ return errorResult(`No app linked in ${dir}.`);
613
+ }
614
+ await apiRequest(`/api/cli/apps/${appId}/start`, {
615
+ method: "POST",
616
+ });
617
+ return successResult({
618
+ message: "Deployment started",
619
+ appId,
620
+ });
621
+ }
622
+ async function handleCancel(directory) {
623
+ requireAuth();
624
+ const dir = directory || process.cwd();
625
+ const appId = await getLinkedAppId(dir);
626
+ if (!appId) {
627
+ return errorResult(`No app linked in ${dir}.`);
628
+ }
629
+ await apiRequest(`/api/cli/apps/${appId}/cancel`, {
630
+ method: "POST",
631
+ });
632
+ return successResult({
633
+ message: "Deployment canceled",
634
+ appId,
635
+ note: "The in-progress build or deployment has been canceled",
636
+ });
637
+ }
638
+ async function handleAppsDelete(appId) {
639
+ requireAuth();
640
+ const { app } = await apiRequest(`/api/cli/apps/${appId}`);
641
+ await apiRequest(`/api/cli/apps/${appId}`, {
642
+ method: "DELETE",
643
+ });
644
+ return successResult({
645
+ message: `App "${app.name}" deleted successfully`,
646
+ app: {
647
+ id: app.id,
648
+ name: app.name,
649
+ slug: app.slug,
650
+ },
651
+ note: "All deployments and resources have been removed",
652
+ });
653
+ }
413
654
  // ============================================
414
655
  // HELPERS
415
656
  // ============================================
@@ -453,7 +694,7 @@ function formatError(error) {
453
694
  async function main() {
454
695
  const server = new Server({
455
696
  name: "funforge",
456
- version: "0.1.0",
697
+ version: "0.3.0",
457
698
  }, {
458
699
  capabilities: {
459
700
  tools: {},
@@ -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.2",
3
+ "version": "0.4.0",
4
4
  "description": "Deploy without git, without leaving your editor",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -9,10 +9,7 @@
9
9
  "funforge": "./dist/cli.js",
10
10
  "funforge-mcp": "./dist/mcp.js"
11
11
  },
12
- "files": [
13
- "dist",
14
- "README.md"
15
- ],
12
+ "files": ["dist", "README.md"],
16
13
  "scripts": {
17
14
  "build": "tsc",
18
15
  "dev": "tsc --watch",
@@ -26,19 +23,14 @@
26
23
  "release:publish": "pnpm run build && pnpm run test && npm publish --access public",
27
24
  "release:dry": "pnpm run build && pnpm run test && npm publish --access public --dry-run"
28
25
  },
29
- "keywords": [
30
- "cli",
31
- "deploy",
32
- "funforge",
33
- "fungsi",
34
- "mcp"
35
- ],
26
+ "keywords": ["cli", "deploy", "funforge", "fungsi", "mcp"],
36
27
  "author": "Fungsi",
37
28
  "license": "MIT",
38
29
  "engines": {
39
30
  "node": ">=18.0.0"
40
31
  },
41
32
  "dependencies": {
33
+ "@inquirer/prompts": "^8.2.0",
42
34
  "@modelcontextprotocol/sdk": "^1.12.1",
43
35
  "chalk": "^5.4.1",
44
36
  "commander": "^13.1.0",