@apso/cli 0.27.0 → 0.28.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.
@@ -9,6 +9,7 @@ export default class Deploy extends BaseCommand {
9
9
  "skip-push": import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
10
10
  repo: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
11
11
  branch: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
12
+ "local-git": import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
12
13
  };
13
14
  run(): Promise<void>;
14
15
  /**
@@ -11,6 +11,7 @@ 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
13
  const interactive_1 = require("../lib/utils/interactive");
14
+ const server_push_1 = require("../lib/deploy/server-push");
14
15
  class Deploy extends base_command_1.default {
15
16
  async run() {
16
17
  const { flags } = await this.parse(Deploy);
@@ -68,7 +69,7 @@ class Deploy extends base_command_1.default {
68
69
  // Sync local code to the connected GitHub repo (the deploy builds from it).
69
70
  // Skip for redeploys of already-synced code.
70
71
  if (!flags["skip-push"]) {
71
- await this.syncCodeToGithub(service, flags.repo, flags.branch);
72
+ await this.syncCodeToGithub(service, flags.repo, flags.branch, flags["local-git"]);
72
73
  }
73
74
  // Determine source
74
75
  if (service.githubRepo || flags.repo) {
@@ -132,12 +133,16 @@ class Deploy extends base_command_1.default {
132
133
  * can clone it at deploy time. No code passes through Apso — we read the local
133
134
  * git and push straight to GitHub.
134
135
  */
