@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.
Files changed (80) hide show
  1. package/LICENSE +7 -7
  2. package/README.md +770 -770
  3. package/agents/AGENTS.md +192 -192
  4. package/agents/project/custom.md +21 -21
  5. package/agents/project/next-mui.md +21 -21
  6. package/agents/project/next.md +24 -24
  7. package/agents/project/vanilla.md +22 -22
  8. package/agents/project/vite.md +19 -19
  9. package/bin/hawk-app.mjs +0 -0
  10. package/docs/bridge/README.md +1315 -1315
  11. package/docs/showcase/README.md +47 -47
  12. package/docs/showcase/functions/echo.ts +12 -12
  13. package/docs/showcase/functions/long-task.ts +31 -31
  14. package/docs/showcase/functions/showcase.ts +186 -186
  15. package/docs/showcase/hawk-app.json +73 -73
  16. package/docs/showcase/queries/assistant-prompts-by-day.json +6 -6
  17. package/docs/showcase/queries/assistant-prompts-by-day.sql +8 -8
  18. package/docs/showcase/queries/searches-by-day.json +6 -6
  19. package/docs/showcase/queries/searches-by-day.sql +7 -7
  20. package/docs/showcase/src/App.tsx +1436 -1436
  21. package/docs/showcase/src/main.tsx +10 -10
  22. package/docs/showcase/src/styles.css +71 -71
  23. package/docs/showcase/src/useHawkApp.ts +44 -44
  24. package/docs/showcase/src/vite-env.d.ts +1 -1
  25. package/docs/ui/README.md +166 -166
  26. package/package.json +1 -1
  27. package/schema/hawk-app.schema.json +611 -611
  28. package/src/accessibility.mjs +105 -105
  29. package/src/agents.mjs +145 -145
  30. package/src/aliases.mjs +30 -30
  31. package/src/app-icon.mjs +82 -82
  32. package/src/channel.mjs +116 -116
  33. package/src/cli.mjs +825 -825
  34. package/src/clients.mjs +221 -221
  35. package/src/config.mjs +148 -148
  36. package/src/create.mjs +186 -186
  37. package/src/credentials.mjs +56 -56
  38. package/src/data.mjs +169 -169
  39. package/src/deploy.mjs +349 -349
  40. package/src/dev.mjs +459 -459
  41. package/src/doctor.mjs +176 -176
  42. package/src/env.mjs +140 -140
  43. package/src/functions.mjs +313 -313
  44. package/src/login.mjs +166 -166
  45. package/src/logs.mjs +64 -64
  46. package/src/manifest.mjs +425 -425
  47. package/src/mcp-docs.mjs +168 -168
  48. package/src/mcp-hawksearch.mjs +342 -342
  49. package/src/mcp-reports.mjs +130 -130
  50. package/src/mcp-rules.mjs +109 -109
  51. package/src/mcp.mjs +539 -539
  52. package/src/platform.mjs +116 -116
  53. package/src/project-checks.mjs +153 -153
  54. package/src/queries.mjs +289 -289
  55. package/src/scope-check.mjs +91 -91
  56. package/src/secrets.mjs +86 -86
  57. package/src/slug-guard.mjs +80 -80
  58. package/src/sso.mjs +182 -182
  59. package/src/telemetry.mjs +50 -50
  60. package/src/upgrade.mjs +406 -406
  61. package/src/versions.mjs +102 -102
  62. package/templates/next/next.config.ts +13 -13
  63. package/templates/next/package.json +26 -26
  64. package/templates/next/src/lib/useHawkApp.ts +46 -46
  65. package/templates/next-mui/next.config.ts +13 -13
  66. package/templates/next-mui/package.json +30 -30
  67. package/templates/next-mui/src/app/layout.tsx +19 -19
  68. package/templates/next-mui/src/app/page.tsx +78 -78
  69. package/templates/next-mui/src/app/providers.tsx +10 -10
  70. package/templates/preact/package.json +26 -26
  71. package/templates/preact/src/useHawkApp.ts +44 -44
  72. package/templates/preact/src/vite-env.d.ts +1 -1
  73. package/templates/preact/tsconfig.json +17 -17
  74. package/templates/preact/vite.config.ts +11 -11
  75. package/templates/react/package.json +29 -29
  76. package/templates/react/src/useHawkApp.ts +44 -44
  77. package/templates/react/src/vite-env.d.ts +1 -1
  78. package/templates/react/tsconfig.json +16 -16
  79. package/templates/react/vite.config.ts +11 -11
  80. 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
+ }