@apso/cli 0.21.0 → 0.23.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.
@@ -23,7 +23,7 @@ class Deploy extends base_command_1.default {
23
23
  this.error("Project not linked. Run 'apso link' first.");
24
24
  }
25
25
  // Fetch service
26
- const service = await services_1.servicesApi.get(link.workspaceSlug, link.serviceSlug);
26
+ const service = await services_1.servicesApi.get(link.workspaceId, link.serviceSlug);
27
27
  // Run migration sandbox (unless skipped)
28
28
  let migrationCount = 0;
29
29
  if (!flags["skip-migrate"]) {
@@ -60,7 +60,7 @@ class Init extends base_command_1.default {
60
60
  },
61
61
  ]);
62
62
  // Select service
63
- const servicesResponse = await services_1.servicesApi.list(workspace.slug);
63
+ const servicesResponse = await services_1.servicesApi.list(workspace.id);
64
64
  if (servicesResponse.data.length === 0) {
65
65
  this.error(`No services found in workspace "${workspace.name}". Create one first or choose "Create a new project".`);
66
66
  }
@@ -177,7 +177,7 @@ class Init extends base_command_1.default {
177
177
  // Create service on platform
178
178
  this.log(`Creating service "${projectName}" in workspace "${workspace.name}"...`);
179
179
  const serviceName = projectName;
180
- const service = await (0, upgrade_1.withUpgradeRetry)(() => services_1.servicesApi.create(workspace.slug, {
180
+ const service = await (0, upgrade_1.withUpgradeRetry)(() => services_1.servicesApi.create(workspace.id, {
181
181
  name: serviceName,
182
182
  }));
183
183
  // Clone template
@@ -63,10 +63,10 @@ class Link extends base_command_1.default {
63
63
  // Determine service
64
64
  let service;
65
65
  if (flags.service) {
66
- service = await services_1.servicesApi.get(workspace.slug, flags.service);
66
+ service = await services_1.servicesApi.get(workspace.id, flags.service);
67
67
  }
68
68
  else {
69
- const servicesResponse = await services_1.servicesApi.list(workspace.slug);
69
+ const servicesResponse = await services_1.servicesApi.list(workspace.id);
70
70
  const services = servicesResponse.data;
71
71
  const choices = [
72
72
  ...services.map((svc) => ({
@@ -97,7 +97,7 @@ class Link extends base_command_1.default {
97
97
  },
98
98
  ]);
99
99
  this.log(`Creating service "${serviceName}"...`);
100
- service = await (0, upgrade_1.withUpgradeRetry)(() => services_1.servicesApi.create(workspace.slug, {
100
+ service = await (0, upgrade_1.withUpgradeRetry)(() => services_1.servicesApi.create(workspace.id, {
101
101
  name: serviceName,
102
102
  }));
103
103
  }
@@ -298,9 +298,9 @@ class Login extends base_command_1.default {
298
298
  this.log(`Successfully logged in as ${tokenResponse.user.email}`);
299
299
  this.log("");
300
300
  this.log("Next steps:");
301
- this.log(" apso whoami - View your account info");
302
- this.log(" apso services - List your services");
303
- this.log(" apso tui - Launch interactive UI");
301
+ this.log(" apso use - Select your workspace");
302
+ this.log(" apso whoami - View your account + workspace");
303
+ this.log(" apso projects - List services in a workspace");
304
304
  }
305
305
  catch (error) {
306
306
  if (error instanceof Error) {
@@ -13,17 +13,28 @@ class Projects extends base_command_1.default {
13
13
  if (!config_1.credentials.isValid()) {
14
14
  this.error("Not logged in. Run 'apso login' first.");
15
15
  }
16
- // Determine workspace
16
+ // Determine workspace. The BFF scopes services by the NUMERIC workspace id,
17
+ // so resolve slugs to an id before listing.
18
+ let workspaceId;
17
19
  let workspaceSlug;
18
20
  if (flags.workspace) {
19
- workspaceSlug = flags.workspace;
21
+ const ws = await services_1.workspacesApi.get(flags.workspace);
22
+ workspaceId = String(ws.id);
23
+ workspaceSlug = ws.slug;
20
24
  }
21
25
  else {
22
- // Try from existing link
26
+ // Prefer an existing link, then the active workspace set via `apso use`,
27
+ // and only prompt as a last resort.
23
28
  const link = config_1.projectLink.read();
29
+ const active = config_1.globalConfig.read();
24
30
  if (link) {
31
+ workspaceId = String(link.workspaceId);
25
32
  workspaceSlug = link.workspaceSlug;
26
33
  }
34
+ else if (active.activeWorkspaceId) {
35
+ workspaceId = active.activeWorkspaceId;
36
+ workspaceSlug = active.activeWorkspaceSlug || active.activeWorkspaceId;
37
+ }
27
38
  else {
28
39
  // Prompt
29
40
  const workspaces = await services_1.workspacesApi.list();
@@ -41,11 +52,12 @@ class Projects extends base_command_1.default {
41
52
  })),
42
53
  },
43
54
  ]);
55
+ workspaceId = String(response.workspace.id);
44
56
  workspaceSlug = response.workspace.slug;
45
57
  }
46
58
  }
47
- // Fetch services
48
- const servicesResponse = await services_1.servicesApi.list(workspaceSlug);
59
+ // Fetch services (scoped by numeric workspace id)
60
+ const servicesResponse = await services_1.servicesApi.list(workspaceId);
49
61
  const services = servicesResponse.data;
50
62
  if (services.length === 0) {
51
63
  this.log(`No services found in workspace "${workspaceSlug}".`);
@@ -16,7 +16,7 @@ class Status extends base_command_1.default {
16
16
  this.error("Project not linked. Run 'apso link' first.");
17
17
  }
18
18
  // Fetch service
19
- const service = await services_1.servicesApi.get(link.workspaceSlug, link.serviceSlug);
19
+ const service = await services_1.servicesApi.get(link.workspaceId, link.serviceSlug);
20
20
  this.log(`Service: ${service.name} (${service.slug})`);
21
21
  this.log(`Workspace: ${link.workspaceSlug}`);
22
22
  this.log(`Status: ${service.status}`);
@@ -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;
@@ -0,0 +1,14 @@
1
+ import BaseCommand from "../lib/base-command";
2
+ /**
3
+ * Select the active workspace for subsequent CLI commands. With no argument it
4
+ * shows an interactive picker; pass a slug/name to set it directly. The choice
5
+ * persists to ~/.apso/config.json and scopes every workspace-aware command.
6
+ */
7
+ export default class Use extends BaseCommand {
8
+ static description: string;
9
+ static examples: string[];
10
+ static args: {
11
+ workspace: import("@oclif/core/lib/interfaces/parser").Arg<string | undefined, Record<string, unknown>>;
12
+ };
13
+ run(): Promise<void>;
14
+ }
@@ -0,0 +1,86 @@
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 inquirer_1 = tslib_1.__importDefault(require("inquirer"));
6
+ const base_command_1 = tslib_1.__importDefault(require("../lib/base-command"));
7
+ const config_1 = require("../lib/config");
8
+ const services_1 = require("../lib/api/services");
9
+ /**
10
+ * Select the active workspace for subsequent CLI commands. With no argument it
11
+ * shows an interactive picker; pass a slug/name to set it directly. The choice
12
+ * persists to ~/.apso/config.json and scopes every workspace-aware command.
13
+ */
14
+ class Use extends base_command_1.default {
15
+ async run() {
16
+ const { args } = await this.parse(Use);
17
+ if (!config_1.credentials.isValid()) {
18
+ this.error("Not logged in. Run 'apso login' first.");
19
+ }
20
+ const workspaces = await services_1.workspacesApi.list();
21
+ if (workspaces.length === 0) {
22
+ this.error("No workspaces found. Create one at https://app.apso.cloud first.");
23
+ }
24
+ let ws;
25
+ if (args.workspace) {
26
+ ws = workspaces.find((w) => w.slug === args.workspace || w.name === args.workspace);
27
+ if (!ws) {
28
+ this.error(`Workspace not found: ${args.workspace}\nRun 'apso use' to pick from a list.`);
29
+ }
30
+ }
31
+ else if (workspaces.length === 1) {
32
+ ws = workspaces[0];
33
+ }
34
+ else {
35
+ const answer = await inquirer_1.default.prompt([
36
+ {
37
+ type: "list",
38
+ name: "workspace",
39
+ message: "Select your workspace:",
40
+ choices: workspaces.map((w) => ({
41
+ name: `${w.name} (${w.slug})`,
42
+ value: w,
43
+ })),
44
+ },
45
+ ]);
46
+ ws = answer.workspace;
47
+ }
48
+ config_1.globalConfig.write({
49
+ activeWorkspaceId: String(ws.id),
50
+ activeWorkspaceSlug: ws.slug,
51
+ activeWorkspaceName: ws.name,
52
+ });
53
+ this.log("");
54
+ this.log(` Now using workspace: ${ws.name} (${ws.slug})`);
55
+ // Show what's in the workspace so the next step is obvious.
56
+ try {
57
+ const services = (await services_1.servicesApi.list(ws.id)).data;
58
+ this.log("");
59
+ if (services.length > 0) {
60
+ this.log(" Services:");
61
+ for (const s of services) {
62
+ this.log(` - ${s.name} (${s.slug})`);
63
+ }
64
+ this.log("");
65
+ this.log(" Run 'apso link' inside a project to work with one.");
66
+ }
67
+ else {
68
+ this.log(" No services yet.");
69
+ this.log(" Run 'apso init' to scaffold one, then 'apso deploy' to ship it.");
70
+ }
71
+ }
72
+ catch {
73
+ // listing is a convenience; never fail `use` over it
74
+ }
75
+ this.log("");
76
+ }
77
+ }
78
+ Use.description = "Select the active workspace for CLI commands";
79
+ Use.examples = ["$ apso use", "$ apso use my-workspace"];
80
+ Use.args = {
81
+ workspace: core_1.Args.string({
82
+ description: "Workspace slug or name (skips the picker)",
83
+ required: false,
84
+ }),
85
+ };
86
+ exports.default = Use;
@@ -71,12 +71,14 @@ class Whoami extends base_command_1.default {
71
71
  this.log(` Workspace: ${link.workspaceSlug}`);
72
72
  this.log(` Service: ${link.serviceSlug}`);
73
73
  }
74
- else if (config.defaultWorkspace) {
75
- this.log(` Workspace: ${config.defaultWorkspace} (default)`);
76
- this.log(" Service: None (run 'apso use <service>' to set)");
74
+ else if (config.activeWorkspaceName || config.activeWorkspaceSlug) {
75
+ const name = config.activeWorkspaceName || config.activeWorkspaceSlug;
76
+ const slug = config.activeWorkspaceSlug;
77
+ this.log(` Workspace: ${name}${slug ? ` (${slug})` : ""}`);
78
+ this.log(" Service: None (run 'apso link' in a project to set)");
77
79
  }
78
80
  else {
79
- this.log(" Workspace: None (run 'apso use <service>' to set)");
81
+ this.log(" Workspace: None (run 'apso use' to select one)");
80
82
  this.log(" Service: None");
81
83
  }
82
84
  this.log("");
@@ -225,7 +225,9 @@ async function apiRequest(path, options = {}) {
225
225
  // Resolve the active workspace id from, in order: an explicit header the
226
226
  // caller passed, the APSO_WORKSPACE_ID env override, or the linked project.
227
227
  if (!requestHeaders["X-Workspace-Id"]) {
228
- const activeWorkspaceId = process.env.APSO_WORKSPACE_ID || ((_a = config_1.projectLink.read()) === null || _a === void 0 ? void 0 : _a.workspaceId);
228
+ const activeWorkspaceId = process.env.APSO_WORKSPACE_ID ||
229
+ ((_a = config_1.projectLink.read()) === null || _a === void 0 ? void 0 : _a.workspaceId) ||
230
+ config_1.globalConfig.read().activeWorkspaceId;
229
231
  if (activeWorkspaceId) {
230
232
  requestHeaders["X-Workspace-Id"] = String(activeWorkspaceId);
231
233
  }
@@ -37,15 +37,15 @@ export declare const workspacesApi: {
37
37
  * Services API
38
38
  */
39
39
  export declare const servicesApi: {
40
- list(_workspaceSlug?: string, options?: {
40
+ list(workspaceId?: string | number, options?: {
41
41
  status?: string;
42
42
  page?: number;
43
43
  pageSize?: number;
44
44
  }): Promise<PaginatedResponse<Service>>;
45
45
  getById(id: string): Promise<Service>;
46
- /** Resolve (workspace, serviceSlug) to a service via the workspace-scoped list. */
47
- get(_workspaceSlug: string, serviceSlug: string): Promise<Service>;
48
- create(_workspaceSlug: string, data: {
46
+ /** Resolve (workspaceId, serviceSlug) to a service via the workspace-scoped list. */
47
+ get(workspaceId: string, serviceSlug: string): Promise<Service>;
48
+ create(workspaceId: string, data: {
49
49
  name: string;
50
50
  slug?: string;
51
51
  description?: string;
@@ -128,30 +128,39 @@ exports.workspacesApi = {
128
128
  * Services API
129
129
  */
130
130
  exports.servicesApi = {
131
- async list(_workspaceSlug, options) {
131
+ async list(workspaceId, options) {
132
132
  const params = {};
133
133
  if (options === null || options === void 0 ? void 0 : options.pageSize)
134
134
  params.limit = options.pageSize;
135
135
  if ((options === null || options === void 0 ? void 0 : options.page) && (options === null || options === void 0 ? void 0 : options.pageSize)) {
136
136
  params.offset = (options.page - 1) * options.pageSize;
137
137
  }
138
- const raw = await client_1.api.get("/api/services", { params });
138
+ // The BFF scopes /api/services by the NUMERIC workspace id in the
139
+ // X-Workspace-Id header (not the slug). Pass it explicitly so the call
140
+ // works even before a project is linked.
141
+ const headers = workspaceId
142
+ ? { "X-Workspace-Id": String(workspaceId) }
143
+ : undefined;
144
+ const raw = await client_1.api.get("/api/services", { params, headers });
139
145
  return adaptPaginated(raw, adaptService);
140
146
  },
141
147
  async getById(id) {
142
148
  const raw = await client_1.api.get(`/api/services/${id}`);
143
149
  return adaptService(raw);
144
150
  },
145
- /** Resolve (workspace, serviceSlug) to a service via the workspace-scoped list. */
146
- async get(_workspaceSlug, serviceSlug) {
147
- const list = await this.list();
151
+ /** Resolve (workspaceId, serviceSlug) to a service via the workspace-scoped list. */
152
+ async get(workspaceId, serviceSlug) {
153
+ const list = await this.list(workspaceId);
148
154
  const match = list.data.find((s) => s.slug === serviceSlug);
149
155
  if (!match)
150
156
  throw new Error(`Service not found: ${serviceSlug}`);
151
157
  return match;
152
158
  },
153
- async create(_workspaceSlug, data) {
154
- const raw = await client_1.api.post("/api/services", { name: data.name });
159
+ async create(workspaceId, data) {
160
+ const headers = workspaceId
161
+ ? { "X-Workspace-Id": String(workspaceId) }
162
+ : undefined;
163
+ const raw = await client_1.api.post("/api/services", { name: data.name }, { headers });
155
164
  return adaptService(raw);
156
165
  },
157
166
  async update(_workspaceSlug, serviceSlugOrId, data) {
@@ -72,6 +72,15 @@ exports.globalConfig = {
72
72
  if (fileConfig) {
73
73
  Object.assign(config, fileConfig);
74
74
  }
75
+ // Auto-migrate legacy apso.ai hosts written by an older CLI. Without this a
76
+ // saved config keeps pointing login/API at the retired app.apso.ai/
77
+ // api.apso.ai instead of the current apso.cloud defaults.
78
+ if (config.apiUrl && /(?:app|api)\.apso\.ai\b/.test(config.apiUrl)) {
79
+ config.apiUrl = types_1.DEFAULT_GLOBAL_CONFIG.apiUrl;
80
+ }
81
+ if (config.webUrl && /(?:app|api)\.apso\.ai\b/.test(config.webUrl)) {
82
+ config.webUrl = types_1.DEFAULT_GLOBAL_CONFIG.webUrl;
83
+ }
75
84
  // Override with environment variables
76
85
  if (process.env.APSO_API_URL) {
77
86
  config.apiUrl = process.env.APSO_API_URL;
@@ -96,6 +96,13 @@ export interface GlobalConfigFile {
96
96
  * Default workspace slug to use
97
97
  */
98
98
  defaultWorkspace?: string;
99
+ /**
100
+ * Active workspace, set via `apso use`. The numeric id scopes API calls
101
+ * (X-Workspace-Id); slug/name are for display.
102
+ */
103
+ activeWorkspaceId?: string;
104
+ activeWorkspaceSlug?: string;
105
+ activeWorkspaceName?: string;
99
106
  /**
100
107
  * Platform API base URL
101
108
  */
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@apso/cli",
3
- "version": "0.21.0",
3
+ "version": "0.23.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@apso/cli",
9
- "version": "0.21.0",
9
+ "version": "0.23.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.21.0",
2
+ "version": "0.23.0",
3
3
  "commands": {
4
4
  "config": {
5
5
  "id": "config",
@@ -501,6 +501,51 @@
501
501
  },
502
502
  "args": {}
503
503
  },
504
+ "update": {
505
+ "id": "update",
506
+ "description": "Update the Apso CLI to the latest version",
507
+ "strict": true,
508
+ "pluginName": "@apso/cli",
509
+ "pluginAlias": "@apso/cli",
510
+ "pluginType": "core",
511
+ "aliases": [],
512
+ "hiddenAliases": [],
513
+ "examples": [
514
+ "$ apso update",
515
+ "$ apso update --check"
516
+ ],
517
+ "flags": {
518
+ "check": {
519
+ "name": "check",
520
+ "type": "boolean",
521
+ "description": "Only check whether a newer version is available",
522
+ "allowNo": false
523
+ }
524
+ },
525
+ "args": {}
526
+ },
527
+ "use": {
528
+ "id": "use",
529
+ "description": "Select the active workspace for CLI commands",
530
+ "strict": true,
531
+ "pluginName": "@apso/cli",
532
+ "pluginAlias": "@apso/cli",
533
+ "pluginType": "core",
534
+ "aliases": [],
535
+ "hiddenAliases": [],
536
+ "examples": [
537
+ "$ apso use",
538
+ "$ apso use my-workspace"
539
+ ],
540
+ "flags": {},
541
+ "args": {
542
+ "workspace": {
543
+ "name": "workspace",
544
+ "description": "Workspace slug or name (skips the picker)",
545
+ "required": false
546
+ }
547
+ }
548
+ },
504
549
  "whoami": {
505
550
  "id": "whoami",
506
551
  "description": "Display information about the current user and context",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apso/cli",
3
- "version": "0.21.0",
3
+ "version": "0.23.0",
4
4
  "mcpName": "io.github.apsoai/apso",
5
5
  "description": "Apso CLI",
6
6
  "author": "Apso by Mavric - @mavric",