@table-bro/mcp 0.0.0-stage → 1.1.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/LICENSE +21 -0
- package/README.md +89 -2
- package/dist/docs/docs.d.ts +21 -0
- package/dist/docs/docs.js +103 -0
- package/dist/docs/pages.d.ts +25 -0
- package/dist/docs/pages.js +1404 -0
- package/dist/docs/topics.d.ts +11 -0
- package/dist/docs/topics.js +396 -0
- package/dist/extension.d.ts +23 -0
- package/dist/extension.js +1 -0
- package/dist/generate/angular.d.ts +26 -0
- package/dist/generate/angular.js +241 -0
- package/dist/generate/cells.d.ts +22 -0
- package/dist/generate/cells.js +157 -0
- package/dist/generate/code.d.ts +48 -0
- package/dist/generate/code.js +153 -0
- package/dist/generate/config.d.ts +16 -0
- package/dist/generate/config.js +95 -0
- package/dist/generate/fillings.d.ts +49 -0
- package/dist/generate/fillings.js +130 -0
- package/dist/generate/filters.d.ts +36 -0
- package/dist/generate/filters.js +254 -0
- package/dist/generate/generate-code.d.ts +34 -0
- package/dist/generate/generate-code.js +59 -0
- package/dist/generate/names.d.ts +52 -0
- package/dist/generate/names.js +93 -0
- package/dist/generate/rows.d.ts +5 -0
- package/dist/generate/rows.js +22 -0
- package/dist/generate/scaffold.d.ts +35 -0
- package/dist/generate/scaffold.js +62 -0
- package/dist/generate/service.d.ts +16 -0
- package/dist/generate/service.js +320 -0
- package/dist/generate/setup.d.ts +34 -0
- package/dist/generate/setup.js +156 -0
- package/dist/generate/vanilla.d.ts +10 -0
- package/dist/generate/vanilla.js +160 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +10 -0
- package/dist/project/inspect-project.d.ts +53 -0
- package/dist/project/inspect-project.js +223 -0
- package/dist/server/main.d.ts +2 -0
- package/dist/server/main.js +8 -0
- package/dist/server/server.d.ts +17 -0
- package/dist/server/server.js +212 -0
- package/dist/spec/builtin-specs.d.ts +13 -0
- package/dist/spec/builtin-specs.js +167 -0
- package/dist/spec/css-variables.d.ts +22 -0
- package/dist/spec/css-variables.js +54 -0
- package/dist/spec/schema.d.ts +19 -0
- package/dist/spec/schema.js +278 -0
- package/dist/spec/skin-config.d.ts +30 -0
- package/dist/spec/skin-config.js +67 -0
- package/dist/spec/spec.d.ts +157 -0
- package/dist/spec/spec.js +2 -0
- package/dist/spec/validate-spec.d.ts +54 -0
- package/dist/spec/validate-spec.js +979 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +2 -0
- package/package.json +57 -4
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { cssVariables } from '../spec/css-variables.js';
|
|
2
|
+
import { cellDescription, colsValue, modelDescription } from './angular.js';
|
|
3
|
+
import { byColumns, cellCode } from './cells.js';
|
|
4
|
+
import { imports, literal, quote } from './code.js';
|
|
5
|
+
import { configCode } from './config.js';
|
|
6
|
+
import { BroFiltersCode } from './filters.js';
|
|
7
|
+
import { BroNames } from './names.js';
|
|
8
|
+
import { rowTypeCode } from './rows.js';
|
|
9
|
+
/**
|
|
10
|
+
* The table for a page without a framework, in the skins bs and nv: an ES module with a function that renders
|
|
11
|
+
* <table-bro> into a container, with its cells, its filter drawer and its CSS variables. The module imports the config
|
|
12
|
+
* of the skin from its package; the setup of the project registers the skin with the optional elements of the table.
|
|
13
|
+
*/
|
|
14
|
+
export function vanillaTable(spec, skin, fillings, tagSuffix = '', extensions = []) {
|
|
15
|
+
const names = BroNames.of(spec, tagSuffix);
|
|
16
|
+
const folder = names.folder;
|
|
17
|
+
const config = spec.config ?? {};
|
|
18
|
+
const server = Boolean(config.server);
|
|
19
|
+
const filters = new BroFiltersCode(spec, fillings);
|
|
20
|
+
const hasFilters = filters.filters.length > 0;
|
|
21
|
+
const specRows = Boolean(spec.rows?.length) && !server;
|
|
22
|
+
const cells = (spec.cells ?? []).map(cell => cellCode(spec, cell, 'vanilla', fillings.cells?.[cell.name]?.[skin], tagSuffix));
|
|
23
|
+
const conf = configCode(spec, skin, fillings, '', tagSuffix, extensions);
|
|
24
|
+
const files = [];
|
|
25
|
+
const todo = [];
|
|
26
|
+
for (const cell of byColumns(spec, cells)) {
|
|
27
|
+
files.push({ path: `${folder}/${cell.file}.ts`, content: cell.content, description: cellDescription(cell) });
|
|
28
|
+
if (!cell.filled) {
|
|
29
|
+
todo.push(`${folder}/${cell.file}.ts: write the render() of the ${cell.cell.kind === 'details' ? 'row details' : 'cell'}: ${cell.cell.shows}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const actions = [
|
|
33
|
+
...(spec.cells ?? []).filter(cell => cell.kind !== 'details').flatMap(cell => cell.actions ?? []),
|
|
34
|
+
...(spec.toolbar?.buttons ?? []).map(button => button.action),
|
|
35
|
+
...(spec.toolbar?.menu?.items ?? []).map(item => item.action)
|
|
36
|
+
];
|
|
37
|
+
const detailsButton = spec.cols.some(col => col.component && 'element' in col.component && col.component.element === 'details-button');
|
|
38
|
+
const tableBro = [
|
|
39
|
+
...conf.imports['@table-bro/core'], 'type BroCol', 'type TableBroHTMLElement',
|
|
40
|
+
...(server ? ['type BroQuery'] : []),
|
|
41
|
+
...(hasFilters ? ['dispatchAction', 'defineTableBroFilterDrawerContainer'] : []),
|
|
42
|
+
...((hasFilters || detailsButton || server) ? ['events'] : [])
|
|
43
|
+
];
|
|
44
|
+
const cssVars = Object.entries(spec.cssVars ?? {});
|
|
45
|
+
const tableVars = cssVars.filter(([name]) => cssVariables.find(item => item.name === name)?.target !== 'drawer');
|
|
46
|
+
const drawerVars = hasFilters ? cssVars.filter(([name]) => cssVariables.find(item => item.name === name)?.target !== 'table') : [];
|
|
47
|
+
const shadows = (config.shadowsEnabled !== false);
|
|
48
|
+
const body = [];
|
|
49
|
+
if (cells.length) {
|
|
50
|
+
body.push(' defineCustomElements();', '');
|
|
51
|
+
}
|
|
52
|
+
body.push(' const table = document.createElement(\'table-bro\');', '', ` table.id = '${names.tableId}';`,
|
|
53
|
+
// The table reads its variables once a render, so they are set before configure
|
|
54
|
+
...tableVars.map(([name, value]) => ` table.style.setProperty('${name}', '${value}');`), '', ` table.configure({cols${specRows ? ', rows' : ''}, conf});`);
|
|
55
|
+
if (!specRows && !server) {
|
|
56
|
+
body.push('', ' // TODO: load the rows and set them with table.replaceRows(rows)');
|
|
57
|
+
}
|
|
58
|
+
// The page delivers the rows of each query, as the page of Angular does: the first page of other filters or of
|
|
59
|
+
// another sort replaces the rows, the next pages of the same query add to them
|
|
60
|
+
if (server) {
|
|
61
|
+
body.push('', ' /** The filters and the sort of the rows the table has: another query starts from the first page. */', ' let loadedQuery: string = \'\';', ' /** The last query: an answer to an older one comes late and is left out. */', ` let lastQuery: BroQuery<${spec.rowType}> | undefined;`, '', ` const onQueryTable = (query: BroQuery<${spec.rowType}>): void => {`, ' const queryKey: string = JSON.stringify([query.filter, query.sort]);', '', ' lastQuery = query;', '', ` loadPage(query).then(({rows, total}: ${names.apiPageType}) => {`, ' if (query !== lastQuery) {', ' return;', ' }', '', ' // One batch for one delivery: the rows go with the total of the same answer; an empty answer to a query with', ' // a search or filters is the state of no rows found', ' if (queryKey !== loadedQuery) {', ' loadedQuery = queryKey;', ' table.replaceRows(rows, {total, emptyState: Object.values(query.filter ?? {}).some(value => value !== undefined && value !== null && value !== \'\') ? \'not-found\' : undefined});', ' } else {', ' table.appendRows(rows, {total});', ' }', ' }, () => {', ' // A failed load shows the failure state of the table', ' if (query === lastQuery) {', ' table.setState(\'fail\');', ' }', ' });', ' };');
|
|
62
|
+
}
|
|
63
|
+
const cases = [];
|
|
64
|
+
if (server) {
|
|
65
|
+
cases.push(' case events.filtersServerLoadedEvent:', ' onQueryTable(event.detail.data);', ' break;');
|
|
66
|
+
}
|
|
67
|
+
const customActions = [...new Set(actions)];
|
|
68
|
+
for (const action of customActions) {
|
|
69
|
+
const row = (spec.cells ?? []).some(cell => cell.actions?.includes(action));
|
|
70
|
+
cases.push(` case ${quote(action)}:`, ` // TODO: ${row ? 'the row is event.detail.data?.row' : 'the action of the toolbar'}`, ' break;');
|
|
71
|
+
}
|
|
72
|
+
if (cases.length) {
|
|
73
|
+
body.push('',
|
|
74
|
+
// The names of the custom actions let the switch narrow the data of the actions of the table
|
|
75
|
+
` table.addEventListener<${spec.rowType}, ${customActions.map(quote).join(' | ') || 'never'}>('bro-action', event => {`, ' switch (event.detail.action) {', ...cases, ' }', ' });');
|
|
76
|
+
}
|
|
77
|
+
if (hasFilters) {
|
|
78
|
+
body.push('', ' // The filters live outside of the table, in the drawer next to it; the Filters button of the toolbar opens it', ' defineTableBroFilterDrawerContainer();', '', ' const drawer: HTMLElement = document.createElement(\'table-bro-filter-drawer-container\');', '', ` drawer.id = '${names.drawerId}';`, ...(shadows ? [' drawer.className = \'bro-table-shadow-container\';'] : []), ` drawer.setAttribute('skin', '${skin}');`, ...drawerVars.map(([name, value]) => ` drawer.style.setProperty('${name}', '${value}');`), '', ` const filters = render${names.filtersClass}((value: ${names.filtersType}) => {`, ' dispatchAction(table, events.filtersFilterEvent, value);', ' });', '', ' drawer.append(table, filters);', ' container.append(drawer);');
|
|
79
|
+
}
|
|
80
|
+
else if (shadows) {
|
|
81
|
+
body.push('', ' // The shadows of the table fall into the margins of this element: the visible table takes its full width', ' const host: HTMLElement = document.createElement(\'div\');', '', ' host.className = \'bro-table-shadow-container\';', ' host.append(table);', ' container.append(host);');
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
body.push('', ' container.append(table);');
|
|
85
|
+
}
|
|
86
|
+
// The table asks for the next pages and for the first page of other filters; the page asks for its first page
|
|
87
|
+
if (server) {
|
|
88
|
+
body.push('', ` onQueryTable({page: 1, pageSize: ${config.pageSize ?? 5}});`);
|
|
89
|
+
}
|
|
90
|
+
body.push('', ' return table;');
|
|
91
|
+
const localModules = {
|
|
92
|
+
[`./${names.kebab}`]: [`type ${spec.rowType}`, ...(hasFilters ? [`type ${names.filtersType}`] : [])],
|
|
93
|
+
...Object.fromEntries(cells.map(cell => [`./${cell.file}`, [cell.className]])),
|
|
94
|
+
...(hasFilters ? { [`./${names.filtersFile}`]: [`render${names.filtersClass}`] } : {})
|
|
95
|
+
};
|
|
96
|
+
const module = [
|
|
97
|
+
imports({ ...conf.imports, '@table-bro/core': tableBro, ...localModules }),
|
|
98
|
+
'',
|
|
99
|
+
...conf.preamble.flatMap(declaration => [declaration, '']),
|
|
100
|
+
`const cols: BroCol<${spec.rowType}>[] = ${literal(colsValue(spec, skin, cells))};`,
|
|
101
|
+
'',
|
|
102
|
+
...(specRows ? [`const rows: ${spec.rowType}[] = ${literal(spec.rows)};`, ''] : []),
|
|
103
|
+
`const conf: BroConfig<${spec.rowType}> = ${conf.code};`,
|
|
104
|
+
'',
|
|
105
|
+
`/** Renders the table of the type ${spec.rowType} into the container and returns it. */`,
|
|
106
|
+
`export function render${spec.rowType}Table(container: HTMLElement): TableBroHTMLElement {`,
|
|
107
|
+
...body,
|
|
108
|
+
'}',
|
|
109
|
+
...(server
|
|
110
|
+
? [
|
|
111
|
+
'',
|
|
112
|
+
'/** An answer of your API: the rows of a page and the number of all rows. Replace it with the answer of your API. */',
|
|
113
|
+
`interface ${names.apiPageType} {`,
|
|
114
|
+
` rows: ${spec.rowType}[];`,
|
|
115
|
+
' total: number;',
|
|
116
|
+
'}',
|
|
117
|
+
'',
|
|
118
|
+
'/** A page of the query: the request of your API, and its answer as the rows of the table with their total. */',
|
|
119
|
+
`function loadPage(query: BroQuery<${spec.rowType}>): Promise<${names.apiPageType}> {`,
|
|
120
|
+
' // TODO: the address of your API, and its parameters for the sort (query.sort), the search and the filters (query.filter)',
|
|
121
|
+
' const params: URLSearchParams = new URLSearchParams({page: `${query.page}`, pageSize: `${query.pageSize}`});',
|
|
122
|
+
'',
|
|
123
|
+
` return fetch(\`/api/${names.kebab}s?\${params}\`).then((response: Response) => {`,
|
|
124
|
+
' if (!response.ok) {',
|
|
125
|
+
' throw new Error(`${response.status} ${response.statusText}`);',
|
|
126
|
+
' }',
|
|
127
|
+
'',
|
|
128
|
+
' return response.json();',
|
|
129
|
+
' });',
|
|
130
|
+
'}'
|
|
131
|
+
]
|
|
132
|
+
: []),
|
|
133
|
+
...(cells.length
|
|
134
|
+
? [
|
|
135
|
+
'',
|
|
136
|
+
'/** The cells are Custom Elements: the table creates them by their tags, so they are defined before it. */',
|
|
137
|
+
'function defineCustomElements(): void {',
|
|
138
|
+
` if (customElements.get(${quote(cells[0].tag)})) {`,
|
|
139
|
+
' return;',
|
|
140
|
+
' }',
|
|
141
|
+
'',
|
|
142
|
+
...cells.map(cell => ` customElements.define(${quote(cell.tag)}, ${cell.className});`),
|
|
143
|
+
'}'
|
|
144
|
+
]
|
|
145
|
+
: []),
|
|
146
|
+
''
|
|
147
|
+
].join('\n');
|
|
148
|
+
files.unshift({ path: `${folder}/${names.kebab}.ts`, content: rowTypeCode(spec), description: modelDescription(hasFilters) }, { path: `${folder}/${names.kebab}-table.ts`, content: module, description: `the table. Here are its columns, \`cols\`${specRows ? ', its rows, `rows`,' : ''} and its config, \`conf\`, and its rendering.` });
|
|
149
|
+
// The filters matter least to a reader: their form comes last
|
|
150
|
+
if (hasFilters) {
|
|
151
|
+
files.push({ path: `${folder}/${names.filtersFile}.ts`, content: filters.vanillaForm(skin), description: 'the filters of the drawer: their form, which gives the table its filter.' });
|
|
152
|
+
}
|
|
153
|
+
if (server) {
|
|
154
|
+
todo.push(`${folder}/${names.kebab}-table.ts: the address of your API in loadPage, and the map of the query of the table to its parameters and of its answer to the rows with their total`);
|
|
155
|
+
}
|
|
156
|
+
if (!specRows && !server) {
|
|
157
|
+
todo.push(`${folder}/${names.kebab}-table.ts: load the rows and set them with table.replaceRows(rows)`);
|
|
158
|
+
}
|
|
159
|
+
return { files, todo };
|
|
160
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export * from './docs/docs.js';
|
|
2
|
+
export type { BroDocTopic } from './docs/topics.js';
|
|
3
|
+
export type { BroMcpExtension } from './extension.js';
|
|
4
|
+
export * from './generate/fillings.js';
|
|
5
|
+
export * from './generate/generate-code.js';
|
|
6
|
+
export type { BroGeneratedFile } from './generate/angular.js';
|
|
7
|
+
export type { BroSetupStep, BroTarget } from './generate/setup.js';
|
|
8
|
+
export * from './spec/builtin-specs.js';
|
|
9
|
+
export * from './spec/css-variables.js';
|
|
10
|
+
export * from './spec/schema.js';
|
|
11
|
+
export * from './spec/skin-config.js';
|
|
12
|
+
export * from './spec/spec.js';
|
|
13
|
+
export * from './spec/validate-spec.js';
|
|
14
|
+
export * from './version.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from './docs/docs.js';
|
|
2
|
+
export * from './generate/fillings.js';
|
|
3
|
+
export * from './generate/generate-code.js';
|
|
4
|
+
export * from './spec/builtin-specs.js';
|
|
5
|
+
export * from './spec/css-variables.js';
|
|
6
|
+
export * from './spec/schema.js';
|
|
7
|
+
export * from './spec/skin-config.js';
|
|
8
|
+
export * from './spec/spec.js';
|
|
9
|
+
export * from './spec/validate-spec.js';
|
|
10
|
+
export * from './version.js';
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { BroSpecSkin } from '../spec/spec.js';
|
|
2
|
+
export interface BroProjectProblem {
|
|
3
|
+
code: string;
|
|
4
|
+
message: string;
|
|
5
|
+
fix: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The profile of the project of a developer: the answers an agent would collect from ten reads of files.
|
|
9
|
+
* The search goes by the text of the files, so a missing item means "not found", not "absent".
|
|
10
|
+
*/
|
|
11
|
+
export interface BroProjectProfile {
|
|
12
|
+
/** The folder that the profile describes. */
|
|
13
|
+
folder: string;
|
|
14
|
+
tableBro: {
|
|
15
|
+
version: string | null;
|
|
16
|
+
packages: Record<string, string>;
|
|
17
|
+
/** The installed paid modules with their part of the MCP server. */
|
|
18
|
+
extensions: string[];
|
|
19
|
+
};
|
|
20
|
+
framework: {
|
|
21
|
+
name: 'angular' | 'none';
|
|
22
|
+
version: string | null;
|
|
23
|
+
};
|
|
24
|
+
ui: {
|
|
25
|
+
material: string | null;
|
|
26
|
+
bootstrap: string | null;
|
|
27
|
+
};
|
|
28
|
+
skins: {
|
|
29
|
+
registered: BroSpecSkin[];
|
|
30
|
+
where: string[];
|
|
31
|
+
};
|
|
32
|
+
styles: string[];
|
|
33
|
+
customElements: {
|
|
34
|
+
tag: string;
|
|
35
|
+
file: string;
|
|
36
|
+
}[];
|
|
37
|
+
tables: string[];
|
|
38
|
+
problems: BroProjectProblem[];
|
|
39
|
+
/** The lists cut to their first items, with the number of the items each lost. */
|
|
40
|
+
omitted?: Partial<Record<BroProjectList, number>>;
|
|
41
|
+
}
|
|
42
|
+
type BroProjectList = 'styles' | 'customElements' | 'tables' | 'problems';
|
|
43
|
+
/** Looks at a project as text: its packages, its setup of Table, Bro, its elements and its tables. */
|
|
44
|
+
export declare function inspectProject(root: string): BroProjectProfile;
|
|
45
|
+
/**
|
|
46
|
+
* The paid modules of the scope @table-bro-extended with a part of the MCP server: the field table-bro.mcp of their manifest,
|
|
47
|
+
* a file inside the package.
|
|
48
|
+
*/
|
|
49
|
+
export declare function findExtensions(root: string): {
|
|
50
|
+
name: string;
|
|
51
|
+
entry: string;
|
|
52
|
+
}[];
|
|
53
|
+
export {};
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { tableBroVersion } from '../version.js';
|
|
4
|
+
const tableBroPackages = ['@table-bro/core', '@table-bro/angular', '@table-bro/angular-material', '@table-bro/bootstrap', '@table-bro/native'];
|
|
5
|
+
/** The tags of the elements of the skins and of the core: a column may name them without a definition of the project. */
|
|
6
|
+
const readyTags = /^bro-(mat|bs|nv)-|^bro-(switch|button|details-button|paginator|sort-header|text-ellipsis|toolbar|toolbar-main)$/;
|
|
7
|
+
const sourceExtensions = ['.ts', '.html', '.js', '.mjs'];
|
|
8
|
+
const skippedFolders = new Set(['node_modules', 'dist', '.angular', '.git', 'coverage', 'out-tsc', '.check']);
|
|
9
|
+
const maxFiles = 5000;
|
|
10
|
+
/** The longest walk: a folder with more folders than this is no project, as a home folder. */
|
|
11
|
+
const maxFolders = 10000;
|
|
12
|
+
/** The longest list of a profile: the profile of a big monorepo would fill the context of the agent. */
|
|
13
|
+
const maxItems = 20;
|
|
14
|
+
/** The first version of Table, Bro with an MCP server: an older project has no server of its version. */
|
|
15
|
+
const firstMcpVersion = '1.1.0';
|
|
16
|
+
/**
|
|
17
|
+
* An exact version, as npm installs it: the only text of the project that goes into the command of a fix. The project
|
|
18
|
+
* is foreign text, and an agent runs the commands of the fixes.
|
|
19
|
+
*/
|
|
20
|
+
const exactVersion = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$/;
|
|
21
|
+
/** Looks at a project as text: its packages, its setup of Table, Bro, its elements and its tables. */
|
|
22
|
+
export function inspectProject(root) {
|
|
23
|
+
const folderMissing = !fs.existsSync(root) || !fs.statSync(root).isDirectory();
|
|
24
|
+
const manifest = (folderMissing ? null : readJson(path.join(root, 'package.json'))) ?? {};
|
|
25
|
+
const declared = { ...manifest.dependencies, ...manifest.devDependencies };
|
|
26
|
+
const version = (name) => readJson(path.join(root, 'node_modules', name, 'package.json'))?.version ?? declared[name] ?? null;
|
|
27
|
+
const packages = Object.fromEntries(tableBroPackages.map(name => [name, version(name)]).filter(([, value]) => value));
|
|
28
|
+
const profile = {
|
|
29
|
+
folder: root,
|
|
30
|
+
tableBro: { version: version('@table-bro/core'), packages, extensions: findExtensions(root).map(extension => extension.name) },
|
|
31
|
+
framework: version('@angular/core') ? { name: 'angular', version: version('@angular/core') } : { name: 'none', version: null },
|
|
32
|
+
ui: { material: version('@angular/material'), bootstrap: version('bootstrap') },
|
|
33
|
+
skins: { registered: [], where: [] },
|
|
34
|
+
styles: angularStyles(root),
|
|
35
|
+
customElements: [],
|
|
36
|
+
tables: [],
|
|
37
|
+
problems: []
|
|
38
|
+
};
|
|
39
|
+
const selectors = [];
|
|
40
|
+
const walk = folderMissing ? { files: [], cut: false } : sourceFiles(root);
|
|
41
|
+
for (const file of walk.files) {
|
|
42
|
+
const text = readText(file);
|
|
43
|
+
// The paths of the profile have the slashes of the web on every system
|
|
44
|
+
const relative = path.relative(root, file).split(path.sep).join('/');
|
|
45
|
+
if (text === null) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
for (const [skin, pattern] of [
|
|
49
|
+
['mat', /provideTableBroMatSkin\(|registerSkin\(\s*['"]mat['"]/],
|
|
50
|
+
['bs', /from\s+['"]@table-bro\/bootstrap['"]|import\s+['"]@table-bro\/bootstrap['"]|registerSkin\(\s*['"]bs['"]/],
|
|
51
|
+
['nv', /from\s+['"]@table-bro\/native['"]|import\s+['"]@table-bro\/native['"]|registerSkin\(\s*['"]nv['"]/]
|
|
52
|
+
]) {
|
|
53
|
+
if (pattern.test(text) && !profile.skins.registered.includes(skin)) {
|
|
54
|
+
profile.skins.registered.push(skin);
|
|
55
|
+
profile.skins.where.push(relative);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
for (const match of text.matchAll(/customElements\.define\(\s*['"]([a-z][a-z0-9-]*)['"]/g)) {
|
|
59
|
+
profile.customElements.push({ tag: match[1], file: relative });
|
|
60
|
+
}
|
|
61
|
+
// The tag a column names in component.selector; the selector of an Angular component, as the filters, is no such tag
|
|
62
|
+
for (const match of text.matchAll(/component:\s*\{[^{}]*?selector:\s*['"](bro-[a-z0-9-]+)['"]/g)) {
|
|
63
|
+
selectors.push({ tag: match[1], file: relative });
|
|
64
|
+
}
|
|
65
|
+
if (/<table-bro-ng\b|<table-bro-filter-drawer-container(-ng)?\b|createElement\(\s*['"]table-bro['"]\s*\)/.test(text)) {
|
|
66
|
+
profile.tables.push(relative);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
profile.problems = problems(profile, selectors, Boolean(version('@angular/elements')));
|
|
70
|
+
if (walk.cut) {
|
|
71
|
+
profile.problems.unshift({ code: 'search-cut', message: `the search stopped after ${maxFiles} source files or ${maxFolders} folders: ${root} is larger than a project`, fix: 'pass the root of the project in path' });
|
|
72
|
+
}
|
|
73
|
+
// A folder that is no project: the other problems follow from it, so it comes first
|
|
74
|
+
if (folderMissing) {
|
|
75
|
+
profile.problems.unshift({ code: 'folder-missing', message: `${root} is not a folder`, fix: 'pass the root of the project in path, or start the server in it' });
|
|
76
|
+
}
|
|
77
|
+
else if (!fs.existsSync(path.join(root, 'package.json'))) {
|
|
78
|
+
profile.problems.unshift({ code: 'package-json-missing', message: `${root} has no package.json: it is not the root of a project`, fix: 'pass the root of the project in path, or start the server in it; for a new project create it first, as ng new for Angular' });
|
|
79
|
+
}
|
|
80
|
+
// The problems of the setup come before the tags, so a cut list keeps them
|
|
81
|
+
for (const list of ['styles', 'customElements', 'tables', 'problems']) {
|
|
82
|
+
const lost = profile[list].splice(maxItems).length;
|
|
83
|
+
if (lost) {
|
|
84
|
+
profile.omitted = { ...profile.omitted, [list]: lost };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return profile;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* The paid modules of the scope @table-bro-extended with a part of the MCP server: the field table-bro.mcp of their manifest,
|
|
91
|
+
* a file inside the package.
|
|
92
|
+
*/
|
|
93
|
+
export function findExtensions(root) {
|
|
94
|
+
const scope = path.join(root, 'node_modules', '@table-bro-extended');
|
|
95
|
+
if (!fs.existsSync(scope)) {
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
return fs.readdirSync(scope).flatMap(folder => {
|
|
99
|
+
const directory = path.join(scope, folder);
|
|
100
|
+
const manifest = readJson(path.join(directory, 'package.json'));
|
|
101
|
+
const entry = manifest?.['table-bro.mcp'];
|
|
102
|
+
if (typeof entry !== 'string' || typeof manifest.name !== 'string') {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
const file = path.resolve(directory, entry);
|
|
106
|
+
const relative = path.relative(directory, file);
|
|
107
|
+
return (relative && !relative.startsWith('..') && !path.isAbsolute(relative)) ? [{ name: manifest.name, entry: file }] : [];
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
function problems(profile, selectors, elements) {
|
|
111
|
+
const list = [];
|
|
112
|
+
const { packages, version } = profile.tableBro;
|
|
113
|
+
const angular = profile.framework.name === 'angular';
|
|
114
|
+
if (!version) {
|
|
115
|
+
list.push({ code: 'table-bro-missing', message: '@table-bro/core is not found among the packages', fix: 'npm install @table-bro/core, the wrapper and the skin of the project, as get_docs with the topic setup says' });
|
|
116
|
+
}
|
|
117
|
+
else if (version.replace(/^[^\d]*/, '') !== tableBroVersion) {
|
|
118
|
+
const project = version.replace(/^[^\d]*/, '');
|
|
119
|
+
let fix = 'run the server of the version of Table, Bro of the project';
|
|
120
|
+
if (older(project, firstMcpVersion)) {
|
|
121
|
+
fix = `@table-bro/mcp starts with Table, Bro ${firstMcpVersion}: update the packages of Table, Bro of the project to ${tableBroVersion}, or check the answers of this server against the project`;
|
|
122
|
+
}
|
|
123
|
+
else if (exactVersion.test(project)) {
|
|
124
|
+
fix = `run the server of the same version: npx -y @table-bro/mcp@${project}`;
|
|
125
|
+
}
|
|
126
|
+
list.push({ code: 'version-mismatch', message: `this MCP server describes Table, Bro ${tableBroVersion}, the project has ${JSON.stringify(version)}`, fix });
|
|
127
|
+
}
|
|
128
|
+
if (angular && version && !packages['@table-bro/angular']) {
|
|
129
|
+
list.push({ code: 'wrapper-missing', message: 'the Angular wrapper @table-bro/angular is not found', fix: 'npm install @table-bro/angular' });
|
|
130
|
+
}
|
|
131
|
+
if (packages['@table-bro/angular-material'] && !profile.framework.version) {
|
|
132
|
+
list.push({ code: 'angular-missing', message: '@table-bro/angular-material needs Angular', fix: 'use the skin bs or nv without Angular' });
|
|
133
|
+
}
|
|
134
|
+
const usesMat = Boolean(packages['@table-bro/angular-material']) || profile.skins.registered.includes('mat');
|
|
135
|
+
if (usesMat && !elements) {
|
|
136
|
+
list.push({ code: 'elements-package-missing', message: 'the mat skin and the cells of Angular need @angular/elements', fix: 'npm install @angular/elements' });
|
|
137
|
+
}
|
|
138
|
+
for (const [skin, name] of [['mat', '@table-bro/angular-material'], ['bs', '@table-bro/bootstrap'], ['nv', '@table-bro/native']]) {
|
|
139
|
+
if (packages[name] && !profile.skins.registered.includes(skin)) {
|
|
140
|
+
list.push({ code: 'skin-not-registered', message: `the registration of the skin ${skin} is not found`, fix: 'see get_docs with the topic setup' });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (angular && version) {
|
|
144
|
+
const core = profile.styles.some(style => /table-bro(\.min)?\.s?css$/.test(style));
|
|
145
|
+
const skinStyles = profile.styles.some(style => /table-bro\.(material|bootstrap|native)|table-(bootstrap|native)/.test(style));
|
|
146
|
+
if (!core || !skinStyles) {
|
|
147
|
+
list.push({ code: 'css-missing', message: `the CSS of ${!core ? 'the core' : 'the skin'} is not found in "styles" of angular.json`, fix: 'see get_docs with the topic setup' });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const defined = new Set(profile.customElements.map(element => element.tag));
|
|
151
|
+
for (const { tag, file } of selectors) {
|
|
152
|
+
if (!defined.has(tag) && !readyTags.test(tag)) {
|
|
153
|
+
list.push({ code: 'tag-not-defined', message: `${file} names the tag ${tag}, and its customElements.define is not found`, fix: `define ${tag} before the table is created` });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return list;
|
|
157
|
+
}
|
|
158
|
+
/** Whether the version a comes before the version b, as 1.0.2 before 1.1.0. */
|
|
159
|
+
function older(a, b) {
|
|
160
|
+
const [first, second] = [a, b].map(version => version.split('.').map(part => parseInt(part, 10) || 0));
|
|
161
|
+
const index = first.findIndex((part, position) => part !== second[position]);
|
|
162
|
+
return index !== -1 && first[index] < second[index];
|
|
163
|
+
}
|
|
164
|
+
function angularStyles(root) {
|
|
165
|
+
const workspace = readJson(path.join(root, 'angular.json'));
|
|
166
|
+
if (!workspace?.projects) {
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
169
|
+
return Object.values(workspace.projects).flatMap(project => {
|
|
170
|
+
const styles = project?.architect?.build?.options?.styles ?? [];
|
|
171
|
+
return styles.map((style) => (typeof style === 'string') ? style : style.input);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
/** The source files of a project, up to the limits of the walk; a folder that the server may not read stays out. */
|
|
175
|
+
function sourceFiles(root) {
|
|
176
|
+
const files = [];
|
|
177
|
+
const stack = [root];
|
|
178
|
+
let folders = 0;
|
|
179
|
+
while (stack.length) {
|
|
180
|
+
if (files.length >= maxFiles || ++folders > maxFolders) {
|
|
181
|
+
return { files, cut: true };
|
|
182
|
+
}
|
|
183
|
+
const directory = stack.pop();
|
|
184
|
+
let entries;
|
|
185
|
+
try {
|
|
186
|
+
entries = fs.readdirSync(directory, { withFileTypes: true });
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
for (const entry of entries) {
|
|
192
|
+
if (entry.isDirectory()) {
|
|
193
|
+
if (!skippedFolders.has(entry.name) && !entry.name.startsWith('.')) {
|
|
194
|
+
stack.push(path.join(directory, entry.name));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
else if (sourceExtensions.includes(path.extname(entry.name))) {
|
|
198
|
+
if (files.length >= maxFiles) {
|
|
199
|
+
return { files, cut: true };
|
|
200
|
+
}
|
|
201
|
+
files.push(path.join(directory, entry.name));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return { files, cut: false };
|
|
206
|
+
}
|
|
207
|
+
/** The text of a file, or null for one that the server may not read. */
|
|
208
|
+
function readText(file) {
|
|
209
|
+
try {
|
|
210
|
+
return fs.readFileSync(file, 'utf8');
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function readJson(file) {
|
|
217
|
+
try {
|
|
218
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { createServer, enabledExtensions, loadExtensions } from './server.js';
|
|
4
|
+
// The agent starts the server in the folder of the project: the project and its paid modules are there. A paid module
|
|
5
|
+
// joins by its name only: --extension @table-bro-extended/island-details
|
|
6
|
+
const root = process.cwd();
|
|
7
|
+
const server = createServer(root, await loadExtensions(root, enabledExtensions(process.argv.slice(2))));
|
|
8
|
+
await server.connect(new StdioServerTransport());
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import type { BroMcpExtension } from '../extension.js';
|
|
3
|
+
/** The paid modules that the arguments of the server name: --extension <package>, once for each module. */
|
|
4
|
+
export declare function enabledExtensions(args: string[]): string[];
|
|
5
|
+
/**
|
|
6
|
+
* Loads the parts of the server of the installed paid modules that the server is started with, from the scope
|
|
7
|
+
* @table-bro-extended only: the server runs no code of a package that nobody named. A module that fails to load, or has
|
|
8
|
+
* no part of the server, is left out with a message on stderr, and the server works without it.
|
|
9
|
+
* @param root - the folder of the project
|
|
10
|
+
* @param enabled - the packages of the modules that the arguments of the server name
|
|
11
|
+
*/
|
|
12
|
+
export declare function loadExtensions(root: string, enabled: string[]): Promise<BroMcpExtension[]>;
|
|
13
|
+
/**
|
|
14
|
+
* The MCP server of Table, Bro: six tools over the pure functions of the package. It reads the project and writes
|
|
15
|
+
* nothing: the agent writes the files and shows the changes to the developer.
|
|
16
|
+
*/
|
|
17
|
+
export declare function createServer(root: string, extensions?: BroMcpExtension[]): McpServer;
|