135
- async syncCodeToGithub(service, repoFlag, branch) {
136
+ async syncCodeToGithub(service, repoFlag, branch, useLocalGit) {
136
137
  const cwd = process.cwd();
137
- if (!git.gitAvailable()) {
138
- this.error("git is required to deploy. Install git and try again.");
138
+ // --local-git needs git + a local repo. The default server-side push reads
139
+ // the files directly and pushes through the platform, so neither is needed.
140
+ if (useLocalGit) {
141
+ if (!git.gitAvailable()) {
142
+ this.error("git is required for --local-git. Install git, or drop --local-git.");
143
+ }
144
+ git.initRepo(cwd);
139
145
  }
140
- git.initRepo(cwd);
141
146
  // Pick the repo: prefer what the local project is already wired to, then the
142
147
  // service's connected repo, then --repo, then ask.
143
148
  let conn = await services_1.githubApi.getConnection();
@@ -166,7 +171,10 @@ class Deploy extends base_command_1.default {
166
171
  this.error("GitHub still isn't connected. Complete 'apso github connect' and re-run deploy.");
167
172
  }
168
173
  }
169
- let repoFullName = git.localRepoFullName(cwd) || service.githubRepo || repoFlag;
174
+ // Only read the local git remote when git is present (a bare machine using
175
+ // the server-side push may not have git at all).
176
+ const localRepo = git.gitAvailable() ? git.localRepoFullName(cwd) : "";
177
+ let repoFullName = localRepo || service.githubRepo || repoFlag;
170
178
  if (!repoFullName) {
171
179
  repoFullName = await this.resolveRepo(conn.connectionId, service);
172
180
  }
@@ -176,18 +184,39 @@ class Deploy extends base_command_1.default {
176
184
  this.log(`Connecting repository ${repoFullName}...`);
177
185
  await services_1.githubApi.connectRepo(service.id, conn.connectionId, repoFullName);
178
186
  }
179
- // Point the local repo at it and push with the user's git credentials.
180
- git.setOrigin(cwd, git.httpsUrlFor(repoFullName));
181
- const committed = git.commitAll(cwd, "Deploy via Apso CLI");
182
- this.log(committed
183
- ? `Committed local changes; pushing to ${repoFullName}#${branch}...`
184
- : `Pushing to ${repoFullName}#${branch}...`);
185
- try {
186
- git.pushHead(cwd, branch);
187
+ if (useLocalGit) {
188
+ // Push with the user's local git credentials.
189
+ git.setOrigin(cwd, git.httpsUrlFor(repoFullName));
190
+ const committed = git.commitAll(cwd, "Deploy via Apso CLI");
191
+ this.log(committed
192
+ ? `Committed local changes; pushing to ${repoFullName}#${branch}...`
193
+ : `Pushing to ${repoFullName}#${branch}...`);
194
+ try {
195
+ git.pushHead(cwd, branch);
196
+ }
197
+ catch (error) {
198
+ const msg = error instanceof Error ? error.message : String(error);
199
+ this.error(`git push to ${repoFullName} failed. Ensure you have push access and your git credentials are set up.\n${msg}`);
200
+ }
187
201
  }
188
- catch (error) {
189
- const msg = error instanceof Error ? error.message : String(error);
190
- this.error(`git push to ${repoFullName} failed. Ensure you have push access and your git credentials are set up.\n${msg}`);
202
+ else {
203
+ // Default: upload the project and push through the Apso GitHub connection.
204
+ // No local git or gh required.
205
+ this.log(`Uploading code and pushing to ${repoFullName}#${branch} via your Apso GitHub connection...`);
206
+ try {
207
+ await (0, server_push_1.serverSidePush)({
208
+ cwd,
209
+ serviceId: service.id,
210
+ connectionId: conn.connectionId,
211
+ branch,
212
+ message: "Deploy via Apso CLI",
213
+ });
214
+ this.log(`Pushed to ${repoFullName}#${branch}.`);
215
+ }
216
+ catch (error) {
217
+ const msg = error instanceof Error ? error.message : String(error);
218
+ this.error(`Push failed: ${msg}\nRetry, or use --local-git to push with your local git instead.`);
219
+ }
191
220
  }
192
221
  }
193
222
  /**
@@ -268,5 +297,9 @@ Deploy.flags = {
268
297
  description: "Branch to push to",
269
298
  default: "main",
270
299
  }),
300
+ "local-git": core_1.Flags.boolean({
301
+ description: "Push using your local git instead of the platform (needs local GitHub auth)",
302
+ default: false,
303
+ }),
271
304
  };
272
305
  exports.default = Deploy;
@@ -6,6 +6,8 @@ export default class McpServe extends BaseCommand {
6
6
  help: import("@oclif/core/lib/interfaces").BooleanFlag<void>;
7
7
  };
8
8
  run(): Promise<void>;
9
+ /** Wrap every registered tool handler with usage telemetry. */
10
+ private instrumentTools;
9
11
  private registerTools;
10
12
  private registerResources;
11
13
  }
@@ -10,6 +10,7 @@ const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
10
10
  const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
11
11
  const zod_1 = require("zod");
12
12
  const base_command_1 = tslib_1.__importDefault(require("../../lib/base-command"));
13
+ const telemetry_1 = require("../../lib/telemetry/telemetry");
13
14
  const apsorc_parser_1 = require("../../lib/apsorc-parser");
14
15
  const lib_1 = require("../../lib");
15
16
  const runner_1 = require("../../lib/doctor/runner");
@@ -23,11 +24,50 @@ class McpServe extends base_command_1.default {
23
24
  name: "apso",
24
25
  version: "0.10.2",
25
26
  });
27
+ // Emit a PostHog event per MCP tool call so agent usage of the server shows
28
+ // up in the same analytics as the CLI. Wrapping server.tool once covers all
29
+ // tools without touching each handler.
30
+ this.instrumentTools(server);
26
31
  this.registerTools(server);
27
32
  this.registerResources(server);
28
33
  const transport = new stdio_js_1.StdioServerTransport();
29
34
  await server.connect(transport);
30
35
  }
36
+ /** Wrap every registered tool handler with usage telemetry. */
37
+ instrumentTools(server) {
38
+ const original = server.tool.bind(server);
39
+ server.tool = (...args) => {
40
+ const name = String(args[0]);
41
+ const lastIndex = args.length - 1;
42
+ const handler = args[lastIndex];
43
+ if (typeof handler === "function") {
44
+ const orig = handler;
45
+ args[lastIndex] = async (...hArgs) => {
46
+ const start = Date.now();
47
+ try {
48
+ const result = await orig(...hArgs);
49
+ (0, telemetry_1.track)("mcp_tool_called", {
50
+ tool: name,
51
+ success: true,
52
+ duration_ms: Date.now() - start,
53
+ });
54
+ return result;
55
+ }
56
+ catch (error) {
57
+ (0, telemetry_1.track)("mcp_tool_called", {
58
+ tool: name,
59
+ success: false,
60
+ duration_ms: Date.now() - start,
61
+ error: error instanceof Error ? error.message : String(error),
62
+ });
63
+ (0, telemetry_1.captureException)(error, { mcp_tool: name });
64
+ throw error;
65
+ }
66
+ };
67
+ }
68
+ return original(...args);
69
+ };
70
+ }
31
71
  registerTools(server) {
32
72
  // ── design_schema ──────────────────────────────────────────────
33
73
  server.tool("design_schema", "Design a database schema from application requirements. Takes a description of entities, relationships, and business rules. Returns a validated .apsorc schema definition ready for code generation.", {
@@ -127,4 +127,16 @@ export declare const githubApi: {
127
127
  message?: string;
128
128
  }): Promise<Raw>;
129
129
  };
130
+ /**
131
+ * Service code API — used for the server-side deploy (upload the project zip to
132
+ * S3; the backend then pushes it to the repo, so no local git is required).
133
+ */
134
+ export declare const codeApi: {
135
+ /** Presigned S3 PUT URL to upload the service's code zip. */
136
+ getUploadUrl(serviceId: string): Promise<{
137
+ url: string;
138
+ bucket?: string;
139
+ key?: string;
140
+ }>;
141
+ };
130
142
  export {};
@@ -12,7 +12,7 @@
12
12
  * below.
13
13
  */
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.githubApi = exports.buildApi = exports.schemaApi = exports.servicesApi = exports.workspacesApi = exports.authApi = void 0;
15
+ exports.codeApi = exports.githubApi = 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;
@@ -366,3 +366,13 @@ exports.githubApi = {
366
366
  });
