@apso/cli 0.47.0 → 0.47.1

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.
@@ -100,7 +100,7 @@ class Deploy extends base_command_1.default {
100
100
  }
101
101
  }
102
102
  // Trigger build
103
- const build = await (0, upgrade_1.withUpgradeRetry)(() => services_1.buildApi.trigger(link.workspaceSlug, link.serviceSlug, env));
103
+ const build = await (0, upgrade_1.withUpgradeRetry)(() => services_1.buildApi.trigger(link.workspaceSlug, link.serviceSlug, env), { workspaceSlug: link.workspaceSlug, serviceId: service.id });
104
104
  this.log(`Build started: ${build.id}`);
105
105
  if (flags["no-wait"]) {
106
106
  this.log(`Deploy triggered. Run 'apso status --env ${env}' to check progress.`);
@@ -5,6 +5,7 @@ const core_1 = require("@oclif/core");
5
5
  const base_command_1 = tslib_1.__importDefault(require("../../lib/base-command"));
6
6
  const services_1 = require("../../lib/api/services");
7
7
  const environments_1 = require("../../lib/environments");
8
+ const upgrade_1 = require("../../lib/upgrade");
8
9
  class EnvAdd extends base_command_1.default {
9
10
  async run() {
10
11
  const { flags } = await this.parse(EnvAdd);
@@ -16,7 +17,7 @@ class EnvAdd extends base_command_1.default {
16
17
  const ok = await (0, environments_1.confirmOrExit)(this, flags.yes, `Add staging to "${service.name}"? This starts the paid staging add-on.`);
17
18
  if (!ok)
18
19
  return this.log("Cancelled.");
19
- await services_1.envApi.addStaging(service.id);
20
+ await (0, upgrade_1.withUpgradeRetry)(() => services_1.envApi.addStaging(service.id), (0, environments_1.pageContext)(service));
20
21
  this.log(`Staging is being created for "${service.name}". Deploy to it with 'apso deploy --env staging'.`);
21
22
  }
22
23
  }
@@ -15,6 +15,10 @@ class EnvList extends base_command_1.default {
15
15
  this.log(JSON.stringify(envs, null, 2));
16
16
  return;
17
17
  }
18
+ // Production's row appears with the first deploy; until then say so rather than print nothing.
19
+ if (!envs.some((e) => e.environment === "production")) {
20
+ this.log(`${"production".padEnd(12)}${"not deployed yet".padEnd(22)}-`);
21
+ }
18
22
  for (const e of envs) {
19
23
  this.log(`${e.environment.padEnd(12)}${(e.buildStatus || e.status || "-").padEnd(22)}${(_a = e.url) !== null && _a !== void 0 ? _a : "-"}`);
20
24
  }
@@ -36,7 +36,7 @@ class Open extends base_command_1.default {
36
36
  }
37
37
  else {
38
38
  // Open the dashboard (default)
39
- const dashboardUrl = `${config.webUrl}/workspaces/${link.workspaceSlug}/services/${link.serviceSlug}`;
39
+ const dashboardUrl = `${config.webUrl}/${link.workspaceSlug}/services/${link.serviceId}/dashboard`;
40
40
  this.log(`Opening dashboard: ${dashboardUrl}`);
41
41
  openUrl(dashboardUrl);
42
42
  }
@@ -6,6 +6,7 @@ const core_1 = require("@oclif/core");
6
6
  const base_command_1 = tslib_1.__importDefault(require("../lib/base-command"));
7
7
  const services_1 = require("../lib/api/services");
8
8
  const environments_1 = require("../lib/environments");
9
+ const upgrade_1 = require("../lib/upgrade");
9
10
  const spinner_1 = require("../lib/utils/spinner");
10
11
  class Promote extends base_command_1.default {
11
12
  async run() {
@@ -23,7 +24,7 @@ class Promote extends base_command_1.default {
23
24
  const ok = await (0, environments_1.confirmOrExit)(this, flags.yes, `Promote staging to production for "${service.name}"?`);
24
25
  if (!ok)
25
26
  return this.log("Cancelled.");
26
- await services_1.envApi.promote(service.id);
27
+ await (0, upgrade_1.withUpgradeRetry)(() => services_1.envApi.promote(service.id), (0, environments_1.pageContext)(service));
27
28
  if (flags["no-wait"]) {
28
29
  this.log("Promote started. Run 'apso status' to check progress.");
29
30
  return;
@@ -9,6 +9,12 @@
9
9
  * - Timeout handling
10
10
  */
11
11
  import { ApiRequestOptions } from "./types";
12
+ /**
13
+ * Refusals the user resolves in the web app, and where: "service" is the
14
+ * service's Deployments page (size and staging), "billing" the workspace's
15
+ * Billing page.
16
+ */
17
+ export declare const APP_FIX_PAGES: Record<string, "service" | "billing">;
12
18
  /**
13
19
  * Custom error class for API errors
14
20
  */
@@ -10,7 +10,7 @@
10
10
  * - Timeout handling
11
11
  */
12
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
- exports.api = exports.apiRequest = exports.checkNetworkConnectivity = exports.ApiClientError = void 0;
13
+ exports.api = exports.apiRequest = exports.checkNetworkConnectivity = exports.ApiClientError = exports.APP_FIX_PAGES = void 0;
14
14
  const config_1 = require("../config");
15
15
  /**
16
16
  * Default request timeout in milliseconds
@@ -24,6 +24,17 @@ const MAX_RETRIES = 3;
24
24
  * Base delay for exponential backoff in milliseconds
25
25
  */
26
26
  const BACKOFF_BASE_DELAY = 1000;
27
+ /**
28
+ * Refusals the user resolves in the web app, and where: "service" is the
29
+ * service's Deployments page (size and staging), "billing" the workspace's
30
+ * Billing page.
31
+ */
32
+ exports.APP_FIX_PAGES = {
33
+ STAGING_REQUIRES_PAID_SERVICE: "service",
34
+ STAGING_ADDON_REQUIRED: "service",
35
+ STAGING_ADDON_PURCHASE_FAILED: "service",
36
+ SERVICE_SUSPENDED: "billing",
37
+ };
27
38
  /**
28
39
  * Custom error class for API errors
29
40
  */
@@ -45,7 +56,9 @@ class ApiClientError extends Error {
45
56
  * upgrading (e.g. the free-tier service or deploy limit).
46
57
  */
47
58
  isLimitError() {
48
- return (this.statusCode === 403 &&
59
+ if (this.code && this.code in exports.APP_FIX_PAGES)
60
+ return true;
61
+ return ((this.statusCode === 403 || this.statusCode === 402) &&
49
62
  (this.code === "SERVICE_LIMIT_REACHED" ||
50
63
  this.code === "LIMIT_REACHED" ||
51
64
  Boolean(this.upgradeUrl)));
@@ -306,7 +319,12 @@ async function apiRequest(path, options = {}) {
306
319
  }
307
320
  // Client errors - don't retry
308
321
  const limit = extractLimitInfo(errorData);
309
- const clientError = new ApiClientError(limit.message || errorData.message || "Request failed", response.status, errorData.details);
322
+ // The app's API routes put their sentence in `.error`; the backend uses `.message`.
323
+ const appError = errorData.error;
324
+ const clientError = new ApiClientError(limit.message ||
325
+ errorData.message ||
326
+ (typeof appError === "string" ? appError : undefined) ||
327
+ "Request failed", response.status, errorData.details);
310
328
  clientError.code = limit.code;
311
329
  clientError.upgradeUrl = limit.upgradeUrl;
312
330
  throw clientError;
@@ -1,7 +1,9 @@
1
1
  import type { Command } from "@oclif/core";
2
2
  import { type ServiceEnv } from "./api/services";
3
3
  import type { Service } from "./api/types";
4
+ import type { PageContext } from "./upgrade";
4
5
  export declare const envFlag: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
5
6
  export declare const asEnv: (value: string | undefined) => ServiceEnv;
6
7
  export declare function linkedService(cmd: Command): Promise<Service>;
8
+ export declare function pageContext(service: Service): PageContext;
7
9
  export declare function confirmOrExit(cmd: Command, yes: boolean, question: string): Promise<boolean>;
@@ -23,7 +23,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
23
23
  return result;
24
24
  };
25
25
  Object.defineProperty(exports, "__esModule", { value: true });
26
- exports.confirmOrExit = exports.linkedService = exports.asEnv = exports.envFlag = void 0;
26
+ exports.confirmOrExit = exports.pageContext = exports.linkedService = exports.asEnv = exports.envFlag = void 0;
27
27
  const core_1 = require("@oclif/core");
28
28
  const config_1 = require("./config");
29
29
  const services_1 = require("./api/services");
@@ -46,6 +46,12 @@ async function linkedService(cmd) {
46
46
  return services_1.servicesApi.get(link.workspaceId, link.serviceSlug);
47
47
  }
48
48
  exports.linkedService = linkedService;
49
+ // Where the linked service lives in the web app, for links to the page that fixes a refusal.
50
+ function pageContext(service) {
51
+ var _a;
52
+ return { workspaceSlug: (_a = config_1.projectLink.read()) === null || _a === void 0 ? void 0 : _a.workspaceSlug, serviceId: service.id };
53
+ }
54
+ exports.pageContext = pageContext;
49
55
  // Asks before a step that costs money or replaces something; headless runs must pass --yes.
50
56
  async function confirmOrExit(cmd, yes, question) {
51
57
  if (yes)
@@ -7,13 +7,23 @@
7
7
  * "you've hit a limit, upgrade?" flow developers expect from modern CLIs.
8
8
  */
9
9
  import { ApiClientError } from "./api/client";
10
+ /** Where the user is when the refusal happened, so the link can go to the exact page. */
11
+ export interface PageContext {
12
+ workspaceSlug?: string;
13
+ serviceId?: string;
14
+ }
15
+ /** The web app page that resolves this refusal, and how to name it in a prompt. */
16
+ export declare function fixPage(error: ApiClientError, ctx?: PageContext): {
17
+ url: string;
18
+ label: string;
19
+ };
10
20
  /**
11
- * Prompt the user to upgrade after a limit error, open the upgrade page, and
12
- * wait for them to finish. Returns true if the caller should retry the action.
21
+ * Prompt the user to resolve a plan or billing refusal in the web app, open the
22
+ * page, and wait for them to finish. Returns true if the caller should retry.
13
23
  */
14
- export declare function promptUpgrade(error: ApiClientError): Promise<boolean>;
24
+ export declare function promptUpgrade(error: ApiClientError, ctx?: PageContext): Promise<boolean>;
15
25
  /**
16
26
  * Run an action; if it fails with an upgradeable plan-limit error, offer to
17
27
  * upgrade and retry once.
18
28
  */
19
- export declare function withUpgradeRetry<T>(action: () => Promise<T>): Promise<T>;
29
+ export declare function withUpgradeRetry<T>(action: () => Promise<T>, ctx?: PageContext): Promise<T>;
@@ -8,7 +8,7 @@
8
8
  * "you've hit a limit, upgrade?" flow developers expect from modern CLIs.
9
9
  */
10
10
  Object.defineProperty(exports, "__esModule", { value: true });
11
- exports.withUpgradeRetry = exports.promptUpgrade = void 0;
11
+ exports.withUpgradeRetry = exports.promptUpgrade = exports.fixPage = void 0;
12
12
  const tslib_1 = require("tslib");
13
13
  const child_process_1 = require("child_process");
14
14
  const inquirer_1 = tslib_1.__importDefault(require("inquirer"));
@@ -29,13 +29,31 @@ function openBrowser(url) {
29
29
  (0, child_process_1.exec)(command, () => resolve());
30
30
  });
31
31
  }
32
+ /** The web app page that resolves this refusal, and how to name it in a prompt. */
33
+ function fixPage(error, ctx = {}) {
34
+ const web = config_1.globalConfig.read().webUrl;
35
+ const where = error.code ? client_1.APP_FIX_PAGES[error.code] : undefined;
36
+ if (where === "service" && ctx.workspaceSlug && ctx.serviceId) {
37
+ return {
38
+ url: `${web}/${ctx.workspaceSlug}/services/${ctx.serviceId}/deployments`,
39
+ label: "the service's Deployments page",
40
+ };
41
+ }
42
+ if (where === "billing" && ctx.workspaceSlug) {
43
+ return { url: `${web}/${ctx.workspaceSlug}/billing`, label: "Billing" };
44
+ }
45
+ return {
46
+ url: `${web}${error.upgradeUrl || "/billing/upgrade"}`,
47
+ label: "the upgrade page",
48
+ };
49
+ }
50
+ exports.fixPage = fixPage;
32
51
  /**
33
- * Prompt the user to upgrade after a limit error, open the upgrade page, and
34
- * wait for them to finish. Returns true if the caller should retry the action.
52
+ * Prompt the user to resolve a plan or billing refusal in the web app, open the
53
+ * page, and wait for them to finish. Returns true if the caller should retry.
35
54
  */
36
- async function promptUpgrade(error) {
37
- const config = config_1.globalConfig.read();
38
- const upgradeUrl = `${config.webUrl}${error.upgradeUrl || "/billing/upgrade"}`;
55
+ async function promptUpgrade(error, ctx = {}) {
56
+ const { url: upgradeUrl, label } = fixPage(error, ctx);
39
57
  console.log("");
40
58
  console.log(` ⚠ ${error.message || "You've reached a plan limit."}`);
41
59
  console.log("");
@@ -43,12 +61,12 @@ async function promptUpgrade(error) {
43
61
  {
44
62
  type: "confirm",
45
63
  name: "proceed",
46
- message: "Open the upgrade page in your browser?",
64
+ message: `Open ${label} in your browser?`,
47
65
  default: true,
48
66
  },
49
67
  ]);
50
68
  if (!proceed) {
51
- console.log(` You can upgrade any time at: ${upgradeUrl}`);
69
+ console.log(` You can do this any time at: ${upgradeUrl}`);
52
70
  return false;
53
71
  }
54
72
  console.log("");
@@ -59,7 +77,7 @@ async function promptUpgrade(error) {
59
77
  {
60
78
  type: "confirm",
61
79
  name: "done",
62
- message: "Press enter once your upgrade is complete to retry",
80
+ message: "Press enter once you are done there to retry",
63
81
  default: true,
64
82
  },
65
83
  ]);
@@ -70,7 +88,7 @@ exports.promptUpgrade = promptUpgrade;
70
88
  * Run an action; if it fails with an upgradeable plan-limit error, offer to
71
89
  * upgrade and retry once.
72
90
  */
73
- async function withUpgradeRetry(action) {
91
+ async function withUpgradeRetry(action, ctx = {}) {
74
92
  try {
75
93
  return await action();
76
94
  }
@@ -79,10 +97,10 @@ async function withUpgradeRetry(action) {
79
97
  // Headless (agent/CI): never open a browser or prompt — surface the limit
80
98
  // as a clear error with the upgrade URL so the caller can act on it.
81
99
  if (!(0, interactive_1.isInteractive)()) {
82
- const upgradeUrl = `${config_1.globalConfig.read().webUrl}${error.upgradeUrl || "/billing/upgrade"}`;
83
- throw new Error(`${error.message || "Plan limit reached."} Upgrade at ${upgradeUrl}`);
100
+ const { url } = fixPage(error, ctx);
101
+ throw new Error(`${(error.message || "Plan limit reached").replace(/\.?$/, ".")} Resolve it at ${url}`);
84
102
  }
85
- const retry = await promptUpgrade(error);
103
+ const retry = await promptUpgrade(error, ctx);
86
104
  if (retry) {
87
105
  return action();
88
106
  }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@apso/cli",
3
- "version": "0.47.0",
3
+ "version": "0.47.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@apso/cli",
9
- "version": "0.47.0",
9
+ "version": "0.47.1",
10
10
  "license": "Apache-2.0",
11
11
  "dependencies": {
12
12
  "@apso/schema-tools": "^0.2.0",
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.47.0",
2
+ "version": "0.47.1",
3
3
  "commands": {
4
4
  "config": {
5
5
  "id": "config",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apso/cli",
3
- "version": "0.47.0",
3
+ "version": "0.47.1",
4
4
  "mcpName": "io.github.apsoai/apso",
5
5
  "description": "Apso CLI",
6
6
  "author": "Apso by Mavric - @mavric",