@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/data.mjs CHANGED
@@ -1,169 +1,169 @@
1
- import { readFile } from 'node:fs/promises';
2
- import { basename } from 'node:path';
3
- import { checkValue, parseParamValue, validateQuery } from './queries.mjs';
4
-
5
- const MAX_ROWS_SHOWN = 50;
6
- const MAX_CELL_WIDTH = 40;
7
- const RUN_TIMEOUT_MS = 5 * 60_000;
8
- const MAX_POLL_DELAY_MS = 5_000;
9
- const CLIENT_ID = /^[A-Za-z0-9]+$/;
10
-
11
- const USAGE = `Usage:
12
- hawk-app data tables [--app <slug>]
13
- hawk-app data describe <table>
14
- hawk-app data run <queries/name.sql> --client <id> [--param name=value …]`;
15
-
16
- const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
17
-
18
- /**
19
- * `hawk-app data …`: the tables app queries can use, and running a query against a client's data before deploying it.
20
- * `client` is a factory, so a usage mistake shows before any login check.
21
- */
22
- export async function dataCommand({ client, sub, args = [], flags = {}, sleep = wait, log = console.log }) {
23
- switch (sub) {
24
- case 'tables': {
25
- const app = typeof flags.app === 'string' ? flags.app : undefined;
26
- const all = (await client().dataTables(app)).tables ?? [];
27
- const tables = all.filter((t) => !t.sensitive);
28
- const sensitive = all.filter((t) => t.sensitive);
29
- const width = Math.max(0, ...all.map((t) => t.name.length));
30
- const line = (table) => ` ${table.name.padEnd(width)} ${String(table.columns?.length ?? 0).padStart(3)} columns${table.partitioned ? ' partitioned (use {{partitions(from, to)}})' : ''}`;
31
- log(`${tables.length} table${tables.length === 1 ? '' : 's'} available to queries (\`hawk-app data describe <table>\` for columns):`);
32
- for (const table of tables) log(line(table));
33
- if (sensitive.length) {
34
- log('');
35
- log(app
36
- ? `Sensitive tables (visitor-level data; only for apps Engineering opted in), for ${app}:`
37
- : 'Sensitive tables (visitor-level data; only for apps Engineering opted in; pass --app <slug> to check yours):');
38
- for (const table of sensitive) log(`${line(table)} ${table.available ? 'available' : 'not available'}`);
39
- }
40
- return 0;
41
- }
42
-
43
- case 'describe': {
44
- const name = args[0];
45
- if (!name) throw new Error(USAGE);
46
- const { tables = [] } = await client().dataTables();
47
- const table = tables.find((t) => t.name.toLowerCase() === name.toLowerCase());
48
- if (!table) throw new Error(`Unknown table "${name}". Run \`hawk-app data tables\` to list the tables queries can use.`);
49
- log(`${table.name}${table.partitioned ? ' (partitioned by year/month: filter with {{partitions(from, to)}})' : ''}`);
50
- if (table.sensitive) log(' Sensitive: only apps Engineering opted in can query it.');
51
- const width = Math.max(0, ...table.columns.map((c) => c.name.length));
52
- for (const column of table.columns) log(` ${column.name.padEnd(width)} ${column.type}`);
53
- return 0;
54
- }
55
-
56
- case 'run':
57
- return run({ client, file: args[0], flags, sleep, log });
58
-
59
- default:
60
- throw new Error(sub ? `Unknown data command "${sub}".\n${USAGE}` : USAGE);
61
- }
62
- }
63
-
64
- async function run({ client, file, flags, sleep, log }) {
65
- if (!file || !file.endsWith('.sql')) throw new Error(USAGE);
66
- const clientId = flags.client;
67
- if (typeof clientId !== 'string' || !CLIENT_ID.test(clientId)) throw new Error('Pass the client to run against: --client <id> (letters and digits, e.g. e3141).');
68
-
69
- const jsonFile = file.replace(/\.sql$/, '.json');
70
- const name = basename(file, '.sql');
71
- const sql = await readText(file);
72
- let definition;
73
- try {
74
- definition = JSON.parse(await readText(jsonFile));
75
- } catch (error) {
76
- throw new Error(error instanceof SyntaxError ? `${jsonFile} is not valid JSON: ${error.message}` : error.message);
77
- }
78
- const errors = validateQuery(name, sql, definition);
79
- if (errors.length) {
80
- for (const error of errors) log(`✖ ${error}`);
81
- throw new Error(`Fix ${file} first.`);
82
- }
83
-
84
- const params = definition.params ?? [];
85
- const values = {};
86
- for (const entry of flags.param ?? []) {
87
- const eq = typeof entry === 'string' ? entry.indexOf('=') : -1;
88
- if (eq <= 0) throw new Error(`--param needs name=value, not "${entry}".`);
89
- const key = entry.slice(0, eq);
90
- const param = params.find((p) => p.name === key);
91
- if (!param) throw new Error(`"${key}" is not a parameter of ${name} (declared: ${params.map((p) => p.name).join(', ') || 'none'}).`);
92
- const value = parseParamValue(param, entry.slice(eq + 1));
93
- const problem = checkValue(param, value);
94
- if (problem) throw new Error(`--param ${key} ${problem}.`);
95
- values[key] = value;
96
- }
97
- const missing = params.filter((p) => p.required && p.default === undefined && !(p.name in values)).map((p) => p.name);
98
- if (missing.length) throw new Error(`Missing required parameter${missing.length === 1 ? '' : 's'}: ${missing.map((m) => `--param ${m}=…`).join(' ')}`);
99
-
100
- const platform = client();
101
- const started = Date.now();
102
- // 429 (too many queries running): wait and send again, 1 s, 2 s, 4 s, then every 5 s, never sooner than
103
- // Retry-After, until the run's time is up.
104
- let waited = false;
105
- const retrying = async (send) => {
106
- for (let attempt = 0; ; attempt++) {
107
- try {
108
- return await send();
109
- } catch (error) {
110
- if (error.status !== 429) throw error;
111
- const wait = Math.max(Math.min(1000 * 2 ** attempt, MAX_POLL_DELAY_MS), error.retryAfterMs ?? 0);
112
- if (Date.now() - started + wait > RUN_TIMEOUT_MS) throw error;
113
- if (!waited) log('Too many queries are running; retrying…');
114
- waited = true;
115
- await sleep(wait);
116
- }
117
- }
118
- };
119
- let { status, data } = await retrying(() => platform.dataRun({ clientId, sql, params, values })).catch((error) => {
120
- if (error.status === 403 && error.data?.code === 'not_available') {
121
- error.message += '\n `hawk-app data run` only works against the dev platform (--platform <dev url>).';
122
- }
123
- throw error;
124
- });
125
- for (let attempt = 0; status === 202; attempt++) {
126
- if (Date.now() - started > RUN_TIMEOUT_MS) throw new Error(`The query did not finish within ${RUN_TIMEOUT_MS / 60_000} minutes.`);
127
- if (attempt === 0) log('Running…');
128
- await sleep(Math.min(Math.max(1000 * 2 ** attempt, data.retryAfterMs ?? 0), MAX_POLL_DELAY_MS));
129
- ({ status, data } = await retrying(() => platform.dataExecution(data.executionId)));
130
- }
131
-
132
-
133
- printTable(data.columns ?? [], data.rows ?? [], log);
134
- const rowCount = data.rowCount ?? data.rows?.length ?? 0;
135
- const shown = Math.min(rowCount, MAX_ROWS_SHOWN, data.rows?.length ?? 0);
136
- log(`${rowCount} row${rowCount === 1 ? '' : 's'}${rowCount > shown ? ` (showing the first ${shown})` : ''}, ${formatBytes(data.bytesScanned ?? 0)} scanned${data.cached ? ' (cached result)' : ''}.`);
137
- return 0;
138
- }
139
-
140
- async function readText(file) {
141
- try {
142
- return await readFile(file, 'utf8');
143
- } catch {
144
- throw new Error(`Cannot read ${file}.`);
145
- }
146
- }
147
-
148
- /** Prints up to 50 rows as an aligned text table. */
149
- export function printTable(columns, rows, log) {
150
- if (!columns.length) return;
151
- const cell = (value) => {
152
- const text = value === null || value === undefined ? '' : typeof value === 'object' ? JSON.stringify(value) : String(value);
153
- const flat = text.replace(/\s+/g, ' ');
154
- return flat.length > MAX_CELL_WIDTH ? `${flat.slice(0, MAX_CELL_WIDTH - 1)}…` : flat;
155
- };
156
- const shown = rows.slice(0, MAX_ROWS_SHOWN).map((row) => columns.map((_, i) => cell(row[i])));
157
- const widths = columns.map((c, i) => Math.max(cell(c.name).length, ...shown.map((r) => r[i].length)));
158
- const line = (cells) => cells.map((c, i) => c.padEnd(widths[i])).join(' ').trimEnd();
159
- log(line(columns.map((c) => cell(c.name))));
160
- log(widths.map((w) => '-'.repeat(w)).join(' '));
161
- for (const row of shown) log(line(row));
162
- }
163
-
164
- export function formatBytes(bytes) {
165
- if (bytes < 1024) return `${bytes} B`;
166
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
167
- if (bytes < 1024 ** 3) return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
168
- return `${(bytes / 1024 ** 3).toFixed(2)} GB`;
169
- }
1
+ import { readFile } from 'node:fs/promises';
2
+ import { basename } from 'node:path';
3
+ import { checkValue, parseParamValue, validateQuery } from './queries.mjs';
4
+
5
+ const MAX_ROWS_SHOWN = 50;
6
+ const MAX_CELL_WIDTH = 40;
7
+ const RUN_TIMEOUT_MS = 5 * 60_000;
8
+ const MAX_POLL_DELAY_MS = 5_000;
9
+ const CLIENT_ID = /^[A-Za-z0-9]+$/;
10
+
11
+ const USAGE = `Usage:
12
+ hawk-app data tables [--app <slug>]
13
+ hawk-app data describe <table>
14
+ hawk-app data run <queries/name.sql> --client <id> [--param name=value …]`;
15
+
16
+ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
17
+
18
+ /**
19
+ * `hawk-app data …`: the tables app queries can use, and running a query against a client's data before deploying it.
20
+ * `client` is a factory, so a usage mistake shows before any login check.
21
+ */
22
+ export async function dataCommand({ client, sub, args = [], flags = {}, sleep = wait, log = console.log }) {
23
+ switch (sub) {
24
+ case 'tables': {
25
+ const app = typeof flags.app === 'string' ? flags.app : undefined;
26
+ const all = (await client().dataTables(app)).tables ?? [];
27
+ const tables = all.filter((t) => !t.sensitive);
28
+ const sensitive = all.filter((t) => t.sensitive);
29
+ const width = Math.max(0, ...all.map((t) => t.name.length));
30
+ const line = (table) => ` ${table.name.padEnd(width)} ${String(table.columns?.length ?? 0).padStart(3)} columns${table.partitioned ? ' partitioned (use {{partitions(from, to)}})' : ''}`;
31
+ log(`${tables.length} table${tables.length === 1 ? '' : 's'} available to queries (\`hawk-app data describe <table>\` for columns):`);
32
+ for (const table of tables) log(line(table));
33
+ if (sensitive.length) {
34
+ log('');
35
+ log(app
36
+ ? `Sensitive tables (visitor-level data; only for apps Engineering opted in), for ${app}:`
37
+ : 'Sensitive tables (visitor-level data; only for apps Engineering opted in; pass --app <slug> to check yours):');
38
+ for (const table of sensitive) log(`${line(table)} ${table.available ? 'available' : 'not available'}`);
39
+ }
40
+ return 0;
41
+ }
42
+
43
+ case 'describe': {
44
+ const name = args[0];
45
+ if (!name) throw new Error(USAGE);
46
+ const { tables = [] } = await client().dataTables();
47
+ const table = tables.find((t) => t.name.toLowerCase() === name.toLowerCase());
48
+ if (!table) throw new Error(`Unknown table "${name}". Run \`hawk-app data tables\` to list the tables queries can use.`);
49
+ log(`${table.name}${table.partitioned ? ' (partitioned by year/month: filter with {{partitions(from, to)}})' : ''}`);
50
+ if (table.sensitive) log(' Sensitive: only apps Engineering opted in can query it.');
51
+ const width = Math.max(0, ...table.columns.map((c) => c.name.length));
52
+ for (const column of table.columns) log(` ${column.name.padEnd(width)} ${column.type}`);
53
+ return 0;
54
+ }
55
+
56
+ case 'run':
57
+ return run({ client, file: args[0], flags, sleep, log });
58
+
59
+ default:
60
+ throw new Error(sub ? `Unknown data command "${sub}".\n${USAGE}` : USAGE);
61
+ }
62
+ }
63
+
64
+ async function run({ client, file, flags, sleep, log }) {
65
+ if (!file || !file.endsWith('.sql')) throw new Error(USAGE);
66
+ const clientId = flags.client;
67
+ if (typeof clientId !== 'string' || !CLIENT_ID.test(clientId)) throw new Error('Pass the client to run against: --client <id> (letters and digits, e.g. e3141).');
68
+
69
+ const jsonFile = file.replace(/\.sql$/, '.json');
70
+ const name = basename(file, '.sql');
71
+ const sql = await readText(file);
72
+ let definition;
73
+ try {
74
+ definition = JSON.parse(await readText(jsonFile));
75
+ } catch (error) {
76
+ throw new Error(error instanceof SyntaxError ? `${jsonFile} is not valid JSON: ${error.message}` : error.message);
77
+ }
78
+ const errors = validateQuery(name, sql, definition);
79
+ if (errors.length) {
80
+ for (const error of errors) log(`✖ ${error}`);
81
+ throw new Error(`Fix ${file} first.`);
82
+ }
83
+
84
+ const params = definition.params ?? [];
85
+ const values = {};
86
+ for (const entry of flags.param ?? []) {
87
+ const eq = typeof entry === 'string' ? entry.indexOf('=') : -1;
88
+ if (eq <= 0) throw new Error(`--param needs name=value, not "${entry}".`);
89
+ const key = entry.slice(0, eq);
90
+ const param = params.find((p) => p.name === key);
91
+ if (!param) throw new Error(`"${key}" is not a parameter of ${name} (declared: ${params.map((p) => p.name).join(', ') || 'none'}).`);
92
+ const value = parseParamValue(param, entry.slice(eq + 1));
93
+ const problem = checkValue(param, value);
94
+ if (problem) throw new Error(`--param ${key} ${problem}.`);
95
+ values[key] = value;
96
+ }
97
+ const missing = params.filter((p) => p.required && p.default === undefined && !(p.name in values)).map((p) => p.name);
98
+ if (missing.length) throw new Error(`Missing required parameter${missing.length === 1 ? '' : 's'}: ${missing.map((m) => `--param ${m}=…`).join(' ')}`);
99
+
100
+ const platform = client();
101
+ const started = Date.now();
102
+ // 429 (too many queries running): wait and send again, 1 s, 2 s, 4 s, then every 5 s, never sooner than
103
+ // Retry-After, until the run's time is up.
104
+ let waited = false;
105
+ const retrying = async (send) => {
106
+ for (let attempt = 0; ; attempt++) {
107
+ try {
108
+ return await send();
109
+ } catch (error) {
110
+ if (error.status !== 429) throw error;
111
+ const wait = Math.max(Math.min(1000 * 2 ** attempt, MAX_POLL_DELAY_MS), error.retryAfterMs ?? 0);
112
+ if (Date.now() - started + wait > RUN_TIMEOUT_MS) throw error;
113
+ if (!waited) log('Too many queries are running; retrying…');
114
+ waited = true;
115
+ await sleep(wait);
116
+ }
117
+ }
118
+ };
119
+ let { status, data } = await retrying(() => platform.dataRun({ clientId, sql, params, values })).catch((error) => {
120
+ if (error.status === 403 && error.data?.code === 'not_available') {
121
+ error.message += '\n `hawk-app data run` only works against the dev platform (--platform <dev url>).';
122
+ }
123
+ throw error;
124
+ });
125
+ for (let attempt = 0; status === 202; attempt++) {
126
+ if (Date.now() - started > RUN_TIMEOUT_MS) throw new Error(`The query did not finish within ${RUN_TIMEOUT_MS / 60_000} minutes.`);
127
+ if (attempt === 0) log('Running…');
128
+ await sleep(Math.min(Math.max(1000 * 2 ** attempt, data.retryAfterMs ?? 0), MAX_POLL_DELAY_MS));
129
+ ({ status, data } = await retrying(() => platform.dataExecution(data.executionId)));
130
+ }
131
+
132
+
133
+ printTable(data.columns ?? [], data.rows ?? [], log);
134
+ const rowCount = data.rowCount ?? data.rows?.length ?? 0;
135
+ const shown = Math.min(rowCount, MAX_ROWS_SHOWN, data.rows?.length ?? 0);
136
+ log(`${rowCount} row${rowCount === 1 ? '' : 's'}${rowCount > shown ? ` (showing the first ${shown})` : ''}, ${formatBytes(data.bytesScanned ?? 0)} scanned${data.cached ? ' (cached result)' : ''}.`);
137
+ return 0;
138
+ }
139
+
140
+ async function readText(file) {
141
+ try {
142
+ return await readFile(file, 'utf8');
143
+ } catch {
144
+ throw new Error(`Cannot read ${file}.`);
145
+ }
146
+ }
147
+
148
+ /** Prints up to 50 rows as an aligned text table. */
149
+ export function printTable(columns, rows, log) {
150
+ if (!columns.length) return;
151
+ const cell = (value) => {
152
+ const text = value === null || value === undefined ? '' : typeof value === 'object' ? JSON.stringify(value) : String(value);
153
+ const flat = text.replace(/\s+/g, ' ');
154
+ return flat.length > MAX_CELL_WIDTH ? `${flat.slice(0, MAX_CELL_WIDTH - 1)}…` : flat;
155
+ };
156
+ const shown = rows.slice(0, MAX_ROWS_SHOWN).map((row) => columns.map((_, i) => cell(row[i])));
157
+ const widths = columns.map((c, i) => Math.max(cell(c.name).length, ...shown.map((r) => r[i].length)));
158
+ const line = (cells) => cells.map((c, i) => c.padEnd(widths[i])).join(' ').trimEnd();
159
+ log(line(columns.map((c) => cell(c.name))));
160
+ log(widths.map((w) => '-'.repeat(w)).join(' '));
161
+ for (const row of shown) log(line(row));
162
+ }
163
+
164
+ export function formatBytes(bytes) {
165
+ if (bytes < 1024) return `${bytes} B`;
166
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
167
+ if (bytes < 1024 ** 3) return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
168
+ return `${(bytes / 1024 ** 3).toFixed(2)} GB`;
169
+ }