367
367
  },
368
368
  };
369
+ /**
370
+ * Service code API — used for the server-side deploy (upload the project zip to
371
+ * S3; the backend then pushes it to the repo, so no local git is required).
372
+ */
373
+ exports.codeApi = {
374
+ /** Presigned S3 PUT URL to upload the service's code zip. */
375
+ async getUploadUrl(serviceId) {
376
+ return client_1.api.get(`/api/services/${serviceId}/code/upload-url`);
377
+ },
378
+ };
@@ -0,0 +1,15 @@
1
+ /// <reference types="node" />
2
+ /** Zip the project source into a buffer, skipping build/vcs/dependency dirs. */
3
+ export declare function zipProject(cwd: string): Buffer;
4
+ /**
5
+ * Server-side push: upload the project zip to S3 and have the platform push it
6
+ * to the connected repo through the user's GitHub connection. No local git or
7
+ * `gh` required — works on a bare machine that has only `apso login`.
8
+ */
9
+ export declare function serverSidePush(opts: {
10
+ cwd: string;
11
+ serviceId: string;
12
+ connectionId: string;
13
+ branch: string;
14
+ message: string;
15
+ }): Promise<void>;
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.serverSidePush = exports.zipProject = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const adm_zip_1 = tslib_1.__importDefault(require("adm-zip"));
6
+ const path = tslib_1.__importStar(require("path"));
7
+ const services_1 = require("../api/services");
8
+ // Never ship these into the code zip — the backend reconstitutes the runnable
9
+ // base template and only needs the source + schema + user edits.
10
+ const EXCLUDE_DIRS = new Set([
11
+ "node_modules",
12
+ ".git",
13
+ "dist",
14
+ ".next",
15
+ ".apso",
16
+ "coverage",
17
+ ]);
18
+ /** Zip the project source into a buffer, skipping build/vcs/dependency dirs. */
19
+ function zipProject(cwd) {
20
+ const zip = new adm_zip_1.default();
21
+ zip.addLocalFolder(cwd, "", (filename) => {
22
+ const parts = filename.split(path.sep);
23
+ return !parts.some((p) => EXCLUDE_DIRS.has(p));
24
+ });
25
+ return zip.toBuffer();
26
+ }
27
+ exports.zipProject = zipProject;
28
+ /**
29
+ * Server-side push: upload the project zip to S3 and have the platform push it
30
+ * to the connected repo through the user's GitHub connection. No local git or
31
+ * `gh` required — works on a bare machine that has only `apso login`.
32
+ */
33
+ async function serverSidePush(opts) {
34
+ const buffer = zipProject(opts.cwd);
35
+ const { url } = await services_1.codeApi.getUploadUrl(opts.serviceId);
36
+ const res = await fetch(url, {
37
+ method: "PUT",
38
+ headers: { "Content-Type": "application/zip" },
39
+ body: buffer,
40
+ });
41
+ if (!res.ok) {
42
+ throw new Error(`Code upload failed (${res.status}). ${await res.text().catch(() => "")}`);
43
+ }
44
+ await services_1.githubApi.push(opts.serviceId, opts.connectionId, {
45
+ branch: opts.branch,
46
+ message: opts.message,
47
+ });
48
+ }
49
+ exports.serverSidePush = serverSidePush;
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@apso/cli",
3
- "version": "0.27.0",
3
+ "version": "0.28.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@apso/cli",
9
- "version": "0.27.0",
9
+ "version": "0.28.0",
10
10
  "license": "Apache-2.0",
