@checkstack/backend 0.8.1 → 0.9.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/CHANGELOG.md +280 -0
- package/drizzle/0001_slim_mordo.sql +34 -0
- package/drizzle/meta/0001_snapshot.json +444 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +14 -9
- package/src/index.ts +460 -23
- package/src/plugin-deregistration.test.ts +137 -0
- package/src/plugin-manager/api-router.ts +35 -11
- package/src/plugin-manager/core-services.ts +21 -2
- package/src/plugin-manager/plugin-loader.ts +94 -0
- package/src/plugin-manager.ts +324 -105
- package/src/router-incremental.test.ts +49 -0
- package/src/schema.ts +79 -1
- package/src/services/compatibility-checker.test.ts +146 -0
- package/src/services/compatibility-checker.ts +137 -0
- package/src/services/dev-auth.test.ts +87 -0
- package/src/services/dev-auth.ts +56 -0
- package/src/services/plugin-artifact-store.ts +131 -0
- package/src/services/plugin-bundle-resolver.ts +76 -0
- package/src/services/plugin-event-recorder.ts +87 -0
- package/src/services/plugin-installers/catalog-installer.ts +33 -0
- package/src/services/plugin-installers/github-installer.ts +207 -0
- package/src/services/plugin-installers/install-from-tarball.ts +69 -0
- package/src/services/plugin-installers/installer-registry.ts +51 -0
- package/src/services/plugin-installers/npm-installer.ts +156 -0
- package/src/services/plugin-installers/plugin-install-error.ts +37 -0
- package/src/services/plugin-installers/tarball-installer.ts +80 -0
- package/src/services/plugin-installers/tarball-utils.test.ts +200 -0
- package/src/services/plugin-installers/tarball-utils.ts +172 -0
- package/src/services/plugin-manager-orchestrator.ts +522 -0
- package/src/services/plugin-manager-router.ts +219 -0
- package/src/services/readiness-registry.test.ts +124 -0
- package/src/services/readiness-registry.ts +103 -0
- package/src/utils/plugin-discovery.test.ts +6 -0
- package/src/utils/plugin-discovery.ts +6 -1
- package/tsconfig.json +36 -1
- package/src/plugin-lifecycle.test.ts +0 -276
- package/src/plugin-manager/plugin-admin-router.ts +0 -89
- package/src/services/plugin-installer.test.ts +0 -90
- package/src/services/plugin-installer.ts +0 -70
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { implement, ORPCError } from "@orpc/server";
|
|
2
|
+
import { desc } from "drizzle-orm";
|
|
3
|
+
import {
|
|
4
|
+
autoAuthMiddleware,
|
|
5
|
+
coreServices,
|
|
6
|
+
type RpcContext,
|
|
7
|
+
type SafeDatabase,
|
|
8
|
+
} from "@checkstack/backend-api";
|
|
9
|
+
import {
|
|
10
|
+
pluginManagerContract,
|
|
11
|
+
type InstalledPlugin,
|
|
12
|
+
} from "@checkstack/pluginmanager-common";
|
|
13
|
+
import {
|
|
14
|
+
pluginPackageTypeSchema,
|
|
15
|
+
pluginSourceSchema,
|
|
16
|
+
} from "@checkstack/common";
|
|
17
|
+
import type { PluginManager } from "../plugin-manager";
|
|
18
|
+
import type { ServiceRegistry } from "./service-registry";
|
|
19
|
+
import type { PluginEventRecorder } from "./plugin-event-recorder";
|
|
20
|
+
import { plugins } from "../schema";
|
|
21
|
+
import {
|
|
22
|
+
installOriginator,
|
|
23
|
+
previewInstallOriginator,
|
|
24
|
+
previewUninstallOriginator,
|
|
25
|
+
uninstallOriginator,
|
|
26
|
+
} from "./plugin-manager-orchestrator";
|
|
27
|
+
import {
|
|
28
|
+
PluginInstallError,
|
|
29
|
+
type PluginInstallErrorCode,
|
|
30
|
+
} from "./plugin-installers/plugin-install-error";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Map our installer/orchestrator error codes onto the closest oRPC error
|
|
34
|
+
* codes so the UI sees a meaningful HTTP status + message instead of a
|
|
35
|
+
* generic `INTERNAL_SERVER_ERROR`.
|
|
36
|
+
*
|
|
37
|
+
* oRPC's built-in `ORPCError` codes match common HTTP semantics; we route
|
|
38
|
+
* `COMPATIBILITY_FAILED` and `VALIDATION_FAILED` to `BAD_REQUEST` because
|
|
39
|
+
* neither is a server fault — both indicate user-correctable input.
|
|
40
|
+
*/
|
|
41
|
+
const ORPC_CODE_MAP: Record<
|
|
42
|
+
PluginInstallErrorCode,
|
|
43
|
+
| "BAD_REQUEST"
|
|
44
|
+
| "UNAUTHORIZED"
|
|
45
|
+
| "FORBIDDEN"
|
|
46
|
+
| "NOT_FOUND"
|
|
47
|
+
| "PAYLOAD_TOO_LARGE"
|
|
48
|
+
| "BAD_GATEWAY"
|
|
49
|
+
| "NOT_IMPLEMENTED"
|
|
50
|
+
> = {
|
|
51
|
+
NOT_FOUND: "NOT_FOUND",
|
|
52
|
+
BAD_REQUEST: "BAD_REQUEST",
|
|
53
|
+
UNAUTHORIZED: "UNAUTHORIZED",
|
|
54
|
+
FORBIDDEN: "FORBIDDEN",
|
|
55
|
+
PAYLOAD_TOO_LARGE: "PAYLOAD_TOO_LARGE",
|
|
56
|
+
BAD_GATEWAY: "BAD_GATEWAY",
|
|
57
|
+
VALIDATION_FAILED: "BAD_REQUEST",
|
|
58
|
+
COMPATIBILITY_FAILED: "BAD_REQUEST",
|
|
59
|
+
NOT_IMPLEMENTED: "NOT_IMPLEMENTED",
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Run an originator function and translate any `PluginInstallError` into
|
|
64
|
+
* an `ORPCError` with the right code + the user-facing message intact.
|
|
65
|
+
* Other errors propagate untouched and surface as 500s — those represent
|
|
66
|
+
* genuine bugs and stay loud.
|
|
67
|
+
*/
|
|
68
|
+
async function withTranslatedErrors<T>(fn: () => Promise<T>): Promise<T> {
|
|
69
|
+
try {
|
|
70
|
+
return await fn();
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (error instanceof PluginInstallError) {
|
|
73
|
+
const data: Record<string, unknown> = { kind: error.code };
|
|
74
|
+
if (error.details) Object.assign(data, error.details);
|
|
75
|
+
throw new ORPCError(ORPC_CODE_MAP[error.code], {
|
|
76
|
+
message: error.message,
|
|
77
|
+
data,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* oRPC router that backs `pluginManagerContract`.
|
|
86
|
+
*
|
|
87
|
+
* Lives in `core/backend` because it directly references the in-process
|
|
88
|
+
* `PluginManager` instance. Wired into the request pipeline via
|
|
89
|
+
* `pluginManager.registerCoreRouter("pluginmanager", ...)`.
|
|
90
|
+
*/
|
|
91
|
+
export function createPluginManagerRouter({
|
|
92
|
+
db,
|
|
93
|
+
pluginManager,
|
|
94
|
+
registry,
|
|
95
|
+
eventRecorder,
|
|
96
|
+
workspaceRoot,
|
|
97
|
+
runtimeDir,
|
|
98
|
+
}: {
|
|
99
|
+
db: SafeDatabase<Record<string, unknown>>;
|
|
100
|
+
pluginManager: PluginManager;
|
|
101
|
+
registry: ServiceRegistry;
|
|
102
|
+
eventRecorder: PluginEventRecorder;
|
|
103
|
+
workspaceRoot: string;
|
|
104
|
+
runtimeDir: string;
|
|
105
|
+
}) {
|
|
106
|
+
// touch coreServices to keep the import alive in case future router
|
|
107
|
+
// procedures need to resolve services on demand.
|
|
108
|
+
void coreServices;
|
|
109
|
+
|
|
110
|
+
const impl = implement(pluginManagerContract)
|
|
111
|
+
.$context<RpcContext>()
|
|
112
|
+
.use(autoAuthMiddleware);
|
|
113
|
+
|
|
114
|
+
return impl.router({
|
|
115
|
+
list: impl.list.handler(async () => {
|
|
116
|
+
const rows = await db
|
|
117
|
+
.select()
|
|
118
|
+
.from(plugins)
|
|
119
|
+
.orderBy(desc(plugins.id));
|
|
120
|
+
const list: InstalledPlugin[] = rows.map((row) => {
|
|
121
|
+
// type may legitimately not match the enum for legacy/tooling rows
|
|
122
|
+
// — fall back to a safe value rather than 500-ing on parse.
|
|
123
|
+
const typeParsed = pluginPackageTypeSchema.safeParse(row.type);
|
|
124
|
+
// metadata is JSONB; drizzle returns `unknown | null`. The contract
|
|
125
|
+
// declares it as `optional()` (accepts undefined, NOT null), so
|
|
126
|
+
// coerce. Also accept only objects — string/number/array would fail
|
|
127
|
+
// schema validation.
|
|
128
|
+
const metadata =
|
|
129
|
+
row.metadata && typeof row.metadata === "object" && !Array.isArray(row.metadata)
|
|
130
|
+
? (row.metadata as InstalledPlugin["metadata"])
|
|
131
|
+
: undefined;
|
|
132
|
+
// source: null for monorepo-local; validated PluginSource for
|
|
133
|
+
// runtime-installed. Wrap parse in safeParse so a malformed
|
|
134
|
+
// legacy row doesn't break the whole list response.
|
|
135
|
+
const sourceParsed = row.source
|
|
136
|
+
? pluginSourceSchema.safeParse(row.source)
|
|
137
|
+
: undefined;
|
|
138
|
+
return {
|
|
139
|
+
name: row.name,
|
|
140
|
+
version: row.version ?? "",
|
|
141
|
+
type: typeParsed.success ? typeParsed.data : "backend",
|
|
142
|
+
enabled: row.enabled,
|
|
143
|
+
isUninstallable: row.isUninstallable,
|
|
144
|
+
isPrimary: row.isPrimary,
|
|
145
|
+
bundleId: row.bundleId,
|
|
146
|
+
source: sourceParsed?.success ? sourceParsed.data : null,
|
|
147
|
+
metadata,
|
|
148
|
+
};
|
|
149
|
+
});
|
|
150
|
+
return { plugins: list };
|
|
151
|
+
}),
|
|
152
|
+
|
|
153
|
+
previewInstall: impl.previewInstall.handler(({ input }) =>
|
|
154
|
+
withTranslatedErrors(() =>
|
|
155
|
+
previewInstallOriginator({
|
|
156
|
+
source: input.source,
|
|
157
|
+
registry,
|
|
158
|
+
workspaceRoot,
|
|
159
|
+
runtimeDir,
|
|
160
|
+
}),
|
|
161
|
+
),
|
|
162
|
+
),
|
|
163
|
+
|
|
164
|
+
install: impl.install.handler(async ({ input, context }) => {
|
|
165
|
+
const userId = (context.user as { id?: string } | undefined)?.id;
|
|
166
|
+
const result = await withTranslatedErrors(() =>
|
|
167
|
+
installOriginator({
|
|
168
|
+
source: input.source,
|
|
169
|
+
pluginManager,
|
|
170
|
+
db,
|
|
171
|
+
registry,
|
|
172
|
+
workspaceRoot,
|
|
173
|
+
runtimeDir,
|
|
174
|
+
eventRecorder,
|
|
175
|
+
userId,
|
|
176
|
+
}),
|
|
177
|
+
);
|
|
178
|
+
return {
|
|
179
|
+
success: true,
|
|
180
|
+
bundleId: result.bundleId,
|
|
181
|
+
installedPackages: result.installedPackages,
|
|
182
|
+
};
|
|
183
|
+
}),
|
|
184
|
+
|
|
185
|
+
previewUninstall: impl.previewUninstall.handler(({ input }) =>
|
|
186
|
+
withTranslatedErrors(() =>
|
|
187
|
+
previewUninstallOriginator({
|
|
188
|
+
pluginName: input.pluginName,
|
|
189
|
+
db,
|
|
190
|
+
}),
|
|
191
|
+
),
|
|
192
|
+
),
|
|
193
|
+
|
|
194
|
+
uninstall: impl.uninstall.handler(async ({ input, context }) => {
|
|
195
|
+
const userId = (context.user as { id?: string } | undefined)?.id;
|
|
196
|
+
const result = await withTranslatedErrors(() =>
|
|
197
|
+
uninstallOriginator({
|
|
198
|
+
pluginName: input.pluginName,
|
|
199
|
+
deleteSchema: input.deleteSchema,
|
|
200
|
+
deleteConfigs: input.deleteConfigs,
|
|
201
|
+
cascade: input.cascade,
|
|
202
|
+
pluginManager,
|
|
203
|
+
db,
|
|
204
|
+
eventRecorder,
|
|
205
|
+
userId,
|
|
206
|
+
}),
|
|
207
|
+
);
|
|
208
|
+
return { success: true, uninstalledPackages: result.uninstalledPackages };
|
|
209
|
+
}),
|
|
210
|
+
|
|
211
|
+
events: impl.events.handler(async ({ input }) => {
|
|
212
|
+
const events = await eventRecorder.list({
|
|
213
|
+
pluginName: input.pluginName,
|
|
214
|
+
limit: input.limit,
|
|
215
|
+
});
|
|
216
|
+
return { events };
|
|
217
|
+
}),
|
|
218
|
+
});
|
|
219
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, mock } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
CoreReadinessRegistry,
|
|
4
|
+
createScopedReadinessRegistry,
|
|
5
|
+
} from "./readiness-registry";
|
|
6
|
+
import { createMockLogger } from "@checkstack/test-utils-backend";
|
|
7
|
+
|
|
8
|
+
const mockLogger = createMockLogger();
|
|
9
|
+
mock.module("../logger", () => ({
|
|
10
|
+
rootLogger: mockLogger,
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
describe("CoreReadinessRegistry", () => {
|
|
14
|
+
let registry: CoreReadinessRegistry;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
registry = new CoreReadinessRegistry();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("starts empty", () => {
|
|
21
|
+
expect(registry.isEmpty()).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("evaluates to ready=true with no probes", async () => {
|
|
25
|
+
const snapshot = await registry.evaluate();
|
|
26
|
+
expect(snapshot.ready).toBe(true);
|
|
27
|
+
expect(snapshot.checks).toHaveLength(0);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("aggregates passing probes", async () => {
|
|
31
|
+
registry.register({
|
|
32
|
+
name: "db",
|
|
33
|
+
check: async () => ({ ok: true }),
|
|
34
|
+
});
|
|
35
|
+
registry.register({
|
|
36
|
+
name: "queue",
|
|
37
|
+
check: async () => ({ ok: true }),
|
|
38
|
+
});
|
|
39
|
+
const snapshot = await registry.evaluate();
|
|
40
|
+
expect(snapshot.ready).toBe(true);
|
|
41
|
+
expect(snapshot.checks).toHaveLength(2);
|
|
42
|
+
expect(snapshot.checks.every((c) => c.ok)).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("ready=false when a critical probe fails", async () => {
|
|
46
|
+
registry.register({
|
|
47
|
+
name: "db",
|
|
48
|
+
check: async () => ({ ok: false, message: "down" }),
|
|
49
|
+
});
|
|
50
|
+
const snapshot = await registry.evaluate();
|
|
51
|
+
expect(snapshot.ready).toBe(false);
|
|
52
|
+
expect(snapshot.checks[0].message).toBe("down");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("ready=true when only a non-critical probe fails", async () => {
|
|
56
|
+
registry.register({
|
|
57
|
+
name: "warmup",
|
|
58
|
+
critical: false,
|
|
59
|
+
check: async () => ({ ok: false }),
|
|
60
|
+
});
|
|
61
|
+
registry.register({
|
|
62
|
+
name: "db",
|
|
63
|
+
check: async () => ({ ok: true }),
|
|
64
|
+
});
|
|
65
|
+
const snapshot = await registry.evaluate();
|
|
66
|
+
expect(snapshot.ready).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("treats thrown probes as failed and surfaces the error", async () => {
|
|
70
|
+
registry.register({
|
|
71
|
+
name: "boom",
|
|
72
|
+
check: async () => {
|
|
73
|
+
throw new Error("kaboom");
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
const snapshot = await registry.evaluate();
|
|
77
|
+
expect(snapshot.ready).toBe(false);
|
|
78
|
+
expect(snapshot.checks[0].ok).toBe(false);
|
|
79
|
+
expect(snapshot.checks[0].error).toBe("kaboom");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("overwrites duplicate names with a warning", async () => {
|
|
83
|
+
registry.register({
|
|
84
|
+
name: "db",
|
|
85
|
+
check: async () => ({ ok: false }),
|
|
86
|
+
});
|
|
87
|
+
registry.register({
|
|
88
|
+
name: "db",
|
|
89
|
+
check: async () => ({ ok: true }),
|
|
90
|
+
});
|
|
91
|
+
const snapshot = await registry.evaluate();
|
|
92
|
+
expect(snapshot.checks).toHaveLength(1);
|
|
93
|
+
expect(snapshot.checks[0].ok).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("runs probes in parallel (total time ~ slowest probe)", async () => {
|
|
97
|
+
const delay = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
98
|
+
registry.register({
|
|
99
|
+
name: "slow-1",
|
|
100
|
+
check: async () => {
|
|
101
|
+
await delay(50);
|
|
102
|
+
return { ok: true };
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
registry.register({
|
|
106
|
+
name: "slow-2",
|
|
107
|
+
check: async () => {
|
|
108
|
+
await delay(50);
|
|
109
|
+
return { ok: true };
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
const start = Date.now();
|
|
113
|
+
await registry.evaluate();
|
|
114
|
+
const elapsed = Date.now() - start;
|
|
115
|
+
// Sequential would be ~100ms; parallel should be ~50ms.
|
|
116
|
+
expect(elapsed).toBeLessThan(95);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("scoped registry forwards register() to the global", () => {
|
|
120
|
+
const scoped = createScopedReadinessRegistry(registry);
|
|
121
|
+
scoped.register({ name: "x", check: async () => ({ ok: true }) });
|
|
122
|
+
expect(registry.isEmpty()).toBe(false);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ReadinessCheck,
|
|
3
|
+
ReadinessCheckResult,
|
|
4
|
+
ReadinessRegistry,
|
|
5
|
+
} from "@checkstack/backend-api";
|
|
6
|
+
import { extractErrorMessage } from "@checkstack/common";
|
|
7
|
+
import { rootLogger } from "../logger";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Snapshot returned to /ready callers.
|
|
11
|
+
*/
|
|
12
|
+
export interface ReadinessSnapshot {
|
|
13
|
+
ready: boolean;
|
|
14
|
+
checks: Array<{
|
|
15
|
+
name: string;
|
|
16
|
+
critical: boolean;
|
|
17
|
+
ok: boolean;
|
|
18
|
+
message?: string;
|
|
19
|
+
/** Set when the probe threw (treated as ok=false for critical checks). */
|
|
20
|
+
error?: string;
|
|
21
|
+
/** Wall-clock duration for the probe (milliseconds). */
|
|
22
|
+
durationMs: number;
|
|
23
|
+
}>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Core implementation backing both `coreServices.readinessRegistry` (plugin-facing)
|
|
28
|
+
* and the `/ready` endpoint (server-facing). Plugins call `register`; the server
|
|
29
|
+
* calls `evaluate()`.
|
|
30
|
+
*/
|
|
31
|
+
export class CoreReadinessRegistry {
|
|
32
|
+
private checks: ReadinessCheck[] = [];
|
|
33
|
+
|
|
34
|
+
register(check: ReadinessCheck): void {
|
|
35
|
+
if (this.checks.some((c) => c.name === check.name)) {
|
|
36
|
+
rootLogger.warn(
|
|
37
|
+
`ReadinessRegistry: probe '${check.name}' is already registered. Overwriting.`,
|
|
38
|
+
);
|
|
39
|
+
this.checks = this.checks.filter((c) => c.name !== check.name);
|
|
40
|
+
}
|
|
41
|
+
this.checks.push(check);
|
|
42
|
+
rootLogger.debug(
|
|
43
|
+
` -> Registered readiness probe '${check.name}' (critical=${check.critical ?? true})`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Run every probe in parallel. Critical failures set `ready = false`.
|
|
49
|
+
* Throws are caught and reported as `ok: false`.
|
|
50
|
+
*/
|
|
51
|
+
async evaluate(): Promise<ReadinessSnapshot> {
|
|
52
|
+
const results = await Promise.all(
|
|
53
|
+
this.checks.map(async (c) => {
|
|
54
|
+
const start = performance.now();
|
|
55
|
+
const critical = c.critical ?? true;
|
|
56
|
+
try {
|
|
57
|
+
const r: ReadinessCheckResult = await c.check();
|
|
58
|
+
return {
|
|
59
|
+
name: c.name,
|
|
60
|
+
critical,
|
|
61
|
+
ok: r.ok,
|
|
62
|
+
message: r.message,
|
|
63
|
+
durationMs: Math.round(performance.now() - start),
|
|
64
|
+
};
|
|
65
|
+
} catch (error) {
|
|
66
|
+
return {
|
|
67
|
+
name: c.name,
|
|
68
|
+
critical,
|
|
69
|
+
ok: false,
|
|
70
|
+
error: extractErrorMessage(error, String(error)),
|
|
71
|
+
durationMs: Math.round(performance.now() - start),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const ready = results.every((r) => r.ok || !r.critical);
|
|
78
|
+
return { ready, checks: results };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Returns true while no probes are registered. Used to give a stable answer
|
|
83
|
+
* before plugins have had a chance to register their checks.
|
|
84
|
+
*/
|
|
85
|
+
isEmpty(): boolean {
|
|
86
|
+
return this.checks.length === 0;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Plugin-facing scoped view (currently identical to the underlying registry —
|
|
92
|
+
* we intentionally don't namespace probe names by plugin so operators can read
|
|
93
|
+
* them at a glance, but plugins are encouraged to prefix their own names).
|
|
94
|
+
*/
|
|
95
|
+
export function createScopedReadinessRegistry(
|
|
96
|
+
global: CoreReadinessRegistry,
|
|
97
|
+
): ReadinessRegistry {
|
|
98
|
+
return {
|
|
99
|
+
register(check: ReadinessCheck) {
|
|
100
|
+
global.register(check);
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
@@ -51,6 +51,7 @@ describe("extractPluginMetadata", () => {
|
|
|
51
51
|
pluginPath: "/fake/path/test-backend",
|
|
52
52
|
type: "backend",
|
|
53
53
|
enabled: true,
|
|
54
|
+
version: "0.0.1",
|
|
54
55
|
});
|
|
55
56
|
});
|
|
56
57
|
|
|
@@ -288,6 +289,7 @@ describe("syncPluginsToDatabase", () => {
|
|
|
288
289
|
pluginPath: "/workspace/plugins/new-backend",
|
|
289
290
|
type: "backend",
|
|
290
291
|
enabled: true,
|
|
292
|
+
version: "2.1.0",
|
|
291
293
|
},
|
|
292
294
|
];
|
|
293
295
|
|
|
@@ -310,6 +312,7 @@ describe("syncPluginsToDatabase", () => {
|
|
|
310
312
|
type: "backend",
|
|
311
313
|
enabled: true,
|
|
312
314
|
isUninstallable: false,
|
|
315
|
+
version: "2.1.0",
|
|
313
316
|
});
|
|
314
317
|
});
|
|
315
318
|
|
|
@@ -320,6 +323,7 @@ describe("syncPluginsToDatabase", () => {
|
|
|
320
323
|
pluginPath: "/workspace/plugins/new-location",
|
|
321
324
|
type: "backend",
|
|
322
325
|
enabled: true,
|
|
326
|
+
version: "1.0.0",
|
|
323
327
|
},
|
|
324
328
|
];
|
|
325
329
|
|
|
@@ -347,6 +351,7 @@ describe("syncPluginsToDatabase", () => {
|
|
|
347
351
|
expect(updateCall.set).toHaveBeenCalledWith({
|
|
348
352
|
path: "/workspace/plugins/new-location",
|
|
349
353
|
type: "backend",
|
|
354
|
+
version: "1.0.0",
|
|
350
355
|
});
|
|
351
356
|
});
|
|
352
357
|
|
|
@@ -357,6 +362,7 @@ describe("syncPluginsToDatabase", () => {
|
|
|
357
362
|
pluginPath: "/workspace/plugins/remote-backend",
|
|
358
363
|
type: "backend",
|
|
359
364
|
enabled: true,
|
|
365
|
+
version: "0.0.0",
|
|
360
366
|
},
|
|
361
367
|
];
|
|
362
368
|
|
|
@@ -10,6 +10,7 @@ export interface PluginMetadata {
|
|
|
10
10
|
pluginPath: string; // Absolute path to plugin directory
|
|
11
11
|
type: "backend" | "frontend" | "common";
|
|
12
12
|
enabled: boolean;
|
|
13
|
+
version: string; // From package.json "version"
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
/**
|
|
@@ -64,6 +65,7 @@ export function extractPluginMetadata({
|
|
|
64
65
|
pluginPath: pluginDir,
|
|
65
66
|
type,
|
|
66
67
|
enabled: true, // Local plugins are always enabled
|
|
68
|
+
version: typeof pkgJson.version === "string" ? pkgJson.version : "",
|
|
67
69
|
};
|
|
68
70
|
} catch (error) {
|
|
69
71
|
rootLogger.debug(`⚠️ Failed to read package.json for ${pluginDir}:`, error);
|
|
@@ -148,16 +150,19 @@ export async function syncPluginsToDatabase({
|
|
|
148
150
|
type: plugin.type,
|
|
149
151
|
enabled: plugin.enabled,
|
|
150
152
|
isUninstallable: false, // Local plugins are part of monorepo
|
|
153
|
+
version: plugin.version,
|
|
151
154
|
});
|
|
152
155
|
} else {
|
|
153
156
|
// Update existing plugin ONLY if it's a local plugin (not remotely installed)
|
|
154
|
-
// This handles the case where a plugin directory was renamed
|
|
157
|
+
// This handles the case where a plugin directory was renamed AND keeps
|
|
158
|
+
// `version` in lockstep with each release.
|
|
155
159
|
if (!existing[0].isUninstallable) {
|
|
156
160
|
await db
|
|
157
161
|
.update(plugins)
|
|
158
162
|
.set({
|
|
159
163
|
path: plugin.pluginPath,
|
|
160
164
|
type: plugin.type,
|
|
165
|
+
version: plugin.version,
|
|
161
166
|
})
|
|
162
167
|
.where(
|
|
163
168
|
and(
|
package/tsconfig.json
CHANGED
|
@@ -2,5 +2,40 @@
|
|
|
2
2
|
"extends": "@checkstack/tsconfig/backend.json",
|
|
3
3
|
"include": [
|
|
4
4
|
"src"
|
|
5
|
+
],
|
|
6
|
+
"references": [
|
|
7
|
+
{
|
|
8
|
+
"path": "../api-docs-common"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"path": "../auth-common"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"path": "../backend-api"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"path": "../cache-api"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"path": "../common"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"path": "../drizzle-helper"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"path": "../pluginmanager-common"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"path": "../queue-api"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"path": "../signal-backend"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"path": "../signal-common"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"path": "../test-utils-backend"
|
|
39
|
+
}
|
|
5
40
|
]
|
|
6
|
-
}
|
|
41
|
+
}
|