@apso/cli 0.37.1 → 0.39.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 +11 -0
- package/dist/commands/mcp/serve.js +5 -0
- package/dist/commands/schema/lint.d.ts +18 -0
- package/dist/commands/schema/lint.js +74 -0
- package/dist/commands/schema/validate.js +20 -0
- package/dist/lib/apsorc-parser.d.ts +23 -0
- package/dist/lib/apsorc-parser.js +43 -1
- package/dist/lib/base-command.d.ts +5 -0
- package/dist/lib/base-command.js +15 -1
- package/dist/lib/config/types.d.ts +4 -0
- package/dist/lib/mcp-schema-tools.d.ts +31 -0
- package/dist/lib/mcp-schema-tools.js +69 -0
- package/dist/lib/telemetry/telemetry.js +43 -11
- package/npm-shrinkwrap.json +12 -2
- package/oclif.manifest.json +31 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -57,6 +57,17 @@ Add to your MCP configuration:
|
|
|
57
57
|
| `start_dev_server` | Start local dev environment via Docker Compose |
|
|
58
58
|
| `deploy_api` | Deploy to the Apso platform |
|
|
59
59
|
|
|
60
|
+
**Schema edit tools** (from `@apso/schema-tools`) change the project's `.apsorc` one step at a time. Each write is linted with the rules `apso generate` enforces; a change that would add a lint error is rejected and the file is left as it was.
|
|
61
|
+
|
|
62
|
+
| Tool | Description |
|
|
63
|
+
|------|-------------|
|
|
64
|
+
| `get_schema`, `describe_entity` | Read the schema outline, or one entity and its relationships |
|
|
65
|
+
| `add_entity`, `rename_entity`, `remove_entity` | Entities (relationships and foreign key indexes follow renames and removals) |
|
|
66
|
+
| `add_field`, `update_field`, `rename_field`, `remove_field` | Fields (indexes and uniques follow renames and removals) |
|
|
67
|
+
| `add_relationship`, `remove_relationship` | Relationships |
|
|
68
|
+
| `add_index` | Index or unique index |
|
|
69
|
+
| `lint_schema` | Lint report |
|
|
70
|
+
|
|
60
71
|
**Available resources:**
|
|
61
72
|
|
|
62
73
|
| Resource | URI | Description |
|
|
@@ -16,6 +16,7 @@ const lib_1 = require("../../lib");
|
|
|
16
16
|
const runner_1 = require("../../lib/doctor/runner");
|
|
17
17
|
const file_system_1 = require("../../lib/utils/file-system");
|
|
18
18
|
const schema_convert_1 = require("../../lib/utils/schema-convert");
|
|
19
|
+
const mcp_schema_tools_1 = require("../../lib/mcp-schema-tools");
|
|
19
20
|
const perf_hooks_1 = require("perf_hooks");
|
|
20
21
|
class McpServe extends base_command_1.default {
|
|
21
22
|
async run() {
|
|
@@ -69,6 +70,10 @@ class McpServe extends base_command_1.default {
|
|
|
69
70
|
};
|
|
70
71
|
}
|
|
71
72
|
registerTools(server) {
|
|
73
|
+
// ── granular schema edits (@apso/schema-tools) ─────────────────
|
|
74
|
+
// get_schema, describe_entity, add_entity, add_field, rename_field, ...
|
|
75
|
+
// on the project's .apsorc, each linted before it is written.
|
|
76
|
+
(0, mcp_schema_tools_1.registerSchemaTools)(server);
|
|
72
77
|
// ── design_schema ──────────────────────────────────────────────
|
|
73
78
|
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.", {
|
|
74
79
|
requirements: zod_1.z
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { LintIssue, LintResult } from "@apso/schema-tools";
|
|
2
|
+
import BaseCommand from "../../lib/base-command";
|
|
3
|
+
import { ApsorcType } from "../../lib/apsorc-parser";
|
|
4
|
+
/** Lints (and with `fix`, repairs and saves) an .apsorc file. */
|
|
5
|
+
export declare function lintApsorc(configPath: string, apsorc: ApsorcType, fix: boolean): {
|
|
6
|
+
result: LintResult;
|
|
7
|
+
fixed: LintIssue[];
|
|
8
|
+
backupPath?: string;
|
|
9
|
+
};
|
|
10
|
+
export default class SchemaLint extends BaseCommand {
|
|
11
|
+
static description: string;
|
|
12
|
+
static examples: string[];
|
|
13
|
+
static flags: {
|
|
14
|
+
fix: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
15
|
+
json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
16
|
+
};
|
|
17
|
+
run(): Promise<void>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.lintApsorc = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const core_1 = require("@oclif/core");
|
|
6
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
7
|
+
const path = tslib_1.__importStar(require("path"));
|
|
8
|
+
const schema_tools_1 = require("@apso/schema-tools");
|
|
9
|
+
const base_command_1 = tslib_1.__importDefault(require("../../lib/base-command"));
|
|
10
|
+
const config_1 = require("../../lib/config");
|
|
11
|
+
const apsorc_parser_1 = require("../../lib/apsorc-parser");
|
|
12
|
+
/** Lints (and with `fix`, repairs and saves) an .apsorc file. */
|
|
13
|
+
function lintApsorc(configPath, apsorc, fix) {
|
|
14
|
+
if (!fix)
|
|
15
|
+
return { result: (0, schema_tools_1.lintSchema)(apsorc), fixed: [] };
|
|
16
|
+
const { schema, applied } = (0, schema_tools_1.fixSchema)(apsorc);
|
|
17
|
+
let backupPath;
|
|
18
|
+
if (applied.length > 0) {
|
|
19
|
+
backupPath = (0, config_1.createBackup)(configPath, path.dirname(configPath));
|
|
20
|
+
fs.writeFileSync(configPath, `${JSON.stringify(schema, null, 2)}\n`, "utf-8");
|
|
21
|
+
}
|
|
22
|
+
return { result: (0, schema_tools_1.lintSchema)(schema), fixed: applied, backupPath };
|
|
23
|
+
}
|
|
24
|
+
exports.lintApsorc = lintApsorc;
|
|
25
|
+
class SchemaLint extends base_command_1.default {
|
|
26
|
+
async run() {
|
|
27
|
+
const { flags } = await this.parse(SchemaLint);
|
|
28
|
+
let file;
|
|
29
|
+
try {
|
|
30
|
+
file = (0, apsorc_parser_1.readApsorcFile)();
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
this.error(`Failed to read .apsorc: ${error instanceof Error ? error.message : String(error)}`);
|
|
34
|
+
}
|
|
35
|
+
if (file.apsorc.version !== 2) {
|
|
36
|
+
this.error("apso schema lint supports version 2 .apsorc files only.");
|
|
37
|
+
}
|
|
38
|
+
const { result, fixed, backupPath } = lintApsorc(file.configPath, file.apsorc, flags.fix);
|
|
39
|
+
if (flags.json) {
|
|
40
|
+
this.log(JSON.stringify({ ...result, fixed }, null, 2));
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
if (fixed.length > 0) {
|
|
44
|
+
this.log(`Fixed ${fixed.length} issue(s) in ${file.configPath} (backup: ${backupPath}):`);
|
|
45
|
+
for (const issue of fixed)
|
|
46
|
+
this.log(` - ${issue.rule}: ${issue.message}`);
|
|
47
|
+
this.log("");
|
|
48
|
+
}
|
|
49
|
+
this.log((0, schema_tools_1.formatLintReport)(result));
|
|
50
|
+
if (!flags.fix && result.issues.some((issue) => issue.fixable)) {
|
|
51
|
+
this.log('\nRun "apso schema lint --fix" to apply the auto-fixable changes.');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (result.errorCount > 0)
|
|
55
|
+
this.exit(1);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
SchemaLint.description = "Check the local .apsorc for names and settings that would break the generated code";
|
|
59
|
+
SchemaLint.examples = [
|
|
60
|
+
"$ apso schema lint",
|
|
61
|
+
"$ apso schema lint --fix",
|
|
62
|
+
"$ apso schema lint --json",
|
|
63
|
+
];
|
|
64
|
+
SchemaLint.flags = {
|
|
65
|
+
fix: core_1.Flags.boolean({
|
|
66
|
+
description: "Apply the safe automatic fixes and save .apsorc (a backup is kept)",
|
|
67
|
+
default: false,
|
|
68
|
+
}),
|
|
69
|
+
json: core_1.Flags.boolean({
|
|
70
|
+
description: "Print the result as JSON",
|
|
71
|
+
default: false,
|
|
72
|
+
}),
|
|
73
|
+
};
|
|
74
|
+
exports.default = SchemaLint;
|
|
@@ -5,12 +5,32 @@ const core_1 = require("@oclif/core");
|
|
|
5
5
|
const base_command_1 = tslib_1.__importDefault(require("../../lib/base-command"));
|
|
6
6
|
const config_1 = require("../../lib/config");
|
|
7
7
|
const services_1 = require("../../lib/api/services");
|
|
8
|
+
const schema_tools_1 = require("@apso/schema-tools");
|
|
8
9
|
const apsorc_parser_1 = require("../../lib/apsorc-parser");
|
|
9
10
|
const schema_convert_1 = require("../../lib/utils/schema-convert");
|
|
10
11
|
class SchemaValidate extends base_command_1.default {
|
|
11
12
|
async run() {
|
|
12
13
|
var _a;
|
|
13
14
|
const { flags } = await this.parse(SchemaValidate);
|
|
15
|
+
// Lint first so every issue is listed, not just the first one parseApsorc throws on
|
|
16
|
+
let file;
|
|
17
|
+
try {
|
|
18
|
+
file = (0, apsorc_parser_1.readApsorcFile)();
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
22
|
+
this.error(`Failed to read .apsorc: ${msg}`);
|
|
23
|
+
}
|
|
24
|
+
if (file.apsorc.version === 2) {
|
|
25
|
+
const lint = (0, schema_tools_1.lintSchema)(file.apsorc);
|
|
26
|
+
if (lint.issues.length > 0) {
|
|
27
|
+
this.log((0, schema_tools_1.formatLintReport)(lint));
|
|
28
|
+
this.log("");
|
|
29
|
+
}
|
|
30
|
+
if (lint.errorCount > 0) {
|
|
31
|
+
this.error(`Validation failed with ${lint.errorCount} lint error(s). Run "apso schema lint --fix" to apply the safe fixes.`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
14
34
|
// Parse .apsorc
|
|
15
35
|
let parsed;
|
|
16
36
|
try {
|
|
@@ -51,6 +51,24 @@ type ParsedApsorc = {
|
|
|
51
51
|
coAuthor?: boolean;
|
|
52
52
|
};
|
|
53
53
|
export declare const parseApsorcV1: (apsorc: ApsorcType) => ParsedApsorcData;
|
|
54
|
+
/**
|
|
55
|
+
* A schema problem that would make `apso generate` emit code that does not
|
|
56
|
+
* compile. `code` is the @apso/schema-tools rule id (FIELD_RELATIONSHIP_COLLISION,
|
|
57
|
+
* DUPLICATE_FIELD_NAME, ...), the same codes the apso-client-v2 validator used.
|
|
58
|
+
*/
|
|
59
|
+
export declare class ApsorcNamingError extends Error {
|
|
60
|
+
code: string;
|
|
61
|
+
entity: string;
|
|
62
|
+
field?: string | undefined;
|
|
63
|
+
constructor(message: string, code: string, entity: string, field?: string | undefined);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Throws an ApsorcNamingError for the first @apso/schema-tools error, e.g. a
|
|
67
|
+
* text field `notes` on Contact plus Note ManyToOne Contact, whose inverse
|
|
68
|
+
* side is `notes: Note[]` on Contact (TS2300 Duplicate identifier). The client
|
|
69
|
+
* schema builder runs the same rules, so a schema that passes there passes here.
|
|
70
|
+
*/
|
|
71
|
+
export declare const assertCodegenSafeNames: (apsorc: Pick<ApsorcType, "entities" | "relationships">) => void;
|
|
54
72
|
export declare const parseApsorcV2: (apsorc: ApsorcType) => ParsedApsorcData;
|
|
55
73
|
export declare const parseApsorc: () => ParsedApsorc;
|
|
56
74
|
/**
|
|
@@ -58,5 +76,10 @@ export declare const parseApsorc: () => ParsedApsorc;
|
|
|
58
76
|
* Searches upwards from the current working directory.
|
|
59
77
|
* @returns The absolute path to the .apsorc file, or null if not found.
|
|
60
78
|
*/
|
|
79
|
+
/** Reads the .apsorc file itself (not rc-merged), for commands that rewrite it. */
|
|
80
|
+
export declare const readApsorcFile: () => {
|
|
81
|
+
configPath: string;
|
|
82
|
+
apsorc: ApsorcType;
|
|
83
|
+
};
|
|
61
84
|
export declare const findConfigPath: () => string | null;
|
|
62
85
|
export {};
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.findConfigPath = exports.parseApsorc = exports.parseApsorcV2 = exports.parseApsorcV1 = exports.ApiType = void 0;
|
|
3
|
+
exports.findConfigPath = exports.readApsorcFile = exports.parseApsorc = exports.parseApsorcV2 = exports.assertCodegenSafeNames = exports.ApsorcNamingError = exports.parseApsorcV1 = exports.ApiType = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
const fs = tslib_1.__importStar(require("fs"));
|
|
6
6
|
const path = tslib_1.__importStar(require("path"));
|
|
7
7
|
const rc_1 = tslib_1.__importDefault(require("rc"));
|
|
8
8
|
const relationships_1 = require("./utils/relationships");
|
|
9
9
|
const perf_hooks_1 = require("perf_hooks");
|
|
10
|
+
// @apso/schema-tools is published on npm (apsoai/apso-packages).
|
|
11
|
+
const schema_tools_1 = require("@apso/schema-tools");
|
|
10
12
|
var ApiType;
|
|
11
13
|
(function (ApiType) {
|
|
12
14
|
ApiType["Graphql"] = "graphql";
|
|
@@ -21,8 +23,39 @@ const parseApsorcV1 = (apsorc) => {
|
|
|
21
23
|
return { entities, relationshipMap };
|
|
22
24
|
};
|
|
23
25
|
exports.parseApsorcV1 = parseApsorcV1;
|
|
26
|
+
/**
|
|
27
|
+
* A schema problem that would make `apso generate` emit code that does not
|
|
28
|
+
* compile. `code` is the @apso/schema-tools rule id (FIELD_RELATIONSHIP_COLLISION,
|
|
29
|
+
* DUPLICATE_FIELD_NAME, ...), the same codes the apso-client-v2 validator used.
|
|
30
|
+
*/
|
|
31
|
+
class ApsorcNamingError extends Error {
|
|
32
|
+
constructor(message, code, entity, field) {
|
|
33
|
+
super(message);
|
|
34
|
+
this.code = code;
|
|
35
|
+
this.entity = entity;
|
|
36
|
+
this.field = field;
|
|
37
|
+
this.name = "ApsorcNamingError";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
exports.ApsorcNamingError = ApsorcNamingError;
|
|
41
|
+
/**
|
|
42
|
+
* Throws an ApsorcNamingError for the first @apso/schema-tools error, e.g. a
|
|
43
|
+
* text field `notes` on Contact plus Note ManyToOne Contact, whose inverse
|
|
44
|
+
* side is `notes: Note[]` on Contact (TS2300 Duplicate identifier). The client
|
|
45
|
+
* schema builder runs the same rules, so a schema that passes there passes here.
|
|
46
|
+
*/
|
|
47
|
+
const assertCodegenSafeNames = (apsorc) => {
|
|
48
|
+
const { issues } = (0, schema_tools_1.lintSchema)(apsorc);
|
|
49
|
+
const error = issues.find((issue) => issue.severity === "error");
|
|
50
|
+
if (!error)
|
|
51
|
+
return;
|
|
52
|
+
const more = issues.filter((issue) => issue.severity === "error").length - 1;
|
|
53
|
+
throw new ApsorcNamingError(`${error.message}${more > 0 ? ` (${more} more error${more === 1 ? "" : "s"})` : ""} Run "apso schema lint" to see every issue, or "apso schema lint --fix" to apply the safe fixes.`, error.rule, error.entity || "", error.field);
|
|
54
|
+
};
|
|
55
|
+
exports.assertCodegenSafeNames = assertCodegenSafeNames;
|
|
24
56
|
const parseApsorcV2 = (apsorc) => {
|
|
25
57
|
const { entities, relationships: apsoRelationships } = apsorc;
|
|
58
|
+
(0, exports.assertCodegenSafeNames)(apsorc);
|
|
26
59
|
const relationshipMap = (0, relationships_1.parseRelationships)(apsoRelationships);
|
|
27
60
|
return { entities, relationshipMap };
|
|
28
61
|
};
|
|
@@ -115,6 +148,15 @@ exports.parseApsorc = parseApsorc;
|
|
|
115
148
|
* Searches upwards from the current working directory.
|
|
116
149
|
* @returns The absolute path to the .apsorc file, or null if not found.
|
|
117
150
|
*/
|
|
151
|
+
/** Reads the .apsorc file itself (not rc-merged), for commands that rewrite it. */
|
|
152
|
+
const readApsorcFile = () => {
|
|
153
|
+
const configPath = (0, exports.findConfigPath)();
|
|
154
|
+
if (!configPath) {
|
|
155
|
+
throw new Error("No .apsorc found in this directory or any parent.");
|
|
156
|
+
}
|
|
157
|
+
return { configPath, apsorc: JSON.parse(fs.readFileSync(configPath).toString()) };
|
|
158
|
+
};
|
|
159
|
+
exports.readApsorcFile = readApsorcFile;
|
|
118
160
|
const findConfigPath = () => {
|
|
119
161
|
let currentDir = process.cwd();
|
|
120
162
|
while (currentDir !== path.parse(currentDir).root) {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { Command } from "@oclif/core";
|
|
2
|
+
/** True for errors oclif raised deliberately (this.error, this.exit, parse errors). */
|
|
3
|
+
export declare function isUserError(err: unknown): boolean;
|
|
2
4
|
export default abstract class BaseCommand extends Command {
|
|
3
5
|
/**
|
|
4
6
|
* Report failures to Sentry + PostHog before oclif exits. The postrun hook
|
|
@@ -7,6 +9,9 @@ export default abstract class BaseCommand extends Command {
|
|
|
7
9
|
catch(err: Error & {
|
|
8
10
|
exitCode?: number;
|
|
9
11
|
code?: string;
|
|
12
|
+
oclif?: {
|
|
13
|
+
exit?: number;
|
|
14
|
+
};
|
|
10
15
|
}): Promise<any>;
|
|
11
16
|
runCommand(command: string, args: string[], silent?: boolean): Promise<void>;
|
|
12
17
|
runNpmCommand(args: string[], silent?: boolean): Promise<void>;
|
package/dist/lib/base-command.js
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isUserError = void 0;
|
|
3
4
|
const tslib_1 = require("tslib");
|
|
4
5
|
const core_1 = require("@oclif/core");
|
|
5
6
|
const child_process_1 = require("child_process");
|
|
6
7
|
const os_1 = tslib_1.__importDefault(require("os"));
|
|
7
8
|
const telemetry_1 = require("./telemetry/telemetry");
|
|
9
|
+
/** True for errors oclif raised deliberately (this.error, this.exit, parse errors). */
|
|
10
|
+
function isUserError(err) {
|
|
11
|
+
return Boolean(err && typeof err === "object" && "oclif" in err);
|
|
12
|
+
}
|
|
13
|
+
exports.isUserError = isUserError;
|
|
8
14
|
class BaseCommand extends core_1.Command {
|
|
9
15
|
/**
|
|
10
16
|
* Report failures to Sentry + PostHog before oclif exits. The postrun hook
|
|
@@ -12,12 +18,20 @@ class BaseCommand extends core_1.Command {
|
|
|
12
18
|
*/
|
|
13
19
|
async catch(err) {
|
|
14
20
|
try {
|
|
15
|
-
|
|
21
|
+
// oclif marks errors it raised on purpose (this.error, this.exit, flag
|
|
22
|
+
// and arg parse failures) with an `oclif` property. Those are messages
|
|
23
|
+
// we chose to show the user, e.g. "No entity named X", not crashes, so
|
|
24
|
+
// they go to PostHog only. Everything else is a real bug for Sentry.
|
|
25
|
+
const userError = isUserError(err);
|
|
26
|
+
if (!userError) {
|
|
27
|
+
(0, telemetry_1.captureException)(err, { command: this.id });
|
|
28
|
+
}
|
|
16
29
|
(0, telemetry_1.track)("cli_command_failed", {
|
|
17
30
|
command: this.id,
|
|
18
31
|
duration_ms: (0, telemetry_1.elapsedMs)(),
|
|
19
32
|
error: err === null || err === void 0 ? void 0 : err.message,
|
|
20
33
|
error_code: err === null || err === void 0 ? void 0 : err.code,
|
|
34
|
+
user_error: userError,
|
|
21
35
|
});
|
|
22
36
|
await (0, telemetry_1.shutdownTelemetry)();
|
|
23
37
|
}
|
|
@@ -133,6 +133,10 @@ export interface GlobalConfigFile {
|
|
|
133
133
|
* Whether the first-run telemetry transparency notice has been shown.
|
|
134
134
|
*/
|
|
135
135
|
telemetryNoticeShown: boolean;
|
|
136
|
+
/**
|
|
137
|
+
* User id this install's anonymous telemetry id was last aliased to.
|
|
138
|
+
*/
|
|
139
|
+
telemetryAliasedUserId?: string;
|
|
136
140
|
}
|
|
137
141
|
/**
|
|
138
142
|
* Project link stored in .apso/link.json
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface SchemaToolFileResult {
|
|
2
|
+
ok: boolean;
|
|
3
|
+
message: string;
|
|
4
|
+
data?: unknown;
|
|
5
|
+
configPath: string;
|
|
6
|
+
/** True when the operation changed the schema and .apsorc was rewritten. */
|
|
7
|
+
saved: boolean;
|
|
8
|
+
lint: {
|
|
9
|
+
errorCount: number;
|
|
10
|
+
warningCount: number;
|
|
11
|
+
report: string;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Run one @apso/schema-tools operation on an .apsorc file: read it, apply the
|
|
16
|
+
* operation, write it back when the operation changed the schema, and return
|
|
17
|
+
* the lint result. An operation that would add a lint error is rejected by
|
|
18
|
+
* the tool and the file is left untouched.
|
|
19
|
+
*/
|
|
20
|
+
export declare function applySchemaToolToFile(configPath: string, name: string, input: unknown): SchemaToolFileResult;
|
|
21
|
+
type ToolServer = {
|
|
22
|
+
tool: (...args: any[]) => unknown;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Register every @apso/schema-tools operation as a granular MCP tool on the
|
|
26
|
+
* project's .apsorc (found from the current directory up), next to the coarse
|
|
27
|
+
* design/validate/scaffold tools. The JSON Schema inputs from the package are
|
|
28
|
+
* converted to zod for the MCP SDK.
|
|
29
|
+
*/
|
|
30
|
+
export declare function registerSchemaTools(server: ToolServer): void;
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerSchemaTools = exports.applySchemaToolToFile = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
6
|
+
const zod_1 = require("zod");
|
|
7
|
+
const schema_tools_1 = require("@apso/schema-tools");
|
|
8
|
+
const apsorc_parser_1 = require("./apsorc-parser");
|
|
9
|
+
/**
|
|
10
|
+
* Run one @apso/schema-tools operation on an .apsorc file: read it, apply the
|
|
11
|
+
* operation, write it back when the operation changed the schema, and return
|
|
12
|
+
* the lint result. An operation that would add a lint error is rejected by
|
|
13
|
+
* the tool and the file is left untouched.
|
|
14
|
+
*/
|
|
15
|
+
function applySchemaToolToFile(configPath, name, input) {
|
|
16
|
+
// eslint-disable-next-line unicorn/prefer-json-parse-buffer -- JSON.parse's TS type requires a string
|
|
17
|
+
const apsorc = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
18
|
+
if (apsorc.version !== 2) {
|
|
19
|
+
throw new Error("The schema tools support version 2 .apsorc files only.");
|
|
20
|
+
}
|
|
21
|
+
const result = (0, schema_tools_1.runSchemaTool)(apsorc, name, input);
|
|
22
|
+
const saved = result.ok && result.schema !== apsorc;
|
|
23
|
+
if (saved)
|
|
24
|
+
fs.writeFileSync(configPath, `${JSON.stringify(result.schema, null, 2)}\n`, "utf-8");
|
|
25
|
+
const lint = (0, schema_tools_1.lintSchema)(result.schema);
|
|
26
|
+
return {
|
|
27
|
+
ok: result.ok,
|
|
28
|
+
message: result.message,
|
|
29
|
+
...(result.data === undefined ? {} : { data: result.data }),
|
|
30
|
+
configPath,
|
|
31
|
+
saved,
|
|
32
|
+
lint: { errorCount: lint.errorCount, warningCount: lint.warningCount, report: (0, schema_tools_1.formatLintReport)(lint) },
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
exports.applySchemaToolToFile = applySchemaToolToFile;
|
|
36
|
+
/**
|
|
37
|
+
* Register every @apso/schema-tools operation as a granular MCP tool on the
|
|
38
|
+
* project's .apsorc (found from the current directory up), next to the coarse
|
|
39
|
+
* design/validate/scaffold tools. The JSON Schema inputs from the package are
|
|
40
|
+
* converted to zod for the MCP SDK.
|
|
41
|
+
*/
|
|
42
|
+
function registerSchemaTools(server) {
|
|
43
|
+
for (const t of schema_tools_1.schemaTools) {
|
|
44
|
+
const shape = zod_1.z.fromJSONSchema(t.inputSchema).shape;
|
|
45
|
+
server.tool(t.name, `${t.description} Operates on the .apsorc in the current project.${t.readOnly ? "" : " Writes the file only when the change lints clean of new errors."}`, shape, { readOnlyHint: t.readOnly }, async (input) => {
|
|
46
|
+
const configPath = (0, apsorc_parser_1.findConfigPath)();
|
|
47
|
+
if (!configPath) {
|
|
48
|
+
return {
|
|
49
|
+
content: [{ type: "text", text: "No .apsorc found in this directory or its parents. Run `apso init` first." }],
|
|
50
|
+
isError: true,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const result = applySchemaToolToFile(configPath, t.name, input);
|
|
55
|
+
return {
|
|
56
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
57
|
+
isError: !result.ok,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
return {
|
|
62
|
+
content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }],
|
|
63
|
+
isError: true,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
exports.registerSchemaTools = registerSchemaTools;
|
|
@@ -45,6 +45,9 @@ let enabled = false;
|
|
|
45
45
|
let sentryOn = false;
|
|
46
46
|
let authenticatedUser = false;
|
|
47
47
|
let distinctId = "cli-anonymous";
|
|
48
|
+
let workspaceId;
|
|
49
|
+
let workspaceSlug;
|
|
50
|
+
let serviceId;
|
|
48
51
|
let environment = "production";
|
|
49
52
|
let startedAt = Date.now();
|
|
50
53
|
let noticePending = false;
|
|
@@ -85,7 +88,7 @@ function getOrCreateInstallId(currentInstallId) {
|
|
|
85
88
|
* when disabled — it just no-ops everything downstream.
|
|
86
89
|
*/
|
|
87
90
|
function initTelemetry() {
|
|
88
|
-
var _a, _b
|
|
91
|
+
var _a, _b;
|
|
89
92
|
try {
|
|
90
93
|
startedAt = Date.now();
|
|
91
94
|
const cfg = config_1.globalConfig.read();
|
|
@@ -101,14 +104,33 @@ function initTelemetry() {
|
|
|
101
104
|
noticePending = !cfg.telemetryNoticeShown;
|
|
102
105
|
const creds = config_1.credentials.read();
|
|
103
106
|
authenticatedUser = Boolean((_a = creds === null || creds === void 0 ? void 0 : creds.user) === null || _a === void 0 ? void 0 : _a.id);
|
|
104
|
-
|
|
107
|
+
const installId = getOrCreateInstallId(cfg.installId);
|
|
108
|
+
// Logged in: the Apso user id (the same distinct id the web app uses), so
|
|
109
|
+
// CLI and app activity land on one person. No email or name is sent.
|
|
110
|
+
distinctId = ((_b = creds === null || creds === void 0 ? void 0 : creds.user) === null || _b === void 0 ? void 0 : _b.id) ? String(creds.user.id) : installId;
|
|
111
|
+
// Workspace context: the linked project (.apso/link.json) wins, else the
|
|
112
|
+
// workspace chosen with `apso use`.
|
|
113
|
+
let link = null;
|
|
114
|
+
try {
|
|
115
|
+
link = config_1.projectLink.read();
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
link = null;
|
|
119
|
+
}
|
|
120
|
+
workspaceId = (link === null || link === void 0 ? void 0 : link.workspaceId) || cfg.activeWorkspaceId || undefined;
|
|
121
|
+
workspaceSlug = (link === null || link === void 0 ? void 0 : link.workspaceSlug) || cfg.activeWorkspaceSlug || undefined;
|
|
122
|
+
serviceId = (link === null || link === void 0 ? void 0 : link.serviceId) || undefined;
|
|
105
123
|
posthog = new posthog_node_1.PostHog(environment === "staging" ? POSTHOG_KEY_STAGING : POSTHOG_KEY_PROD, { host: POSTHOG_HOST, flushAt: 1, flushInterval: 0 });
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
124
|
+
// First run after login: merge this install's earlier anonymous events
|
|
125
|
+
// into the user, once per user.
|
|
126
|
+
if (authenticatedUser && installId !== distinctId && cfg.telemetryAliasedUserId !== distinctId) {
|
|
127
|
+
posthog.alias({ distinctId, alias: installId });
|
|
128
|
+
try {
|
|
129
|
+
config_1.globalConfig.write({ telemetryAliasedUserId: distinctId });
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// ignore — worst case we alias again next run
|
|
133
|
+
}
|
|
112
134
|
}
|
|
113
135
|
const dsn = process.env.APSO_CLI_SENTRY_DSN || SENTRY_DSN_DEFAULT;
|
|
114
136
|
if (dsn) {
|
|
@@ -118,8 +140,12 @@ function initTelemetry() {
|
|
|
118
140
|
release: `apso-cli@${cliVersion}`,
|
|
119
141
|
tracesSampleRate: 0,
|
|
120
142
|
});
|
|
121
|
-
Sentry.setUser({ id: distinctId
|
|
143
|
+
Sentry.setUser({ id: distinctId });
|
|
122
144
|
Sentry.setTag("cli_version", cliVersion);
|
|
145
|
+
if (workspaceId)
|
|
146
|
+
Sentry.setTag("workspace_id", workspaceId);
|
|
147
|
+
if (serviceId)
|
|
148
|
+
Sentry.setTag("service_id", serviceId);
|
|
123
149
|
sentryOn = true;
|
|
124
150
|
}
|
|
125
151
|
}
|
|
@@ -136,14 +162,18 @@ function commonProps() {
|
|
|
136
162
|
arch: os_1.default.arch(),
|
|
137
163
|
node_version: process.version,
|
|
138
164
|
authenticated: authenticatedUser,
|
|
165
|
+
...(workspaceId ? { workspace_id: workspaceId } : {}),
|
|
166
|
+
...(workspaceSlug ? { workspace_slug: workspaceSlug } : {}),
|
|
167
|
+
...(serviceId ? { service_id: serviceId } : {}),
|
|
139
168
|
};
|
|
140
169
|
}
|
|
141
170
|
/**
|
|
142
171
|
* The one-time transparency notice text (issue #96). Printed to stderr by the
|
|
143
172
|
* init hook so it never pollutes stdout / piped output.
|
|
144
173
|
*/
|
|
145
|
-
exports.FIRST_RUN_NOTICE = "Apso collects
|
|
146
|
-
"
|
|
174
|
+
exports.FIRST_RUN_NOTICE = "Apso collects usage data (command name, CLI version, OS) to improve the tool. When you're\n" +
|
|
175
|
+
"logged in, it's tied to your Apso account id and workspace id. No email, code, schema, or\n" +
|
|
176
|
+
"file contents are collected.\n" +
|
|
147
177
|
"Opt out any time: apso config set telemetry off (or APSO_TELEMETRY=0, or DO_NOT_TRACK=1)";
|
|
148
178
|
/**
|
|
149
179
|
* True once per install: telemetry is enabled and the notice hasn't been shown
|
|
@@ -173,6 +203,8 @@ function track(event, properties = {}) {
|
|
|
173
203
|
distinctId,
|
|
174
204
|
event,
|
|
175
205
|
properties: { ...commonProps(), ...properties },
|
|
206
|
+
// PostHog group analytics: roll activity up per workspace.
|
|
207
|
+
...(workspaceId ? { groups: { workspace: workspaceId } } : {}),
|
|
176
208
|
});
|
|
177
209
|
}
|
|
178
210
|
catch {
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@apso/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.39.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.39.0",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"dependencies": {
|
|
12
|
+
"@apso/schema-tools": "^0.1.0",
|
|
12
13
|
"@biomejs/biome": "^1.9.4",
|
|
13
14
|
"@electric-sql/pglite": "^0.2.17",
|
|
14
15
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
@@ -147,6 +148,15 @@
|
|
|
147
148
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
|
148
149
|
"license": "MIT"
|
|
149
150
|
},
|
|
151
|
+
"node_modules/@apso/schema-tools": {
|
|
152
|
+
"version": "0.1.0",
|
|
153
|
+
"resolved": "https://registry.npmjs.org/@apso/schema-tools/-/schema-tools-0.1.0.tgz",
|
|
154
|
+
"integrity": "sha512-c/vvsyo+ZMxq4DK4V1UlP3N/De9ijTfcAM/9VrZHmLxeA6JiXx3J+baSBAseffBBTUeNYDp4D/c4iLz8CcCePw==",
|
|
155
|
+
"license": "Apache-2.0",
|
|
156
|
+
"dependencies": {
|
|
157
|
+
"pluralize": "^8.0.0"
|
|
158
|
+
}
|
|
159
|
+
},
|
|
150
160
|
"node_modules/@babel/code-frame": {
|
|
151
161
|
"version": "7.21.4",
|
|
152
162
|
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz",
|
package/oclif.manifest.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.
|
|
2
|
+
"version": "0.39.0",
|
|
3
3
|
"commands": {
|
|
4
4
|
"config": {
|
|
5
5
|
"id": "config",
|
|
@@ -784,6 +784,36 @@
|
|
|
784
784
|
"flags": {},
|
|
785
785
|
"args": {}
|
|
786
786
|
},
|
|
787
|
+
"schema:lint": {
|
|
788
|
+
"id": "schema:lint",
|
|
789
|
+
"description": "Check the local .apsorc for names and settings that would break the generated code",
|
|
790
|
+
"strict": true,
|
|
791
|
+
"pluginName": "@apso/cli",
|
|
792
|
+
"pluginAlias": "@apso/cli",
|
|
793
|
+
"pluginType": "core",
|
|
794
|
+
"aliases": [],
|
|
795
|
+
"hiddenAliases": [],
|
|
796
|
+
"examples": [
|
|
797
|
+
"$ apso schema lint",
|
|
798
|
+
"$ apso schema lint --fix",
|
|
799
|
+
"$ apso schema lint --json"
|
|
800
|
+
],
|
|
801
|
+
"flags": {
|
|
802
|
+
"fix": {
|
|
803
|
+
"name": "fix",
|
|
804
|
+
"type": "boolean",
|
|
805
|
+
"description": "Apply the safe automatic fixes and save .apsorc (a backup is kept)",
|
|
806
|
+
"allowNo": false
|
|
807
|
+
},
|
|
808
|
+
"json": {
|
|
809
|
+
"name": "json",
|
|
810
|
+
"type": "boolean",
|
|
811
|
+
"description": "Print the result as JSON",
|
|
812
|
+
"allowNo": false
|
|
813
|
+
}
|
|
814
|
+
},
|
|
815
|
+
"args": {}
|
|
816
|
+
},
|
|
787
817
|
"schema:pull": {
|
|
788
818
|
"id": "schema:pull",
|
|
789
819
|
"description": "Pull remote schema to local .apsorc",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@apso/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.39.0",
|
|
4
4
|
"mcpName": "io.github.apsoai/apso",
|
|
5
5
|
"description": "Apso CLI",
|
|
6
6
|
"author": "Apso by Mavric - @mavric",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"/oclif.manifest.json"
|
|
25
25
|
],
|
|
26
26
|
"dependencies": {
|
|
27
|
+
"@apso/schema-tools": "^0.1.0",
|
|
27
28
|
"@biomejs/biome": "^1.9.4",
|
|
28
29
|
"@electric-sql/pglite": "^0.2.17",
|
|
29
30
|
"@modelcontextprotocol/sdk": "^1.29.0",
|