@apso/cli 0.22.0 → 0.24.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.
@@ -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
  {
@@ -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>;
@@ -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: "Service slug",
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",
@@ -0,0 +1,14 @@
1
+ import BaseCommand from "../lib/base-command";
2
+ /**
3
+ * Update the CLI in place. Detects whether it was installed via Homebrew or npm
4
+ * and runs the matching upgrade command, so users don't have to remember the
5
+ * incantation. `--check` only reports whether an update is available.
6
+ */
7
+ export default class Update extends BaseCommand {
8
+ static description: string;
9
+ static examples: string[];
10
+ static flags: {
11
+ check: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
12
+ };
13
+ run(): Promise<void>;
14
+ }
@@ -0,0 +1,69 @@
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 child_process_1 = require("child_process");
6
+ const base_command_1 = tslib_1.__importDefault(require("../lib/base-command"));
7
+ /**
8
+ * Update the CLI in place. Detects whether it was installed via Homebrew or npm
9
+ * and runs the matching upgrade command, so users don't have to remember the
10
+ * incantation. `--check` only reports whether an update is available.
11
+ */
12
+ class Update extends base_command_1.default {
13
+ async run() {
14
+ const { flags } = await this.parse(Update);
15
+ const current = this.config.version;
16
+ let latest = "";
17
+ try {
18
+ latest = (0, child_process_1.execSync)("npm view @apso/cli version", {
19
+ encoding: "utf-8",
20
+ timeout: 15000,
21
+ }).trim();
22
+ }
23
+ catch {
24
+ this.warn("Could not reach npm to check for the latest version.");
25
+ }
26
+ if (latest) {
27
+ this.log(` Installed: ${current}`);
28
+ this.log(` Latest: ${latest}`);
29
+ this.log("");
30
+ }
31
+ if (latest && latest === current) {
32
+ this.log("You're on the latest version.");
33
+ return;
34
+ }
35
+ if (flags.check) {
36
+ if (latest)
37
+ this.log(`Update available: ${current} -> ${latest}`);
38
+ this.log("Run 'apso update' to upgrade.");
39
+ return;
40
+ }
41
+ // Pick the upgrade command based on how the CLI was installed.
42
+ const installRoot = `${this.config.root || ""} ${process.execPath}`;
43
+ const isBrew = /(?:Cellar|homebrew|\/opt\/homebrew)/i.test(installRoot);
44
+ const cmd = isBrew
45
+ ? "brew upgrade apsoai/tap/apso"
46
+ : "npm install -g @apso/cli@latest";
47
+ this.log(`Updating via: ${cmd}`);
48
+ this.log("");
49
+ try {
50
+ (0, child_process_1.execSync)(cmd, { stdio: "inherit" });
51
+ }
52
+ catch {
53
+ this.error(`Update failed. Run it manually:\n ${cmd}` +
54
+ (isBrew ? "" : "\n(or 'brew upgrade apsoai/tap/apso' if you used Homebrew)"));
55
+ }
56
+ this.log("");
57
+ this.log(`Updated to ${latest || "the latest version"}.`);
58
+ this.log("Run 'apso --version' to confirm.");
59
+ }
60
+ }
61
+ Update.description = "Update the Apso CLI to the latest version";
62
+ Update.examples = ["$ apso update", "$ apso update --check"];
63
+ Update.flags = {
64
+ check: core_1.Flags.boolean({
65
+ description: "Only check whether a newer version is available",
66
+ default: false,
67
+ }),
68
+ };
69
+ exports.default = Update;
@@ -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;
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@apso/cli",
3
- "version": "0.22.0",
3
+ "version": "0.24.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@apso/cli",
9
- "version": "0.22.0",
9
+ "version": "0.24.0",
10
10
  "license": "Apache-2.0",
11
11
  "dependencies": {
12
12
  "@biomejs/biome": "^1.9.4",
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.22.0",
2
+ "version": "0.24.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": "Service slug",
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": {
@@ -501,6 +509,29 @@
501
509
  },
502
510
  "args": {}
503
511
  },
512
+ "update": {
513
+ "id": "update",
514
+ "description": "Update the Apso CLI to the latest version",
515
+ "strict": true,
516
+ "pluginName": "@apso/cli",
517
+ "pluginAlias": "@apso/cli",
518
+ "pluginType": "core",
519
+ "aliases": [],
520
+ "hiddenAliases": [],
521
+ "examples": [
522
+ "$ apso update",
523
+ "$ apso update --check"
524
+ ],
525
+ "flags": {
526
+ "check": {
527
+ "name": "check",
528
+ "type": "boolean",
529
+ "description": "Only check whether a newer version is available",
530
+ "allowNo": false
531
+ }
532
+ },
533
+ "args": {}
534
+ },
504
535
  "use": {
505
536
  "id": "use",
506
537
  "description": "Select the active workspace for CLI commands",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apso/cli",
3
- "version": "0.22.0",
3
+ "version": "0.24.0",
4
4
  "mcpName": "io.github.apsoai/apso",
5
5
  "description": "Apso CLI",
6
6
  "author": "Apso by Mavric - @mavric",