@apso/cli 0.39.1 → 0.40.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.
@@ -0,0 +1,11 @@
1
+ import BaseCommand from "../../lib/base-command";
2
+ export default class MigrateGenerate extends BaseCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ baseline: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
7
+ name: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
8
+ json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
9
+ };
10
+ run(): Promise<void>;
11
+ }
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ const path = tslib_1.__importStar(require("path"));
5
+ const core_1 = require("@oclif/core");
6
+ const base_command_1 = tslib_1.__importDefault(require("../../lib/base-command"));
7
+ const verify_1 = require("../../lib/verify");
8
+ class MigrateGenerate extends base_command_1.default {
9
+ async run() {
10
+ const { flags } = await this.parse(MigrateGenerate);
11
+ const result = await (0, verify_1.generateMigration)(process.cwd(), path.resolve(flags.baseline), flags.name, (0, verify_1.cliBinPath)(this.config.root));
12
+ const summary = result.needed
13
+ ? `Created ${result.file}`
14
+ : "The database already matches; no migration needed.";
15
+ (0, verify_1.report)((s) => this.log(s), result, flags.json, result.ok ? summary : undefined);
16
+ if (!result.ok)
17
+ this.exit(1);
18
+ }
19
+ }
20
+ MigrateGenerate.description = "Generate a TypeORM migration from the deployed schema to this project's entities, using a local PGlite database (TypeScript services)";
21
+ MigrateGenerate.examples = [`$ apso migrate generate --baseline deployed.apsorc`];
22
+ MigrateGenerate.flags = {
23
+ baseline: core_1.Flags.string({
24
+ description: "The deployed .apsorc to migrate from",
25
+ required: true,
26
+ }),
27
+ name: core_1.Flags.string({
28
+ description: "Migration name",
29
+ default: "SchemaUpdate",
30
+ }),
31
+ json: core_1.Flags.boolean({
32
+ description: "Print the result as JSON",
33
+ default: false,
34
+ }),
35
+ };
36
+ exports.default = MigrateGenerate;
@@ -0,0 +1,11 @@
1
+ import BaseCommand from "../../lib/base-command";
2
+ export default class MigrateVerify extends BaseCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ baseline: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
7
+ migration: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
8
+ json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
9
+ };
10
+ run(): Promise<void>;
11
+ }
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ const path = tslib_1.__importStar(require("path"));
5
+ const core_1 = require("@oclif/core");
6
+ const base_command_1 = tslib_1.__importDefault(require("../../lib/base-command"));
7
+ const verify_1 = require("../../lib/verify");
8
+ class MigrateVerify extends base_command_1.default {
9
+ async run() {
10
+ const { flags } = await this.parse(MigrateVerify);
11
+ const result = await (0, verify_1.verifyMigration)(process.cwd(), path.resolve(flags.baseline), flags.migration, (0, verify_1.cliBinPath)(this.config.root));
12
+ (0, verify_1.report)((s) => this.log(s), result, flags.json, result.ok ? "Migration verified." : undefined);
13
+ if (!result.ok)
14
+ this.exit(1);
15
+ }
16
+ }
17
+ MigrateVerify.description = "Run a migration on a local PGlite copy of the deployed schema, then check the database matches this project's entities (TypeScript services)";
18
+ MigrateVerify.examples = [
19
+ `$ apso migrate verify --baseline deployed.apsorc --migration src/migrations/1700000000000-SchemaUpdate-migration.ts`,
20
+ ];
21
+ MigrateVerify.flags = {
22
+ baseline: core_1.Flags.string({
23
+ description: "The deployed .apsorc the migration starts from",
24
+ required: true,
25
+ }),
26
+ migration: core_1.Flags.string({
27
+ description: "The migration file to run",
28
+ required: true,
29
+ }),
30
+ json: core_1.Flags.boolean({
31
+ description: "Print the result as JSON",
32
+ default: false,
33
+ }),
34
+ };
35
+ exports.default = MigrateVerify;
@@ -0,0 +1,9 @@
1
+ import BaseCommand from "../../lib/base-command";
2
+ export default class TestSmoke extends BaseCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
7
+ };
8
+ run(): Promise<void>;
9
+ }
@@ -0,0 +1,28 @@
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 base_command_1 = tslib_1.__importDefault(require("../../lib/base-command"));
6
+ const apsorc_parser_1 = require("../../lib/apsorc-parser");
7
+ const verify_1 = require("../../lib/verify");
8
+ class TestSmoke extends base_command_1.default {
9
+ async run() {
10
+ const { flags } = await this.parse(TestSmoke);
11
+ const { entities, auth } = (0, apsorc_parser_1.parseApsorc)();
12
+ // With auth configured every entity route needs a session, so only /health is checked.
13
+ const names = auth ? [] : entities.map((e) => e.name);
14
+ const result = await (0, verify_1.smokeTest)(process.cwd(), names);
15
+ (0, verify_1.report)((s) => this.log(s), result, flags.json, result.ok ? "Smoke test passed." : undefined);
16
+ if (!result.ok)
17
+ this.exit(1);
18
+ }
19
+ }
20
+ TestSmoke.description = "Start the service on a local PGlite database and call /health and every entity's list endpoint (TypeScript services)";
21
+ TestSmoke.examples = [`$ apso test smoke`];
22
+ TestSmoke.flags = {
23
+ json: core_1.Flags.boolean({
24
+ description: "Print the result as JSON",
25
+ default: false,
26
+ }),
27
+ };
28
+ exports.default = TestSmoke;
@@ -60,6 +60,8 @@ export declare class ApsorcNamingError extends Error {
60
60
  code: string;
61
61
  entity: string;
62
62
  field?: string | undefined;
63
+ /** A problem in the user's .apsorc, not a CLI bug: PostHog only, never Sentry. */
64
+ readonly userError = true;
63
65
  constructor(message: string, code: string, entity: string, field?: string | undefined);
64
66
  }
