@apso/cli 0.23.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/deploy.js +28 -2
- package/dist/commands/init.js +59 -26
- package/dist/commands/link.d.ts +1 -0
- package/dist/commands/link.js +29 -5
- package/dist/lib/upgrade.js +7 -0
- package/dist/lib/utils/interactive.d.ts +15 -0
- package/dist/lib/utils/interactive.js +31 -0
- package/npm-shrinkwrap.json +2 -2
- package/oclif.manifest.json +11 -3
- package/package.json +1 -1
package/dist/commands/deploy.js
CHANGED
|
@@ -10,6 +10,7 @@ const git = tslib_1.__importStar(require("../lib/git"));
|
|
|
10
10
|
const apsorc_parser_1 = require("../lib/apsorc-parser");
|
|
11
11
|
const sandbox_1 = require("../lib/migrate/sandbox");
|
|
12
12
|
const spinner_1 = require("../lib/utils/spinner");
|
|
13
|
+
const interactive_1 = require("../lib/utils/interactive");
|
|
13
14
|
class Deploy extends base_command_1.default {
|
|
14
15
|
async run() {
|
|
15
16
|
const { flags } = await this.parse(Deploy);
|
|
@@ -78,6 +79,9 @@ class Deploy extends base_command_1.default {
|
|
|
78
79
|
}
|
|
79
80
|
// Confirmation
|
|
80
81
|
if (!flags.yes && migrationCount > 0) {
|
|
82
|
+
if (!(0, interactive_1.isInteractive)()) {
|
|
83
|
+
this.error((0, interactive_1.missingFlag)(`About to deploy ${migrationCount} migration statement(s). Pass --yes to proceed.`));
|
|
84
|
+
}
|
|
81
85
|
const inquirer = await Promise.resolve().then(() => tslib_1.__importStar(require("inquirer")));
|
|
82
86
|
const { confirm } = await inquirer.default.prompt([
|
|
83
87
|
{
|
|
@@ -136,9 +140,31 @@ class Deploy extends base_command_1.default {
|
|
|
136
140
|
git.initRepo(cwd);
|
|
137
141
|
// Pick the repo: prefer what the local project is already wired to, then the
|
|
138
142
|
// service's connected repo, then --repo, then ask.
|
|
139
|
-
|
|
143
|
+
let conn = await services_1.githubApi.getConnection();
|
|
140
144
|
if (!conn.connected || !conn.connectionId) {
|
|
141
|
-
|
|
145
|
+
// Connecting GitHub is a browser OAuth flow a machine can't complete.
|
|
146
|
+
// Headless: fail with instructions. Interactive: offer to launch it now
|
|
147
|
+
// (opens the browser), wait for the human, then continue the deploy.
|
|
148
|
+
if (!(0, interactive_1.isInteractive)()) {
|
|
149
|
+
this.error("GitHub is not connected. Run 'apso github connect' first, then re-run deploy.");
|
|
150
|
+
}
|
|
151
|
+
const inquirer = await Promise.resolve().then(() => tslib_1.__importStar(require("inquirer")));
|
|
152
|
+
const { connectNow } = await inquirer.default.prompt([
|
|
153
|
+
{
|
|
154
|
+
type: "confirm",
|
|
155
|
+
name: "connectNow",
|
|
156
|
+
message: "GitHub is not connected (required to push code). Connect now? This opens your browser.",
|
|
157
|
+
default: true,
|
|
158
|
+
},
|
|
159
|
+
]);
|
|
160
|
+
if (!connectNow) {
|
|
161
|
+
this.error("GitHub connection is required to deploy. Run 'apso github connect' when ready, then re-run deploy.");
|
|
162
|
+
}
|
|
163
|
+
await this.config.runCommand("github:connect", []);
|
|
164
|
+
conn = await services_1.githubApi.getConnection();
|
|
165
|
+
if (!conn.connected || !conn.connectionId) {
|
|
166
|
+
this.error("GitHub still isn't connected. Complete 'apso github connect' and re-run deploy.");
|
|
167
|
+
}
|
|
142
168
|
}
|
|
143
169
|
let repoFullName = git.localRepoFullName(cwd) || service.githubRepo || repoFlag;
|
|
144
170
|
if (!repoFullName) {
|
package/dist/commands/init.js
CHANGED
|
@@ -10,6 +10,8 @@ const base_command_1 = tslib_1.__importDefault(require("../lib/base-command"));
|
|
|
10
10
|
const config_1 = require("../lib/config");
|
|
11
11
|
const services_1 = require("../lib/api/services");
|
|
12
12
|
const upgrade_1 = require("../lib/upgrade");
|
|
13
|
+
const interactive_1 = require("../lib/utils/interactive");
|
|
14
|
+
const config_2 = require("../lib/config");
|
|
13
15
|
const template_1 = require("../lib/utils/template");
|
|
14
16
|
class Init extends base_command_1.default {
|
|
15
17
|
async run() {
|
|
@@ -28,18 +30,28 @@ class Init extends base_command_1.default {
|
|
|
28
30
|
await (isAuthenticated ? this.runAuthenticated(flags) : this.runOffline(flags));
|
|
29
31
|
}
|
|
30
32
|
async runAuthenticated(flags) {
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
33
|
+
// Headless: init is the guided human path; require the essentials up-front so
|
|
34
|
+
// no prompt is reached. Agents should prefer 'apso generate' + 'apso link --create'.
|
|
35
|
+
if (!(0, interactive_1.isInteractive)() && (!flags.name || !flags.language)) {
|
|
36
|
+
this.error((0, interactive_1.missingFlag)("apso init needs --name and --language non-interactively (or use 'apso generate' + 'apso link --create')."));
|
|
37
|
+
}
|
|
38
|
+
// Ask: create new or clone existing. Headless: default to "create" (the
|
|
39
|
+
// scaffold path); requires --name/--language, which createNew enforces.
|
|
40
|
+
let action = "create";
|
|
41
|
+
if ((0, interactive_1.isInteractive)()) {
|
|
42
|
+
const answer = await inquirer_1.default.prompt([
|
|
43
|
+
{
|
|
44
|
+
type: "list",
|
|
45
|
+
name: "action",
|
|
46
|
+
message: "What would you like to do?",
|
|
47
|
+
choices: [
|
|
48
|
+
{ name: "Create a new project", value: "create" },
|
|
49
|
+
{ name: "Clone an existing project", value: "clone" },
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
]);
|
|
53
|
+
action = answer.action;
|
|
54
|
+
}
|
|
43
55
|
await (action === "clone" ? this.cloneExisting() : this.createNew(flags));
|
|
44
56
|
}
|
|
45
57
|
async cloneExisting() {
|
|
@@ -158,26 +170,41 @@ class Init extends base_command_1.default {
|
|
|
158
170
|
if (!resolvedPath.startsWith(path.resolve(process.cwd()))) {
|
|
159
171
|
this.error("Invalid project name: path traversal detected");
|
|
160
172
|
}
|
|
161
|
-
// Select workspace
|
|
173
|
+
// Select workspace: active workspace (apso use), else prompt (interactive),
|
|
174
|
+
// else error headless.
|
|
162
175
|
const workspaces = await services_1.workspacesApi.list();
|
|
163
176
|
if (workspaces.length === 0) {
|
|
164
177
|
this.error("No workspaces found. Create a workspace at https://app.apso.cloud first.");
|
|
165
178
|
}
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
179
|
+
const activeSlug = config_2.globalConfig.read().activeWorkspaceSlug;
|
|
180
|
+
let workspace = activeSlug
|
|
181
|
+
? workspaces.find((w) => w.slug === activeSlug)
|
|
182
|
+
: undefined;
|
|
183
|
+
if (!workspace) {
|
|
184
|
+
if (!(0, interactive_1.isInteractive)()) {
|
|
185
|
+
this.error((0, interactive_1.missingFlag)("Set a workspace first with 'apso use <slug>'."));
|
|
186
|
+
}
|
|
187
|
+
const answer = await inquirer_1.default.prompt([
|
|
188
|
+
{
|
|
189
|
+
type: "list",
|
|
190
|
+
name: "workspace",
|
|
191
|
+
message: "Select a workspace:",
|
|
192
|
+
choices: workspaces.map((ws) => ({
|
|
193
|
+
name: `${ws.name} (${ws.slug})`,
|
|
194
|
+
value: ws,
|
|
195
|
+
})),
|
|
196
|
+
},
|
|
197
|
+
]);
|
|
198
|
+
workspace = answer.workspace;
|
|
199
|
+
}
|
|
200
|
+
if (!workspace) {
|
|
201
|
+
throw new Error("No workspace selected.");
|
|
202
|
+
}
|
|
203
|
+
const ws = workspace;
|
|
177
204
|
// Create service on platform
|
|
178
|
-
this.log(`Creating service "${projectName}" in workspace "${
|
|
205
|
+
this.log(`Creating service "${projectName}" in workspace "${ws.name}"...`);
|
|
179
206
|
const serviceName = projectName;
|
|
180
|
-
const service = await (0, upgrade_1.withUpgradeRetry)(() => services_1.servicesApi.create(
|
|
207
|
+
const service = await (0, upgrade_1.withUpgradeRetry)(() => services_1.servicesApi.create(ws.id, {
|
|
181
208
|
name: serviceName,
|
|
182
209
|
}));
|
|
183
210
|
// Clone template
|
|
@@ -196,6 +223,12 @@ class Init extends base_command_1.default {
|
|
|
196
223
|
this.log(`\nProject created at ${projectPath}`);
|
|
197
224
|
this.log(`Language: ${language}`);
|
|
198
225
|
this.log(`Linked to workspace "${workspace.name}" / service "${service.name}"`);
|
|
226
|
+
this.log("");
|
|
227
|
+
this.log("Next steps:");
|
|
228
|
+
this.log(` cd ${projectName}`);
|
|
229
|
+
this.log(" # edit .apsorc to shape your schema, then:");
|
|
230
|
+
this.log(" apso generate # scaffold code from the schema");
|
|
231
|
+
this.log(" apso deploy # ship it (prompts to connect GitHub if needed)");
|
|
199
232
|
}
|
|
200
233
|
async runOffline(flags) {
|
|
201
234
|
// Prompt for name if not provided
|
package/dist/commands/link.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export default class Link extends BaseCommand {
|
|
|
5
5
|
static flags: {
|
|
6
6
|
workspace: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
|
|
7
7
|
service: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
|
|
8
|
+
create: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
|
|
8
9
|
force: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
9
10
|
};
|
|
10
11
|
run(): Promise<void>;
|
package/dist/commands/link.js
CHANGED
|
@@ -9,6 +9,7 @@ const base_command_1 = tslib_1.__importDefault(require("../lib/base-command"));
|
|
|
9
9
|
const config_1 = require("../lib/config");
|
|
10
10
|
const services_1 = require("../lib/api/services");
|
|
11
11
|
const upgrade_1 = require("../lib/upgrade");
|
|
12
|
+
const interactive_1 = require("../lib/utils/interactive");
|
|
12
13
|
class Link extends base_command_1.default {
|
|
13
14
|
async run() {
|
|
14
15
|
const { flags } = await this.parse(Link);
|
|
@@ -24,6 +25,9 @@ class Link extends base_command_1.default {
|
|
|
24
25
|
// Check if already linked
|
|
25
26
|
if (config_1.projectLink.exists() && !flags.force) {
|
|
26
27
|
const existing = config_1.projectLink.read();
|
|
28
|
+
if (!(0, interactive_1.isInteractive)()) {
|
|
29
|
+
this.error((0, interactive_1.missingFlag)(`Already linked to ${existing === null || existing === void 0 ? void 0 : existing.workspaceSlug}/${existing === null || existing === void 0 ? void 0 : existing.serviceSlug}. Pass --force to overwrite.`));
|
|
30
|
+
}
|
|
27
31
|
const { confirm } = await inquirer_1.default.prompt([
|
|
28
32
|
{
|
|
29
33
|
type: "confirm",
|
|
@@ -37,16 +41,24 @@ class Link extends base_command_1.default {
|
|
|
37
41
|
return;
|
|
38
42
|
}
|
|
39
43
|
}
|
|
40
|
-
// Determine workspace
|
|
44
|
+
// Determine workspace: --workspace flag, then the active workspace, then a
|
|
45
|
+
// prompt (interactive only).
|
|
41
46
|
let workspace;
|
|
47
|
+
const active = config_1.globalConfig.read();
|
|
42
48
|
if (flags.workspace) {
|
|
43
49
|
workspace = await services_1.workspacesApi.get(flags.workspace);
|
|
44
50
|
}
|
|
51
|
+
else if (active.activeWorkspaceSlug) {
|
|
52
|
+
workspace = await services_1.workspacesApi.get(active.activeWorkspaceSlug);
|
|
53
|
+
}
|
|
45
54
|
else {
|
|
46
55
|
const workspaces = await services_1.workspacesApi.list();
|
|
47
56
|
if (workspaces.length === 0) {
|
|
48
57
|
this.error("No workspaces found. Create a workspace at https://app.apso.cloud first.");
|
|
49
58
|
}
|
|
59
|
+
if (!(0, interactive_1.isInteractive)()) {
|
|
60
|
+
this.error((0, interactive_1.missingFlag)("Pass --workspace <slug> or set one with 'apso use'."));
|
|
61
|
+
}
|
|
50
62
|
const response = await inquirer_1.default.prompt([
|
|
51
63
|
{
|
|
52
64
|
type: "list",
|
|
@@ -60,11 +72,19 @@ class Link extends base_command_1.default {
|
|
|
60
72
|
]);
|
|
61
73
|
workspace = response.workspace;
|
|
62
74
|
}
|
|
63
|
-
// Determine service
|
|
75
|
+
// Determine service: --service links an existing one, --create makes a new
|
|
76
|
+
// one, otherwise prompt (interactive only).
|
|
64
77
|
let service;
|
|
65
78
|
if (flags.service) {
|
|
66
79
|
service = await services_1.servicesApi.get(workspace.id, flags.service);
|
|
67
80
|
}
|
|
81
|
+
else if (flags.create) {
|
|
82
|
+
this.log(`Creating service "${flags.create}"...`);
|
|
83
|
+
service = await (0, upgrade_1.withUpgradeRetry)(() => services_1.servicesApi.create(workspace.id, { name: flags.create }));
|
|
84
|
+
}
|
|
85
|
+
else if (!(0, interactive_1.isInteractive)()) {
|
|
86
|
+
this.error((0, interactive_1.missingFlag)("Pass --service <slug> to link an existing service, or --create <name> to make a new one."));
|
|
87
|
+
}
|
|
68
88
|
else {
|
|
69
89
|
const servicesResponse = await services_1.servicesApi.list(workspace.id);
|
|
70
90
|
const services = servicesResponse.data;
|
|
@@ -87,7 +107,6 @@ class Link extends base_command_1.default {
|
|
|
87
107
|
service = selectedService;
|
|
88
108
|
}
|
|
89
109
|
else {
|
|
90
|
-
// Create new service
|
|
91
110
|
const { serviceName } = await inquirer_1.default.prompt([
|
|
92
111
|
{
|
|
93
112
|
type: "input",
|
|
@@ -120,16 +139,21 @@ Link.description = "Link this project to a platform service";
|
|
|
120
139
|
Link.examples = [
|
|
121
140
|
`$ apso link`,
|
|
122
141
|
`$ apso link --workspace my-team --service my-api`,
|
|
142
|
+
`$ apso link --workspace my-team --create my-new-api`,
|
|
123
143
|
`$ apso link --force`,
|
|
124
144
|
];
|
|
125
145
|
Link.flags = {
|
|
126
146
|
workspace: core_1.Flags.string({
|
|
127
147
|
char: "w",
|
|
128
|
-
description: "Workspace slug",
|
|
148
|
+
description: "Workspace slug (defaults to the active workspace)",
|
|
129
149
|
}),
|
|
130
150
|
service: core_1.Flags.string({
|
|
131
151
|
char: "s",
|
|
132
|
-
description: "
|
|
152
|
+
description: "Existing service slug to link",
|
|
153
|
+
}),
|
|
154
|
+
create: core_1.Flags.string({
|
|
155
|
+
char: "c",
|
|
156
|
+
description: "Create a new service with this name and link it",
|
|
133
157
|
}),
|
|
134
158
|
force: core_1.Flags.boolean({
|
|
135
159
|
char: "f",
|
package/dist/lib/upgrade.js
CHANGED
|
@@ -14,6 +14,7 @@ const child_process_1 = require("child_process");
|
|
|
14
14
|
const inquirer_1 = tslib_1.__importDefault(require("inquirer"));
|
|
15
15
|
const client_1 = require("./api/client");
|
|
16
16
|
const config_1 = require("./config");
|
|
17
|
+
const interactive_1 = require("./utils/interactive");
|
|
17
18
|
/**
|
|
18
19
|
* Open a URL in the default browser (best-effort).
|
|
19
20
|
*/
|
|
@@ -75,6 +76,12 @@ async function withUpgradeRetry(action) {
|
|
|
75
76
|
}
|
|
76
77
|
catch (error) {
|
|
77
78
|
if (error instanceof client_1.ApiClientError && error.isLimitError()) {
|
|
79
|
+
// Headless (agent/CI): never open a browser or prompt — surface the limit
|
|
80
|
+
// as a clear error with the upgrade URL so the caller can act on it.
|
|
81
|
+
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}`);
|
|
84
|
+
}
|
|
78
85
|
const retry = await promptUpgrade(error);
|
|
79
86
|
if (retry) {
|
|
80
87
|
return action();
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive vs headless detection.
|
|
3
|
+
*
|
|
4
|
+
* Humans get prompts; agents and CI must run headless — every value comes from
|
|
5
|
+
* a flag and the CLI never blocks on a prompt. Headless is forced by
|
|
6
|
+
* APSO_NONINTERACTIVE=1, by CI=true, or auto-detected when stdin/stdout is not
|
|
7
|
+
* a TTY (e.g. an agent capturing output). Commands should check `isInteractive()`
|
|
8
|
+
* and, when false, require the equivalent flag instead of prompting.
|
|
9
|
+
*/
|
|
10
|
+
export declare function isInteractive(): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Message for a missing required value in headless mode. Name the flag that
|
|
13
|
+
* would have supplied it so an agent can retry deterministically.
|
|
14
|
+
*/
|
|
15
|
+
export declare function missingFlag(details: string): string;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.missingFlag = exports.isInteractive = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Interactive vs headless detection.
|
|
6
|
+
*
|
|
7
|
+
* Humans get prompts; agents and CI must run headless — every value comes from
|
|
8
|
+
* a flag and the CLI never blocks on a prompt. Headless is forced by
|
|
9
|
+
* APSO_NONINTERACTIVE=1, by CI=true, or auto-detected when stdin/stdout is not
|
|
10
|
+
* a TTY (e.g. an agent capturing output). Commands should check `isInteractive()`
|
|
11
|
+
* and, when false, require the equivalent flag instead of prompting.
|
|
12
|
+
*/
|
|
13
|
+
function isInteractive() {
|
|
14
|
+
if (process.env.APSO_NONINTERACTIVE === "1" ||
|
|
15
|
+
process.env.APSO_NONINTERACTIVE === "true") {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
if (process.env.CI === "true" || process.env.CI === "1") {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
22
|
+
}
|
|
23
|
+
exports.isInteractive = isInteractive;
|
|
24
|
+
/**
|
|
25
|
+
* Message for a missing required value in headless mode. Name the flag that
|
|
26
|
+
* would have supplied it so an agent can retry deterministically.
|
|
27
|
+
*/
|
|
28
|
+
function missingFlag(details) {
|
|
29
|
+
return `Running non-interactively (no TTY / CI / APSO_NONINTERACTIVE). ${details}`;
|
|
30
|
+
}
|
|
31
|
+
exports.missingFlag = missingFlag;
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@apso/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@apso/cli",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.25.0",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@biomejs/biome": "^1.9.4",
|
package/oclif.manifest.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.
|
|
2
|
+
"version": "0.25.0",
|
|
3
3
|
"commands": {
|
|
4
4
|
"config": {
|
|
5
5
|
"id": "config",
|
|
@@ -259,6 +259,7 @@
|
|
|
259
259
|
"examples": [
|
|
260
260
|
"$ apso link",
|
|
261
261
|
"$ apso link --workspace my-team --service my-api",
|
|
262
|
+
"$ apso link --workspace my-team --create my-new-api",
|
|
262
263
|
"$ apso link --force"
|
|
263
264
|
],
|
|
264
265
|
"flags": {
|
|
@@ -266,14 +267,21 @@
|
|
|
266
267
|
"name": "workspace",
|
|
267
268
|
"type": "option",
|
|
268
269
|
"char": "w",
|
|
269
|
-
"description": "Workspace slug",
|
|
270
|
+
"description": "Workspace slug (defaults to the active workspace)",
|
|
270
271
|
"multiple": false
|
|
271
272
|
},
|
|
272
273
|
"service": {
|
|
273
274
|
"name": "service",
|
|
274
275
|
"type": "option",
|
|
275
276
|
"char": "s",
|
|
276
|
-
"description": "
|
|
277
|
+
"description": "Existing service slug to link",
|
|
278
|
+
"multiple": false
|
|
279
|
+
},
|
|
280
|
+
"create": {
|
|
281
|
+
"name": "create",
|
|
282
|
+
"type": "option",
|
|
283
|
+
"char": "c",
|
|
284
|
+
"description": "Create a new service with this name and link it",
|
|
277
285
|
"multiple": false
|
|
278
286
|
},
|
|
279
287
|
"force": {
|