@apso/cli 0.47.0 → 0.47.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/deploy.js +1 -1
- package/dist/commands/env/add.js +2 -1
- package/dist/commands/env/list.js +4 -0
- package/dist/commands/generate.js +1 -0
- package/dist/commands/mcp/serve.js +1 -0
- package/dist/commands/open.js +1 -1
- package/dist/commands/promote.js +2 -1
- package/dist/lib/api/client.d.ts +6 -0
- package/dist/lib/api/client.js +21 -3
- package/dist/lib/environments.d.ts +2 -0
- package/dist/lib/environments.js +7 -1
- package/dist/lib/generators/go.js +4 -0
- package/dist/lib/generators/typescript.js +18 -4
- package/dist/lib/templates/go/dto/dto.eta +1 -1
- package/dist/lib/templates/guards/scope.guard.eta +1 -1
- package/dist/lib/templates/rest/controller-rest.eta +22 -2
- package/dist/lib/types/generator.d.ts +2 -0
- package/dist/lib/upgrade.d.ts +14 -4
- package/dist/lib/upgrade.js +31 -13
- package/npm-shrinkwrap.json +2 -2
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
package/dist/commands/deploy.js
CHANGED
|
@@ -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.`);
|
package/dist/commands/env/add.js
CHANGED
|
@@ -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
|
}
|
package/dist/commands/open.js
CHANGED
|
@@ -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}
|
|
39
|
+
const dashboardUrl = `${config.webUrl}/${link.workspaceSlug}/services/${link.serviceId}/dashboard`;
|
|
40
40
|
this.log(`Opening dashboard: ${dashboardUrl}`);
|
|
41
41
|
openUrl(dashboardUrl);
|
|
42
42
|
}
|
package/dist/commands/promote.js
CHANGED
|
@@ -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;
|
package/dist/lib/api/client.d.ts
CHANGED
|
@@ -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
|
*/
|
package/dist/lib/api/client.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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>;
|
package/dist/lib/environments.js
CHANGED
|
@@ -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)
|
|
@@ -244,8 +244,10 @@ class GoGenerator extends base_1.BaseGenerator {
|
|
|
244
244
|
];
|
|
245
245
|
}
|
|
246
246
|
async generateDto(options) {
|
|
247
|
+
var _a;
|
|
247
248
|
const { entity, relationships, allEntities, apiType } = options;
|
|
248
249
|
const { name: entityName, fields = [] } = entity;
|
|
250
|
+
const scopeFields = new Set((_a = (0, guards_1.entityScope)(entity, Boolean(options.auth))) === null || _a === void 0 ? void 0 : _a.fields.map((f) => f.field));
|
|
249
251
|
const columns = this.getGoFieldsForTemplate(fields, entityName);
|
|
250
252
|
const relationshipsTemplate = (0, relationships_1.getRelationshipForTemplate)(entityName, relationships, allEntities);
|
|
251
253
|
// Add Go-specific properties to relationships
|
|
@@ -258,6 +260,8 @@ class GoGenerator extends base_1.BaseGenerator {
|
|
|
258
260
|
camelReferenceName: (0, casing_1.camelCase)(rel.referenceName || rel.name),
|
|
259
261
|
pluralName: (0, casing_1.pascalCase)((0, pluralize_1.default)(rel.name)),
|
|
260
262
|
camelPluralName: (0, casing_1.camelCase)((0, pluralize_1.default)(rel.name)),
|
|
263
|
+
// The service sets a scopeBy foreign key from the caller on create.
|
|
264
|
+
scoped: scopeFields.has(`${(0, casing_1.camelCase)(rel.referenceName || rel.name)}Id`),
|
|
261
265
|
};
|
|
262
266
|
});
|
|
263
267
|
const primaryKeyColumns = columns.filter((column) => column.primary === true);
|
|
@@ -109,7 +109,7 @@ class TypeScriptGenerator extends base_1.BaseGenerator {
|
|
|
109
109
|
];
|
|
110
110
|
}
|
|
111
111
|
async generateController(options) {
|
|
112
|
-
var _a
|
|
112
|
+
var _a;
|
|
113
113
|
const { entity, relationships, relationshipMap, auth } = options;
|
|
114
114
|
const { name: entityName } = entity;
|
|
115
115
|
const relationshipsForTemplate = (0, relationships_1.getRelationshipForTemplate)(entityName, relationships);
|
|
@@ -128,9 +128,22 @@ class TypeScriptGenerator extends base_1.BaseGenerator {
|
|
|
128
128
|
const ctrlName = `${entityName}Controller`;
|
|
129
129
|
const pluralEntityName = (0, pluralize_1.default)(entityName);
|
|
130
130
|
const primaryKeyType = entity.primaryKeyType || "serial";
|
|
131
|
-
const
|
|
132
|
-
? (
|
|
133
|
-
:
|
|
131
|
+
const scoped = auth
|
|
132
|
+
? (0, guards_1.getScopedEntities)([entity], relationshipMap)[0]
|
|
133
|
+
: undefined;
|
|
134
|
+
const scopes = (_a = scoped === null || scoped === void 0 ? void 0 : scoped.scopes) !== null && _a !== void 0 ? _a : [];
|
|
135
|
+
// Only emitted when scopeOptions differ from the defaults, so services
|
|
136
|
+
// without them keep the same generated controller.
|
|
137
|
+
const scopeOptions = scoped &&
|
|
138
|
+
(scoped.bypassRoles.length > 0 ||
|
|
139
|
+
!scoped.injectOnCreate ||
|
|
140
|
+
["find", "get", "create", "update", "delete"].some((op) => !scoped.enforceOn.includes(op)))
|
|
141
|
+
? {
|
|
142
|
+
enforceOn: scoped.enforceOn,
|
|
143
|
+
bypassRoles: scoped.bypassRoles,
|
|
144
|
+
injectOnCreate: scoped.injectOnCreate,
|
|
145
|
+
}
|
|
146
|
+
: undefined;
|
|
134
147
|
const data = {
|
|
135
148
|
svcName,
|
|
136
149
|
ctrlName,
|
|
@@ -145,6 +158,7 @@ class TypeScriptGenerator extends base_1.BaseGenerator {
|
|
|
145
158
|
// ids ScopeGuard loaded for the caller into req.apsoScope.
|
|
146
159
|
crudAuth: scopes.filter((s) => s.direct),
|
|
147
160
|
crudAuthNested: scopes.filter((s) => s.fkField),
|
|
161
|
+
scopeOptions,
|
|
148
162
|
};
|
|
149
163
|
const controllerContent = await this.renderTemplate("./rest/controller-rest", data);
|
|
150
164
|
const specContent = await this.renderTemplate("./rest/controller-rest-spec", data);
|
|
@@ -51,7 +51,7 @@ type <%= it.entityName %>Create struct {
|
|
|
51
51
|
<% }); -%>
|
|
52
52
|
<% it.associations.forEach((assoc) => { -%>
|
|
53
53
|
<% if (assoc.type === 'ManyToOne' || (assoc.type === 'OneToOne' && assoc.isOwner)) { -%>
|
|
54
|
-
<%= assoc.referenceName || assoc.name %>ID <%= assoc.primaryKeyGoType || 'uint' %> `json:"<%= assoc.camelReferenceName || assoc.camelName %>Id" validate:"required"
|
|
54
|
+
<%= assoc.referenceName || assoc.name %>ID <%= assoc.primaryKeyGoType || 'uint' %> `json:"<%= assoc.camelReferenceName || assoc.camelName %>Id"<% if (!assoc.scoped) { %> validate:"required"<% } %>`
|
|
55
55
|
<% } -%>
|
|
56
56
|
<% }) -%>
|
|
57
57
|
}
|
|
@@ -168,7 +168,7 @@ export class ScopeGuard implements CanActivate {
|
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
// Check if user has bypass role
|
|
171
|
-
const userRoles: string[] = request.user?.roles
|
|
171
|
+
const userRoles: string[] = request.auth?.roles ?? request.user?.roles ?? request.roles ?? [];
|
|
172
172
|
if (scopeConfig.bypassRoles.some(role => userRoles.includes(role))) {
|
|
173
173
|
return true;
|
|
174
174
|
}
|
|
@@ -15,6 +15,26 @@ const nestedScope = (req: any, fk: string) => {
|
|
|
15
15
|
return { [fk]: ids.length ? { $in: ids } : { $isnull: true, $notnull: true } };
|
|
16
16
|
};
|
|
17
17
|
<% } %>
|
|
18
|
+
<% if (it.scopeOptions && (it.crudAuth.length || it.crudAuthNested.length)) { %>
|
|
19
|
+
// .apsorc scopeOptions: which requests the tenant filter covers and which
|
|
20
|
+
// roles skip it.
|
|
21
|
+
const ENFORCE_ON = [<%~ it.scopeOptions.enforceOn.map((op) => `'${op}'`).join(', ') %>];
|
|
22
|
+
const BYPASS_ROLES: string[] = [<%~ it.scopeOptions.bypassRoles.map((r) => `'${r}'`).join(', ') %>];
|
|
23
|
+
const scopeOperation = (req: any) => {
|
|
24
|
+
switch (req.method) {
|
|
25
|
+
case 'GET': return req.params?.id ? 'get' : 'find';
|
|
26
|
+
case 'POST': return 'create';
|
|
27
|
+
case 'PUT':
|
|
28
|
+
case 'PATCH': return 'update';
|
|
29
|
+
case 'DELETE': return 'delete';
|
|
30
|
+
default: return 'find';
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
const skipScope = (req: any) => {
|
|
34
|
+
const roles: string[] = req.auth?.roles ?? req.user?.roles ?? [];
|
|
35
|
+
return roles.some((r) => BYPASS_ROLES.includes(r)) || !ENFORCE_ON.includes(scopeOperation(req));
|
|
36
|
+
};
|
|
37
|
+
<% } %>
|
|
18
38
|
@Crud({
|
|
19
39
|
model: {
|
|
20
40
|
type: <%= it.entityName %>
|
|
@@ -63,7 +83,7 @@ const nestedScope = (req: any, fk: string) => {
|
|
|
63
83
|
<% if (it.crudAuth.length || it.crudAuthNested.length) { %>
|
|
64
84
|
// Every query and write is limited to the caller's tenant (.apsorc scopeBy).
|
|
65
85
|
@CrudAuth({
|
|
66
|
-
filter: (req: any) => ({
|
|
86
|
+
filter: (req: any) => <% if (it.scopeOptions) { %>skipScope(req) ? {} : <% } %>({
|
|
67
87
|
<% it.crudAuth.forEach((s) => { %>
|
|
68
88
|
<%= s.field %>: req.<%= s.contextKey %> ?? req.user?.<%= s.contextKey %> ?? req.auth?.<%= s.contextKey %> ?? req.auth?.workspaceId,
|
|
69
89
|
<% }) %>
|
|
@@ -71,7 +91,7 @@ const nestedScope = (req: any, fk: string) => {
|
|
|
71
91
|
...nestedScope(req, '<%= s.fkField %>'),
|
|
72
92
|
<% }) %>
|
|
73
93
|
}),
|
|
74
|
-
persist: (req: any) => ({
|
|
94
|
+
persist: (req: any) => <% if (it.scopeOptions) { %>skipScope(req)<% if (!it.scopeOptions.injectOnCreate) { %> || scopeOperation(req) === 'create'<% } %> ? {} : <% } %>({
|
|
75
95
|
<% it.crudAuth.forEach((s) => { %>
|
|
76
96
|
<%= s.field %>: req.<%= s.contextKey %> ?? req.user?.<%= s.contextKey %> ?? req.auth?.<%= s.contextKey %> ?? req.auth?.workspaceId,
|
|
77
97
|
<% }) %>
|
|
@@ -181,6 +181,8 @@ export interface DtoGenerationOptions extends EntityGenerationOptions {
|
|
|
181
181
|
* Include validation decorators/rules
|
|
182
182
|
*/
|
|
183
183
|
includeValidation?: boolean;
|
|
184
|
+
/** With scopeBy, the service sets the tenant field, so create doesn't require it. */
|
|
185
|
+
auth?: AuthConfig;
|
|
184
186
|
}
|
|
185
187
|
/**
|
|
186
188
|
* Options for generating migrations
|
package/dist/lib/upgrade.d.ts
CHANGED
|
@@ -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
|
|
12
|
-
* wait for them to finish. Returns true if the caller should retry
|
|
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
|
|
29
|
+
export declare function withUpgradeRetry<T>(action: () => Promise<T>, ctx?: PageContext): Promise<T>;
|
package/dist/lib/upgrade.js
CHANGED
|
@@ -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
|
|
34
|
-
* wait for them to finish. Returns true if the caller should retry
|
|
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
|
|
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:
|
|
64
|
+
message: `Open ${label} in your browser?`,
|
|
47
65
|
default: true,
|
|
48
66
|
},
|
|
49
67
|
]);
|
|
50
68
|
if (!proceed) {
|
|
51
|
-
console.log(` You can
|
|
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
|
|
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
|
|
83
|
-
throw new Error(`${error.message || "Plan limit reached."}
|
|
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
|
}
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@apso/cli",
|
|
3
|
-
"version": "0.47.
|
|
3
|
+
"version": "0.47.2",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@apso/cli",
|
|
9
|
-
"version": "0.47.
|
|
9
|
+
"version": "0.47.2",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@apso/schema-tools": "^0.2.0",
|
package/oclif.manifest.json
CHANGED