65
67
  /**
@@ -34,6 +34,8 @@ class ApsorcNamingError extends Error {
34
34
  this.code = code;
35
35
  this.entity = entity;
36
36
  this.field = field;
37
+ /** A problem in the user's .apsorc, not a CLI bug: PostHog only, never Sentry. */
38
+ this.userError = true;
37
39
  this.name = "ApsorcNamingError";
38
40
  }
39
41
  }
@@ -1,5 +1,9 @@
1
1
  import { Command } from "@oclif/core";
2
- /** True for errors oclif raised deliberately (this.error, this.exit, parse errors). */
2
+ /**
3
+ * True for errors oclif raised deliberately (this.error, this.exit, parse
4
+ * errors) and for errors that mark themselves `userError` (problems in the
5
+ * user's input, such as an .apsorc that would not compile).
6
+ */
3
7
  export declare function isUserError(err: unknown): boolean;
4
8
  export default abstract class BaseCommand extends Command {
5
9
  /**
@@ -6,9 +6,15 @@ const core_1 = require("@oclif/core");
6
6
  const child_process_1 = require("child_process");
7
7
  const os_1 = tslib_1.__importDefault(require("os"));
8
8
  const telemetry_1 = require("./telemetry/telemetry");
9
- /** True for errors oclif raised deliberately (this.error, this.exit, parse errors). */
9
+ /**
10
+ * True for errors oclif raised deliberately (this.error, this.exit, parse
11
+ * errors) and for errors that mark themselves `userError` (problems in the
12
+ * user's input, such as an .apsorc that would not compile).
13
+ */
10
14
  function isUserError(err) {
11
- return Boolean(err && typeof err === "object" && "oclif" in err);
15
+ if (!err || typeof err !== "object")
16
+ return false;
17
+ return "oclif" in err || err.userError === true;
12
18
  }
13
19
  exports.isUserError = isUserError;
14
20
  class BaseCommand extends core_1.Command {
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Pre-deploy checks for a generated TypeScript service, run against PGlite:
3
+ * generate the migration from the deployed schema, run it and check for drift,
4
+ * and smoke test the endpoints. The CLI and the in-browser editor (which runs
5
+ * the CLI in a WebContainer) share these steps.
6
+ */
7
+ /// <reference types="node" />
8
+ /// <reference types="node" />
9
+ /// <reference types="node" />
10
+ /// <reference types="mocha" />
11
+ /// <reference types="node" />
12
+ /// <reference types="node" />
13
+ /// <reference types="node" />
14
+ /// <reference types="node" />
15
+ /// <reference types="node" />
16
+ export interface Step {
17
+ name: string;
18
+ ok: boolean;
19
+ output?: string;
20
+ }
21
+ export declare function run(cmd: string, args: string[], cwd: string, env?: NodeJS.ProcessEnv): Promise<{
22
+ ok: boolean;
23
+ output: string;
24
+ }>;
25
+ /** Env that points the service's orm.config at a file-backed PGlite database. */
26
+ export declare const pgliteEnv: (dataDir: string) => NodeJS.ProcessEnv;
27
+ /**
28
+ * Load the deployed schema into a fresh PGlite database at dataDir: scaffold
29
+ * the baseline .apsorc in a copy of the project (sharing node_modules), build
30
+ * it, and schema:sync.
31
+ */
32
+ export declare function buildBaseline(projectDir: string, baselineApsorc: string, dataDir: string, cliBin: string): Promise<Step>;
33
+ export interface GenerateResult {
34
+ ok: boolean;
35
+ needed: boolean;
36
+ file?: string;
37
+ steps: Step[];
38
+ }
39
+ /** Generate a migration from the deployed schema (baselineApsorc) to the project's entities. */
40
+ export declare function generateMigration(projectDir: string, baselineApsorc: string, name: string, cliBin: string): Promise<GenerateResult>;
41
+ export interface CheckResult {
42
+ ok: boolean;
43
+ steps: Step[];
44
+ }
45
+ /**
46
+ * Run a migration on a fresh copy of the deployed schema, then check that the
47
+ * database matches the project's entities (a second generate finds nothing).
48
+ */
49
+ export declare function verifyMigration(projectDir: string, baselineApsorc: string, migrationFile: string, cliBin: string): Promise<CheckResult>;
50
+ /**
51
+ * Start the built service on a PGlite database synced to its entities and call
52
+ * GET /health and the list endpoint of every entity.
53
+ * ponytail: list only, no create/read/delete; add when payloads can be built
54
+ * from required fields and relationships.
55
+ */
56
+ export declare function smokeTest(projectDir: string, entityNames: string[], timeoutMs?: number): Promise<CheckResult>;
57
+ /** The CLI's own entry point, for running `generate` in the baseline copy. */
58
+ export declare const cliBinPath: (root: string) => string;
59
+ /** Print a check's steps (or JSON) through the command's logger. */
60
+ export declare function report(log: (s: string) => void, result: CheckResult, json: boolean, summary?: string): void;
@@ -0,0 +1,273 @@
1
+ "use strict";
2
+ /**
3
+ * Pre-deploy checks for a generated TypeScript service, run against PGlite:
4
+ * generate the migration from the deployed schema, run it and check for drift,
5
+ * and smoke test the endpoints. The CLI and the in-browser editor (which runs
6
+ * the CLI in a WebContainer) share these steps.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.report = exports.cliBinPath = exports.smokeTest = exports.verifyMigration = exports.generateMigration = exports.buildBaseline = exports.pgliteEnv = exports.run = void 0;
10
+ const tslib_1 = require("tslib");
11
+ const child_process_1 = require("child_process");
12
+ const fs = tslib_1.__importStar(require("fs"));
13
+ const http = tslib_1.__importStar(require("http"));
14
+ const net = tslib_1.__importStar(require("net"));
15
+ const os = tslib_1.__importStar(require("os"));
16
+ const path = tslib_1.__importStar(require("path"));
17
+ const pluralize_1 = tslib_1.__importDefault(require("pluralize"));
18
+ // eslint-disable-next-line no-control-regex
19
+ const plain = (s) => s.replace(/\u001B\[[\d;]*m/g, "");
20
+ const tail = (s, n = 4000) => {
21
+ const p = plain(s);
22
+ return p.length > n ? p.slice(-n) : p;
23
+ };
24
+ function run(cmd, args, cwd, env = {}) {
25
+ return new Promise((resolve) => {
26
+ const p = (0, child_process_1.spawn)(cmd, args, { cwd, env: { ...process.env, ...env } });
27
+ let output = "";
28
+ p.stdout.on("data", (d) => {
29
+ output += d;
30
+ });
31
+ p.stderr.on("data", (d) => {
32
+ output += d;
33
+ });
34
+ p.on("error", (e) => resolve({ ok: false, output: output + e.message }));
35
+ p.on("close", (code) => resolve({ ok: code === 0, output }));
36
+ });
37
+ }
38
+ exports.run = run;
39
+ /** Env that points the service's orm.config at a file-backed PGlite database. */
40
+ const pgliteEnv = (dataDir) => ({
41
+ DATABASE_TYPE: "pglite",
42
+ DATABASE_SYNC: "false",
43
+ DATABASE_LOGGING: "false",
44
+ PGLITE_DATA_DIR: dataDir,
45
+ });
46
+ exports.pgliteEnv = pgliteEnv;
47
+ const typeorm = (cwd, args, env) => run("npm", ["run", "typeorm", "--", ...args, "-d", "./dist/orm.config.js"], cwd, env);
48
+ const build = (cwd) => run("npm", ["run", "build"], cwd);
49
+ const step = (name, r) => ({
50
+ name,
51
+ ok: r.ok,
52
+ output: r.ok ? undefined : tail(r.output),
53
+ });
54
+ /**
55
+ * Load the deployed schema into a fresh PGlite database at dataDir: scaffold
56
+ * the baseline .apsorc in a copy of the project (sharing node_modules), build
57
+ * it, and schema:sync.
58
+ */
59
+ async function buildBaseline(projectDir, baselineApsorc, dataDir, cliBin) {
60
+ const copy = fs.mkdtempSync(path.join(os.tmpdir(), "apso-baseline-"));
61
+ try {
62
+ fs.cpSync(projectDir, copy, {
63
+ recursive: true,
64
+ filter: (src) => !["node_modules", "dist", ".git"].includes(path.relative(projectDir, src).split(path.sep)[0]),
65
+ });
66
+ fs.symlinkSync(path.join(projectDir, "node_modules"), path.join(copy, "node_modules"), "dir");
67
+ fs.copyFileSync(baselineApsorc, path.join(copy, ".apsorc"));
68
+ // The CLI rebuilds src/autogen from the entities present; a stale one fails the build.
69
+ fs.rmSync(path.join(copy, "src", "autogen"), {
70
+ recursive: true,
71
+ force: true,
72
+ });
73
+ const gen = await run(process.execPath, [cliBin, "generate", "--skip-format", "--language", "typescript"], copy);
74
+ if (!gen.ok)
75
+ return step("baseline", gen);
76
+ const built = await build(copy);
77
+ if (!built.ok)
78
+ return step("baseline", built);
79
+ return step("baseline", await typeorm(copy, ["schema:sync"], (0, exports.pgliteEnv)(dataDir)));
80
+ }
81
+ finally {
82
+ fs.rmSync(copy, { recursive: true, force: true });
83
+ }
84
+ }
85
+ exports.buildBaseline = buildBaseline;
86
+ /** Generate a migration from the deployed schema (baselineApsorc) to the project's entities. */
87
+ async function generateMigration(projectDir, baselineApsorc, name, cliBin) {
88
+ const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "apso-pglite-"));
89
+ const steps = [];
90
+ try {
91
+ const baseline = await buildBaseline(projectDir, baselineApsorc, dataDir, cliBin);
92
+ steps.push(baseline);
93
+ if (!baseline.ok)
94
+ return { ok: false, needed: false, steps };
95
+ const built = await build(projectDir);
96
+ steps.push(step("build", built));
97
+ if (!built.ok)
98
+ return { ok: false, needed: false, steps };
99
+ const migrationsDir = path.join(projectDir, "src", "migrations");
100
+ fs.mkdirSync(migrationsDir, { recursive: true });
101
+ const before = new Set(fs.readdirSync(migrationsDir));
102
+ const gen = await typeorm(projectDir, ["migration:generate", `./src/migrations/${name}-migration`], (0, exports.pgliteEnv)(dataDir));
103
+ if (/No changes in database schema were found/.test(gen.output)) {
104
+ steps.push({ name: "generate", ok: true });
105
+ return { ok: true, needed: false, steps };
106
+ }
107
+ const file = fs
108
+ .readdirSync(migrationsDir)
109
+ .find((f) => !before.has(f) && f.endsWith(".ts"));
110
+ const ok = gen.ok && Boolean(file);
111
+ steps.push(step("generate", { ok, output: gen.output }));
112
+ return {
113
+ ok,
114
+ needed: true,
115
+ file: file && path.join("src", "migrations", file),
116
+ steps,
117
+ };
118
+ }
119
+ finally {
120
+ fs.rmSync(dataDir, { recursive: true, force: true });
121
+ }
122
+ }
123
+ exports.generateMigration = generateMigration;
124
+ // Runs only the given compiled migration, so migrations the deployed database
125
+ // already ran are not replayed on the baseline.
126
+ const RUN_ONE_MIGRATION = `
127
+ const path = require("path");
128
+ const mod = require(path.resolve("dist/orm.config.js"));
129
+ const ds = mod.default || mod;
130
+ ds.setOptions({ migrations: [path.resolve(process.argv[1])] });
131
+ ds.initialize()
132
+ .then(() => ds.runMigrations({ transaction: "all" }))
133
+ .then((ran) => { console.log("ran " + ran.map((m) => m.name).join(", ")); return ds.destroy(); })
134
+ .catch((e) => { console.error(e && e.message ? e.message : e); process.exit(1); });
135
+ `;
136
+ /**
137
+ * Run a migration on a fresh copy of the deployed schema, then check that the
138
+ * database matches the project's entities (a second generate finds nothing).
139
+ */
140
+ async function verifyMigration(projectDir, baselineApsorc, migrationFile, cliBin) {
141
+ const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "apso-pglite-"));
142
+ const steps = [];
143
+ const done = (s) => {
144
+ steps.push(s);
145
+ return s.ok;
146
+ };
147
+ try {
148
+ if (!done(await buildBaseline(projectDir, baselineApsorc, dataDir, cliBin)))
149
+ return { ok: false, steps };
150
+ if (!done(step("build", await build(projectDir))))
151
+ return { ok: false, steps };
152
+ const compiled = path
153
+ .join("dist", path.relative("src", migrationFile))
154
+ .replace(/\.ts$/, ".js");
155
+ const migrate = await run(process.execPath, ["-e", RUN_ONE_MIGRATION, compiled], projectDir, (0, exports.pgliteEnv)(dataDir));
156
+ if (!done({ ...step("migrate", migrate), output: tail(migrate.output) }))
157
+ return { ok: false, steps };
158
+ const drift = await typeorm(projectDir, ["migration:generate", "./src/migrations/drift", "--check"], (0, exports.pgliteEnv)(dataDir));
159
+ return { ok: done(step("drift", drift)), steps };
160
+ }
161
+ finally {
162
+ fs.rmSync(dataDir, { recursive: true, force: true });
163
+ }
164
+ }
165
+ exports.verifyMigration = verifyMigration;
166
+ const freePort = () => new Promise((resolve, reject) => {
167
+ const s = net.createServer();
168
+ s.listen(0, () => {
169
+ const { port } = s.address();
170
+ s.close(() => resolve(port));
171
+ });
172
+ s.on("error", reject);
173
+ });
174
+ const get = (url) => new Promise((resolve) => {
175
+ http
176
+ .get(url, (res) => {
177
+ let body = "";
178
+ res.on("data", (d) => {
179
+ body += d;
180
+ });
181
+ res.on("end", () => { var _a; return resolve({ status: (_a = res.statusCode) !== null && _a !== void 0 ? _a : 0, body }); });
182
+ })
183
+ .on("error", (e) => resolve({ status: 0, body: e.message }));
184
+ });
185
+ /**
186
+ * Start the built service on a PGlite database synced to its entities and call
187
+ * GET /health and the list endpoint of every entity.
188
+ * ponytail: list only, no create/read/delete; add when payloads can be built
189
+ * from required fields and relationships.
190
+ */
191
+ async function smokeTest(projectDir, entityNames, timeoutMs = 60000) {
192
+ var _a, _b;
193
+ const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "apso-pglite-"));
194
+ const steps = [];
195
+ const done = (s) => {
196
+ steps.push(s);
197
+ return s.ok;
198
+ };
199
+ let server;
200
+ try {
201
+ if (!done(step("build", await build(projectDir))))
202
+ return { ok: false, steps };
203
+ if (!done(step("schema", await typeorm(projectDir, ["schema:sync"], (0, exports.pgliteEnv)(dataDir)))))
204
+ return { ok: false, steps };
205
+ const port = await freePort();
206
+ let log = "";
207
+ server = (0, child_process_1.spawn)(process.execPath, ["dist/main"], {
208
+ cwd: projectDir,
209
+ env: { ...process.env, ...(0, exports.pgliteEnv)(dataDir), APP_PORT: String(port) },
210
+ });
211
+ (_a = server.stdout) === null || _a === void 0 ? void 0 : _a.on("data", (d) => {
212
+ log += d;
213
+ });
214
+ (_b = server.stderr) === null || _b === void 0 ? void 0 : _b.on("data", (d) => {
215
+ log += d;
216
+ });
217
+ const base = `http://127.0.0.1:${port}`;
218
+ const deadline = Date.now() + timeoutMs;
219
+ let health = await get(`${base}/health`);
220
+ while (health.status !== 200 &&
221
+ Date.now() < deadline &&
222
+ server.exitCode === null) {
223
+ // eslint-disable-next-line no-await-in-loop
224
+ await new Promise((r) => {
225
+ setTimeout(r, 500);
226
+ });
227
+ // eslint-disable-next-line no-await-in-loop
228
+ health = await get(`${base}/health`);
229
+ }
230
+ if (!done({
231
+ name: "start",
232
+ ok: health.status === 200,
233
+ output: health.status === 200 ? undefined : tail(log),
234
+ }))
235
+ return { ok: false, steps };
236
+ for (const name of entityNames) {
237
+ const route = `/${(0, pluralize_1.default)(name)}`;
238
+ // eslint-disable-next-line no-await-in-loop
239
+ const res = await get(base + route);
240
+ done({
241
+ name: `GET ${route}`,
242
+ ok: res.status === 200,
243
+ output: res.status === 200
244
+ ? undefined
245
+ : `${res.status} ${tail(res.body, 500)}`,
246
+ });
247
+ }
248
+ return { ok: steps.every((s) => s.ok), steps };
249
+ }
250
+ finally {
251
+ server === null || server === void 0 ? void 0 : server.kill();
252
+ fs.rmSync(dataDir, { recursive: true, force: true });
253
+ }
254
+ }
255
+ exports.smokeTest = smokeTest;
256
+ /** The CLI's own entry point, for running `generate` in the baseline copy. */
257
+ const cliBinPath = (root) => path.join(root, "bin", "run");
258
+ exports.cliBinPath = cliBinPath;
259
+ /** Print a check's steps (or JSON) through the command's logger. */
260
+ function report(log, result, json, summary) {
261
+ if (json) {
262
+ log(JSON.stringify(result));
263
+ return;
264
+ }
265
+ for (const s of result.steps) {
266
+ log(`${s.ok ? "ok " : "FAIL"} ${s.name}`);
267
+ if (s.output)
268
+ log(s.output.replace(/^/gm, " "));
269
+ }
270
+ if (summary)
271
+ log(summary);
272
+ }
273
+ exports.report = report;
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@apso/cli",
3
- "version": "0.39.1",
3
+ "version": "0.40.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@apso/cli",
9
- "version": "0.39.1",
9
+ "version": "0.40.0",
10
10
  "license": "Apache-2.0",
