@happyvertical/smrt-workbench 0.40.66
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/AGENTS.md +28 -0
- package/CLAUDE.md +1 -0
- package/LICENSE +7 -0
- package/README.md +31 -0
- package/dist/chunks/discovery-G9-foyW8.js +979 -0
- package/dist/chunks/discovery-G9-foyW8.js.map +1 -0
- package/dist/chunks/runtime-BTiu5fuP.js +93 -0
- package/dist/chunks/runtime-BTiu5fuP.js.map +1 -0
- package/dist/discovery.d.ts +14 -0
- package/dist/discovery.d.ts.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/runtime.d.ts +10 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +2 -0
- package/dist/svelte/MarkdownDocument.svelte +200 -0
- package/dist/svelte/MarkdownDocument.svelte.d.ts +7 -0
- package/dist/svelte/MarkdownDocument.svelte.d.ts.map +1 -0
- package/dist/svelte/WorkbenchHost.svelte +2058 -0
- package/dist/svelte/WorkbenchHost.svelte.d.ts +13 -0
- package/dist/svelte/WorkbenchHost.svelte.d.ts.map +1 -0
- package/dist/svelte/command.d.ts +6 -0
- package/dist/svelte/command.d.ts.map +1 -0
- package/dist/svelte/command.js +18 -0
- package/dist/svelte/index.d.ts +2 -0
- package/dist/svelte/index.d.ts.map +1 -0
- package/dist/svelte/index.js +1 -0
- package/dist/types.d.ts +202 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +0 -0
- package/dist/utils.d.ts +3 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/vite.d.ts +6 -0
- package/dist/vite.d.ts.map +1 -0
- package/dist/vite.js +124 -0
- package/dist/vite.js.map +1 -0
- package/host/README.md +12 -0
- package/host/package.json +21 -0
- package/host/src/app.html +11 -0
- package/host/src/hooks.client.ts +5 -0
- package/host/src/routes/+error.svelte +44 -0
- package/host/src/routes/+page.svelte +15 -0
- package/host/svelte.config.js +12 -0
- package/host/tsconfig.json +10 -0
- package/host/vite.config.ts +202 -0
- package/package.json +96 -0
- package/src/discovery.ts +1748 -0
- package/src/runtime.ts +165 -0
- package/src/types.ts +243 -0
- package/src/utils.ts +16 -0
- package/src/vite.ts +303 -0
package/src/discovery.ts
ADDED
|
@@ -0,0 +1,1748 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import {
|
|
4
|
+
dirname,
|
|
5
|
+
extname,
|
|
6
|
+
isAbsolute,
|
|
7
|
+
join,
|
|
8
|
+
relative,
|
|
9
|
+
resolve,
|
|
10
|
+
} from 'node:path';
|
|
11
|
+
import { pathToFileURL } from 'node:url';
|
|
12
|
+
import fg from 'fast-glob';
|
|
13
|
+
import { coerceWorkbenchModules } from './runtime.js';
|
|
14
|
+
import type {
|
|
15
|
+
DiscoveredWorkbenchTarget,
|
|
16
|
+
SmrtWorkbenchModule,
|
|
17
|
+
SmrtWorkbenchProject,
|
|
18
|
+
SmrtWorkbenchScopeMode,
|
|
19
|
+
SmrtWorkbenchVitePluginOptions,
|
|
20
|
+
WorkbenchApiObjectFieldSummary,
|
|
21
|
+
WorkbenchApiObjectSummary,
|
|
22
|
+
WorkbenchApiParameterLocation,
|
|
23
|
+
WorkbenchApiParameterSummary,
|
|
24
|
+
WorkbenchApiSummary,
|
|
25
|
+
WorkbenchCliCommandSummary,
|
|
26
|
+
WorkbenchDocumentSummary,
|
|
27
|
+
WorkbenchExampleSummary,
|
|
28
|
+
WorkbenchKnowledgeSummary,
|
|
29
|
+
WorkbenchMcpToolSummary,
|
|
30
|
+
WorkbenchPackageSummary,
|
|
31
|
+
WorkbenchRestEndpointSummary,
|
|
32
|
+
WorkbenchScopeResolution,
|
|
33
|
+
} from './types.js';
|
|
34
|
+
import { commandIdForScript } from './utils.js';
|
|
35
|
+
|
|
36
|
+
const require = createRequire(import.meta.url);
|
|
37
|
+
const TS_SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.mts', '.cts']);
|
|
38
|
+
const DOCUMENT_LIMIT = 20_000;
|
|
39
|
+
const EXAMPLE_LIMIT = 8_000;
|
|
40
|
+
|
|
41
|
+
interface PackageJsonLike {
|
|
42
|
+
name?: string;
|
|
43
|
+
version?: string;
|
|
44
|
+
description?: string;
|
|
45
|
+
scripts?: Record<string, string>;
|
|
46
|
+
dependencies?: Record<string, string>;
|
|
47
|
+
devDependencies?: Record<string, string>;
|
|
48
|
+
peerDependencies?: Record<string, string>;
|
|
49
|
+
exports?: unknown;
|
|
50
|
+
packageManager?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface WorkbenchPackageDir {
|
|
54
|
+
packageDir: string;
|
|
55
|
+
packageJson: PackageJsonLike;
|
|
56
|
+
source: 'workspace' | 'package' | 'app';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function findWorkspaceRoot(startDir = process.cwd()): string | null {
|
|
60
|
+
let current = resolve(startDir);
|
|
61
|
+
|
|
62
|
+
while (true) {
|
|
63
|
+
if (existsSync(join(current, 'pnpm-workspace.yaml'))) {
|
|
64
|
+
return current;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const parent = dirname(current);
|
|
68
|
+
if (parent === current) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
current = parent;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function findSmrtWorkbenchWorkspaceRoot(
|
|
76
|
+
startDir = process.cwd(),
|
|
77
|
+
): string | null {
|
|
78
|
+
const workspaceRoot = findWorkspaceRoot(startDir);
|
|
79
|
+
if (!workspaceRoot) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const hostPackageJsonPath = join(
|
|
84
|
+
workspaceRoot,
|
|
85
|
+
'packages',
|
|
86
|
+
'smrt-workbench',
|
|
87
|
+
'host',
|
|
88
|
+
'package.json',
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
return existsSync(hostPackageJsonPath) ? workspaceRoot : null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function findProjectRoot(startDir = process.cwd()): string {
|
|
95
|
+
let current = resolve(startDir);
|
|
96
|
+
|
|
97
|
+
while (true) {
|
|
98
|
+
if (existsSync(join(current, 'package.json'))) {
|
|
99
|
+
return current;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const parent = dirname(current);
|
|
103
|
+
if (parent === current) {
|
|
104
|
+
return resolve(startDir);
|
|
105
|
+
}
|
|
106
|
+
current = parent;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function findPackageDir(
|
|
111
|
+
startDir = process.cwd(),
|
|
112
|
+
workspaceRoot?: string,
|
|
113
|
+
): string | null {
|
|
114
|
+
let current = resolve(startDir);
|
|
115
|
+
const boundary = workspaceRoot ? resolve(workspaceRoot) : null;
|
|
116
|
+
|
|
117
|
+
while (true) {
|
|
118
|
+
if (boundary && current === boundary) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const packageJsonPath = join(current, 'package.json');
|
|
123
|
+
if (existsSync(packageJsonPath)) {
|
|
124
|
+
return current;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const parent = dirname(current);
|
|
128
|
+
if (parent === current) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
current = parent;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function detectWorkbenchMode(
|
|
136
|
+
projectRoot = process.cwd(),
|
|
137
|
+
): 'workspace' | 'consumer' {
|
|
138
|
+
return findSmrtWorkbenchWorkspaceRoot(projectRoot) ? 'workspace' : 'consumer';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function readJson<T = Record<string, unknown>>(path: string): T {
|
|
142
|
+
return JSON.parse(readFileSync(path, 'utf-8')) as T;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function readJsonIfExists<T = Record<string, unknown>>(path: string): T | null {
|
|
146
|
+
return existsSync(path) ? readJson<T>(path) : null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function detectPackageManager(projectRoot: string): 'pnpm' | 'yarn' | 'npm' {
|
|
150
|
+
let current = resolve(projectRoot);
|
|
151
|
+
|
|
152
|
+
while (true) {
|
|
153
|
+
if (existsSync(join(current, 'pnpm-lock.yaml'))) return 'pnpm';
|
|
154
|
+
if (existsSync(join(current, 'yarn.lock'))) return 'yarn';
|
|
155
|
+
|
|
156
|
+
const packageManager = readJsonIfExists<PackageJsonLike>(
|
|
157
|
+
join(current, 'package.json'),
|
|
158
|
+
)?.packageManager;
|
|
159
|
+
if (packageManager?.startsWith('pnpm@')) return 'pnpm';
|
|
160
|
+
if (packageManager?.startsWith('yarn@')) return 'yarn';
|
|
161
|
+
if (packageManager?.startsWith('npm@')) return 'npm';
|
|
162
|
+
|
|
163
|
+
const parent = dirname(current);
|
|
164
|
+
if (parent === current) return 'npm';
|
|
165
|
+
current = parent;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
170
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function stringRecord(value: unknown): Record<string, string> {
|
|
174
|
+
if (!isRecord(value)) {
|
|
175
|
+
return {};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return Object.fromEntries(
|
|
179
|
+
Object.entries(value).filter(
|
|
180
|
+
(entry): entry is [string, string] => typeof entry[1] === 'string',
|
|
181
|
+
),
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function countItems(value: unknown): number {
|
|
186
|
+
if (Array.isArray(value)) {
|
|
187
|
+
return value.length;
|
|
188
|
+
}
|
|
189
|
+
if (isRecord(value)) {
|
|
190
|
+
return Object.keys(value).length;
|
|
191
|
+
}
|
|
192
|
+
return 0;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function truncate(
|
|
196
|
+
value: string,
|
|
197
|
+
limit: number,
|
|
198
|
+
): {
|
|
199
|
+
content: string;
|
|
200
|
+
truncated: boolean;
|
|
201
|
+
} {
|
|
202
|
+
if (value.length <= limit) {
|
|
203
|
+
return { content: value, truncated: false };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return { content: value.slice(0, limit), truncated: true };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function readDocument(
|
|
210
|
+
packageDir: string,
|
|
211
|
+
fileName: string,
|
|
212
|
+
kind: WorkbenchDocumentSummary['kind'],
|
|
213
|
+
): WorkbenchDocumentSummary | null {
|
|
214
|
+
const path = join(packageDir, fileName);
|
|
215
|
+
if (!existsSync(path)) {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const { content, truncated } = truncate(
|
|
220
|
+
readFileSync(path, 'utf-8'),
|
|
221
|
+
DOCUMENT_LIMIT,
|
|
222
|
+
);
|
|
223
|
+
return {
|
|
224
|
+
kind,
|
|
225
|
+
title: fileName,
|
|
226
|
+
path,
|
|
227
|
+
content,
|
|
228
|
+
truncated,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function exportKeys(exportsField: unknown): string[] {
|
|
233
|
+
if (!exportsField) {
|
|
234
|
+
return [];
|
|
235
|
+
}
|
|
236
|
+
if (typeof exportsField === 'string') {
|
|
237
|
+
return ['.'];
|
|
238
|
+
}
|
|
239
|
+
if (isRecord(exportsField)) {
|
|
240
|
+
return Object.keys(exportsField).sort();
|
|
241
|
+
}
|
|
242
|
+
return [];
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function dependencyNames(packageJson: PackageJsonLike): string[] {
|
|
246
|
+
return Object.keys({
|
|
247
|
+
...packageJson.dependencies,
|
|
248
|
+
...packageJson.devDependencies,
|
|
249
|
+
...packageJson.peerDependencies,
|
|
250
|
+
}).sort();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function smrtDependencyNames(packageJson: PackageJsonLike): string[] {
|
|
254
|
+
return dependencyNames(packageJson).filter((name) =>
|
|
255
|
+
name.startsWith('@happyvertical/smrt-'),
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function sdkDependencyNames(packageJson: PackageJsonLike): string[] {
|
|
260
|
+
return dependencyNames(packageJson).filter(
|
|
261
|
+
(name) =>
|
|
262
|
+
name.startsWith('@happyvertical/') &&
|
|
263
|
+
!name.startsWith('@happyvertical/smrt-'),
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function readKnowledgeSummary(packageDir: string): WorkbenchKnowledgeSummary {
|
|
268
|
+
const knowledgeCandidates = [
|
|
269
|
+
join(packageDir, '.smrt', 'smrt-knowledge.json'),
|
|
270
|
+
join(packageDir, 'dist', 'smrt-knowledge.json'),
|
|
271
|
+
];
|
|
272
|
+
const manifestCandidates = [
|
|
273
|
+
join(packageDir, '.smrt', 'manifest.json'),
|
|
274
|
+
join(packageDir, 'dist', 'manifest.json'),
|
|
275
|
+
join(packageDir, 'src', 'manifest', 'manifest.json'),
|
|
276
|
+
];
|
|
277
|
+
|
|
278
|
+
const knowledgePath = knowledgeCandidates.find((path) => existsSync(path));
|
|
279
|
+
const manifestPath = manifestCandidates.find((path) => existsSync(path));
|
|
280
|
+
const knowledge = knowledgePath
|
|
281
|
+
? readJsonIfExists<Record<string, unknown>>(knowledgePath)
|
|
282
|
+
: null;
|
|
283
|
+
const manifest = manifestPath
|
|
284
|
+
? readJsonIfExists<Record<string, unknown>>(manifestPath)
|
|
285
|
+
: null;
|
|
286
|
+
const knowledgeObjects = knowledge?.objects;
|
|
287
|
+
const manifestObjects = manifest?.objects;
|
|
288
|
+
const objects =
|
|
289
|
+
countItems(knowledgeObjects) > 0 ? knowledgeObjects : manifestObjects;
|
|
290
|
+
const objectNames = objectNamesFrom(objects);
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
manifestPath,
|
|
294
|
+
knowledgePath,
|
|
295
|
+
objectCount: countItems(objects),
|
|
296
|
+
relationshipCount:
|
|
297
|
+
countItems(knowledge?.relationshipsV2) ||
|
|
298
|
+
countItems(knowledge?.relationships),
|
|
299
|
+
promptCount: countItems(knowledge?.prompts),
|
|
300
|
+
mcpToolCount: countItems(knowledge?.mcpTools),
|
|
301
|
+
surfaceCount: countItems(knowledge?.surfaces),
|
|
302
|
+
tags: Array.isArray(knowledge?.tags)
|
|
303
|
+
? knowledge.tags.filter((tag): tag is string => typeof tag === 'string')
|
|
304
|
+
: [],
|
|
305
|
+
risks: Array.isArray(knowledge?.risks)
|
|
306
|
+
? knowledge.risks.filter(
|
|
307
|
+
(risk): risk is string => typeof risk === 'string',
|
|
308
|
+
)
|
|
309
|
+
: [],
|
|
310
|
+
objectNames,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function objectNamesFrom(value: unknown): string[] {
|
|
315
|
+
if (Array.isArray(value)) {
|
|
316
|
+
return value
|
|
317
|
+
.map((item) => {
|
|
318
|
+
if (!isRecord(item)) {
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
return stringValue(item.qualifiedName) || stringValue(item.name);
|
|
322
|
+
})
|
|
323
|
+
.filter((name): name is string => Boolean(name))
|
|
324
|
+
.sort();
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (isRecord(value)) {
|
|
328
|
+
return Object.keys(value).sort();
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return [];
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function stringValue(value: unknown): string | null {
|
|
335
|
+
return typeof value === 'string' && value.length > 0 ? value : null;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function booleanValue(value: unknown): boolean | undefined {
|
|
339
|
+
return typeof value === 'boolean' ? value : undefined;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function objectRecordsFrom(value: unknown): Record<string, unknown>[] {
|
|
343
|
+
if (Array.isArray(value)) {
|
|
344
|
+
return value.filter(isRecord);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (!isRecord(value)) {
|
|
348
|
+
return [];
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return Object.entries(value).map(([key, item]) => {
|
|
352
|
+
if (isRecord(item)) {
|
|
353
|
+
return {
|
|
354
|
+
key,
|
|
355
|
+
...item,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return {
|
|
360
|
+
key,
|
|
361
|
+
name: key,
|
|
362
|
+
};
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function fieldSummariesFrom(value: unknown): WorkbenchApiObjectFieldSummary[] {
|
|
367
|
+
if (Array.isArray(value)) {
|
|
368
|
+
return value
|
|
369
|
+
.filter(isRecord)
|
|
370
|
+
.map((field) => ({
|
|
371
|
+
name: stringValue(field.name) || 'unknown',
|
|
372
|
+
type: stringValue(field.type) || undefined,
|
|
373
|
+
required: booleanValue(field.required),
|
|
374
|
+
related: stringValue(field.related) || undefined,
|
|
375
|
+
}))
|
|
376
|
+
.filter((field) => field.name !== 'unknown');
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (!isRecord(value)) {
|
|
380
|
+
return [];
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return Object.entries(value)
|
|
384
|
+
.map(([name, field]) => {
|
|
385
|
+
const fieldRecord = isRecord(field) ? field : {};
|
|
386
|
+
return {
|
|
387
|
+
name,
|
|
388
|
+
type: stringValue(fieldRecord.type) || undefined,
|
|
389
|
+
required: booleanValue(fieldRecord.required),
|
|
390
|
+
related: stringValue(fieldRecord.related) || undefined,
|
|
391
|
+
};
|
|
392
|
+
})
|
|
393
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function resolveManifestSourcePath(
|
|
397
|
+
rootDir: string,
|
|
398
|
+
packageDir: string,
|
|
399
|
+
sourcePath: string | null,
|
|
400
|
+
): string | undefined {
|
|
401
|
+
if (!sourcePath) {
|
|
402
|
+
return undefined;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (isAbsolute(sourcePath)) {
|
|
406
|
+
return sourcePath;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const rootRelativePath = resolve(rootDir, sourcePath);
|
|
410
|
+
if (existsSync(rootRelativePath)) {
|
|
411
|
+
return rootRelativePath;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return resolve(packageDir, sourcePath);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function typedocPackageSlug(packageName: string | undefined): string | null {
|
|
418
|
+
if (!packageName?.startsWith('@happyvertical/smrt-')) {
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return packageName.replace('@happyvertical/smrt-', '');
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function findTypedocClassPath(
|
|
426
|
+
rootDir: string,
|
|
427
|
+
packageDir: string,
|
|
428
|
+
packageName: string | undefined,
|
|
429
|
+
className: string,
|
|
430
|
+
): string | undefined {
|
|
431
|
+
const slug = typedocPackageSlug(packageName);
|
|
432
|
+
const candidates = [
|
|
433
|
+
slug
|
|
434
|
+
? join(
|
|
435
|
+
rootDir,
|
|
436
|
+
'docs',
|
|
437
|
+
'content',
|
|
438
|
+
'api',
|
|
439
|
+
slug,
|
|
440
|
+
'classes',
|
|
441
|
+
`${className}.md`,
|
|
442
|
+
)
|
|
443
|
+
: null,
|
|
444
|
+
join(packageDir, 'docs', 'classes', `${className}.md`),
|
|
445
|
+
join(packageDir, 'docs', `${className}.md`),
|
|
446
|
+
].filter((path): path is string => Boolean(path));
|
|
447
|
+
|
|
448
|
+
return candidates.find((path) => existsSync(path));
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function extractTypedocSummary(content: string): string | undefined {
|
|
452
|
+
const lines = content.split(/\r?\n/);
|
|
453
|
+
const summary: string[] = [];
|
|
454
|
+
let hasSeenTitle = false;
|
|
455
|
+
|
|
456
|
+
for (const line of lines) {
|
|
457
|
+
const trimmed = line.trim();
|
|
458
|
+
|
|
459
|
+
if (!hasSeenTitle) {
|
|
460
|
+
if (trimmed.startsWith('# ')) {
|
|
461
|
+
hasSeenTitle = true;
|
|
462
|
+
}
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (!trimmed || trimmed.startsWith('Defined in:')) {
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
if (trimmed.startsWith('## ')) {
|
|
470
|
+
break;
|
|
471
|
+
}
|
|
472
|
+
if (trimmed.startsWith('> ')) {
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
summary.push(trimmed);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const value = summary.join('\n').trim();
|
|
480
|
+
return value.length > 0 ? value : undefined;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function readTypedocSummary(path: string | undefined): string | undefined {
|
|
484
|
+
if (!path) {
|
|
485
|
+
return undefined;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return extractTypedocSummary(readFileSync(path, 'utf-8'));
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const CRUD_ACTIONS = ['list', 'get', 'create', 'update', 'delete'] as const;
|
|
492
|
+
const SERVER_MANAGED_FIELDS = new Set([
|
|
493
|
+
'id',
|
|
494
|
+
'tenantId',
|
|
495
|
+
'tenant_id',
|
|
496
|
+
'createdAt',
|
|
497
|
+
'created_at',
|
|
498
|
+
'updatedAt',
|
|
499
|
+
'updated_at',
|
|
500
|
+
]);
|
|
501
|
+
|
|
502
|
+
function arrayOfStrings(value: unknown): string[] {
|
|
503
|
+
return Array.isArray(value)
|
|
504
|
+
? value.filter((item): item is string => typeof item === 'string')
|
|
505
|
+
: [];
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function configObject(value: unknown): Record<string, unknown> {
|
|
509
|
+
return isRecord(value) ? value : {};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function parameterRequired(
|
|
513
|
+
value: Record<string, unknown>,
|
|
514
|
+
): boolean | undefined {
|
|
515
|
+
if (typeof value.required === 'boolean') {
|
|
516
|
+
return value.required;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const meta = configObject(value._meta);
|
|
520
|
+
return typeof meta.required === 'boolean' ? meta.required : undefined;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function parameterDescription(
|
|
524
|
+
value: Record<string, unknown>,
|
|
525
|
+
): string | undefined {
|
|
526
|
+
return (
|
|
527
|
+
stringValue(value.description) ||
|
|
528
|
+
stringValue(configObject(value._meta).description) ||
|
|
529
|
+
undefined
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function parameterDefaultValue(
|
|
534
|
+
value: Record<string, unknown>,
|
|
535
|
+
): string | undefined {
|
|
536
|
+
if (!Object.hasOwn(value, 'default')) {
|
|
537
|
+
return undefined;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const defaultValue = value.default;
|
|
541
|
+
if (
|
|
542
|
+
typeof defaultValue === 'string' ||
|
|
543
|
+
typeof defaultValue === 'number' ||
|
|
544
|
+
typeof defaultValue === 'boolean'
|
|
545
|
+
) {
|
|
546
|
+
return String(defaultValue);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (defaultValue === null) {
|
|
550
|
+
return 'null';
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
try {
|
|
554
|
+
return JSON.stringify(defaultValue);
|
|
555
|
+
} catch {
|
|
556
|
+
return undefined;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function fieldParameter(
|
|
561
|
+
name: string,
|
|
562
|
+
field: Record<string, unknown>,
|
|
563
|
+
location: WorkbenchApiParameterLocation,
|
|
564
|
+
requiredOverride?: boolean,
|
|
565
|
+
): WorkbenchApiParameterSummary {
|
|
566
|
+
return {
|
|
567
|
+
name,
|
|
568
|
+
type: stringValue(field.type) || undefined,
|
|
569
|
+
required: requiredOverride ?? parameterRequired(field),
|
|
570
|
+
location,
|
|
571
|
+
description: parameterDescription(field),
|
|
572
|
+
defaultValue: parameterDefaultValue(field),
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function writableFieldEntries(
|
|
577
|
+
object: Record<string, unknown>,
|
|
578
|
+
): Array<[string, Record<string, unknown>]> {
|
|
579
|
+
const fields = configObject(object.fields);
|
|
580
|
+
const apiConfig = configObject(configObject(object.decoratorConfig).api);
|
|
581
|
+
const writableAllowlist = new Set(arrayOfStrings(apiConfig.writable));
|
|
582
|
+
const hasWritableAllowlist = writableAllowlist.size > 0;
|
|
583
|
+
|
|
584
|
+
return Object.entries(fields)
|
|
585
|
+
.filter((entry): entry is [string, Record<string, unknown>] =>
|
|
586
|
+
isRecord(entry[1]),
|
|
587
|
+
)
|
|
588
|
+
.filter(([name, field]) => {
|
|
589
|
+
if (name.startsWith('_')) {
|
|
590
|
+
return false;
|
|
591
|
+
}
|
|
592
|
+
if (SERVER_MANAGED_FIELDS.has(name)) {
|
|
593
|
+
return false;
|
|
594
|
+
}
|
|
595
|
+
if (
|
|
596
|
+
field.readonly === true ||
|
|
597
|
+
configObject(field._meta).readonly === true
|
|
598
|
+
) {
|
|
599
|
+
return false;
|
|
600
|
+
}
|
|
601
|
+
return !hasWritableAllowlist || writableAllowlist.has(name);
|
|
602
|
+
})
|
|
603
|
+
.sort(([left], [right]) => left.localeCompare(right));
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function writableFieldParameters(
|
|
607
|
+
object: Record<string, unknown>,
|
|
608
|
+
location: WorkbenchApiParameterLocation,
|
|
609
|
+
requiredMode: 'field' | 'optional',
|
|
610
|
+
): WorkbenchApiParameterSummary[] {
|
|
611
|
+
return writableFieldEntries(object).map(([name, field]) =>
|
|
612
|
+
fieldParameter(
|
|
613
|
+
name,
|
|
614
|
+
field,
|
|
615
|
+
location,
|
|
616
|
+
requiredMode === 'field' ? parameterRequired(field) : false,
|
|
617
|
+
),
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function methodParameterSummaries(
|
|
622
|
+
object: Record<string, unknown>,
|
|
623
|
+
action: string,
|
|
624
|
+
location: WorkbenchApiParameterLocation,
|
|
625
|
+
): WorkbenchApiParameterSummary[] {
|
|
626
|
+
const parameters = methodDefinition(object, action).parameters;
|
|
627
|
+
if (!Array.isArray(parameters)) {
|
|
628
|
+
return [];
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
return parameters.filter(isRecord).map((parameter) => ({
|
|
632
|
+
name: stringValue(parameter.name) || 'parameter',
|
|
633
|
+
type: stringValue(parameter.type) || undefined,
|
|
634
|
+
required: parameter.optional !== true,
|
|
635
|
+
location,
|
|
636
|
+
description: parameterDescription(parameter),
|
|
637
|
+
defaultValue: parameterDefaultValue(parameter),
|
|
638
|
+
}));
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function pathParameterNames(path: string): string[] {
|
|
642
|
+
return Array.from(path.matchAll(/\{([^}]+)\}/g))
|
|
643
|
+
.map((match) => match[1]?.trim())
|
|
644
|
+
.filter((name): name is string => Boolean(name));
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function pathParameters(path: string): WorkbenchApiParameterSummary[] {
|
|
648
|
+
return pathParameterNames(path).map((name) => ({
|
|
649
|
+
name,
|
|
650
|
+
type: 'string',
|
|
651
|
+
required: true,
|
|
652
|
+
location: 'path',
|
|
653
|
+
}));
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function restCrudParameters(
|
|
657
|
+
object: Record<string, unknown>,
|
|
658
|
+
action: string,
|
|
659
|
+
path: string,
|
|
660
|
+
): WorkbenchApiParameterSummary[] {
|
|
661
|
+
if (action === 'list') {
|
|
662
|
+
return [
|
|
663
|
+
{
|
|
664
|
+
name: 'limit',
|
|
665
|
+
type: 'integer',
|
|
666
|
+
required: false,
|
|
667
|
+
location: 'query',
|
|
668
|
+
description: 'Maximum number of items to return.',
|
|
669
|
+
defaultValue: '50',
|
|
670
|
+
},
|
|
671
|
+
{
|
|
672
|
+
name: 'offset',
|
|
673
|
+
type: 'integer',
|
|
674
|
+
required: false,
|
|
675
|
+
location: 'query',
|
|
676
|
+
description: 'Number of items to skip.',
|
|
677
|
+
defaultValue: '0',
|
|
678
|
+
},
|
|
679
|
+
];
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
if (action === 'create') {
|
|
683
|
+
return writableFieldParameters(object, 'body', 'field');
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
if (action === 'update') {
|
|
687
|
+
return [
|
|
688
|
+
...pathParameters(path),
|
|
689
|
+
...writableFieldParameters(object, 'body', 'optional'),
|
|
690
|
+
];
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
return pathParameters(path);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function restCustomParameters(
|
|
697
|
+
object: Record<string, unknown>,
|
|
698
|
+
action: string,
|
|
699
|
+
method: string,
|
|
700
|
+
path: string,
|
|
701
|
+
): WorkbenchApiParameterSummary[] {
|
|
702
|
+
const pathParams = pathParameters(path);
|
|
703
|
+
const pathParamNameSet = new Set(pathParams.map((param) => param.name));
|
|
704
|
+
const bodyOrQueryParams = methodParameterSummaries(
|
|
705
|
+
object,
|
|
706
|
+
action,
|
|
707
|
+
method === 'GET' ? 'query' : 'body',
|
|
708
|
+
).filter((param) => !pathParamNameSet.has(param.name));
|
|
709
|
+
|
|
710
|
+
return [...pathParams, ...bodyOrQueryParams];
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function cliCrudParameters(
|
|
714
|
+
object: Record<string, unknown>,
|
|
715
|
+
action: string,
|
|
716
|
+
): WorkbenchApiParameterSummary[] {
|
|
717
|
+
if (action === 'list') {
|
|
718
|
+
return [
|
|
719
|
+
{
|
|
720
|
+
name: '--limit',
|
|
721
|
+
type: 'integer',
|
|
722
|
+
required: false,
|
|
723
|
+
location: 'option',
|
|
724
|
+
description: 'Maximum number of items to return.',
|
|
725
|
+
defaultValue: '50',
|
|
726
|
+
},
|
|
727
|
+
{
|
|
728
|
+
name: '--offset',
|
|
729
|
+
type: 'integer',
|
|
730
|
+
required: false,
|
|
731
|
+
location: 'option',
|
|
732
|
+
description: 'Number of items to skip.',
|
|
733
|
+
defaultValue: '0',
|
|
734
|
+
},
|
|
735
|
+
{
|
|
736
|
+
name: '--order-by',
|
|
737
|
+
type: 'string',
|
|
738
|
+
required: false,
|
|
739
|
+
location: 'option',
|
|
740
|
+
description: 'Ordering expression, for example "created_at DESC".',
|
|
741
|
+
},
|
|
742
|
+
{
|
|
743
|
+
name: '--where',
|
|
744
|
+
type: 'object',
|
|
745
|
+
required: false,
|
|
746
|
+
location: 'option',
|
|
747
|
+
description: 'JSON filter conditions.',
|
|
748
|
+
},
|
|
749
|
+
];
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
if (action === 'get' || action === 'delete') {
|
|
753
|
+
return [
|
|
754
|
+
{
|
|
755
|
+
name: 'id',
|
|
756
|
+
type: 'string',
|
|
757
|
+
required: true,
|
|
758
|
+
location: 'argument',
|
|
759
|
+
description: 'Object ID. May also be passed as --id.',
|
|
760
|
+
},
|
|
761
|
+
];
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const bodyParameters = writableFieldParameters(
|
|
765
|
+
object,
|
|
766
|
+
'option',
|
|
767
|
+
action === 'create' ? 'field' : 'optional',
|
|
768
|
+
).map((param) => ({
|
|
769
|
+
...param,
|
|
770
|
+
name: `--${param.name}`,
|
|
771
|
+
}));
|
|
772
|
+
|
|
773
|
+
if (action === 'create') {
|
|
774
|
+
return [
|
|
775
|
+
{
|
|
776
|
+
name: '--from-file',
|
|
777
|
+
type: 'path',
|
|
778
|
+
required: false,
|
|
779
|
+
location: 'option',
|
|
780
|
+
description: 'Path to a JSON object payload.',
|
|
781
|
+
},
|
|
782
|
+
...bodyParameters,
|
|
783
|
+
];
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
return [
|
|
787
|
+
{
|
|
788
|
+
name: 'id',
|
|
789
|
+
type: 'string',
|
|
790
|
+
required: true,
|
|
791
|
+
location: 'argument',
|
|
792
|
+
description: 'Object ID. May also be passed as --id.',
|
|
793
|
+
},
|
|
794
|
+
{
|
|
795
|
+
name: '--from-file',
|
|
796
|
+
type: 'path',
|
|
797
|
+
required: false,
|
|
798
|
+
location: 'option',
|
|
799
|
+
description: 'Path to a JSON object payload.',
|
|
800
|
+
},
|
|
801
|
+
...bodyParameters,
|
|
802
|
+
];
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
function cliCustomParameters(
|
|
806
|
+
object: Record<string, unknown>,
|
|
807
|
+
action: string,
|
|
808
|
+
): WorkbenchApiParameterSummary[] {
|
|
809
|
+
const params = methodParameterSummaries(object, action, 'option').map(
|
|
810
|
+
(param) => ({
|
|
811
|
+
...param,
|
|
812
|
+
name: `--${param.name}`,
|
|
813
|
+
required: false,
|
|
814
|
+
}),
|
|
815
|
+
);
|
|
816
|
+
|
|
817
|
+
return [
|
|
818
|
+
{
|
|
819
|
+
name: 'id',
|
|
820
|
+
type: 'string',
|
|
821
|
+
required: false,
|
|
822
|
+
location: 'argument',
|
|
823
|
+
description:
|
|
824
|
+
'Optional object ID for instance actions. May also be passed as --id.',
|
|
825
|
+
},
|
|
826
|
+
...params,
|
|
827
|
+
];
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function mcpCrudParameters(
|
|
831
|
+
object: Record<string, unknown>,
|
|
832
|
+
action: string,
|
|
833
|
+
): WorkbenchApiParameterSummary[] {
|
|
834
|
+
if (action === 'list') {
|
|
835
|
+
return [
|
|
836
|
+
{
|
|
837
|
+
name: 'limit',
|
|
838
|
+
type: 'integer',
|
|
839
|
+
required: false,
|
|
840
|
+
location: 'input',
|
|
841
|
+
description: 'Maximum number of items to return.',
|
|
842
|
+
defaultValue: '50',
|
|
843
|
+
},
|
|
844
|
+
{
|
|
845
|
+
name: 'offset',
|
|
846
|
+
type: 'integer',
|
|
847
|
+
required: false,
|
|
848
|
+
location: 'input',
|
|
849
|
+
description: 'Number of items to skip.',
|
|
850
|
+
defaultValue: '0',
|
|
851
|
+
},
|
|
852
|
+
{
|
|
853
|
+
name: 'orderBy',
|
|
854
|
+
type: 'string',
|
|
855
|
+
required: false,
|
|
856
|
+
location: 'input',
|
|
857
|
+
description: 'Ordering expression, for example "created_at DESC".',
|
|
858
|
+
},
|
|
859
|
+
{
|
|
860
|
+
name: 'where',
|
|
861
|
+
type: 'object',
|
|
862
|
+
required: false,
|
|
863
|
+
location: 'input',
|
|
864
|
+
description: 'Filter conditions as key-value pairs.',
|
|
865
|
+
},
|
|
866
|
+
];
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
if (action === 'get') {
|
|
870
|
+
return [
|
|
871
|
+
{
|
|
872
|
+
name: 'id',
|
|
873
|
+
type: 'string',
|
|
874
|
+
required: true,
|
|
875
|
+
location: 'input',
|
|
876
|
+
description: 'Unique identifier of the object.',
|
|
877
|
+
},
|
|
878
|
+
{
|
|
879
|
+
name: 'slug',
|
|
880
|
+
type: 'string',
|
|
881
|
+
required: false,
|
|
882
|
+
location: 'input',
|
|
883
|
+
description: 'URL-friendly identifier of the object.',
|
|
884
|
+
},
|
|
885
|
+
];
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
if (action === 'delete') {
|
|
889
|
+
return [
|
|
890
|
+
{
|
|
891
|
+
name: 'id',
|
|
892
|
+
type: 'string',
|
|
893
|
+
required: true,
|
|
894
|
+
location: 'input',
|
|
895
|
+
description: 'ID of the object to delete.',
|
|
896
|
+
},
|
|
897
|
+
];
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
if (action === 'create') {
|
|
901
|
+
return writableFieldParameters(object, 'input', 'field');
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
return [
|
|
905
|
+
{
|
|
906
|
+
name: 'id',
|
|
907
|
+
type: 'string',
|
|
908
|
+
required: true,
|
|
909
|
+
location: 'input',
|
|
910
|
+
description: 'ID of the object to update.',
|
|
911
|
+
},
|
|
912
|
+
...writableFieldParameters(object, 'input', 'optional'),
|
|
913
|
+
];
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
function mcpCustomParameters(
|
|
917
|
+
_object: Record<string, unknown>,
|
|
918
|
+
_action: string,
|
|
919
|
+
): WorkbenchApiParameterSummary[] {
|
|
920
|
+
return [
|
|
921
|
+
{
|
|
922
|
+
name: 'id',
|
|
923
|
+
type: 'string',
|
|
924
|
+
required: true,
|
|
925
|
+
location: 'input',
|
|
926
|
+
description: 'ID of the object to execute the action on.',
|
|
927
|
+
},
|
|
928
|
+
{
|
|
929
|
+
name: 'options',
|
|
930
|
+
type: 'object',
|
|
931
|
+
required: false,
|
|
932
|
+
location: 'input',
|
|
933
|
+
description: 'Additional options for the custom action.',
|
|
934
|
+
},
|
|
935
|
+
];
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
function enabledCrudActions(config: unknown): string[] {
|
|
939
|
+
if (config === false) {
|
|
940
|
+
return [];
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
const include = arrayOfStrings(configObject(config).include);
|
|
944
|
+
const exclude = new Set(arrayOfStrings(configObject(config).exclude));
|
|
945
|
+
const actions =
|
|
946
|
+
include.length > 0
|
|
947
|
+
? include.filter((action) => CRUD_ACTIONS.includes(action as never))
|
|
948
|
+
: [...CRUD_ACTIONS];
|
|
949
|
+
|
|
950
|
+
return actions.filter((action) => !exclude.has(action));
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
function publicCustomMethodNames(object: Record<string, unknown>): string[] {
|
|
954
|
+
return Object.entries(configObject(object.methods))
|
|
955
|
+
.filter(([name, method]) => {
|
|
956
|
+
if (CRUD_ACTIONS.includes(name as never) || !isRecord(method)) {
|
|
957
|
+
return false;
|
|
958
|
+
}
|
|
959
|
+
return method.isPublic === true;
|
|
960
|
+
})
|
|
961
|
+
.map(([name]) => name)
|
|
962
|
+
.sort();
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
function enabledCustomActions(
|
|
966
|
+
object: Record<string, unknown>,
|
|
967
|
+
surface: 'api' | 'cli' | 'mcp',
|
|
968
|
+
): string[] {
|
|
969
|
+
const decoratorConfig = configObject(object.decoratorConfig);
|
|
970
|
+
const surfaceConfig = decoratorConfig[surface];
|
|
971
|
+
if (surfaceConfig === false) {
|
|
972
|
+
return [];
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
const include = arrayOfStrings(configObject(surfaceConfig).include);
|
|
976
|
+
const exclude = new Set(arrayOfStrings(configObject(surfaceConfig).exclude));
|
|
977
|
+
const publicMethods = publicCustomMethodNames(object);
|
|
978
|
+
|
|
979
|
+
if (surface === 'mcp' && include.length > 0) {
|
|
980
|
+
return include
|
|
981
|
+
.filter((name) => !CRUD_ACTIONS.includes(name as never))
|
|
982
|
+
.filter((name) => publicMethods.includes(name))
|
|
983
|
+
.filter((name) => !exclude.has(name));
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
if (surface === 'api') {
|
|
987
|
+
return publicMethods
|
|
988
|
+
.filter((name) => include.length === 0 || include.includes(name))
|
|
989
|
+
.filter((name) => !exclude.has(name));
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
const customMethodsInInclude = include.filter(
|
|
993
|
+
(name) => !CRUD_ACTIONS.includes(name as never),
|
|
994
|
+
);
|
|
995
|
+
return publicMethods
|
|
996
|
+
.filter(
|
|
997
|
+
(name) =>
|
|
998
|
+
customMethodsInInclude.length === 0 ||
|
|
999
|
+
customMethodsInInclude.includes(name),
|
|
1000
|
+
)
|
|
1001
|
+
.filter((name) => !exclude.has(name));
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
function routeOverrides(
|
|
1005
|
+
object: Record<string, unknown>,
|
|
1006
|
+
): Record<string, Record<string, unknown>> {
|
|
1007
|
+
const apiConfig = configObject(configObject(object.decoratorConfig).api);
|
|
1008
|
+
const routes = configObject(apiConfig.routes);
|
|
1009
|
+
|
|
1010
|
+
return Object.fromEntries(
|
|
1011
|
+
Object.entries(routes).filter(
|
|
1012
|
+
(entry): entry is [string, Record<string, unknown>] => isRecord(entry[1]),
|
|
1013
|
+
),
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
function methodDefinition(
|
|
1018
|
+
object: Record<string, unknown>,
|
|
1019
|
+
action: string,
|
|
1020
|
+
): Record<string, unknown> {
|
|
1021
|
+
return configObject(configObject(object.methods)[action]);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
function customRoutePath(
|
|
1025
|
+
object: Record<string, unknown>,
|
|
1026
|
+
action: string,
|
|
1027
|
+
): string {
|
|
1028
|
+
const collection =
|
|
1029
|
+
stringValue(object.collection) || stringValue(object.name) || action;
|
|
1030
|
+
const routeConfig = routeOverrides(object)[action] || {};
|
|
1031
|
+
const method = methodDefinition(object, action);
|
|
1032
|
+
const configuredPath = stringValue(routeConfig.path) || action;
|
|
1033
|
+
const normalizedPath = configuredPath
|
|
1034
|
+
.split('/')
|
|
1035
|
+
.map((segment) => segment.trim())
|
|
1036
|
+
.filter(Boolean)
|
|
1037
|
+
.join('/')
|
|
1038
|
+
.replace(/\[([^\]]+)\]/g, '{$1}');
|
|
1039
|
+
const scope =
|
|
1040
|
+
stringValue(routeConfig.scope) ||
|
|
1041
|
+
(method.isStatic === true ? 'collection' : 'item');
|
|
1042
|
+
|
|
1043
|
+
return scope === 'collection'
|
|
1044
|
+
? `/api/v1/${collection}/${normalizedPath}`
|
|
1045
|
+
: `/api/v1/${collection}/{id}/${normalizedPath}`;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
function restEndpointsFrom(
|
|
1049
|
+
object: Record<string, unknown>,
|
|
1050
|
+
): WorkbenchRestEndpointSummary[] {
|
|
1051
|
+
const className =
|
|
1052
|
+
stringValue(object.className) || stringValue(object.name) || 'Object';
|
|
1053
|
+
const collection = stringValue(object.collection) || className.toLowerCase();
|
|
1054
|
+
const apiConfig = configObject(object.decoratorConfig).api;
|
|
1055
|
+
const endpoints: WorkbenchRestEndpointSummary[] = [];
|
|
1056
|
+
|
|
1057
|
+
const crudActions =
|
|
1058
|
+
stringValue(object.extends) === 'SmrtCollection'
|
|
1059
|
+
? []
|
|
1060
|
+
: enabledCrudActions(apiConfig);
|
|
1061
|
+
for (const action of crudActions) {
|
|
1062
|
+
const route =
|
|
1063
|
+
action === 'list'
|
|
1064
|
+
? ['GET', `/api/v1/${collection}`, `List ${className} objects`]
|
|
1065
|
+
: action === 'create'
|
|
1066
|
+
? ['POST', `/api/v1/${collection}`, `Create ${className}`]
|
|
1067
|
+
: action === 'get'
|
|
1068
|
+
? ['GET', `/api/v1/${collection}/{id}`, `Get ${className} by ID`]
|
|
1069
|
+
: action === 'update'
|
|
1070
|
+
? ['PUT', `/api/v1/${collection}/{id}`, `Update ${className}`]
|
|
1071
|
+
: ['DELETE', `/api/v1/${collection}/{id}`, `Delete ${className}`];
|
|
1072
|
+
|
|
1073
|
+
endpoints.push({
|
|
1074
|
+
objectName: className,
|
|
1075
|
+
action,
|
|
1076
|
+
method: route[0],
|
|
1077
|
+
path: route[1],
|
|
1078
|
+
description: route[2],
|
|
1079
|
+
parameters: restCrudParameters(object, action, route[1]),
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
const overrides = routeOverrides(object);
|
|
1084
|
+
for (const action of enabledCustomActions(object, 'api')) {
|
|
1085
|
+
const routeConfig = overrides[action] || {};
|
|
1086
|
+
const method = stringValue(routeConfig.method) || 'POST';
|
|
1087
|
+
endpoints.push({
|
|
1088
|
+
objectName: className,
|
|
1089
|
+
action,
|
|
1090
|
+
method,
|
|
1091
|
+
path: customRoutePath(object, action),
|
|
1092
|
+
description: `Run ${className}.${action}`,
|
|
1093
|
+
parameters: restCustomParameters(
|
|
1094
|
+
object,
|
|
1095
|
+
action,
|
|
1096
|
+
method,
|
|
1097
|
+
customRoutePath(object, action),
|
|
1098
|
+
),
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
return endpoints;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
function cliCommandsFrom(
|
|
1106
|
+
object: Record<string, unknown>,
|
|
1107
|
+
): WorkbenchCliCommandSummary[] {
|
|
1108
|
+
const className =
|
|
1109
|
+
stringValue(object.className) || stringValue(object.name) || 'Object';
|
|
1110
|
+
const lowerName = className.toLowerCase();
|
|
1111
|
+
const cliConfig = configObject(object.decoratorConfig).cli;
|
|
1112
|
+
const commands: WorkbenchCliCommandSummary[] = [];
|
|
1113
|
+
|
|
1114
|
+
for (const action of enabledCrudActions(cliConfig)) {
|
|
1115
|
+
const description =
|
|
1116
|
+
action === 'list'
|
|
1117
|
+
? `List ${className} objects`
|
|
1118
|
+
: action === 'get'
|
|
1119
|
+
? `Get ${className} by ID or slug`
|
|
1120
|
+
: action === 'create'
|
|
1121
|
+
? `Create new ${className}`
|
|
1122
|
+
: action === 'update'
|
|
1123
|
+
? `Update ${className}`
|
|
1124
|
+
: `Delete ${className}`;
|
|
1125
|
+
|
|
1126
|
+
commands.push({
|
|
1127
|
+
objectName: className,
|
|
1128
|
+
action,
|
|
1129
|
+
command: `${lowerName}:${action}`,
|
|
1130
|
+
description,
|
|
1131
|
+
parameters: cliCrudParameters(object, action),
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
for (const action of enabledCustomActions(object, 'cli')) {
|
|
1136
|
+
commands.push({
|
|
1137
|
+
objectName: className,
|
|
1138
|
+
action,
|
|
1139
|
+
command: `${lowerName}:${action}`,
|
|
1140
|
+
description: `Run ${className}.${action}`,
|
|
1141
|
+
parameters: cliCustomParameters(object, action),
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
return commands;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
function mcpToolsFrom(
|
|
1149
|
+
object: Record<string, unknown>,
|
|
1150
|
+
): WorkbenchMcpToolSummary[] {
|
|
1151
|
+
const className =
|
|
1152
|
+
stringValue(object.className) || stringValue(object.name) || 'Object';
|
|
1153
|
+
const lowerName = className.toLowerCase();
|
|
1154
|
+
const mcpConfig = configObject(object.decoratorConfig).mcp;
|
|
1155
|
+
const tools: WorkbenchMcpToolSummary[] = [];
|
|
1156
|
+
|
|
1157
|
+
for (const action of enabledCrudActions(mcpConfig)) {
|
|
1158
|
+
const description =
|
|
1159
|
+
action === 'list'
|
|
1160
|
+
? `List ${className} objects with optional filtering`
|
|
1161
|
+
: action === 'get'
|
|
1162
|
+
? `Get a specific ${className} by ID or slug`
|
|
1163
|
+
: action === 'create'
|
|
1164
|
+
? `Create a new ${className}`
|
|
1165
|
+
: action === 'update'
|
|
1166
|
+
? `Update an existing ${className}`
|
|
1167
|
+
: `Delete a ${className} by ID`;
|
|
1168
|
+
|
|
1169
|
+
tools.push({
|
|
1170
|
+
objectName: className,
|
|
1171
|
+
action,
|
|
1172
|
+
toolName: `${lowerName}_${action}`,
|
|
1173
|
+
description,
|
|
1174
|
+
parameters: mcpCrudParameters(object, action),
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
for (const action of enabledCustomActions(object, 'mcp')) {
|
|
1179
|
+
tools.push({
|
|
1180
|
+
objectName: className,
|
|
1181
|
+
action,
|
|
1182
|
+
toolName: `${lowerName}_${action}`.toLowerCase(),
|
|
1183
|
+
description: `Execute ${action} action on ${className}`,
|
|
1184
|
+
parameters: mcpCustomParameters(object, action),
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
return tools;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
function readApiSummary(
|
|
1192
|
+
packageDir: string,
|
|
1193
|
+
rootDir: string,
|
|
1194
|
+
packageName: string,
|
|
1195
|
+
knowledge: WorkbenchKnowledgeSummary,
|
|
1196
|
+
routeFiles: string[],
|
|
1197
|
+
): WorkbenchApiSummary {
|
|
1198
|
+
const manifest = knowledge.manifestPath
|
|
1199
|
+
? readJsonIfExists<Record<string, unknown>>(knowledge.manifestPath)
|
|
1200
|
+
: null;
|
|
1201
|
+
const objectRecords = objectRecordsFrom(manifest?.objects);
|
|
1202
|
+
|
|
1203
|
+
const objects: WorkbenchApiObjectSummary[] = objectRecords.map((object) => {
|
|
1204
|
+
const name =
|
|
1205
|
+
stringValue(object.className) ||
|
|
1206
|
+
stringValue(object.name) ||
|
|
1207
|
+
stringValue(object.key) ||
|
|
1208
|
+
'unknown';
|
|
1209
|
+
const className = stringValue(object.className) || name;
|
|
1210
|
+
const sourcePath = resolveManifestSourcePath(
|
|
1211
|
+
rootDir,
|
|
1212
|
+
packageDir,
|
|
1213
|
+
stringValue(object.filePath) || stringValue(object.sourcePath),
|
|
1214
|
+
);
|
|
1215
|
+
const typedocPath = findTypedocClassPath(
|
|
1216
|
+
rootDir,
|
|
1217
|
+
packageDir,
|
|
1218
|
+
packageName,
|
|
1219
|
+
className,
|
|
1220
|
+
);
|
|
1221
|
+
const fields = fieldSummariesFrom(object.fields);
|
|
1222
|
+
|
|
1223
|
+
return {
|
|
1224
|
+
name,
|
|
1225
|
+
className,
|
|
1226
|
+
qualifiedName: stringValue(object.qualifiedName) || undefined,
|
|
1227
|
+
collection: stringValue(object.collection) || undefined,
|
|
1228
|
+
sourcePath,
|
|
1229
|
+
typedocPath,
|
|
1230
|
+
description: readTypedocSummary(typedocPath),
|
|
1231
|
+
fields,
|
|
1232
|
+
};
|
|
1233
|
+
});
|
|
1234
|
+
|
|
1235
|
+
const objectNames =
|
|
1236
|
+
objects.length > 0
|
|
1237
|
+
? objects
|
|
1238
|
+
.map(
|
|
1239
|
+
(object) => object.qualifiedName || object.className || object.name,
|
|
1240
|
+
)
|
|
1241
|
+
.filter(Boolean)
|
|
1242
|
+
.sort()
|
|
1243
|
+
: knowledge.objectNames;
|
|
1244
|
+
const restEndpoints = objectRecords.flatMap(restEndpointsFrom);
|
|
1245
|
+
|
|
1246
|
+
return {
|
|
1247
|
+
objectNames,
|
|
1248
|
+
objects,
|
|
1249
|
+
restEndpoints,
|
|
1250
|
+
cliCommands: objectRecords.flatMap(cliCommandsFrom),
|
|
1251
|
+
mcpTools: objectRecords.flatMap(mcpToolsFrom),
|
|
1252
|
+
endpointCount: restEndpoints.length,
|
|
1253
|
+
routeFiles,
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
async function collectRouteFiles(packageDir: string): Promise<string[]> {
|
|
1258
|
+
return fg(['src/routes/**/*.{ts,svelte}', 'routes/**/*.{ts,svelte}'], {
|
|
1259
|
+
cwd: packageDir,
|
|
1260
|
+
absolute: true,
|
|
1261
|
+
onlyFiles: true,
|
|
1262
|
+
ignore: ['**/node_modules/**', '**/.svelte-kit/**'],
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
async function collectMigrations(packageDir: string): Promise<string[]> {
|
|
1267
|
+
return fg(['migrations/**/*', 'src/migrations/**/*'], {
|
|
1268
|
+
cwd: packageDir,
|
|
1269
|
+
absolute: true,
|
|
1270
|
+
onlyFiles: true,
|
|
1271
|
+
ignore: ['**/node_modules/**'],
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
async function collectExamples(
|
|
1276
|
+
packageDir: string,
|
|
1277
|
+
docs: WorkbenchDocumentSummary[],
|
|
1278
|
+
): Promise<WorkbenchExampleSummary[]> {
|
|
1279
|
+
const fileMatches = await fg(
|
|
1280
|
+
[
|
|
1281
|
+
'examples/**/*.{ts,tsx,svelte,md}',
|
|
1282
|
+
'src/**/*.{example,stories}.{ts,tsx,svelte}',
|
|
1283
|
+
'src/**/examples/**/*.{ts,tsx,svelte,md}',
|
|
1284
|
+
],
|
|
1285
|
+
{
|
|
1286
|
+
cwd: packageDir,
|
|
1287
|
+
absolute: true,
|
|
1288
|
+
onlyFiles: true,
|
|
1289
|
+
ignore: ['**/node_modules/**', '**/dist/**', '**/.svelte-kit/**'],
|
|
1290
|
+
},
|
|
1291
|
+
);
|
|
1292
|
+
|
|
1293
|
+
const fileExamples = fileMatches.slice(0, 16).map((path, index) => ({
|
|
1294
|
+
id: `file:${index}`,
|
|
1295
|
+
title: relative(packageDir, path),
|
|
1296
|
+
path,
|
|
1297
|
+
source: 'file' as const,
|
|
1298
|
+
}));
|
|
1299
|
+
|
|
1300
|
+
const readme = docs.find((doc) => doc.kind === 'readme');
|
|
1301
|
+
const readmeExamples = readme?.content
|
|
1302
|
+
? extractReadmeCodeBlocks(readme.content).slice(0, 8)
|
|
1303
|
+
: [];
|
|
1304
|
+
|
|
1305
|
+
return [...fileExamples, ...readmeExamples];
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
function extractReadmeCodeBlocks(content: string): WorkbenchExampleSummary[] {
|
|
1309
|
+
const examples: WorkbenchExampleSummary[] = [];
|
|
1310
|
+
const blockPattern = /```([A-Za-z0-9_-]*)\n([\s\S]*?)```/g;
|
|
1311
|
+
let index = 0;
|
|
1312
|
+
let match = blockPattern.exec(content);
|
|
1313
|
+
|
|
1314
|
+
while (match) {
|
|
1315
|
+
const language = match[1] || undefined;
|
|
1316
|
+
const code = match[2] || '';
|
|
1317
|
+
if (!code.trim()) {
|
|
1318
|
+
match = blockPattern.exec(content);
|
|
1319
|
+
continue;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
const { content: trimmedCode } = truncate(code, EXAMPLE_LIMIT);
|
|
1323
|
+
examples.push({
|
|
1324
|
+
id: `readme:${index}`,
|
|
1325
|
+
title: `README example ${index + 1}`,
|
|
1326
|
+
language,
|
|
1327
|
+
code: trimmedCode,
|
|
1328
|
+
source: 'readme',
|
|
1329
|
+
});
|
|
1330
|
+
index += 1;
|
|
1331
|
+
match = blockPattern.exec(content);
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
return examples;
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
async function summarizePackage(
|
|
1338
|
+
input: WorkbenchPackageDir,
|
|
1339
|
+
rootDir: string,
|
|
1340
|
+
): Promise<WorkbenchPackageSummary> {
|
|
1341
|
+
const { packageDir, packageJson, source } = input;
|
|
1342
|
+
const packageName =
|
|
1343
|
+
packageJson.name || relative(rootDir, packageDir) || packageDir;
|
|
1344
|
+
const docs = [
|
|
1345
|
+
readDocument(packageDir, 'README.md', 'readme'),
|
|
1346
|
+
readDocument(packageDir, 'AGENTS.md', 'agents'),
|
|
1347
|
+
readDocument(packageDir, 'CHANGELOG.md', 'changelog'),
|
|
1348
|
+
].filter((doc): doc is WorkbenchDocumentSummary => Boolean(doc));
|
|
1349
|
+
const examples = await collectExamples(packageDir, docs);
|
|
1350
|
+
const knowledge = readKnowledgeSummary(packageDir);
|
|
1351
|
+
const routeFiles = await collectRouteFiles(packageDir);
|
|
1352
|
+
const migrations = await collectMigrations(packageDir);
|
|
1353
|
+
const scripts = stringRecord(packageJson.scripts);
|
|
1354
|
+
const api = readApiSummary(
|
|
1355
|
+
packageDir,
|
|
1356
|
+
rootDir,
|
|
1357
|
+
packageName,
|
|
1358
|
+
knowledge,
|
|
1359
|
+
routeFiles,
|
|
1360
|
+
);
|
|
1361
|
+
|
|
1362
|
+
return {
|
|
1363
|
+
name: packageName,
|
|
1364
|
+
version: packageJson.version,
|
|
1365
|
+
description: packageJson.description,
|
|
1366
|
+
source,
|
|
1367
|
+
directory: packageDir,
|
|
1368
|
+
relativeDirectory: relative(rootDir, packageDir) || '.',
|
|
1369
|
+
scripts,
|
|
1370
|
+
dependencies: stringRecord(packageJson.dependencies),
|
|
1371
|
+
devDependencies: stringRecord(packageJson.devDependencies),
|
|
1372
|
+
peerDependencies: stringRecord(packageJson.peerDependencies),
|
|
1373
|
+
smrtDependencies: smrtDependencyNames(packageJson),
|
|
1374
|
+
sdkDependencies: sdkDependencyNames(packageJson),
|
|
1375
|
+
exportKeys: exportKeys(packageJson.exports),
|
|
1376
|
+
docs,
|
|
1377
|
+
examples,
|
|
1378
|
+
knowledge,
|
|
1379
|
+
api,
|
|
1380
|
+
migrations,
|
|
1381
|
+
routeModuleCount: 0,
|
|
1382
|
+
routeCount: 0,
|
|
1383
|
+
playgroundEntryCount: 0,
|
|
1384
|
+
recommendedCommands: Object.keys(scripts)
|
|
1385
|
+
.filter((scriptName) =>
|
|
1386
|
+
['test', 'typecheck', 'check', 'build', 'dev', 'workbench'].includes(
|
|
1387
|
+
scriptName,
|
|
1388
|
+
),
|
|
1389
|
+
)
|
|
1390
|
+
.sort()
|
|
1391
|
+
.map((scriptName) => ({
|
|
1392
|
+
id: commandIdForScript(packageName, scriptName),
|
|
1393
|
+
label: scriptName,
|
|
1394
|
+
command: `pnpm --filter ${packageName} ${scriptName}`,
|
|
1395
|
+
})),
|
|
1396
|
+
};
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
async function discoverWorkspacePackageDirs(
|
|
1400
|
+
workspaceRoot: string,
|
|
1401
|
+
): Promise<WorkbenchPackageDir[]> {
|
|
1402
|
+
const packageJsonPaths = await fg('packages/*/package.json', {
|
|
1403
|
+
cwd: workspaceRoot,
|
|
1404
|
+
absolute: true,
|
|
1405
|
+
onlyFiles: true,
|
|
1406
|
+
});
|
|
1407
|
+
|
|
1408
|
+
return packageJsonPaths
|
|
1409
|
+
.map((packageJsonPath) => ({
|
|
1410
|
+
packageDir: dirname(packageJsonPath),
|
|
1411
|
+
packageJson: readJson<PackageJsonLike>(packageJsonPath),
|
|
1412
|
+
source: 'workspace' as const,
|
|
1413
|
+
}))
|
|
1414
|
+
.filter(
|
|
1415
|
+
(item) =>
|
|
1416
|
+
typeof item.packageJson.name === 'string' &&
|
|
1417
|
+
item.packageJson.name.startsWith('@happyvertical/smrt-'),
|
|
1418
|
+
)
|
|
1419
|
+
.sort((left, right) =>
|
|
1420
|
+
(left.packageJson.name || '').localeCompare(right.packageJson.name || ''),
|
|
1421
|
+
);
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
function resolveNodeModulePackageDir(
|
|
1425
|
+
projectRoot: string,
|
|
1426
|
+
packageName: string,
|
|
1427
|
+
): string | null {
|
|
1428
|
+
const packageJsonPath = join(
|
|
1429
|
+
projectRoot,
|
|
1430
|
+
'node_modules',
|
|
1431
|
+
packageName,
|
|
1432
|
+
'package.json',
|
|
1433
|
+
);
|
|
1434
|
+
return existsSync(packageJsonPath) ? dirname(packageJsonPath) : null;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
async function discoverConsumerPackageDirs(
|
|
1438
|
+
projectRoot: string,
|
|
1439
|
+
): Promise<WorkbenchPackageDir[]> {
|
|
1440
|
+
const packageJsonPath = join(projectRoot, 'package.json');
|
|
1441
|
+
if (!existsSync(packageJsonPath)) {
|
|
1442
|
+
return [];
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
const packageJson = readJson<PackageJsonLike>(packageJsonPath);
|
|
1446
|
+
const dependencies = {
|
|
1447
|
+
...packageJson.dependencies,
|
|
1448
|
+
...packageJson.devDependencies,
|
|
1449
|
+
...packageJson.peerDependencies,
|
|
1450
|
+
};
|
|
1451
|
+
const packageDirs: WorkbenchPackageDir[] = [];
|
|
1452
|
+
|
|
1453
|
+
packageDirs.push({
|
|
1454
|
+
packageDir: projectRoot,
|
|
1455
|
+
packageJson,
|
|
1456
|
+
source: packageJson.name?.startsWith('@happyvertical/smrt-')
|
|
1457
|
+
? 'package'
|
|
1458
|
+
: 'app',
|
|
1459
|
+
});
|
|
1460
|
+
|
|
1461
|
+
for (const dependencyName of Object.keys(dependencies).sort()) {
|
|
1462
|
+
if (
|
|
1463
|
+
!dependencyName.startsWith('@happyvertical/smrt-') ||
|
|
1464
|
+
dependencyName === '@happyvertical/smrt-workbench'
|
|
1465
|
+
) {
|
|
1466
|
+
continue;
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
const packageDir = resolveNodeModulePackageDir(projectRoot, dependencyName);
|
|
1470
|
+
if (!packageDir) {
|
|
1471
|
+
continue;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
packageDirs.push({
|
|
1475
|
+
packageDir,
|
|
1476
|
+
packageJson: readJson<PackageJsonLike>(join(packageDir, 'package.json')),
|
|
1477
|
+
source: 'package',
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
return packageDirs;
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
function resolvePackageByName(
|
|
1485
|
+
workspaceRoot: string,
|
|
1486
|
+
packageName: string,
|
|
1487
|
+
): string | null {
|
|
1488
|
+
const packagesDir = join(workspaceRoot, 'packages');
|
|
1489
|
+
if (!existsSync(packagesDir)) {
|
|
1490
|
+
return null;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
const packageJsonPaths = fg.sync('*/package.json', {
|
|
1494
|
+
cwd: packagesDir,
|
|
1495
|
+
absolute: true,
|
|
1496
|
+
onlyFiles: true,
|
|
1497
|
+
});
|
|
1498
|
+
|
|
1499
|
+
for (const packageJsonPath of packageJsonPaths) {
|
|
1500
|
+
const packageJson = readJson<PackageJsonLike>(packageJsonPath);
|
|
1501
|
+
if (packageJson.name === packageName) {
|
|
1502
|
+
return dirname(packageJsonPath);
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
return null;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
export function resolveWorkbenchScope(
|
|
1510
|
+
cwd = process.cwd(),
|
|
1511
|
+
options: Pick<
|
|
1512
|
+
SmrtWorkbenchVitePluginOptions,
|
|
1513
|
+
'projectRoot' | 'workspaceRoot' | 'packageName'
|
|
1514
|
+
> = {},
|
|
1515
|
+
): WorkbenchScopeResolution {
|
|
1516
|
+
const resolvedCwd = resolve(cwd);
|
|
1517
|
+
const requestedProjectRoot = options.projectRoot
|
|
1518
|
+
? resolve(options.projectRoot)
|
|
1519
|
+
: undefined;
|
|
1520
|
+
const workspaceRoot =
|
|
1521
|
+
options.workspaceRoot ||
|
|
1522
|
+
(requestedProjectRoot
|
|
1523
|
+
? findSmrtWorkbenchWorkspaceRoot(requestedProjectRoot)
|
|
1524
|
+
: null) ||
|
|
1525
|
+
findSmrtWorkbenchWorkspaceRoot(resolvedCwd);
|
|
1526
|
+
|
|
1527
|
+
if (workspaceRoot) {
|
|
1528
|
+
const packageDir =
|
|
1529
|
+
(options.packageName
|
|
1530
|
+
? resolvePackageByName(workspaceRoot, options.packageName)
|
|
1531
|
+
: null) ||
|
|
1532
|
+
findPackageDir(resolvedCwd, workspaceRoot) ||
|
|
1533
|
+
undefined;
|
|
1534
|
+
const packageJson = packageDir
|
|
1535
|
+
? readJsonIfExists<PackageJsonLike>(join(packageDir, 'package.json'))
|
|
1536
|
+
: null;
|
|
1537
|
+
const packageName = options.packageName || packageJson?.name;
|
|
1538
|
+
const mode: SmrtWorkbenchScopeMode = packageName ? 'package' : 'workspace';
|
|
1539
|
+
|
|
1540
|
+
return {
|
|
1541
|
+
mode,
|
|
1542
|
+
cwd: resolvedCwd,
|
|
1543
|
+
projectRoot: workspaceRoot,
|
|
1544
|
+
workspaceRoot,
|
|
1545
|
+
packageName,
|
|
1546
|
+
packageDir,
|
|
1547
|
+
packageManager: 'pnpm',
|
|
1548
|
+
};
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
const projectRoot = requestedProjectRoot || findProjectRoot(resolvedCwd);
|
|
1552
|
+
return {
|
|
1553
|
+
mode: 'consumer',
|
|
1554
|
+
cwd: resolvedCwd,
|
|
1555
|
+
projectRoot,
|
|
1556
|
+
packageName: options.packageName,
|
|
1557
|
+
packageManager: detectPackageManager(projectRoot),
|
|
1558
|
+
};
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
export async function buildWorkbenchProject(
|
|
1562
|
+
scope: WorkbenchScopeResolution,
|
|
1563
|
+
): Promise<SmrtWorkbenchProject> {
|
|
1564
|
+
const packageDirs =
|
|
1565
|
+
scope.mode === 'consumer'
|
|
1566
|
+
? await discoverConsumerPackageDirs(scope.projectRoot)
|
|
1567
|
+
: await discoverWorkspacePackageDirs(scope.projectRoot);
|
|
1568
|
+
|
|
1569
|
+
const filteredPackageDirs = scope.packageName
|
|
1570
|
+
? packageDirs.filter((item) => item.packageJson.name === scope.packageName)
|
|
1571
|
+
: packageDirs;
|
|
1572
|
+
|
|
1573
|
+
const packages = await Promise.all(
|
|
1574
|
+
filteredPackageDirs.map((item) =>
|
|
1575
|
+
summarizePackage(item, scope.projectRoot),
|
|
1576
|
+
),
|
|
1577
|
+
);
|
|
1578
|
+
|
|
1579
|
+
return {
|
|
1580
|
+
generatedAt: new Date().toISOString(),
|
|
1581
|
+
scope,
|
|
1582
|
+
packages,
|
|
1583
|
+
};
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
export async function discoverWorkspaceWorkbenches(
|
|
1587
|
+
workspaceRoot: string,
|
|
1588
|
+
packagesPattern = 'packages/*/src/workbench.ts',
|
|
1589
|
+
): Promise<DiscoveredWorkbenchTarget[]> {
|
|
1590
|
+
const matches = await fg(packagesPattern, {
|
|
1591
|
+
cwd: workspaceRoot,
|
|
1592
|
+
absolute: true,
|
|
1593
|
+
onlyFiles: true,
|
|
1594
|
+
});
|
|
1595
|
+
|
|
1596
|
+
return matches.sort().map((sourcePath) => {
|
|
1597
|
+
const packageDir = dirname(dirname(sourcePath));
|
|
1598
|
+
const packageJsonPath = join(packageDir, 'package.json');
|
|
1599
|
+
const packageJson = readJson<PackageJsonLike>(packageJsonPath);
|
|
1600
|
+
const runtimePath = join(packageDir, 'dist', 'workbench.js');
|
|
1601
|
+
|
|
1602
|
+
return {
|
|
1603
|
+
packageName: packageJson.name,
|
|
1604
|
+
source: 'workspace' as const,
|
|
1605
|
+
sourcePath,
|
|
1606
|
+
runtimePath: existsSync(runtimePath) ? runtimePath : undefined,
|
|
1607
|
+
};
|
|
1608
|
+
});
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
export async function discoverInstalledWorkbenches(
|
|
1612
|
+
projectRoot = process.cwd(),
|
|
1613
|
+
): Promise<DiscoveredWorkbenchTarget[]> {
|
|
1614
|
+
const packageJsonPath = join(projectRoot, 'package.json');
|
|
1615
|
+
if (!existsSync(packageJsonPath)) {
|
|
1616
|
+
return [];
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
const packageJson = readJson<PackageJsonLike>(packageJsonPath);
|
|
1620
|
+
const dependencies = {
|
|
1621
|
+
...packageJson.dependencies,
|
|
1622
|
+
...packageJson.devDependencies,
|
|
1623
|
+
...packageJson.peerDependencies,
|
|
1624
|
+
};
|
|
1625
|
+
const discovered: DiscoveredWorkbenchTarget[] = [];
|
|
1626
|
+
|
|
1627
|
+
for (const dependencyName of Object.keys(dependencies).sort()) {
|
|
1628
|
+
if (
|
|
1629
|
+
!dependencyName.startsWith('@happyvertical/smrt-') ||
|
|
1630
|
+
dependencyName === '@happyvertical/smrt-workbench'
|
|
1631
|
+
) {
|
|
1632
|
+
continue;
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
const packageDir = resolveNodeModulePackageDir(projectRoot, dependencyName);
|
|
1636
|
+
if (!packageDir) {
|
|
1637
|
+
continue;
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
const dependencyPackageJson = readJson<PackageJsonLike>(
|
|
1641
|
+
join(packageDir, 'package.json'),
|
|
1642
|
+
);
|
|
1643
|
+
if (exportKeys(dependencyPackageJson.exports).includes('./workbench')) {
|
|
1644
|
+
discovered.push({
|
|
1645
|
+
packageName: dependencyName,
|
|
1646
|
+
source: 'package',
|
|
1647
|
+
importSpecifier: `${dependencyName}/workbench`,
|
|
1648
|
+
});
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
return discovered;
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
export async function discoverWorkbenchTargets(
|
|
1656
|
+
projectRoot = process.cwd(),
|
|
1657
|
+
mode: 'auto' | 'workspace' | 'consumer' = 'auto',
|
|
1658
|
+
localWorkbenchPath = 'src/workbench.ts',
|
|
1659
|
+
packageName?: string,
|
|
1660
|
+
packagesPattern = 'packages/*/src/workbench.ts',
|
|
1661
|
+
): Promise<DiscoveredWorkbenchTarget[]> {
|
|
1662
|
+
const effectiveMode =
|
|
1663
|
+
mode === 'auto' ? detectWorkbenchMode(projectRoot) : mode;
|
|
1664
|
+
|
|
1665
|
+
if (effectiveMode === 'workspace') {
|
|
1666
|
+
const workspaceRoot =
|
|
1667
|
+
mode === 'workspace'
|
|
1668
|
+
? findWorkspaceRoot(projectRoot)
|
|
1669
|
+
: findSmrtWorkbenchWorkspaceRoot(projectRoot);
|
|
1670
|
+
if (!workspaceRoot) {
|
|
1671
|
+
return [];
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
const targets = await discoverWorkspaceWorkbenches(
|
|
1675
|
+
workspaceRoot,
|
|
1676
|
+
packagesPattern,
|
|
1677
|
+
);
|
|
1678
|
+
return packageName
|
|
1679
|
+
? targets.filter((target) => target.packageName === packageName)
|
|
1680
|
+
: targets;
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
const targets = await discoverInstalledWorkbenches(projectRoot);
|
|
1684
|
+
const localPath = resolve(projectRoot, localWorkbenchPath);
|
|
1685
|
+
if (existsSync(localPath)) {
|
|
1686
|
+
const localPackageJson = readJsonIfExists<PackageJsonLike>(
|
|
1687
|
+
join(projectRoot, 'package.json'),
|
|
1688
|
+
);
|
|
1689
|
+
targets.push({
|
|
1690
|
+
packageName: localPackageJson?.name,
|
|
1691
|
+
source: 'app',
|
|
1692
|
+
sourcePath: localPath,
|
|
1693
|
+
});
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
return packageName
|
|
1697
|
+
? targets.filter((target) => target.packageName === packageName)
|
|
1698
|
+
: targets;
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
export async function importWorkbenchModule(
|
|
1702
|
+
input: string,
|
|
1703
|
+
): Promise<SmrtWorkbenchModule[]> {
|
|
1704
|
+
const imported =
|
|
1705
|
+
isAbsolute(input) || input.startsWith('.')
|
|
1706
|
+
? await importPathModule(resolve(input))
|
|
1707
|
+
: await import(/* @vite-ignore */ input);
|
|
1708
|
+
|
|
1709
|
+
const module = imported.default ?? imported.workbench ?? imported;
|
|
1710
|
+
return module && typeof module === 'object'
|
|
1711
|
+
? coerceWorkbenchModules(module as SmrtWorkbenchModule)
|
|
1712
|
+
: [];
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
async function importPathModule(inputPath: string): Promise<unknown> {
|
|
1716
|
+
if (!TS_SOURCE_EXTENSIONS.has(extname(inputPath))) {
|
|
1717
|
+
return import(/* @vite-ignore */ pathToFileURL(inputPath).href);
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
let tsxApiPath: string;
|
|
1721
|
+
try {
|
|
1722
|
+
tsxApiPath = require.resolve('tsx/esm/api');
|
|
1723
|
+
} catch (tsxError) {
|
|
1724
|
+
throw new Error(
|
|
1725
|
+
`Failed to load workbench module from ${inputPath}: source workbench discovery requires the "tsx" package.`,
|
|
1726
|
+
{ cause: tsxError },
|
|
1727
|
+
);
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
const { tsImport } = await import(
|
|
1731
|
+
/* @vite-ignore */ pathToFileURL(tsxApiPath).href
|
|
1732
|
+
);
|
|
1733
|
+
return tsImport(pathToFileURL(inputPath).href, {
|
|
1734
|
+
parentURL: import.meta.url,
|
|
1735
|
+
});
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
export function describeWorkbenchSource(
|
|
1739
|
+
target: DiscoveredWorkbenchTarget,
|
|
1740
|
+
cwd = process.cwd(),
|
|
1741
|
+
): string {
|
|
1742
|
+
if (target.source === 'package') {
|
|
1743
|
+
return target.importSpecifier || target.packageName || 'installed package';
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
const path = target.sourcePath || target.runtimePath;
|
|
1747
|
+
return path ? relative(cwd, path) || '.' : target.source;
|
|
1748
|
+
}
|