11
11
  "dependencies": {
12
12
  "@biomejs/biome": "^1.9.4",
@@ -17,6 +17,7 @@
17
17
  "@oclif/plugin-plugins": "^3",
18
18
  "@oclif/plugin-warn-if-update-available": "^2.1.1",
19
19
  "@sentry/node": "^10.68.0",
20
+ "adm-zip": "^0.6.0",
20
21
  "debug": "^4.3.4",
21
22
  "eta": "^2.0.0",
22
23
  "inquirer": "^8.2.7",
@@ -35,6 +36,7 @@
35
36
  "devDependencies": {
36
37
  "@jest/globals": "^29.5.0",
37
38
  "@oclif/test": "^2.4.7",
39
+ "@types/adm-zip": "^0.5.8",
38
40
  "@types/chai": "^4",
39
41
  "@types/inquirer": "^8.2.12",
40
42
  "@types/mocha": "^10",
@@ -2659,6 +2661,16 @@
2659
2661
  "url": "https://github.com/sponsors/isaacs"
2660
2662
  }
2661
2663
  },
2664
+ "node_modules/@types/adm-zip": {
2665
+ "version": "0.5.8",
2666
+ "resolved": "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.5.8.tgz",
2667
+ "integrity": "sha512-RVVH7QvZYbN+ihqZ4kX/dMiowf6o+Jk1fNwiSdx0NahBJLU787zkULhGhJM8mf/obmLGmgdMM0bXsQTmyfbR7Q==",
2668
+ "dev": true,
2669
+ "license": "MIT",
2670
+ "dependencies": {
2671
+ "@types/node": "*"
2672
+ }
2673
+ },
2662
2674
  "node_modules/@types/babel__core": {
2663
2675
  "version": "7.20.1",
2664
2676
  "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.1.tgz",
@@ -3556,6 +3568,15 @@
3556
3568
  "node": ">=0.4.0"
3557
3569
  }
3558
3570
  },
3571
+ "node_modules/adm-zip": {
3572
+ "version": "0.6.0",
3573
+ "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.6.0.tgz",
3574
+ "integrity": "sha512-XleryMhbuksdKtofnWZ9Sk+4CUTbms4Mb/EU32SZwToAyZ5RgVos/ki8n+yr0LWHOGKuakbXTuuYNHLQjhddgg==",
3575
+ "license": "MIT",
3576
+ "engines": {
3577
+ "node": ">=14.0"
3578
+ }
3579
+ },
3559
3580
  "node_modules/agent-base": {
3560
3581
  "version": "6.0.2",
3561
3582
  "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.27.0",
2
+ "version": "0.28.0",
3
3
  "commands": {
4
4
  "config": {
5
5
  "id": "config",
@@ -88,6 +88,12 @@
88
88
  "description": "Branch to push to",
89
89
  "multiple": false,
90
90
  "default": "main"
91
+ },
92
+ "local-git": {
93
+ "name": "local-git",
94
+ "type": "boolean",
95
+ "description": "Push using your local git instead of the platform (needs local GitHub auth)",
96
+ "allowNo": false
91
97
  }
92
98
  },
93
99
  "args": {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apso/cli",
3
- "version": "0.27.0",
3
+ "version": "0.28.0",
4
4
  "mcpName": "io.github.apsoai/apso",
5
5
  "description": "Apso CLI",
6
6
  "author": "Apso by Mavric - @mavric",
@@ -32,6 +32,7 @@
32
32
  "@oclif/plugin-plugins": "^3",
33
33
  "@oclif/plugin-warn-if-update-available": "^2.1.1",
34
34
  "@sentry/node": "^10.68.0",
35
+ "adm-zip": "^0.6.0",
35
36
  "debug": "^4.3.4",
36
37
  "eta": "^2.0.0",
37
38
  "inquirer": "^8.2.7",
@@ -47,6 +48,7 @@
47
48
  "devDependencies": {
48
49
  "@jest/globals": "^29.5.0",
49
50
  "@oclif/test": "^2.4.7",
51
+ "@types/adm-zip": "^0.5.8",
50
52
  "@types/chai": "^4",
51
53
  "@types/inquirer": "^8.2.12",
52
54
  "@types/mocha": "^10",