11
11
  "dependencies": {
12
12
  "@apso/schema-tools": "^0.2.0",
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.39.1",
2
+ "version": "0.40.0",
3
3
  "commands": {
4
4
  "config": {
5
5
  "id": "config",
@@ -769,6 +769,78 @@
769
769
  },
770
770
  "args": {}
771
771
  },
772
+ "migrate:generate": {
773
+ "id": "migrate:generate",
774
+ "description": "Generate a TypeORM migration from the deployed schema to this project's entities, using a local PGlite database (TypeScript services)",
775
+ "strict": true,
776
+ "pluginName": "@apso/cli",
777
+ "pluginAlias": "@apso/cli",
778
+ "pluginType": "core",
779
+ "aliases": [],
780
+ "hiddenAliases": [],
781
+ "examples": [
782
+ "$ apso migrate generate --baseline deployed.apsorc"
783
+ ],
784
+ "flags": {
785
+ "baseline": {
786
+ "name": "baseline",
787
+ "type": "option",
788
+ "description": "The deployed .apsorc to migrate from",
789
+ "required": true,
790
+ "multiple": false
791
+ },
792
+ "name": {
793
+ "name": "name",
794
+ "type": "option",
795
+ "description": "Migration name",
796
+ "multiple": false,
797
+ "default": "SchemaUpdate"
798
+ },
799
+ "json": {
800
+ "name": "json",
801
+ "type": "boolean",
802
+ "description": "Print the result as JSON",
803
+ "allowNo": false
804
+ }
805
+ },
806
+ "args": {}
807
+ },
808
+ "migrate:verify": {
809
+ "id": "migrate:verify",
810
+ "description": "Run a migration on a local PGlite copy of the deployed schema, then check the database matches this project's entities (TypeScript services)",
811
+ "strict": true,
812
+ "pluginName": "@apso/cli",
813
+ "pluginAlias": "@apso/cli",
814
+ "pluginType": "core",
815
+ "aliases": [],
816
+ "hiddenAliases": [],
817
+ "examples": [
818
+ "$ apso migrate verify --baseline deployed.apsorc --migration src/migrations/1700000000000-SchemaUpdate-migration.ts"
819
+ ],
820
+ "flags": {
821
+ "baseline": {
822
+ "name": "baseline",
823
+ "type": "option",
824
+ "description": "The deployed .apsorc the migration starts from",
825
+ "required": true,
826
+ "multiple": false
827
+ },
828
+ "migration": {
829
+ "name": "migration",
830
+ "type": "option",
831
+ "description": "The migration file to run",
832
+ "required": true,
833
+ "multiple": false
834
+ },
835
+ "json": {
836
+ "name": "json",
837
+ "type": "boolean",
838
+ "description": "Print the result as JSON",
839
+ "allowNo": false
840
+ }
841
+ },
842
+ "args": {}
843
+ },
772
844
  "schema:diff": {
773
845
  "id": "schema:diff",
774
846
  "description": "Show diff between local and remote schema",
@@ -916,6 +988,28 @@
916
988
  ],
917
989
  "flags": {},
918
990
  "args": {}
991
+ },
992
+ "test:smoke": {
993
+ "id": "test:smoke",
994
+ "description": "Start the service on a local PGlite database and call /health and every entity's list endpoint (TypeScript services)",
995
+ "strict": true,
996
+ "pluginName": "@apso/cli",
997
+ "pluginAlias": "@apso/cli",
998
+ "pluginType": "core",
999
+ "aliases": [],
1000
+ "hiddenAliases": [],
1001
+ "examples": [
1002
+ "$ apso test smoke"
1003
+ ],
1004
+ "flags": {
1005
+ "json": {
1006
+ "name": "json",
1007
+ "type": "boolean",
1008
+ "description": "Print the result as JSON",
1009
+ "allowNo": false
1010
+ }
1011
+ },
1012
+ "args": {}
919
1013
  }
920
1014
  }
921
1015
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apso/cli",
3
- "version": "0.39.1",
3
+ "version": "0.40.0",
4
4
  "mcpName": "io.github.apsoai/apso",
5
5
  "description": "Apso CLI",
6
6
  "author": "Apso by Mavric - @mavric",