@apso/cli 0.46.4 → 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.
- package/dist/commands/deploy.d.ts +1 -0
- package/dist/commands/deploy.js +13 -6
- package/dist/commands/env/add.d.ts +12 -0
- package/dist/commands/env/add.js +32 -0
- package/dist/commands/env/list.d.ts +9 -0
- package/dist/commands/env/list.js +35 -0
- package/dist/commands/env/remove.d.ts +12 -0
- package/dist/commands/env/remove.js +31 -0
- package/dist/commands/open.d.ts +1 -0
- package/dist/commands/open.js +11 -5
- package/dist/commands/promote.d.ts +14 -0
- package/dist/commands/promote.js +71 -0
- package/dist/commands/status.d.ts +3 -1
- package/dist/commands/status.js +20 -2
- package/dist/lib/api/client.d.ts +6 -0
- package/dist/lib/api/client.js +21 -3
- package/dist/lib/api/services.d.ts +41 -3
- package/dist/lib/api/services.js +61 -14
- package/dist/lib/environments.d.ts +9 -0
- package/dist/lib/environments.js +67 -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 +175 -5
- package/package.json +4 -1
|
@@ -3,6 +3,7 @@ export default class Deploy extends BaseCommand {
|
|
|
3
3
|
static description: string;
|
|
4
4
|
static examples: string[];
|
|
5
5
|
static flags: {
|
|
6
|
+
env: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
|
|
6
7
|
"no-wait": import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
7
8
|
yes: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
8
9
|
"skip-migrate": import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
package/dist/commands/deploy.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 config_1 = require("../lib/config");
|
|
7
7
|
const services_1 = require("../lib/api/services");
|
|
8
|
+
const environments_1 = require("../lib/environments");
|
|
8
9
|
const upgrade_1 = require("../lib/upgrade");
|
|
9
10
|
const git = tslib_1.__importStar(require("../lib/git"));
|
|
10
11
|
const apsorc_parser_1 = require("../lib/apsorc-parser");
|
|
@@ -15,6 +16,7 @@ const server_push_1 = require("../lib/deploy/server-push");
|
|
|
15
16
|
class Deploy extends base_command_1.default {
|
|
16
17
|
async run() {
|
|
17
18
|
const { flags } = await this.parse(Deploy);
|
|
19
|
+
const env = (0, environments_1.asEnv)(flags.env);
|
|
18
20
|
// Auth guard
|
|
19
21
|
if (!config_1.credentials.isValid()) {
|
|
20
22
|
this.error("Not logged in. Run 'apso login' first.");
|
|
@@ -73,10 +75,10 @@ class Deploy extends base_command_1.default {
|
|
|
73
75
|
}
|
|
74
76
|
// Determine source
|
|
75
77
|
if (service.githubRepo || flags.repo) {
|
|
76
|
-
this.log(`Deploying "${service.name}" from ${service.githubRepo || flags.repo}#${flags.branch}...`);
|
|
78
|
+
this.log(`Deploying "${service.name}" (${env}) from ${service.githubRepo || flags.repo}#${flags.branch}...`);
|
|
77
79
|
}
|
|
78
80
|
else {
|
|
79
|
-
this.log(`Deploying "${service.name}" via Apso Platform...`);
|
|
81
|
+
this.log(`Deploying "${service.name}" (${env}) via Apso Platform...`);
|
|
80
82
|
}
|
|
81
83
|
// Confirmation
|
|
82
84
|
if (!flags.yes && migrationCount > 0) {
|
|
@@ -98,24 +100,27 @@ class Deploy extends base_command_1.default {
|
|
|
98
100
|
}
|
|
99
101
|
}
|
|
100
102
|
// Trigger build
|
|
101
|
-
const build = await (0, upgrade_1.withUpgradeRetry)(() => services_1.buildApi.trigger(link.workspaceSlug, link.serviceSlug));
|
|
103
|
+
const build = await (0, upgrade_1.withUpgradeRetry)(() => services_1.buildApi.trigger(link.workspaceSlug, link.serviceSlug, env), { workspaceSlug: link.workspaceSlug, serviceId: service.id });
|
|
102
104
|
this.log(`Build started: ${build.id}`);
|
|
103
105
|
if (flags["no-wait"]) {
|
|
104
|
-
this.log(
|
|
106
|
+
this.log(`Deploy triggered. Run 'apso status --env ${env}' to check progress.`);
|
|
105
107
|
return;
|
|
106
108
|
}
|
|
107
109
|
// Poll with spinner
|
|
108
110
|
const spinner = await (0, spinner_1.createSpinner)("Deploying...");
|
|
109
111
|
spinner.start();
|
|
110
112
|
const finalStatus = await services_1.buildApi.waitForCompletion(build.id, {
|
|
113
|
+
env,
|
|
111
114
|
onProgress: (status) => {
|
|
112
115
|
spinner.text = `Deploy status: ${status.status}...`;
|
|
113
116
|
},
|
|
114
117
|
});
|
|
115
118
|
if (finalStatus.status === "success") {
|
|
116
119
|
spinner.succeed("Deploy succeeded.");
|
|
117
|
-
|
|
118
|
-
|
|
120
|
+
const row = env === "production" ? undefined : await services_1.envApi.get(service.id, env);
|
|
121
|
+
const endpoint = row ? row.url : service.endpoint;
|
|
122
|
+
if (endpoint) {
|
|
123
|
+
this.log(`Endpoint: ${endpoint}`);
|
|
119
124
|
}
|
|
120
125
|
}
|
|
121
126
|
else {
|
|
@@ -271,8 +276,10 @@ Deploy.examples = [
|
|
|
271
276
|
`$ apso deploy --yes`,
|
|
272
277
|
`$ apso deploy --skip-migrate`,
|
|
273
278
|
`$ apso deploy --no-wait`,
|
|
279
|
+
`$ apso deploy --env staging`,
|
|
274
280
|
];
|
|
275
281
|
Deploy.flags = {
|
|
282
|
+
env: environments_1.envFlag,
|
|
276
283
|
"no-wait": core_1.Flags.boolean({
|
|
277
284
|
description: "Trigger deploy without waiting for completion",
|
|
278
285
|
default: false,
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import BaseCommand from "../../lib/base-command";
|
|
2
|
+
export default class EnvAdd extends BaseCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static args: {
|
|
6
|
+
environment: import("@oclif/core/lib/interfaces/parser").Arg<string, Record<string, unknown>>;
|
|
7
|
+
};
|
|
8
|
+
static flags: {
|
|
9
|
+
yes: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
};
|
|
11
|
+
run(): Promise<void>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const tslib_1 = require("tslib");
|
|
4
|
+
const core_1 = require("@oclif/core");
|
|
5
|
+
const base_command_1 = tslib_1.__importDefault(require("../../lib/base-command"));
|
|
6
|
+
const services_1 = require("../../lib/api/services");
|
|
7
|
+
const environments_1 = require("../../lib/environments");
|
|
8
|
+
const upgrade_1 = require("../../lib/upgrade");
|
|
9
|
+
class EnvAdd extends base_command_1.default {
|
|
10
|
+
async run() {
|
|
11
|
+
const { flags } = await this.parse(EnvAdd);
|
|
12
|
+
const service = await (0, environments_1.linkedService)(this);
|
|
13
|
+
if (await services_1.envApi.get(service.id, "staging")) {
|
|
14
|
+
this.log(`"${service.name}" already has staging. Deploy to it with 'apso deploy --env staging'.`);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const ok = await (0, environments_1.confirmOrExit)(this, flags.yes, `Add staging to "${service.name}"? This starts the paid staging add-on.`);
|
|
18
|
+
if (!ok)
|
|
19
|
+
return this.log("Cancelled.");
|
|
20
|
+
await (0, upgrade_1.withUpgradeRetry)(() => services_1.envApi.addStaging(service.id), (0, environments_1.pageContext)(service));
|
|
21
|
+
this.log(`Staging is being created for "${service.name}". Deploy to it with 'apso deploy --env staging'.`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
EnvAdd.description = "Add a staging environment to the linked service. It gets its own stack, URL and database branch, and starts the paid staging add-on.";
|
|
25
|
+
EnvAdd.examples = [`$ apso env add staging`, `$ apso env add staging --yes`];
|
|
26
|
+
EnvAdd.args = {
|
|
27
|
+
environment: core_1.Args.string({ description: "Environment to add", options: ["staging"], required: true }),
|
|
28
|
+
};
|
|
29
|
+
EnvAdd.flags = {
|
|
30
|
+
yes: core_1.Flags.boolean({ char: "y", description: "Skip the confirmation prompt", default: false }),
|
|
31
|
+
};
|
|
32
|
+
exports.default = EnvAdd;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import BaseCommand from "../../lib/base-command";
|
|
2
|
+
export default class EnvList extends BaseCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
7
|
+
};
|
|
8
|
+
run(): Promise<void>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const tslib_1 = require("tslib");
|
|
4
|
+
const core_1 = require("@oclif/core");
|
|
5
|
+
const base_command_1 = tslib_1.__importDefault(require("../../lib/base-command"));
|
|
6
|
+
const services_1 = require("../../lib/api/services");
|
|
7
|
+
const environments_1 = require("../../lib/environments");
|
|
8
|
+
class EnvList extends base_command_1.default {
|
|
9
|
+
async run() {
|
|
10
|
+
var _a;
|
|
11
|
+
const { flags } = await this.parse(EnvList);
|
|
12
|
+
const service = await (0, environments_1.linkedService)(this);
|
|
13
|
+
const envs = await services_1.envApi.list(service.id);
|
|
14
|
+
if (flags.json) {
|
|
15
|
+
this.log(JSON.stringify(envs, null, 2));
|
|
16
|
+
return;
|
|
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
|
+
}
|
|
22
|
+
for (const e of envs) {
|
|
23
|
+
this.log(`${e.environment.padEnd(12)}${(e.buildStatus || e.status || "-").padEnd(22)}${(_a = e.url) !== null && _a !== void 0 ? _a : "-"}`);
|
|
24
|
+
}
|
|
25
|
+
if (!envs.some((e) => e.environment === "staging")) {
|
|
26
|
+
this.log("\nNo staging environment. Add one with 'apso env add staging'.");
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
EnvList.description = "List the linked service's environments (production, staging)";
|
|
31
|
+
EnvList.examples = [`$ apso env list`, `$ apso env list --json`];
|
|
32
|
+
EnvList.flags = {
|
|
33
|
+
json: core_1.Flags.boolean({ description: "Output as JSON", default: false }),
|
|
34
|
+
};
|
|
35
|
+
exports.default = EnvList;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import BaseCommand from "../../lib/base-command";
|
|
2
|
+
export default class EnvRemove extends BaseCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static args: {
|
|
6
|
+
environment: import("@oclif/core/lib/interfaces/parser").Arg<string, Record<string, unknown>>;
|
|
7
|
+
};
|
|
8
|
+
static flags: {
|
|
9
|
+
yes: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
};
|
|
11
|
+
run(): Promise<void>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const tslib_1 = require("tslib");
|
|
4
|
+
const core_1 = require("@oclif/core");
|
|
5
|
+
const base_command_1 = tslib_1.__importDefault(require("../../lib/base-command"));
|
|
6
|
+
const services_1 = require("../../lib/api/services");
|
|
7
|
+
const environments_1 = require("../../lib/environments");
|
|
8
|
+
class EnvRemove extends base_command_1.default {
|
|
9
|
+
async run() {
|
|
10
|
+
const { flags } = await this.parse(EnvRemove);
|
|
11
|
+
const service = await (0, environments_1.linkedService)(this);
|
|
12
|
+
if (!(await services_1.envApi.get(service.id, "staging"))) {
|
|
13
|
+
this.log(`"${service.name}" has no staging environment.`);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const ok = await (0, environments_1.confirmOrExit)(this, flags.yes, `Tear down staging for "${service.name}"? Its stack and database branch are deleted.`);
|
|
17
|
+
if (!ok)
|
|
18
|
+
return this.log("Cancelled.");
|
|
19
|
+
await services_1.envApi.removeStaging(service.id);
|
|
20
|
+
this.log("Staging is being torn down. Production is unchanged.");
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
EnvRemove.description = "Tear down the linked service's staging environment and stop the add-on. Production is untouched.";
|
|
24
|
+
EnvRemove.examples = [`$ apso env remove staging`, `$ apso env remove staging --yes`];
|
|
25
|
+
EnvRemove.args = {
|
|
26
|
+
environment: core_1.Args.string({ description: "Environment to remove", options: ["staging"], required: true }),
|
|
27
|
+
};
|
|
28
|
+
EnvRemove.flags = {
|
|
29
|
+
yes: core_1.Flags.boolean({ char: "y", description: "Skip the confirmation prompt", default: false }),
|
|
30
|
+
};
|
|
31
|
+
exports.default = EnvRemove;
|
package/dist/commands/open.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export default class Open extends BaseCommand {
|
|
|
3
3
|
static description: string;
|
|
4
4
|
static examples: string[];
|
|
5
5
|
static flags: {
|
|
6
|
+
env: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
|
|
6
7
|
endpoint: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
7
8
|
dashboard: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
8
9
|
};
|
package/dist/commands/open.js
CHANGED
|
@@ -8,6 +8,7 @@ const base_command_1 = tslib_1.__importDefault(require("../lib/base-command"));
|
|
|
8
8
|
const config_1 = require("../lib/config");
|
|
9
9
|
const config_2 = require("../lib/config");
|
|
10
10
|
const services_1 = require("../lib/api/services");
|
|
11
|
+
const environments_1 = require("../lib/environments");
|
|
11
12
|
class Open extends base_command_1.default {
|
|
12
13
|
async run() {
|
|
13
14
|
const { flags } = await this.parse(Open);
|
|
@@ -24,15 +25,18 @@ class Open extends base_command_1.default {
|
|
|
24
25
|
if (flags.endpoint) {
|
|
25
26
|
// Open the live API endpoint
|
|
26
27
|
const service = await services_1.servicesApi.get(link.workspaceSlug, link.serviceSlug);
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
const env = (0, environments_1.asEnv)(flags.env);
|
|
29
|
+
const row = env === "production" ? undefined : await services_1.envApi.get(service.id, env);
|
|
30
|
+
const endpoint = row ? row.url : service.endpoint;
|
|
31
|
+
if (!endpoint) {
|
|
32
|
+
this.error(`Service has no ${env} endpoint. Deploy first with 'apso deploy --env ${env}'.`);
|
|
29
33
|
}
|
|
30
|
-
this.log(`Opening endpoint: ${
|
|
31
|
-
openUrl(
|
|
34
|
+
this.log(`Opening endpoint: ${endpoint}`);
|
|
35
|
+
openUrl(endpoint);
|
|
32
36
|
}
|
|
33
37
|
else {
|
|
34
38
|
// Open the dashboard (default)
|
|
35
|
-
const dashboardUrl = `${config.webUrl}
|
|
39
|
+
const dashboardUrl = `${config.webUrl}/${link.workspaceSlug}/services/${link.serviceId}/dashboard`;
|
|
36
40
|
this.log(`Opening dashboard: ${dashboardUrl}`);
|
|
37
41
|
openUrl(dashboardUrl);
|
|
38
42
|
}
|
|
@@ -43,8 +47,10 @@ Open.examples = [
|
|
|
43
47
|
`$ apso open`,
|
|
44
48
|
`$ apso open --endpoint`,
|
|
45
49
|
`$ apso open --dashboard`,
|
|
50
|
+
`$ apso open --endpoint --env staging`,
|
|
46
51
|
];
|
|
47
52
|
Open.flags = {
|
|
53
|
+
env: environments_1.envFlag,
|
|
48
54
|
endpoint: core_1.Flags.boolean({
|
|
49
55
|
char: "e",
|
|
50
56
|
description: "Open the service API endpoint",
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import BaseCommand from "../lib/base-command";
|
|
2
|
+
import { type PromotePreview } from "../lib/api/services";
|
|
3
|
+
export default class Promote extends BaseCommand {
|
|
4
|
+
static description: string;
|
|
5
|
+
static examples: string[];
|
|
6
|
+
static flags: {
|
|
7
|
+
yes: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
8
|
+
"dry-run": import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
9
|
+
json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
"no-wait": import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
11
|
+
};
|
|
12
|
+
run(): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
export declare function describePreview(p: PromotePreview): string;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.describePreview = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const core_1 = require("@oclif/core");
|
|
6
|
+
const base_command_1 = tslib_1.__importDefault(require("../lib/base-command"));
|
|
7
|
+
const services_1 = require("../lib/api/services");
|
|
8
|
+
const environments_1 = require("../lib/environments");
|
|
9
|
+
const upgrade_1 = require("../lib/upgrade");
|
|
10
|
+
const spinner_1 = require("../lib/utils/spinner");
|
|
11
|
+
class Promote extends base_command_1.default {
|
|
12
|
+
async run() {
|
|
13
|
+
const { flags } = await this.parse(Promote);
|
|
14
|
+
const service = await (0, environments_1.linkedService)(this);
|
|
15
|
+
const preview = await services_1.envApi.promotePreview(service.id);
|
|
16
|
+
if (flags.json)
|
|
17
|
+
this.log(JSON.stringify(preview, null, 2));
|
|
18
|
+
else
|
|
19
|
+
this.log(describePreview(preview));
|
|
20
|
+
if (preview.blocked)
|
|
21
|
+
this.error(`Promote blocked: ${preview.blocked}`);
|
|
22
|
+
if (flags["dry-run"])
|
|
23
|
+
return;
|
|
24
|
+
const ok = await (0, environments_1.confirmOrExit)(this, flags.yes, `Promote staging to production for "${service.name}"?`);
|
|
25
|
+
if (!ok)
|
|
26
|
+
return this.log("Cancelled.");
|
|
27
|
+
await (0, upgrade_1.withUpgradeRetry)(() => services_1.envApi.promote(service.id), (0, environments_1.pageContext)(service));
|
|
28
|
+
if (flags["no-wait"]) {
|
|
29
|
+
this.log("Promote started. Run 'apso status' to check progress.");
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const spinner = await (0, spinner_1.createSpinner)("Promoting...");
|
|
33
|
+
spinner.start();
|
|
34
|
+
const result = await services_1.buildApi.waitForCompletion(service.id, {
|
|
35
|
+
onProgress: (s) => {
|
|
36
|
+
spinner.text = `Promote status: ${s.status}...`;
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
if (result.status === "success") {
|
|
40
|
+
spinner.succeed("Production now runs staging's build.");
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
spinner.fail("Promote failed. Production is unchanged.");
|
|
44
|
+
this.exit(1);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
Promote.description = "Promote staging to production. Production runs the image staging runs now, with no rebuild. Stops when the platform blocks the promote.";
|
|
49
|
+
Promote.examples = [
|
|
50
|
+
`$ apso promote`,
|
|
51
|
+
`$ apso promote --dry-run`,
|
|
52
|
+
`$ apso promote --dry-run --json`,
|
|
53
|
+
`$ apso promote --yes`,
|
|
54
|
+
];
|
|
55
|
+
Promote.flags = {
|
|
56
|
+
yes: core_1.Flags.boolean({ char: "y", description: "Skip the confirmation prompt", default: false }),
|
|
57
|
+
"dry-run": core_1.Flags.boolean({ description: "Show what would be promoted and stop", default: false }),
|
|
58
|
+
json: core_1.Flags.boolean({ description: "Print the preview as JSON", default: false }),
|
|
59
|
+
"no-wait": core_1.Flags.boolean({ description: "Start the promote without waiting for it to finish", default: false }),
|
|
60
|
+
};
|
|
61
|
+
exports.default = Promote;
|
|
62
|
+
function describePreview(p) {
|
|
63
|
+
const lines = [`Commits: ${p.commits.length}`];
|
|
64
|
+
for (const c of p.commits.slice(0, 20))
|
|
65
|
+
lines.push(` ${c.sha.slice(0, 7)} ${c.message.split("\n")[0]}`);
|
|
66
|
+
lines.push(`Migrations: ${p.migrations.length > 0 ? p.migrations.join(", ") : "none"}`);
|
|
67
|
+
if (p.missingEnvVars.length > 0)
|
|
68
|
+
lines.push(`Missing in production: ${p.missingEnvVars.join(", ")}`);
|
|
69
|
+
return lines.join("\n");
|
|
70
|
+
}
|
|
71
|
+
exports.describePreview = describePreview;
|
|
@@ -2,6 +2,8 @@ import BaseCommand from "../lib/base-command";
|
|
|
2
2
|
export default class Status extends BaseCommand {
|
|
3
3
|
static description: string;
|
|
4
4
|
static examples: string[];
|
|
5
|
-
static flags: {
|
|
5
|
+
static flags: {
|
|
6
|
+
env: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
|
|
7
|
+
};
|
|
6
8
|
run(): Promise<void>;
|
|
7
9
|
}
|
package/dist/commands/status.js
CHANGED
|
@@ -4,8 +4,11 @@ const tslib_1 = require("tslib");
|
|
|
4
4
|
const base_command_1 = tslib_1.__importDefault(require("../lib/base-command"));
|
|
5
5
|
const config_1 = require("../lib/config");
|
|
6
6
|
const services_1 = require("../lib/api/services");
|
|
7
|
+
const environments_1 = require("../lib/environments");
|
|
7
8
|
class Status extends base_command_1.default {
|
|
8
9
|
async run() {
|
|
10
|
+
const { flags } = await this.parse(Status);
|
|
11
|
+
const env = (0, environments_1.asEnv)(flags.env);
|
|
9
12
|
// Auth guard
|
|
10
13
|
if (!config_1.credentials.isValid()) {
|
|
11
14
|
this.error("Not logged in. Run 'apso login' first.");
|
|
@@ -19,6 +22,21 @@ class Status extends base_command_1.default {
|
|
|
19
22
|
const service = await services_1.servicesApi.get(link.workspaceId, link.serviceSlug);
|
|
20
23
|
this.log(`Service: ${service.name} (${service.slug})`);
|
|
21
24
|
this.log(`Workspace: ${link.workspaceSlug}`);
|
|
25
|
+
this.log(`Environment: ${env}`);
|
|
26
|
+
// Staging is its own row (stack, URL, build): show it and stop.
|
|
27
|
+
if (env !== "production") {
|
|
28
|
+
const row = await services_1.envApi.get(service.id, env);
|
|
29
|
+
if (!row) {
|
|
30
|
+
this.error(`"${service.name}" has no ${env} environment. Run 'apso env add ${env}'.`);
|
|
31
|
+
}
|
|
32
|
+
this.log(`Status: ${row.status || "-"}`);
|
|
33
|
+
this.log(`Build: ${row.buildStatus || "-"}`);
|
|
34
|
+
if (row.url)
|
|
35
|
+
this.log(`Endpoint: ${row.url}`);
|
|
36
|
+
if (row.lastDeployedAt)
|
|
37
|
+
this.log(`Last deployed: ${new Date(row.lastDeployedAt).toLocaleString()}`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
22
40
|
this.log(`Status: ${service.status}`);
|
|
23
41
|
// Distinguish the account-level GitHub connection from this service's repo:
|
|
24
42
|
// "GitHub: not connected" was misleading when the account WAS connected but
|
|
@@ -55,6 +73,6 @@ class Status extends base_command_1.default {
|
|
|
55
73
|
}
|
|
56
74
|
}
|
|
57
75
|
Status.description = "Show the current service and latest build status";
|
|
58
|
-
Status.examples = [`$ apso status`];
|
|
59
|
-
Status.flags = {};
|
|
76
|
+
Status.examples = [`$ apso status`, `$ apso status --env staging`];
|
|
77
|
+
Status.flags = { env: environments_1.envFlag };
|
|
60
78
|
exports.default = Status;
|
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;
|
|
@@ -84,16 +84,54 @@ export declare const schemaApi: {
|
|
|
84
84
|
*/
|
|
85
85
|
export declare const buildApi: {
|
|
86
86
|
/** Trigger a deploy for the linked service (POST /api/services/:id/deploy). */
|
|
87
|
-
trigger(_workspaceSlug: string, serviceSlugOrId: string): Promise<BuildStatus>;
|
|
88
|
-
/** No dedicated builds resource
|
|
89
|
-
getStatus(serviceId: string): Promise<BuildStatus>;
|
|
87
|
+
trigger(_workspaceSlug: string, serviceSlugOrId: string, env?: ServiceEnv): Promise<BuildStatus>;
|
|
88
|
+
/** No dedicated builds resource: production reads the service record, staging its own environment row. */
|
|
89
|
+
getStatus(serviceId: string, env?: ServiceEnv): Promise<BuildStatus>;
|
|
90
90
|
getLatest(_workspaceSlug: string, serviceSlugOrId: string): Promise<BuildStatus | null>;
|
|
91
91
|
waitForCompletion(serviceId: string, options?: {
|
|
92
92
|
maxWait?: number | undefined;
|
|
93
93
|
pollInterval?: number | undefined;
|
|
94
94
|
onProgress?: ((status: BuildStatus) => void) | undefined;
|
|
95
|
+
env?: ServiceEnv | undefined;
|
|
95
96
|
} | undefined): Promise<BuildStatus>;
|
|
96
97
|
};
|
|
98
|
+
/** Maps a platform build_status (service record or environment row) to the CLI's four states. */
|
|
99
|
+
export declare function toBuildStatus(serviceId: string, buildStatus: unknown, updatedAt?: string): BuildStatus;
|
|
100
|
+
/**
|
|
101
|
+
* Environments API. Each environment is a ServiceDeployment row with its own
|
|
102
|
+
* stack, URL, logs and database branch. Production always exists; staging is
|
|
103
|
+
* an add-on that `addStaging` starts (and bills) and `removeStaging` tears down.
|
|
104
|
+
*/
|
|
105
|
+
export type ServiceEnv = "production" | "staging";
|
|
106
|
+
export declare const SERVICE_ENVS: ServiceEnv[];
|
|
107
|
+
export interface Environment {
|
|
108
|
+
id: string;
|
|
109
|
+
environment: string;
|
|
110
|
+
status: string;
|
|
111
|
+
buildStatus?: string;
|
|
112
|
+
url?: string;
|
|
113
|
+
lastDeployedAt?: string;
|
|
114
|
+
updatedAt?: string;
|
|
115
|
+
}
|
|
116
|
+
export interface PromotePreview {
|
|
117
|
+
blocked: string | null;
|
|
118
|
+
commits: {
|
|
119
|
+
sha: string;
|
|
120
|
+
message: string;
|
|
121
|
+
author?: string;
|
|
122
|
+
date?: string;
|
|
123
|
+
}[];
|
|
124
|
+
missingEnvVars: string[];
|
|
125
|
+
migrations: string[];
|
|
126
|
+
}
|
|
127
|
+
export declare const envApi: {
|
|
128
|
+
list(serviceId: string): Promise<Environment[]>;
|
|
129
|
+
get(serviceId: string, env: ServiceEnv): Promise<Environment | undefined>;
|
|
130
|
+
addStaging(serviceId: string): Promise<Raw>;
|
|
131
|
+
removeStaging(serviceId: string): Promise<Raw>;
|
|
132
|
+
promotePreview(serviceId: string): Promise<PromotePreview>;
|
|
133
|
+
promote(serviceId: string): Promise<Raw>;
|
|
134
|
+
};
|
|
97
135
|
/**
|
|
98
136
|
* GitHub API
|
|
99
137
|
*/
|
package/dist/lib/api/services.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* below.
|
|
13
13
|
*/
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
-
exports.codeApi = exports.githubApi = exports.buildApi = exports.schemaApi = exports.servicesApi = exports.workspacesApi = exports.authApi = void 0;
|
|
15
|
+
exports.codeApi = exports.githubApi = exports.envApi = exports.SERVICE_ENVS = exports.toBuildStatus = exports.buildApi = exports.schemaApi = exports.servicesApi = exports.workspacesApi = exports.authApi = void 0;
|
|
16
16
|
const client_1 = require("./client");
|
|
17
17
|
function adaptWorkspace(raw) {
|
|
18
18
|
var _a, _b, _c, _d;
|
|
@@ -236,14 +236,20 @@ function adaptSchemaFromService(raw) {
|
|
|
236
236
|
*/
|
|
237
237
|
exports.buildApi = {
|
|
238
238
|
/** Trigger a deploy for the linked service (POST /api/services/:id/deploy). */
|
|
239
|
-
async trigger(_workspaceSlug, serviceSlugOrId) {
|
|
239
|
+
async trigger(_workspaceSlug, serviceSlugOrId, env = "production") {
|
|
240
240
|
const id = await resolveServiceId(serviceSlugOrId);
|
|
241
|
-
await client_1.api.post(`/api/services/${id}/deploy`, { target: "apso" });
|
|
242
|
-
return
|
|
241
|
+
await client_1.api.post(`/api/services/${id}/deploy`, { target: "apso", environment: env });
|
|
242
|
+
return this.getStatus(id, env);
|
|
243
243
|
},
|
|
244
|
-
/** No dedicated builds resource
|
|
245
|
-
async getStatus(serviceId) {
|
|
246
|
-
|
|
244
|
+
/** No dedicated builds resource: production reads the service record, staging its own environment row. */
|
|
245
|
+
async getStatus(serviceId, env = "production") {
|
|
246
|
+
var _a;
|
|
247
|
+
if (env === "production")
|
|
248
|
+
return buildStatusFromService(serviceId);
|
|
249
|
+
const row = await exports.envApi.get(serviceId, env);
|
|
250
|
+
if (!row)
|
|
251
|
+
throw new Error(`This service has no ${env} environment. Run 'apso env add ${env}'.`);
|
|
252
|
+
return toBuildStatus(serviceId, (_a = row.buildStatus) !== null && _a !== void 0 ? _a : row.status, row.updatedAt);
|
|
247
253
|
},
|
|
248
254
|
async getLatest(_workspaceSlug, serviceSlugOrId) {
|
|
249
255
|
const id = await resolveServiceId(serviceSlugOrId);
|
|
@@ -256,7 +262,7 @@ exports.buildApi = {
|
|
|
256
262
|
const startTime = Date.now();
|
|
257
263
|
/* eslint-disable no-await-in-loop */
|
|
258
264
|
while (Date.now() - startTime < maxWait) {
|
|
259
|
-
const status = await this.getStatus(serviceId);
|
|
265
|
+
const status = await this.getStatus(serviceId, options === null || options === void 0 ? void 0 : options.env);
|
|
260
266
|
if (options === null || options === void 0 ? void 0 : options.onProgress)
|
|
261
267
|
options.onProgress(status);
|
|
262
268
|
if (status.status === "success" || status.status === "failed")
|
|
@@ -270,18 +276,59 @@ exports.buildApi = {
|
|
|
270
276
|
},
|
|
271
277
|
};
|
|
272
278
|
async function buildStatusFromService(serviceId) {
|
|
273
|
-
var _a
|
|
279
|
+
var _a;
|
|
274
280
|
const raw = await client_1.api.get(`/api/services/${serviceId}`);
|
|
275
|
-
|
|
281
|
+
return toBuildStatus(String(raw.id), raw.build_status, (_a = raw.updated_at) !== null && _a !== void 0 ? _a : raw.created_at);
|
|
282
|
+
}
|
|
283
|
+
/** Maps a platform build_status (service record or environment row) to the CLI's four states. */
|
|
284
|
+
function toBuildStatus(serviceId, buildStatus, updatedAt) {
|
|
285
|
+
const b = String(buildStatus !== null && buildStatus !== void 0 ? buildStatus : "").toLowerCase();
|
|
276
286
|
const status = b === "ready" ? "success" : b === "failed" || b === "error" ? "failed" : b === "new" || b === "" ? "pending" : "building";
|
|
277
287
|
return {
|
|
278
|
-
id:
|
|
279
|
-
serviceId
|
|
288
|
+
id: serviceId,
|
|
289
|
+
serviceId,
|
|
280
290
|
status,
|
|
281
|
-
startedAt:
|
|
282
|
-
completedAt: status === "success" || status === "failed" ?
|
|
291
|
+
startedAt: updatedAt !== null && updatedAt !== void 0 ? updatedAt : new Date().toISOString(),
|
|
292
|
+
completedAt: status === "success" || status === "failed" ? updatedAt : undefined,
|
|
283
293
|
};
|
|
284
294
|
}
|
|
295
|
+
exports.toBuildStatus = toBuildStatus;
|
|
296
|
+
exports.SERVICE_ENVS = ["production", "staging"];
|
|
297
|
+
exports.envApi = {
|
|
298
|
+
async list(serviceId) {
|
|
299
|
+
var _a;
|
|
300
|
+
const raw = await client_1.api.get(`/api/services/${serviceId}/deployments`);
|
|
301
|
+
const rows = Array.isArray(raw) ? raw : (_a = raw === null || raw === void 0 ? void 0 : raw.data) !== null && _a !== void 0 ? _a : [];
|
|
302
|
+
return rows.map((r) => {
|
|
303
|
+
var _a, _b, _c, _d, _e;
|
|
304
|
+
return ({
|
|
305
|
+
id: String(r.id),
|
|
306
|
+
environment: String(r.environment_type),
|
|
307
|
+
status: String((_a = r.deployment_status) !== null && _a !== void 0 ? _a : ""),
|
|
308
|
+
buildStatus: (_b = r.build_status) !== null && _b !== void 0 ? _b : undefined,
|
|
309
|
+
url: (_c = r.deployment_url) !== null && _c !== void 0 ? _c : undefined,
|
|
310
|
+
lastDeployedAt: (_d = r.last_deployed_at) !== null && _d !== void 0 ? _d : undefined,
|
|
311
|
+
updatedAt: (_e = r.updated_at) !== null && _e !== void 0 ? _e : undefined,
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
},
|
|
315
|
+
async get(serviceId, env) {
|
|
316
|
+
const envs = await this.list(serviceId);
|
|
317
|
+
return envs.find((e) => e.environment === env);
|
|
318
|
+
},
|
|
319
|
+
addStaging(serviceId) {
|
|
320
|
+
return client_1.api.post(`/api/services/${serviceId}/environments/staging`);
|
|
321
|
+
},
|
|
322
|
+
removeStaging(serviceId) {
|
|
323
|
+
return client_1.api.delete(`/api/services/${serviceId}/environments/staging`);
|
|
324
|
+
},
|
|
325
|
+
promotePreview(serviceId) {
|
|
326
|
+
return client_1.api.get(`/api/services/${serviceId}/environments/promote-preview`);
|
|
327
|
+
},
|
|
328
|
+
promote(serviceId) {
|
|
329
|
+
return client_1.api.post(`/api/services/${serviceId}/promote`);
|
|
330
|
+
},
|
|
331
|
+
};
|
|
285
332
|
/**
|
|
286
333
|
* GitHub API
|
|
287
334
|
*/
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Command } from "@oclif/core";
|
|
2
|
+
import { type ServiceEnv } from "./api/services";
|
|
3
|
+
import type { Service } from "./api/types";
|
|
4
|
+
import type { PageContext } from "./upgrade";
|
|
5
|
+
export declare const envFlag: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
|
|
6
|
+
export declare const asEnv: (value: string | undefined) => ServiceEnv;
|
|
7
|
+
export declare function linkedService(cmd: Command): Promise<Service>;
|
|
8
|
+
export declare function pageContext(service: Service): PageContext;
|
|
9
|
+
export declare function confirmOrExit(cmd: Command, yes: boolean, question: string): Promise<boolean>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.confirmOrExit = exports.pageContext = exports.linkedService = exports.asEnv = exports.envFlag = void 0;
|
|
27
|
+
const core_1 = require("@oclif/core");
|
|
28
|
+
const config_1 = require("./config");
|
|
29
|
+
const services_1 = require("./api/services");
|
|
30
|
+
const interactive_1 = require("./utils/interactive");
|
|
31
|
+
// --env for commands that act on one environment. Production is the default, so existing scripts behave as before.
|
|
32
|
+
exports.envFlag = core_1.Flags.string({
|
|
33
|
+
description: "Environment to target",
|
|
34
|
+
options: services_1.SERVICE_ENVS,
|
|
35
|
+
default: "production",
|
|
36
|
+
});
|
|
37
|
+
const asEnv = (value) => value === "staging" ? "staging" : "production";
|
|
38
|
+
exports.asEnv = asEnv;
|
|
39
|
+
// The linked service, after the login and link checks every platform command makes.
|
|
40
|
+
async function linkedService(cmd) {
|
|
41
|
+
if (!config_1.credentials.isValid())
|
|
42
|
+
cmd.error("Not logged in. Run 'apso login' first.");
|
|
43
|
+
const link = config_1.projectLink.read();
|
|
44
|
+
if (!link)
|
|
45
|
+
cmd.error("Project not linked. Run 'apso link' first.");
|
|
46
|
+
return services_1.servicesApi.get(link.workspaceId, link.serviceSlug);
|
|
47
|
+
}
|
|
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;
|
|
55
|
+
// Asks before a step that costs money or replaces something; headless runs must pass --yes.
|
|
56
|
+
async function confirmOrExit(cmd, yes, question) {
|
|
57
|
+
if (yes)
|
|
58
|
+
return true;
|
|
59
|
+
if (!(0, interactive_1.isInteractive)())
|
|
60
|
+
cmd.error((0, interactive_1.missingFlag)(`${question} Pass --yes to proceed.`));
|
|
61
|
+
const inquirer = await Promise.resolve().then(() => __importStar(require("inquirer")));
|
|
62
|
+
const { ok } = await inquirer.default.prompt([
|
|
63
|
+
{ type: "confirm", name: "ok", message: question, default: false },
|
|
64
|
+
]);
|
|
65
|
+
return ok;
|
|
66
|
+
}
|
|
67
|
+
exports.confirmOrExit = confirmOrExit;
|
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.
|
|
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.
|
|
9
|
+
"version": "0.47.1",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@apso/schema-tools": "^0.2.0",
|
package/oclif.manifest.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.
|
|
2
|
+
"version": "0.47.1",
|
|
3
3
|
"commands": {
|
|
4
4
|
"config": {
|
|
5
5
|
"id": "config",
|
|
@@ -49,9 +49,21 @@
|
|
|
49
49
|
"$ apso deploy",
|
|
50
50
|
"$ apso deploy --yes",
|
|
51
51
|
"$ apso deploy --skip-migrate",
|
|
52
|
-
"$ apso deploy --no-wait"
|
|
52
|
+
"$ apso deploy --no-wait",
|
|
53
|
+
"$ apso deploy --env staging"
|
|
53
54
|
],
|
|
54
55
|
"flags": {
|
|
56
|
+
"env": {
|
|
57
|
+
"name": "env",
|
|
58
|
+
"type": "option",
|
|
59
|
+
"description": "Environment to target",
|
|
60
|
+
"multiple": false,
|
|
61
|
+
"options": [
|
|
62
|
+
"production",
|
|
63
|
+
"staging"
|
|
64
|
+
],
|
|
65
|
+
"default": "production"
|
|
66
|
+
},
|
|
55
67
|
"no-wait": {
|
|
56
68
|
"name": "no-wait",
|
|
57
69
|
"type": "boolean",
|
|
@@ -464,9 +476,21 @@
|
|
|
464
476
|
"examples": [
|
|
465
477
|
"$ apso open",
|
|
466
478
|
"$ apso open --endpoint",
|
|
467
|
-
"$ apso open --dashboard"
|
|
479
|
+
"$ apso open --dashboard",
|
|
480
|
+
"$ apso open --endpoint --env staging"
|
|
468
481
|
],
|
|
469
482
|
"flags": {
|
|
483
|
+
"env": {
|
|
484
|
+
"name": "env",
|
|
485
|
+
"type": "option",
|
|
486
|
+
"description": "Environment to target",
|
|
487
|
+
"multiple": false,
|
|
488
|
+
"options": [
|
|
489
|
+
"production",
|
|
490
|
+
"staging"
|
|
491
|
+
],
|
|
492
|
+
"default": "production"
|
|
493
|
+
},
|
|
470
494
|
"endpoint": {
|
|
471
495
|
"name": "endpoint",
|
|
472
496
|
"type": "boolean",
|
|
@@ -515,6 +539,50 @@
|
|
|
515
539
|
},
|
|
516
540
|
"args": {}
|
|
517
541
|
},
|
|
542
|
+
"promote": {
|
|
543
|
+
"id": "promote",
|
|
544
|
+
"description": "Promote staging to production. Production runs the image staging runs now, with no rebuild. Stops when the platform blocks the promote.",
|
|
545
|
+
"strict": true,
|
|
546
|
+
"pluginName": "@apso/cli",
|
|
547
|
+
"pluginAlias": "@apso/cli",
|
|
548
|
+
"pluginType": "core",
|
|
549
|
+
"aliases": [],
|
|
550
|
+
"hiddenAliases": [],
|
|
551
|
+
"examples": [
|
|
552
|
+
"$ apso promote",
|
|
553
|
+
"$ apso promote --dry-run",
|
|
554
|
+
"$ apso promote --dry-run --json",
|
|
555
|
+
"$ apso promote --yes"
|
|
556
|
+
],
|
|
557
|
+
"flags": {
|
|
558
|
+
"yes": {
|
|
559
|
+
"name": "yes",
|
|
560
|
+
"type": "boolean",
|
|
561
|
+
"char": "y",
|
|
562
|
+
"description": "Skip the confirmation prompt",
|
|
563
|
+
"allowNo": false
|
|
564
|
+
},
|
|
565
|
+
"dry-run": {
|
|
566
|
+
"name": "dry-run",
|
|
567
|
+
"type": "boolean",
|
|
568
|
+
"description": "Show what would be promoted and stop",
|
|
569
|
+
"allowNo": false
|
|
570
|
+
},
|
|
571
|
+
"json": {
|
|
572
|
+
"name": "json",
|
|
573
|
+
"type": "boolean",
|
|
574
|
+
"description": "Print the preview as JSON",
|
|
575
|
+
"allowNo": false
|
|
576
|
+
},
|
|
577
|
+
"no-wait": {
|
|
578
|
+
"name": "no-wait",
|
|
579
|
+
"type": "boolean",
|
|
580
|
+
"description": "Start the promote without waiting for it to finish",
|
|
581
|
+
"allowNo": false
|
|
582
|
+
}
|
|
583
|
+
},
|
|
584
|
+
"args": {}
|
|
585
|
+
},
|
|
518
586
|
"status": {
|
|
519
587
|
"id": "status",
|
|
520
588
|
"description": "Show the current service and latest build status",
|
|
@@ -525,9 +593,22 @@
|
|
|
525
593
|
"aliases": [],
|
|
526
594
|
"hiddenAliases": [],
|
|
527
595
|
"examples": [
|
|
528
|
-
"$ apso status"
|
|
596
|
+
"$ apso status",
|
|
597
|
+
"$ apso status --env staging"
|
|
529
598
|
],
|
|
530
|
-
"flags": {
|
|
599
|
+
"flags": {
|
|
600
|
+
"env": {
|
|
601
|
+
"name": "env",
|
|
602
|
+
"type": "option",
|
|
603
|
+
"description": "Environment to target",
|
|
604
|
+
"multiple": false,
|
|
605
|
+
"options": [
|
|
606
|
+
"production",
|
|
607
|
+
"staging"
|
|
608
|
+
],
|
|
609
|
+
"default": "production"
|
|
610
|
+
}
|
|
611
|
+
},
|
|
531
612
|
"args": {}
|
|
532
613
|
},
|
|
533
614
|
"unlink": {
|
|
@@ -629,6 +710,95 @@
|
|
|
629
710
|
},
|
|
630
711
|
"args": {}
|
|
631
712
|
},
|
|
713
|
+
"env:add": {
|
|
714
|
+
"id": "env:add",
|
|
715
|
+
"description": "Add a staging environment to the linked service. It gets its own stack, URL and database branch, and starts the paid staging add-on.",
|
|
716
|
+
"strict": true,
|
|
717
|
+
"pluginName": "@apso/cli",
|
|
718
|
+
"pluginAlias": "@apso/cli",
|
|
719
|
+
"pluginType": "core",
|
|
720
|
+
"aliases": [],
|
|
721
|
+
"hiddenAliases": [],
|
|
722
|
+
"examples": [
|
|
723
|
+
"$ apso env add staging",
|
|
724
|
+
"$ apso env add staging --yes"
|
|
725
|
+
],
|
|
726
|
+
"flags": {
|
|
727
|
+
"yes": {
|
|
728
|
+
"name": "yes",
|
|
729
|
+
"type": "boolean",
|
|
730
|
+
"char": "y",
|
|
731
|
+
"description": "Skip the confirmation prompt",
|
|
732
|
+
"allowNo": false
|
|
733
|
+
}
|
|
734
|
+
},
|
|
735
|
+
"args": {
|
|
736
|
+
"environment": {
|
|
737
|
+
"name": "environment",
|
|
738
|
+
"description": "Environment to add",
|
|
739
|
+
"required": true,
|
|
740
|
+
"options": [
|
|
741
|
+
"staging"
|
|
742
|
+
]
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
},
|
|
746
|
+
"env:list": {
|
|
747
|
+
"id": "env:list",
|
|
748
|
+
"description": "List the linked service's environments (production, staging)",
|
|
749
|
+
"strict": true,
|
|
750
|
+
"pluginName": "@apso/cli",
|
|
751
|
+
"pluginAlias": "@apso/cli",
|
|
752
|
+
"pluginType": "core",
|
|
753
|
+
"aliases": [],
|
|
754
|
+
"hiddenAliases": [],
|
|
755
|
+
"examples": [
|
|
756
|
+
"$ apso env list",
|
|
757
|
+
"$ apso env list --json"
|
|
758
|
+
],
|
|
759
|
+
"flags": {
|
|
760
|
+
"json": {
|
|
761
|
+
"name": "json",
|
|
762
|
+
"type": "boolean",
|
|
763
|
+
"description": "Output as JSON",
|
|
764
|
+
"allowNo": false
|
|
765
|
+
}
|
|
766
|
+
},
|
|
767
|
+
"args": {}
|
|
768
|
+
},
|
|
769
|
+
"env:remove": {
|
|
770
|
+
"id": "env:remove",
|
|
771
|
+
"description": "Tear down the linked service's staging environment and stop the add-on. Production is untouched.",
|
|
772
|
+
"strict": true,
|
|
773
|
+
"pluginName": "@apso/cli",
|
|
774
|
+
"pluginAlias": "@apso/cli",
|
|
775
|
+
"pluginType": "core",
|
|
776
|
+
"aliases": [],
|
|
777
|
+
"hiddenAliases": [],
|
|
778
|
+
"examples": [
|
|
779
|
+
"$ apso env remove staging",
|
|
780
|
+
"$ apso env remove staging --yes"
|
|
781
|
+
],
|
|
782
|
+
"flags": {
|
|
783
|
+
"yes": {
|
|
784
|
+
"name": "yes",
|
|
785
|
+
"type": "boolean",
|
|
786
|
+
"char": "y",
|
|
787
|
+
"description": "Skip the confirmation prompt",
|
|
788
|
+
"allowNo": false
|
|
789
|
+
}
|
|
790
|
+
},
|
|
791
|
+
"args": {
|
|
792
|
+
"environment": {
|
|
793
|
+
"name": "environment",
|
|
794
|
+
"description": "Environment to remove",
|
|
795
|
+
"required": true,
|
|
796
|
+
"options": [
|
|
797
|
+
"staging"
|
|
798
|
+
]
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
},
|
|
632
802
|
"github:connect": {
|
|
633
803
|
"id": "github:connect",
|
|
634
804
|
"description": "Connect a GitHub account to push generated code to a repo",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@apso/cli",
|
|
3
|
-
"version": "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",
|
|
@@ -104,6 +104,9 @@
|
|
|
104
104
|
},
|
|
105
105
|
"import": {
|
|
106
106
|
"description": "Import an existing database into a local .apsorc schema"
|
|
107
|
+
},
|
|
108
|
+
"env": {
|
|
109
|
+
"description": "Manage a service's environments (production, staging)"
|
|
107
110
|
}
|
|
108
111
|
},
|
|
109
112
|
"update": {
|