@apso/cli 0.27.0 → 0.29.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/README.md +28 -1
- package/dist/commands/config.js +29 -0
- package/dist/commands/deploy.d.ts +1 -0
- package/dist/commands/deploy.js +50 -17
- package/dist/commands/dev.d.ts +9 -0
- package/dist/commands/dev.js +29 -2
- package/dist/commands/generate.js +26 -14
- package/dist/commands/github/connect.js +3 -1
- package/dist/commands/link.js +1 -0
- package/dist/commands/mcp/serve.d.ts +2 -0
- package/dist/commands/mcp/serve.js +40 -0
- package/dist/commands/update.js +1 -1
- package/dist/commands/use.js +2 -1
- package/dist/hooks/init/telemetry.js +6 -0
- package/dist/lib/api/services.d.ts +12 -0
- package/dist/lib/api/services.js +18 -6
- package/dist/lib/config/types.d.ts +10 -0
- package/dist/lib/config/types.js +1 -0
- package/dist/lib/deploy/server-push.d.ts +15 -0
- package/dist/lib/deploy/server-push.js +49 -0
- package/dist/lib/generators/base.d.ts +1 -0
- package/dist/lib/generators/go.d.ts +22 -1
- package/dist/lib/generators/go.js +47 -6
- package/dist/lib/generators/python.d.ts +18 -0
- package/dist/lib/generators/python.js +33 -0
- package/dist/lib/generators/typescript.js +3 -0
- package/dist/lib/git.js +1 -1
- package/dist/lib/migrate/sandbox.js +3 -1
- package/dist/lib/telemetry/telemetry.d.ts +20 -1
- package/dist/lib/telemetry/telemetry.js +68 -12
- package/dist/lib/templates/entities/entity.eta +1 -1
- package/dist/lib/templates/go/events/event-emitting-entities.eta +21 -0
- package/dist/lib/templates/go/index-module.eta +6 -6
- package/dist/lib/templates/python/events/event-emitting-entities.eta +22 -0
- package/dist/lib/types/entity.d.ts +6 -0
- package/dist/lib/types/generator.d.ts +1 -0
- package/dist/lib/utils/template.d.ts +10 -0
- package/dist/lib/utils/template.js +25 -1
- package/npm-shrinkwrap.json +23 -2
- package/oclif.manifest.json +8 -1
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -314,7 +314,7 @@ apso config reset # Reset to defaults
|
|
|
314
314
|
| `webUrl` | string | Platform web URL |
|
|
315
315
|
| `verbose` | boolean | Enable verbose output |
|
|
316
316
|
| `noColor` | boolean | Disable colored output |
|
|
317
|
-
| `telemetryDisabled` | boolean | Opt out of anonymous telemetry |
|
|
317
|
+
| `telemetryDisabled` | boolean | Opt out of anonymous telemetry (or `apso config set telemetry off`) |
|
|
318
318
|
| `defaultWorkspace` | string | Default workspace slug |
|
|
319
319
|
|
|
320
320
|
Boolean values accept `true`/`false` or `1`/`0`.
|
|
@@ -327,6 +327,7 @@ Environment variables override config file values:
|
|
|
327
327
|
| `APSO_WEB_URL` | `webUrl` |
|
|
328
328
|
| `APSO_DEBUG=true` | `verbose` |
|
|
329
329
|
| `NO_COLOR` or `APSO_NO_COLOR=true` | `noColor` |
|
|
330
|
+
| `APSO_TELEMETRY=0` / `DO_NOT_TRACK=1` | `telemetryDisabled` |
|
|
330
331
|
|
|
331
332
|
### `apso schema`
|
|
332
333
|
|
|
@@ -348,6 +349,32 @@ apso [command] --help # Show command help
|
|
|
348
349
|
apso [command] --version # Show CLI version
|
|
349
350
|
```
|
|
350
351
|
|
|
352
|
+
## Telemetry & privacy
|
|
353
|
+
|
|
354
|
+
The CLI sends anonymous usage data so we can see which commands are used and
|
|
355
|
+
prioritize accordingly. It is opt-out and carries no personal data.
|
|
356
|
+
|
|
357
|
+
**What is collected:** command name (`generate`, `init`, `deploy`, …), CLI
|
|
358
|
+
version, OS and architecture, Node version, command duration and success, and
|
|
359
|
+
an anonymous install id (a random UUID stored in your CLI config — not derived
|
|
360
|
+
from any machine identifier). If you are logged in, events attribute to your
|
|
361
|
+
Apso account, the same as the web app.
|
|
362
|
+
|
|
363
|
+
**What is never collected:** your source code, your `.apsorc` or schema
|
|
364
|
+
contents, file paths, environment variable values, or any other file contents.
|
|
365
|
+
|
|
366
|
+
**Opt out** any of these ways:
|
|
367
|
+
|
|
368
|
+
```bash
|
|
369
|
+
apso config set telemetry off # persisted in your CLI config
|
|
370
|
+
export APSO_TELEMETRY=0 # per-shell
|
|
371
|
+
export DO_NOT_TRACK=1 # honors the consoledonottrack.com standard
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
On first run the CLI prints a one-time notice pointing at these opt-outs.
|
|
375
|
+
See [`docs/telemetry.md`](docs/telemetry.md) for full detail. Aggregate
|
|
376
|
+
public download stats (npm/PyPI) are available via `node scripts/usage-stats.mjs`.
|
|
377
|
+
|
|
351
378
|
## Supported languages
|
|
352
379
|
|
|
353
380
|
| Language | Framework | ORM | Status |
|
package/dist/commands/config.js
CHANGED
|
@@ -26,6 +26,19 @@ function parseBoolean(value) {
|
|
|
26
26
|
return false;
|
|
27
27
|
return null;
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Parse the friendly `telemetry on|off` value into the underlying
|
|
31
|
+
* `telemetryDisabled` boolean. `off` (opt out) => disabled = true.
|
|
32
|
+
* Accepts on/off, enabled/disabled, and true/false/1/0.
|
|
33
|
+
*/
|
|
34
|
+
function parseTelemetryToggle(value) {
|
|
35
|
+
const lower = value.toLowerCase();
|
|
36
|
+
if (["off", "false", "0", "disabled", "no"].includes(lower))
|
|
37
|
+
return true;
|
|
38
|
+
if (["on", "true", "1", "enabled", "yes"].includes(lower))
|
|
39
|
+
return false;
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
29
42
|
class Config extends base_command_1.default {
|
|
30
43
|
async run() {
|
|
31
44
|
const { args, argv } = await this.parse(Config);
|
|
@@ -46,6 +59,11 @@ class Config extends base_command_1.default {
|
|
|
46
59
|
if (!key) {
|
|
47
60
|
this.error("Usage: apso config get <key>");
|
|
48
61
|
}
|
|
62
|
+
// Friendly read-alias: `telemetry` reports on/off from telemetryDisabled.
|
|
63
|
+
if (key === "telemetry") {
|
|
64
|
+
this.log(config_1.globalConfig.read().telemetryDisabled ? "off" : "on");
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
49
67
|
if (!VALID_KEYS.includes(key)) {
|
|
50
68
|
this.error(`Unknown key: ${key}\nValid keys: ${VALID_KEYS.join(", ")}`);
|
|
51
69
|
}
|
|
@@ -59,6 +77,16 @@ class Config extends base_command_1.default {
|
|
|
59
77
|
if (!key || value === undefined) {
|
|
60
78
|
this.error("Usage: apso config set <key> <value>");
|
|
61
79
|
}
|
|
80
|
+
// Friendly write-alias: `telemetry on|off` maps to telemetryDisabled.
|
|
81
|
+
if (key === "telemetry") {
|
|
82
|
+
const disabled = parseTelemetryToggle(value);
|
|
83
|
+
if (disabled === null) {
|
|
84
|
+
this.error(`Invalid telemetry value: ${value}. Use on or off.`);
|
|
85
|
+
}
|
|
86
|
+
config_1.globalConfig.set("telemetryDisabled", disabled);
|
|
87
|
+
this.log(`telemetry = ${disabled ? "off" : "on"}`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
62
90
|
if (!VALID_KEYS.includes(key)) {
|
|
63
91
|
this.error(`Unknown key: ${key}\nValid keys: ${VALID_KEYS.join(", ")}`);
|
|
64
92
|
}
|
|
@@ -94,6 +122,7 @@ Config.examples = [
|
|
|
94
122
|
`$ apso config`,
|
|
95
123
|
`$ apso config get apiUrl`,
|
|
96
124
|
`$ apso config set verbose true`,
|
|
125
|
+
`$ apso config set telemetry off`,
|
|
97
126
|
`$ apso config reset`,
|
|
98
127
|
];
|
|
99
128
|
Config.strict = false;
|
|
@@ -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
|
/**
|
package/dist/commands/deploy.js
CHANGED
|
@@ -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
|
-
|
|
138
|
-
|
|
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
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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;
|
package/dist/commands/dev.d.ts
CHANGED
|
@@ -13,8 +13,17 @@ export default class Dev extends BaseCommand {
|
|
|
13
13
|
/**
|
|
14
14
|
* TypeScript: run `npm run dev` which reads DATABASE_TYPE from .env.
|
|
15
15
|
* PGlite is handled natively by the NestJS app -- no sidecar needed.
|
|
16
|
+
*
|
|
17
|
+
* Prefer the `dev` script, but fall back to `start:dev` for templates that
|
|
18
|
+
* only define the latter (e.g. the v1 service template). See apsoai/cli#102.
|
|
16
19
|
*/
|
|
17
20
|
private runTypeScript;
|
|
21
|
+
/**
|
|
22
|
+
* Return the first script from `candidates` that exists in the local
|
|
23
|
+
* package.json. If package.json is missing or unparseable, fall back to the
|
|
24
|
+
* first candidate so behavior is unchanged when we cannot inspect it.
|
|
25
|
+
*/
|
|
26
|
+
private resolveNpmScript;
|
|
18
27
|
/**
|
|
19
28
|
* Python: if pglite, start the pglite-server sidecar first, then uvicorn.
|
|
20
29
|
*/
|
package/dist/commands/dev.js
CHANGED
|
@@ -52,10 +52,37 @@ class Dev extends base_command_1.default {
|
|
|
52
52
|
/**
|
|
53
53
|
* TypeScript: run `npm run dev` which reads DATABASE_TYPE from .env.
|
|
54
54
|
* PGlite is handled natively by the NestJS app -- no sidecar needed.
|
|
55
|
+
*
|
|
56
|
+
* Prefer the `dev` script, but fall back to `start:dev` for templates that
|
|
57
|
+
* only define the latter (e.g. the v1 service template). See apsoai/cli#102.
|
|
55
58
|
*/
|
|
56
59
|
async runTypeScript() {
|
|
57
|
-
this.
|
|
58
|
-
|
|
60
|
+
const script = this.resolveNpmScript(["dev", "start:dev"]);
|
|
61
|
+
if (!script) {
|
|
62
|
+
this.error('No "dev" or "start:dev" script found in package.json.\n' +
|
|
63
|
+
"Make sure you are in an Apso TypeScript project root.");
|
|
64
|
+
}
|
|
65
|
+
this.log(`Starting TypeScript dev server (npm run ${script})...`);
|
|
66
|
+
await this.spawnAndWait("npm", ["run", script]);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Return the first script from `candidates` that exists in the local
|
|
70
|
+
* package.json. If package.json is missing or unparseable, fall back to the
|
|
71
|
+
* first candidate so behavior is unchanged when we cannot inspect it.
|
|
72
|
+
*/
|
|
73
|
+
resolveNpmScript(candidates) {
|
|
74
|
+
const pkgPath = path.resolve("package.json");
|
|
75
|
+
if (!fs.existsSync(pkgPath))
|
|
76
|
+
return candidates[0];
|
|
77
|
+
try {
|
|
78
|
+
// eslint-disable-next-line unicorn/prefer-json-parse-buffer -- JSON.parse's TS type requires a string, not a Buffer
|
|
79
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
80
|
+
const scripts = (pkg.scripts || {});
|
|
81
|
+
return candidates.find((c) => typeof scripts[c] === "string");
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return candidates[0];
|
|
85
|
+
}
|
|
59
86
|
}
|
|
60
87
|
/**
|
|
61
88
|
* Python: if pglite, start the pglite-server sidecar first, then uvicorn.
|
|
@@ -96,19 +96,31 @@ class Generate extends base_command_1.default {
|
|
|
96
96
|
console.log(`[apso] Building... ${entity.name}`);
|
|
97
97
|
const entityBuildStart = perf_hooks_1.performance.now();
|
|
98
98
|
const entityRelationships = relationshipMap[entity.name] || [];
|
|
99
|
-
// Effective HTTP
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
99
|
+
// Effective HTTP flag: entity overrides the top-level default, which
|
|
100
|
+
// defaults to true. GraphQL uses resolvers, so the flag doesn't apply
|
|
101
|
+
// there. The flag only does real suppression where a generated route
|
|
102
|
+
// can't be cleanly overridden without removing it (see apsoai/cli#95):
|
|
103
|
+
// - TypeScript (NestJS): suppress the controller + its module wiring.
|
|
104
|
+
// - Go (Gin): suppress the handler + its route registration (Gin
|
|
105
|
+
// panics on a duplicate method+path, so overriding needs removal).
|
|
106
|
+
// - Python (FastAPI): NO-OP. FastAPI matches in registration order, so
|
|
107
|
+
// a hand-written APIRouter included before autogen_router in main.py
|
|
108
|
+
// shadows the generated route without removing it. We only emit an
|
|
109
|
+
// informational notice pointing at that pattern.
|
|
110
|
+
const effectiveHttp = (_b = (_a = entity.http) !== null && _a !== void 0 ? _a : http) !== null && _b !== void 0 ? _b : true;
|
|
111
|
+
const httpSuppressible = (language === "typescript" || language === "go") &&
|
|
112
|
+
lowerCaseApiType === "rest";
|
|
113
|
+
const includeController = httpSuppressible ? effectiveHttp : true;
|
|
114
|
+
if (httpSuppressible && !includeController) {
|
|
115
|
+
const artifact = language === "go" ? "handler + routes" : "controller";
|
|
116
|
+
console.log(`[apso] ${entity.name}: http=false — skipping generated ${artifact}`);
|
|
117
|
+
}
|
|
118
|
+
else if (language === "python" &&
|
|
119
|
+
lowerCaseApiType === "rest" &&
|
|
120
|
+
effectiveHttp === false) {
|
|
121
|
+
console.log(`[apso] ${entity.name}: http=false is a no-op for FastAPI — to replace a ` +
|
|
122
|
+
`generated route, include your own APIRouter before autogen_router in main.py ` +
|
|
123
|
+
`(first match wins). See the custom-endpoints skill.`);
|
|
112
124
|
}
|
|
113
125
|
const generationTasks = [
|
|
114
126
|
generator.generateEntity({
|
|
@@ -177,7 +189,7 @@ class Generate extends base_command_1.default {
|
|
|
177
189
|
await (0, file_system_1.createFile)(fullPath, file.content);
|
|
178
190
|
}
|
|
179
191
|
// Generate index module
|
|
180
|
-
const indexFiles = await generator.generateIndexModule(entities, lowerCaseApiType, { emitEvents });
|
|
192
|
+
const indexFiles = await generator.generateIndexModule(entities, lowerCaseApiType, { emitEvents, http });
|
|
181
193
|
for (const file of indexFiles) {
|
|
182
194
|
const fullPath = path.join(autogenPath, file.path);
|
|
183
195
|
// eslint-disable-next-line no-await-in-loop
|
|
@@ -20,7 +20,9 @@ function openBrowser(url) {
|
|
|
20
20
|
(0, child_process_1.exec)(command, () => resolve());
|
|
21
21
|
});
|
|
22
22
|
}
|
|
23
|
-
const sleep = (ms) => new Promise((r) =>
|
|
23
|
+
const sleep = (ms) => new Promise((r) => {
|
|
24
|
+
setTimeout(r, ms);
|
|
25
|
+
});
|
|
24
26
|
/**
|
|
25
27
|
* `apso github connect` — walk the user through connecting their GitHub
|
|
26
28
|
* account so the CLI can push generated code to a real repository.
|
package/dist/commands/link.js
CHANGED
|
@@ -81,6 +81,7 @@ class Link extends base_command_1.default {
|
|
|
81
81
|
else if (flags.create) {
|
|
82
82
|
this.log(`Creating service "${flags.create}"...`);
|
|
83
83
|
service = await (0, upgrade_1.withUpgradeRetry)(() => services_1.servicesApi.create(workspace.id, { name: flags.create }));
|
|
84
|
+
// eslint-disable-next-line no-negated-condition -- clearer as a non-interactive guard in this else-if chain
|
|
84
85
|
}
|
|
85
86
|
else if (!(0, interactive_1.isInteractive)()) {
|
|
86
87
|
this.error((0, interactive_1.missingFlag)("Pass --service <slug> to link an existing service, or --create <name> to make a new one."));
|
|
@@ -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.", {
|
package/dist/commands/update.js
CHANGED
|
@@ -40,7 +40,7 @@ class Update extends base_command_1.default {
|
|
|
40
40
|
}
|
|
41
41
|
// Pick the upgrade command based on how the CLI was installed.
|
|
42
42
|
const installRoot = `${this.config.root || ""} ${process.execPath}`;
|
|
43
|
-
const isBrew = /
|
|
43
|
+
const isBrew = /cellar|homebrew|\/opt\/homebrew/i.test(installRoot);
|
|
44
44
|
const cmd = isBrew
|
|
45
45
|
? "brew upgrade apsoai/tap/apso"
|
|
46
46
|
: "npm install -g @apso/cli@latest";
|
package/dist/commands/use.js
CHANGED
|
@@ -55,7 +55,8 @@ class Use extends base_command_1.default {
|
|
|
55
55
|
this.log(` Now using workspace: ${ws.name} (${ws.slug})`);
|
|
56
56
|
// Show what's in the workspace so the next step is obvious.
|
|
57
57
|
try {
|
|
58
|
-
const
|
|
58
|
+
const listed = await services_1.servicesApi.list(ws.id);
|
|
59
|
+
const services = listed.data;
|
|
59
60
|
this.log("");
|
|
60
61
|
if (services.length > 0) {
|
|
61
62
|
this.log(" Services:");
|
|
@@ -7,6 +7,12 @@ const telemetry_1 = require("../../lib/telemetry/telemetry");
|
|
|
7
7
|
*/
|
|
8
8
|
const hook = async function (opts) {
|
|
9
9
|
(0, telemetry_1.initTelemetry)();
|
|
10
|
+
// First-run transparency notice (issue #96). Printed to stderr so it never
|
|
11
|
+
// pollutes stdout / piped output, shown once per install.
|
|
12
|
+
if ((0, telemetry_1.shouldShowFirstRunNotice)()) {
|
|
13
|
+
process.stderr.write(`\n${telemetry_1.FIRST_RUN_NOTICE}\n\n`);
|
|
14
|
+
(0, telemetry_1.markFirstRunNoticeShown)();
|
|
15
|
+
}
|
|
10
16
|
if (opts.id) {
|
|
11
17
|
(0, telemetry_1.track)("cli_command_started", { command: opts.id });
|
|
12
18
|
}
|
|
@@ -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 {};
|
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.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;
|
|
@@ -72,9 +72,9 @@ function adaptPaginated(raw, mapItem) {
|
|
|
72
72
|
const items = Array.isArray(raw) ? raw : (_a = raw === null || raw === void 0 ? void 0 : raw.data) !== null && _a !== void 0 ? _a : [];
|
|
73
73
|
const total = (_b = raw === null || raw === void 0 ? void 0 : raw.total) !== null && _b !== void 0 ? _b : items.length;
|
|
74
74
|
const page = (_c = raw === null || raw === void 0 ? void 0 : raw.page) !== null && _c !== void 0 ? _c : 1;
|
|
75
|
-
const pageSize = items.length
|
|
75
|
+
const pageSize = items.length > 0 ? items.length : 1;
|
|
76
76
|
return {
|
|
77
|
-
data: items.map(mapItem),
|
|
77
|
+
data: items.map((item) => mapItem(item)),
|
|
78
78
|
meta: {
|
|
79
79
|
total,
|
|
80
80
|
page,
|
|
@@ -98,7 +98,7 @@ exports.authApi = {
|
|
|
98
98
|
* clears stored credentials). Best-effort, never throws.
|
|
99
99
|
*/
|
|
100
100
|
async logout() {
|
|
101
|
-
|
|
101
|
+
// no-op: logout clears local credentials elsewhere; best-effort, never throws.
|
|
102
102
|
},
|
|
103
103
|
};
|
|
104
104
|
/**
|
|
@@ -109,7 +109,7 @@ exports.workspacesApi = {
|
|
|
109
109
|
var _a;
|
|
110
110
|
const raw = await client_1.api.get("/api/workspaces");
|
|
111
111
|
const items = Array.isArray(raw) ? raw : (_a = raw === null || raw === void 0 ? void 0 : raw.data) !== null && _a !== void 0 ? _a : [];
|
|
112
|
-
return items.map(adaptWorkspace);
|
|
112
|
+
return items.map((workspace) => adaptWorkspace(workspace));
|
|
113
113
|
},
|
|
114
114
|
async getById(id) {
|
|
115
115
|
const raw = await client_1.api.get(`/api/workspaces/${id}`);
|
|
@@ -261,7 +261,9 @@ exports.buildApi = {
|
|
|
261
261
|
options.onProgress(status);
|
|
262
262
|
if (status.status === "success" || status.status === "failed")
|
|
263
263
|
return status;
|
|
264
|
-
await new Promise((resolve) =>
|
|
264
|
+
await new Promise((resolve) => {
|
|
265
|
+
setTimeout(resolve, pollInterval);
|
|
266
|
+
});
|
|
265
267
|
}
|
|
266
268
|
/* eslint-enable no-await-in-loop */
|
|
267
269
|
throw new Error("Build timed out");
|
|
@@ -366,3 +368,13 @@ exports.githubApi = {
|
|
|
366
368
|
});
|
|
367
369
|
},
|
|
368
370
|
};
|
|
371
|
+
/**
|
|
372
|
+
* Service code API — used for the server-side deploy (upload the project zip to
|
|
373
|
+
* S3; the backend then pushes it to the repo, so no local git is required).
|
|
374
|
+
*/
|
|
375
|
+
exports.codeApi = {
|
|
376
|
+
/** Presigned S3 PUT URL to upload the service's code zip. */
|
|
377
|
+
async getUploadUrl(serviceId) {
|
|
378
|
+
return client_1.api.get(`/api/services/${serviceId}/code/upload-url`);
|
|
379
|
+
},
|
|
380
|
+
};
|
|
@@ -123,6 +123,16 @@ export interface GlobalConfigFile {
|
|
|
123
123
|
* Disable telemetry
|
|
124
124
|
*/
|
|
125
125
|
telemetryDisabled: boolean;
|
|
126
|
+
/**
|
|
127
|
+
* Anonymous install id (random UUID) for opt-out CLI telemetry. Generated
|
|
128
|
+
* once on first run and persisted here. Not derived from any machine
|
|
129
|
+
* identifier; carries no PII.
|
|
130
|
+
*/
|
|
131
|
+
installId?: string;
|
|
132
|
+
/**
|
|
133
|
+
* Whether the first-run telemetry transparency notice has been shown.
|
|
134
|
+
*/
|
|
135
|
+
telemetryNoticeShown: boolean;
|
|
126
136
|
}
|
|
127
137
|
/**
|
|
128
138
|
* Project link stored in .apso/link.json
|
package/dist/lib/config/types.js
CHANGED
|
@@ -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;
|