@bridgeline-digital/hawk-app-cli 0.1.0-dev.20261020202308 → 0.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 +7 -7
- package/README.md +770 -770
- package/agents/AGENTS.md +192 -192
- package/agents/project/custom.md +21 -21
- package/agents/project/next-mui.md +21 -21
- package/agents/project/next.md +24 -24
- package/agents/project/vanilla.md +22 -22
- package/agents/project/vite.md +19 -19
- package/bin/hawk-app.mjs +0 -0
- package/docs/bridge/README.md +1315 -1315
- package/docs/showcase/README.md +47 -47
- package/docs/showcase/functions/echo.ts +12 -12
- package/docs/showcase/functions/long-task.ts +31 -31
- package/docs/showcase/functions/showcase.ts +186 -186
- package/docs/showcase/hawk-app.json +73 -73
- package/docs/showcase/queries/assistant-prompts-by-day.json +6 -6
- package/docs/showcase/queries/assistant-prompts-by-day.sql +8 -8
- package/docs/showcase/queries/searches-by-day.json +6 -6
- package/docs/showcase/queries/searches-by-day.sql +7 -7
- package/docs/showcase/src/App.tsx +1436 -1436
- package/docs/showcase/src/main.tsx +10 -10
- package/docs/showcase/src/styles.css +71 -71
- package/docs/showcase/src/useHawkApp.ts +44 -44
- package/docs/showcase/src/vite-env.d.ts +1 -1
- package/docs/ui/README.md +166 -166
- package/package.json +1 -1
- package/schema/hawk-app.schema.json +611 -611
- package/src/accessibility.mjs +105 -105
- package/src/agents.mjs +145 -145
- package/src/aliases.mjs +30 -30
- package/src/app-icon.mjs +82 -82
- package/src/channel.mjs +116 -116
- package/src/cli.mjs +825 -825
- package/src/clients.mjs +221 -221
- package/src/config.mjs +148 -148
- package/src/create.mjs +186 -186
- package/src/credentials.mjs +56 -56
- package/src/data.mjs +169 -169
- package/src/deploy.mjs +349 -349
- package/src/dev.mjs +459 -459
- package/src/doctor.mjs +176 -176
- package/src/env.mjs +140 -140
- package/src/functions.mjs +313 -313
- package/src/login.mjs +166 -166
- package/src/logs.mjs +64 -64
- package/src/manifest.mjs +425 -425
- package/src/mcp-docs.mjs +168 -168
- package/src/mcp-hawksearch.mjs +342 -342
- package/src/mcp-reports.mjs +130 -130
- package/src/mcp-rules.mjs +109 -109
- package/src/mcp.mjs +539 -539
- package/src/platform.mjs +116 -116
- package/src/project-checks.mjs +153 -153
- package/src/queries.mjs +289 -289
- package/src/scope-check.mjs +91 -91
- package/src/secrets.mjs +86 -86
- package/src/slug-guard.mjs +80 -80
- package/src/sso.mjs +182 -182
- package/src/telemetry.mjs +50 -50
- package/src/upgrade.mjs +406 -406
- package/src/versions.mjs +102 -102
- package/templates/next/next.config.ts +13 -13
- package/templates/next/package.json +26 -26
- package/templates/next/src/lib/useHawkApp.ts +46 -46
- package/templates/next-mui/next.config.ts +13 -13
- package/templates/next-mui/package.json +30 -30
- package/templates/next-mui/src/app/layout.tsx +19 -19
- package/templates/next-mui/src/app/page.tsx +78 -78
- package/templates/next-mui/src/app/providers.tsx +10 -10
- package/templates/preact/package.json +26 -26
- package/templates/preact/src/useHawkApp.ts +44 -44
- package/templates/preact/src/vite-env.d.ts +1 -1
- package/templates/preact/tsconfig.json +17 -17
- package/templates/preact/vite.config.ts +11 -11
- package/templates/react/package.json +29 -29
- package/templates/react/src/useHawkApp.ts +44 -44
- package/templates/react/src/vite-env.d.ts +1 -1
- package/templates/react/tsconfig.json +16 -16
- package/templates/react/vite.config.ts +11 -11
- package/templates/vanilla/package.json +15 -15
package/src/mcp-docs.mjs
CHANGED
|
@@ -1,168 +1,168 @@
|
|
|
1
|
-
// The MCP server's `docs` tool: the bridge README, the UI kit README, the CLI README, the hawk-app.json schema and the showcase app (one
|
|
2
|
-
// app that uses every feature), split into sections an assistant can search and read. Each source is found in the
|
|
3
|
-
// app's own node_modules first (the bridge it really uses), then in this repo (when the CLI runs from source, so a
|
|
4
|
-
// leftover packed copy can't be stale), then in the copy the CLI ships (docs/, copied on prepack).
|
|
5
|
-
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
6
|
-
import { join, relative, sep } from 'node:path';
|
|
7
|
-
import { fileURLToPath } from 'node:url';
|
|
8
|
-
|
|
9
|
-
const CLI_DIR = fileURLToPath(new URL('..', import.meta.url));
|
|
10
|
-
const REPO_DIR = join(CLI_DIR, '..', '..');
|
|
11
|
-
const MAX_SECTION = 8000;
|
|
12
|
-
|
|
13
|
-
const firstExisting = (...paths) => paths.find((p) => p && existsSync(p)) ?? null;
|
|
14
|
-
|
|
15
|
-
/** Where each source is, for an app in `appDir`. */
|
|
16
|
-
export function docSources(appDir) {
|
|
17
|
-
return {
|
|
18
|
-
bridge: firstExisting(
|
|
19
|
-
appDir && join(appDir, 'node_modules', '@bridgeline-digital', 'hawk-app-bridge', 'README.md'),
|
|
20
|
-
join(REPO_DIR, 'packages', 'bridge', 'README.md'),
|
|
21
|
-
join(CLI_DIR, 'docs', 'bridge', 'README.md'),
|
|
22
|
-
),
|
|
23
|
-
ui: firstExisting(
|
|
24
|
-
appDir && join(appDir, 'node_modules', '@bridgeline-digital', 'hawk-app-ui', 'README.md'),
|
|
25
|
-
join(REPO_DIR, 'packages', 'ui', 'README.md'),
|
|
26
|
-
join(CLI_DIR, 'docs', 'ui', 'README.md'),
|
|
27
|
-
),
|
|
28
|
-
cli: firstExisting(join(CLI_DIR, 'README.md')),
|
|
29
|
-
schema: firstExisting(join(REPO_DIR, 'schemas', 'hawk-app.schema.json'), join(CLI_DIR, 'schema', 'hawk-app.schema.json')),
|
|
30
|
-
showcase: firstExisting(join(REPO_DIR, 'examples', 'showcase'), join(CLI_DIR, 'docs', 'showcase')),
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** A Markdown file as one section per ## / ### heading, titled "<source>: <h2> > <h3>". */
|
|
35
|
-
export function markdownSections(source, text) {
|
|
36
|
-
const sections = [];
|
|
37
|
-
let h2 = null;
|
|
38
|
-
let current = { title: `${source}: introduction`, lines: [] };
|
|
39
|
-
let fence = false;
|
|
40
|
-
for (const line of text.replace(/\r\n/g, '\n').split('\n')) {
|
|
41
|
-
if (/^\s*```/.test(line)) fence = !fence;
|
|
42
|
-
const heading = fence ? null : /^(#{2,3})\s+(.*)$/.exec(line);
|
|
43
|
-
if (heading) {
|
|
44
|
-
sections.push(current);
|
|
45
|
-
if (heading[1] === '##') h2 = heading[2].trim();
|
|
46
|
-
current = { title: `${source}: ${heading[1] === '##' ? h2 : `${h2} > ${heading[2].trim()}`}`, lines: [line] };
|
|
47
|
-
} else {
|
|
48
|
-
current.lines.push(line);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
sections.push(current);
|
|
52
|
-
return sections.map((s) => ({ title: s.title, text: s.lines.join('\n').trim() })).filter((s) => s.text);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const DECLARATION = /^(?:export\s+)?(?:default\s+)?(?:async\s+)?(?:function\s+(\w+)|const\s+(\w+)\s*[:=])/;
|
|
56
|
-
|
|
57
|
-
/** A source file of the showcase app as one section per top-level declaration (small files stay whole). */
|
|
58
|
-
function codeSections(file, text) {
|
|
59
|
-
const lines = text.replace(/\r\n/g, '\n').split('\n');
|
|
60
|
-
if (text.length <= MAX_SECTION) return [{ title: `showcase: ${file}`, text: `// ${file}\n${text.trim()}` }];
|
|
61
|
-
const sections = [];
|
|
62
|
-
let current = { title: `showcase: ${file} (top)`, lines: [] };
|
|
63
|
-
for (const line of lines) {
|
|
64
|
-
const match = DECLARATION.exec(line);
|
|
65
|
-
if (!match) {
|
|
66
|
-
current.lines.push(line);
|
|
67
|
-
continue;
|
|
68
|
-
}
|
|
69
|
-
// The comment lines right above a declaration belong to it.
|
|
70
|
-
let start = current.lines.length;
|
|
71
|
-
while (start > 0 && /^(\/\*\*|\s\*|\/\/)/.test(current.lines[start - 1])) start--;
|
|
72
|
-
const comment = current.lines.splice(start);
|
|
73
|
-
sections.push(current);
|
|
74
|
-
current = { title: `showcase: ${file} > ${match[1] ?? match[2]}`, lines: [...comment, line] };
|
|
75
|
-
}
|
|
76
|
-
sections.push(current);
|
|
77
|
-
return sections.map((s) => ({ title: s.title, text: `// ${file}\n${s.lines.join('\n').trim()}` })).filter((s) => s.text.split('\n').length > 1);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/** The hawk-app.json schema as one section per field (plus one for the whole file's rules), so a search finds the field. */
|
|
81
|
-
function schemaSections(text) {
|
|
82
|
-
let schema;
|
|
83
|
-
try {
|
|
84
|
-
schema = JSON.parse(text);
|
|
85
|
-
} catch {
|
|
86
|
-
return [{ title: 'schema: hawk-app.json', text: text.trim() }];
|
|
87
|
-
}
|
|
88
|
-
const { properties = {}, ...rest } = schema;
|
|
89
|
-
return [
|
|
90
|
-
{ title: 'schema: hawk-app.json', text: `${schema.description ?? ''}\nFields: ${Object.keys(properties).join(', ')}.\n${JSON.stringify({ ...rest, $defs: undefined }, null, 2)}`.trim() },
|
|
91
|
-
...Object.entries(properties).map(([name, definition]) => ({ title: `schema: ${name}`, text: `"${name}" in hawk-app.json:\n${JSON.stringify(definition, null, 2)}` })),
|
|
92
|
-
];
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function walk(dir) {
|
|
96
|
-
const files = [];
|
|
97
|
-
for (const name of readdirSync(dir)) {
|
|
98
|
-
if (name.startsWith('.') || name === 'node_modules' || name === 'dist') continue;
|
|
99
|
-
const full = join(dir, name);
|
|
100
|
-
if (statSync(full).isDirectory()) files.push(...walk(full));
|
|
101
|
-
else files.push(full);
|
|
102
|
-
}
|
|
103
|
-
return files;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/** Every section of every source: [{ title, text }]. */
|
|
107
|
-
export function loadDocs(appDir) {
|
|
108
|
-
const sources = docSources(appDir);
|
|
109
|
-
const sections = [];
|
|
110
|
-
if (sources.bridge) sections.push(...markdownSections('bridge', readFileSync(sources.bridge, 'utf8')));
|
|
111
|
-
if (sources.ui) sections.push(...markdownSections('ui', readFileSync(sources.ui, 'utf8')));
|
|
112
|
-
if (sources.cli) sections.push(...markdownSections('cli', readFileSync(sources.cli, 'utf8')));
|
|
113
|
-
if (sources.schema) sections.push(...schemaSections(readFileSync(sources.schema, 'utf8')));
|
|
114
|
-
if (sources.showcase) {
|
|
115
|
-
for (const full of walk(sources.showcase)) {
|
|
116
|
-
const file = relative(sources.showcase, full).split(sep).join('/');
|
|
117
|
-
if (!/\.(tsx?|mjs|json|sql|md)$/.test(file) || file === 'package.json' || file.startsWith('tsconfig')) continue;
|
|
118
|
-
const text = readFileSync(full, 'utf8');
|
|
119
|
-
if (file === 'README.md') sections.push(...markdownSections('showcase', text));
|
|
120
|
-
else sections.push(...codeSections(file, text));
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
return sections;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const cap = (text) => (text.length > MAX_SECTION ? `${text.slice(0, MAX_SECTION)}\n… (cut at ${MAX_SECTION} characters; ask for this section by its title to read on with "offset")` : text);
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* The docs tool: no arguments lists the sections; `section` returns one by its title (from `offset` on, for long
|
|
130
|
-
* ones); `query` returns the best matching sections (all of the query's words must appear).
|
|
131
|
-
*/
|
|
132
|
-
export function searchDocs({ appDir, query, section, offset = 0, limit = 4 }) {
|
|
133
|
-
const sections = loadDocs(appDir);
|
|
134
|
-
if (!sections.length) return 'No documentation found. Is @bridgeline-digital/hawk-app-cli installed correctly?';
|
|
135
|
-
|
|
136
|
-
if (section) {
|
|
137
|
-
const found = sections.find((s) => s.title === section) ?? sections.find((s) => s.title.toLowerCase().includes(section.toLowerCase()));
|
|
138
|
-
if (!found) return `No section "${section}". Call docs without arguments to list them.`;
|
|
139
|
-
const rest = found.text.slice(offset);
|
|
140
|
-
return `# ${found.title}\n\n${rest.length > MAX_SECTION ? `${rest.slice(0, MAX_SECTION)}\n… (more: call again with offset ${offset + MAX_SECTION})` : rest}`;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
if (query) {
|
|
144
|
-
const words = query.toLowerCase().split(/[^a-z0-9_:.@/-]+/).filter((w) => w.length > 1);
|
|
145
|
-
const scored = sections
|
|
146
|
-
.map((s) => {
|
|
147
|
-
const title = s.title.toLowerCase();
|
|
148
|
-
const text = s.text.toLowerCase();
|
|
149
|
-
if (!words.every((w) => title.includes(w) || text.includes(w))) return null;
|
|
150
|
-
// Title matches count most; mentions count up to 5 each, so a long section can't win on length alone.
|
|
151
|
-
const score = words.reduce((sum, w) => sum + (title.includes(w) ? 10 : 0) + Math.min(text.split(w).length - 1, 5), 0);
|
|
152
|
-
return { s, score };
|
|
153
|
-
})
|
|
154
|
-
.filter(Boolean)
|
|
155
|
-
.sort((a, b) => b.score - a.score)
|
|
156
|
-
.slice(0, limit);
|
|
157
|
-
if (!scored.length) return `Nothing matches all of "${query}". Try fewer or other words, or call docs without arguments to list the sections.`;
|
|
158
|
-
return scored.map(({ s }) => `# ${s.title}\n\n${cap(s.text)}`).join('\n\n---\n\n');
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const bySource = new Map();
|
|
162
|
-
for (const s of sections) {
|
|
163
|
-
const source = s.title.split(':')[0];
|
|
164
|
-
bySource.set(source, [...(bySource.get(source) ?? []), s.title.slice(source.length + 2)]);
|
|
165
|
-
}
|
|
166
|
-
const labels = { bridge: 'Bridge (app.* APIs, scopes, errors)', cli: 'CLI (commands, deploy, review)', schema: 'hawk-app.json schema', showcase: 'Showcase app (working examples of every feature)' };
|
|
167
|
-
return [...bySource.entries()].map(([source, titles]) => `## ${labels[source] ?? source} — section titles start with "${source}: "\n${titles.map((t) => `- ${t}`).join('\n')}`).join('\n\n');
|
|
168
|
-
}
|
|
1
|
+
// The MCP server's `docs` tool: the bridge README, the UI kit README, the CLI README, the hawk-app.json schema and the showcase app (one
|
|
2
|
+
// app that uses every feature), split into sections an assistant can search and read. Each source is found in the
|
|
3
|
+
// app's own node_modules first (the bridge it really uses), then in this repo (when the CLI runs from source, so a
|
|
4
|
+
// leftover packed copy can't be stale), then in the copy the CLI ships (docs/, copied on prepack).
|
|
5
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
6
|
+
import { join, relative, sep } from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
const CLI_DIR = fileURLToPath(new URL('..', import.meta.url));
|
|
10
|
+
const REPO_DIR = join(CLI_DIR, '..', '..');
|
|
11
|
+
const MAX_SECTION = 8000;
|
|
12
|
+
|
|
13
|
+
const firstExisting = (...paths) => paths.find((p) => p && existsSync(p)) ?? null;
|
|
14
|
+
|
|
15
|
+
/** Where each source is, for an app in `appDir`. */
|
|
16
|
+
export function docSources(appDir) {
|
|
17
|
+
return {
|
|
18
|
+
bridge: firstExisting(
|
|
19
|
+
appDir && join(appDir, 'node_modules', '@bridgeline-digital', 'hawk-app-bridge', 'README.md'),
|
|
20
|
+
join(REPO_DIR, 'packages', 'bridge', 'README.md'),
|
|
21
|
+
join(CLI_DIR, 'docs', 'bridge', 'README.md'),
|
|
22
|
+
),
|
|
23
|
+
ui: firstExisting(
|
|
24
|
+
appDir && join(appDir, 'node_modules', '@bridgeline-digital', 'hawk-app-ui', 'README.md'),
|
|
25
|
+
join(REPO_DIR, 'packages', 'ui', 'README.md'),
|
|
26
|
+
join(CLI_DIR, 'docs', 'ui', 'README.md'),
|
|
27
|
+
),
|
|
28
|
+
cli: firstExisting(join(CLI_DIR, 'README.md')),
|
|
29
|
+
schema: firstExisting(join(REPO_DIR, 'schemas', 'hawk-app.schema.json'), join(CLI_DIR, 'schema', 'hawk-app.schema.json')),
|
|
30
|
+
showcase: firstExisting(join(REPO_DIR, 'examples', 'showcase'), join(CLI_DIR, 'docs', 'showcase')),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** A Markdown file as one section per ## / ### heading, titled "<source>: <h2> > <h3>". */
|
|
35
|
+
export function markdownSections(source, text) {
|
|
36
|
+
const sections = [];
|
|
37
|
+
let h2 = null;
|
|
38
|
+
let current = { title: `${source}: introduction`, lines: [] };
|
|
39
|
+
let fence = false;
|
|
40
|
+
for (const line of text.replace(/\r\n/g, '\n').split('\n')) {
|
|
41
|
+
if (/^\s*```/.test(line)) fence = !fence;
|
|
42
|
+
const heading = fence ? null : /^(#{2,3})\s+(.*)$/.exec(line);
|
|
43
|
+
if (heading) {
|
|
44
|
+
sections.push(current);
|
|
45
|
+
if (heading[1] === '##') h2 = heading[2].trim();
|
|
46
|
+
current = { title: `${source}: ${heading[1] === '##' ? h2 : `${h2} > ${heading[2].trim()}`}`, lines: [line] };
|
|
47
|
+
} else {
|
|
48
|
+
current.lines.push(line);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
sections.push(current);
|
|
52
|
+
return sections.map((s) => ({ title: s.title, text: s.lines.join('\n').trim() })).filter((s) => s.text);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const DECLARATION = /^(?:export\s+)?(?:default\s+)?(?:async\s+)?(?:function\s+(\w+)|const\s+(\w+)\s*[:=])/;
|
|
56
|
+
|
|
57
|
+
/** A source file of the showcase app as one section per top-level declaration (small files stay whole). */
|
|
58
|
+
function codeSections(file, text) {
|
|
59
|
+
const lines = text.replace(/\r\n/g, '\n').split('\n');
|
|
60
|
+
if (text.length <= MAX_SECTION) return [{ title: `showcase: ${file}`, text: `// ${file}\n${text.trim()}` }];
|
|
61
|
+
const sections = [];
|
|
62
|
+
let current = { title: `showcase: ${file} (top)`, lines: [] };
|
|
63
|
+
for (const line of lines) {
|
|
64
|
+
const match = DECLARATION.exec(line);
|
|
65
|
+
if (!match) {
|
|
66
|
+
current.lines.push(line);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
// The comment lines right above a declaration belong to it.
|
|
70
|
+
let start = current.lines.length;
|
|
71
|
+
while (start > 0 && /^(\/\*\*|\s\*|\/\/)/.test(current.lines[start - 1])) start--;
|
|
72
|
+
const comment = current.lines.splice(start);
|
|
73
|
+
sections.push(current);
|
|
74
|
+
current = { title: `showcase: ${file} > ${match[1] ?? match[2]}`, lines: [...comment, line] };
|
|
75
|
+
}
|
|
76
|
+
sections.push(current);
|
|
77
|
+
return sections.map((s) => ({ title: s.title, text: `// ${file}\n${s.lines.join('\n').trim()}` })).filter((s) => s.text.split('\n').length > 1);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The hawk-app.json schema as one section per field (plus one for the whole file's rules), so a search finds the field. */
|
|
81
|
+
function schemaSections(text) {
|
|
82
|
+
let schema;
|
|
83
|
+
try {
|
|
84
|
+
schema = JSON.parse(text);
|
|
85
|
+
} catch {
|
|
86
|
+
return [{ title: 'schema: hawk-app.json', text: text.trim() }];
|
|
87
|
+
}
|
|
88
|
+
const { properties = {}, ...rest } = schema;
|
|
89
|
+
return [
|
|
90
|
+
{ title: 'schema: hawk-app.json', text: `${schema.description ?? ''}\nFields: ${Object.keys(properties).join(', ')}.\n${JSON.stringify({ ...rest, $defs: undefined }, null, 2)}`.trim() },
|
|
91
|
+
...Object.entries(properties).map(([name, definition]) => ({ title: `schema: ${name}`, text: `"${name}" in hawk-app.json:\n${JSON.stringify(definition, null, 2)}` })),
|
|
92
|
+
];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function walk(dir) {
|
|
96
|
+
const files = [];
|
|
97
|
+
for (const name of readdirSync(dir)) {
|
|
98
|
+
if (name.startsWith('.') || name === 'node_modules' || name === 'dist') continue;
|
|
99
|
+
const full = join(dir, name);
|
|
100
|
+
if (statSync(full).isDirectory()) files.push(...walk(full));
|
|
101
|
+
else files.push(full);
|
|
102
|
+
}
|
|
103
|
+
return files;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Every section of every source: [{ title, text }]. */
|
|
107
|
+
export function loadDocs(appDir) {
|
|
108
|
+
const sources = docSources(appDir);
|
|
109
|
+
const sections = [];
|
|
110
|
+
if (sources.bridge) sections.push(...markdownSections('bridge', readFileSync(sources.bridge, 'utf8')));
|
|
111
|
+
if (sources.ui) sections.push(...markdownSections('ui', readFileSync(sources.ui, 'utf8')));
|
|
112
|
+
if (sources.cli) sections.push(...markdownSections('cli', readFileSync(sources.cli, 'utf8')));
|
|
113
|
+
if (sources.schema) sections.push(...schemaSections(readFileSync(sources.schema, 'utf8')));
|
|
114
|
+
if (sources.showcase) {
|
|
115
|
+
for (const full of walk(sources.showcase)) {
|
|
116
|
+
const file = relative(sources.showcase, full).split(sep).join('/');
|
|
117
|
+
if (!/\.(tsx?|mjs|json|sql|md)$/.test(file) || file === 'package.json' || file.startsWith('tsconfig')) continue;
|
|
118
|
+
const text = readFileSync(full, 'utf8');
|
|
119
|
+
if (file === 'README.md') sections.push(...markdownSections('showcase', text));
|
|
120
|
+
else sections.push(...codeSections(file, text));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return sections;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const cap = (text) => (text.length > MAX_SECTION ? `${text.slice(0, MAX_SECTION)}\n… (cut at ${MAX_SECTION} characters; ask for this section by its title to read on with "offset")` : text);
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The docs tool: no arguments lists the sections; `section` returns one by its title (from `offset` on, for long
|
|
130
|
+
* ones); `query` returns the best matching sections (all of the query's words must appear).
|
|
131
|
+
*/
|
|
132
|
+
export function searchDocs({ appDir, query, section, offset = 0, limit = 4 }) {
|
|
133
|
+
const sections = loadDocs(appDir);
|
|
134
|
+
if (!sections.length) return 'No documentation found. Is @bridgeline-digital/hawk-app-cli installed correctly?';
|
|
135
|
+
|
|
136
|
+
if (section) {
|
|
137
|
+
const found = sections.find((s) => s.title === section) ?? sections.find((s) => s.title.toLowerCase().includes(section.toLowerCase()));
|
|
138
|
+
if (!found) return `No section "${section}". Call docs without arguments to list them.`;
|
|
139
|
+
const rest = found.text.slice(offset);
|
|
140
|
+
return `# ${found.title}\n\n${rest.length > MAX_SECTION ? `${rest.slice(0, MAX_SECTION)}\n… (more: call again with offset ${offset + MAX_SECTION})` : rest}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (query) {
|
|
144
|
+
const words = query.toLowerCase().split(/[^a-z0-9_:.@/-]+/).filter((w) => w.length > 1);
|
|
145
|
+
const scored = sections
|
|
146
|
+
.map((s) => {
|
|
147
|
+
const title = s.title.toLowerCase();
|
|
148
|
+
const text = s.text.toLowerCase();
|
|
149
|
+
if (!words.every((w) => title.includes(w) || text.includes(w))) return null;
|
|
150
|
+
// Title matches count most; mentions count up to 5 each, so a long section can't win on length alone.
|
|
151
|
+
const score = words.reduce((sum, w) => sum + (title.includes(w) ? 10 : 0) + Math.min(text.split(w).length - 1, 5), 0);
|
|
152
|
+
return { s, score };
|
|
153
|
+
})
|
|
154
|
+
.filter(Boolean)
|
|
155
|
+
.sort((a, b) => b.score - a.score)
|
|
156
|
+
.slice(0, limit);
|
|
157
|
+
if (!scored.length) return `Nothing matches all of "${query}". Try fewer or other words, or call docs without arguments to list the sections.`;
|
|
158
|
+
return scored.map(({ s }) => `# ${s.title}\n\n${cap(s.text)}`).join('\n\n---\n\n');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const bySource = new Map();
|
|
162
|
+
for (const s of sections) {
|
|
163
|
+
const source = s.title.split(':')[0];
|
|
164
|
+
bySource.set(source, [...(bySource.get(source) ?? []), s.title.slice(source.length + 2)]);
|
|
165
|
+
}
|
|
166
|
+
const labels = { bridge: 'Bridge (app.* APIs, scopes, errors)', cli: 'CLI (commands, deploy, review)', schema: 'hawk-app.json schema', showcase: 'Showcase app (working examples of every feature)' };
|
|
167
|
+
return [...bySource.entries()].map(([source, titles]) => `## ${labels[source] ?? source} — section titles start with "${source}: "\n${titles.map((t) => `- ${t}`).join('\n')}`).join('\n\n');
|
|
168
|
+
}
|