@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/README.md CHANGED
@@ -1,770 +1,770 @@
1
- # @bridgeline-digital/hawk-app-cli
2
-
3
- Build, run and release **Hawk Apps**, that run inside HawkSearch. Sign in with your Bridgeline account (or a
4
- **publisher key** in CI): no AWS account, no bucket names, no platform repo.
5
-
6
- The full developer docs, with guides, the bridge API and every command, are at
7
- **[developers.hawksearchapps.com](https://developers.hawksearchapps.com)** (sign in with your Bridgeline account;
8
- `npx hawk-app docs` opens them).
9
-
10
- ## Quickstart
11
-
12
- ```bash
13
- npx @bridgeline-digital/hawk-app-cli@latest sso # 1. once per machine: sign in with your Bridgeline account
14
- npx @bridgeline-digital/hawk-app-cli@latest create my-app && cd my-app && npm install # 2. a new app
15
- npm run app:dev # 3. run it locally inside a stand-in for HawkSearch
16
- npm run app:deploy # 4. build and upload a new version (a draft)
17
- npx hawk-app open # 5. review it in HawkSearch and submit it for approval
18
- ```
19
-
20
- Then `npx hawk-app status` shows where the app stands, and `npx hawk-app doctor` checks everything (Node, login,
21
- `hawk-app.json`, the project, the scopes the code uses) and says what to fix. `npx hawk-app help` lists the commands by
22
- task; `npx hawk-app help <command>` gives one command's details.
23
-
24
- The templates add the CLI as a dev dependency; elsewhere: `npm i -D @bridgeline-digital/hawk-app-cli`.
25
-
26
- ## Start a new app
27
-
28
- ```bash
29
- npx @bridgeline-digital/hawk-app-cli@latest create keyword-explorer # React + Vite + TypeScript
30
- npx @bridgeline-digital/hawk-app-cli@latest create keyword-explorer --template preact # Preact + Vite + TypeScript (small bundle)
31
- npx @bridgeline-digital/hawk-app-cli@latest create keyword-explorer --template next # Next.js (static export)
32
- npx @bridgeline-digital/hawk-app-cli@latest create keyword-explorer --template next-mui # Next.js + Material UI (the UI kit's components)
33
- npx @bridgeline-digital/hawk-app-cli@latest create keyword-explorer --template vanilla # plain HTML/JS, no build
34
- ```
35
-
36
- The folder name becomes the app's slug. Every template comes ready to run:
37
- - the bridge set up, with the base path read from `HAWK_APP_BASE_PATH` (set by `hawk-app deploy`; `/<slug>/` in local dev)
38
- - Spark's look in the host's colors and mode, from the UI kit
39
- ([`@bridgeline-digital/hawk-app-ui`](../ui/README.md)): `next-mui` its MUI theme, provider and components;
40
- every other template its stylesheet and elements (`<hawk-kpi>`, `<hawk-data-table>`, `<hawk-column-chart>`, …;
41
- `vanilla` loads them from the CDN)
42
- - a working analytics example (total searches for the selected engine and date range, per day)
43
- - a `hawk-app.json` (new apps start as drafts)
44
- - `app:dev` / `app:deploy` scripts and a README
45
- - `AGENTS.md`, `CLAUDE.md` and the MCP server's settings for AI coding assistants (see [AI coding assistants](#ai-coding-assistants-agentsmd))
46
-
47
- Add `--name "Keyword Explorer"` to set the display name, and `--install` to run `npm install` straight away.
48
-
49
- ## Add to an existing app
50
-
51
- ```bash
52
- npx hawk-app init # creates hawk-app.json and AGENTS.md
53
- npx hawk-app validate # checks hawk-app.json and the project (bridge, build script)
54
- ```
55
-
56
- `init` also writes [AGENTS.md](#ai-coding-assistants-agentsmd) with what a template would have done for you: the bridge,
57
- the base path, the build folder and the dev server. `npx hawk-app validate --build` builds the app as deploy does and
58
- checks that its HTML only loads URLs under the version's base path.
59
-
60
- `hawk-app.json` decides the app's **visibility** and what data it can read (which clients have it is managed
61
- separately, see [Clients](#clients)):
62
-
63
- ```jsonc
64
- {
65
- "slug": "keyword-explorer", // unique; each deploy is served at <apps CDN>/keyword-explorer/v<n>/
66
- "name": "Keyword Explorer",
67
- "visibility": "allowList", // allowList: only the app's clients; all: every client except excluded ones;
68
- // internal: Bridgeline users only; store: the app's clients, and every other
69
- // client can request it from the store (needs "store", below) (clients: see "Clients")
70
- "icon": "icon.svg", // the app's icon: an .svg (drawn in the host's color) or a square .png, next to this file
71
- "scopes": ["reports:read"] // reports:read = analytics; hawksearch:read = dashboard API (read);
72
- // hawksearch:write = create/update/delete synonyms, redirects, banners, …
73
- // through the dashboard API (see the bridge's "Writing to HawkSearch");
74
- // data:read = your own SQL queries in queries/ (see "Custom data");
75
- // functions:invoke = server-side functions (see "Functions");
76
- // email:send = send email (see "Email");
77
- // storage = the app's own data and files per client (see the bridge's "Storage");
78
- // hawksearch:sysparams:write = set HawkSearch system parameters
79
- // (with "systemParameters", see "HawkSearch system parameters");
80
- // hawksearch:search = HawkSearch search and autocomplete, live or preview;
81
- // hawksearch:recommendations = the engine's recommendation widgets;
82
- // hawksearch:index = rebuild the engine's index (where HawkSearch allows it)
83
- // and read its indexes; hawksearch:index:items = add, update, delete items;
84
- // hawksearch:index:manage = create, switch and delete indexes, hierarchy, mappings;
85
- // hawksearch:features:write = HawkSearch features on and off;
86
- // lambdas:invoke = Lambdas outside Hawk Apps (with "lambdas");
87
- // ai:invoke = Claude through the platform (see "AI")
88
- // "config": { "signupNotifyEmails": { "type": "emails", "default": ["a@example.com"] } }
89
- // = non-secret settings the app reads with app.config, values set without a deploy (see "App config")
90
- }
91
- ```
92
-
93
- Optional `"listed": false` keeps the app off the host's Apps landing page (the host also hides its own title and back
94
- button for it), while it still opens by its direct URL. It only changes the listing, not who can open the app
95
- (`visibility` and the app's clients still apply); the default is `true`, and `hawk-app status` shows `listed no` when it's off.
96
-
97
- **Store apps.** `"visibility": "store"` offers the app to every client that doesn't have it, on the Store tab of the
98
- Apps page in HawkSearch: a client user requests it, and a Hawk-App-Manager approves (the client is added to its
99
- clients) or declines. It needs the listing:
100
-
101
- ```jsonc
102
- "store": {
103
- "tagline": "Find and fix searches that return nothing", // one line, at most 120 characters
104
- "highlights": ["Zero-result searches by volume"], // optional, up to 5
105
- "screenshots": ["store/overview.png", "store/fix.png"] // 1-5 .png, .jpg or .webp next to hawk-app.json, at most 1 MB each
106
- }
107
- ```
108
-
109
- A view with `"trial": true` (one at most, no params) is the live trial: the card runs it on the client's own data
110
- before they have the app, with a read-only token (only the app's `reports:read`, `hawksearch:read`,
111
- `hawksearch:search`, `hawksearch:recommendations` and `data:read`). There `app.context.trial` is true: hide what needs
112
- other scopes (`validate` lists them); `navigate({ intent: 'app' })` opens the request dialog. Without a trial view, or
113
- for a client without analytics data, the card shows the screenshots.
114
-
115
- `validate` and `deploy` check the screenshots, and `deploy` uploads them with each version (`_hawk/store/1.png`…). The
116
- listing is reviewed like the other settings; the price label is set by Bridgeline, not here. `hawk-app status` shows
117
- whether the app is in the store, its price label and how many requests are open, approved and declined.
118
-
119
- There is no `status` field any more: approvers release versions from the Apps page in HawkSearch (see [Review](#review-in-hawksearch)). An old `status` is ignored with a warning.
120
-
121
- Every deploy is a new version served at `/<slug>/v<n>/`, so the build needs that base path. `hawk-app deploy` runs `npm run build` with `HAWK_APP_BASE_PATH=/<slug>/v<n>`; read it in your build config with `/<slug>` as the local fallback:
122
- - Vite: ``base: `${process.env.HAWK_APP_BASE_PATH ?? '/keyword-explorer'}/` ``
123
- - Next.js (`output: 'export'`): `basePath: process.env.HAWK_APP_BASE_PATH ?? '/keyword-explorer'`
124
- - Other build tools: set the base href / public path from `HAWK_APP_BASE_PATH` the same way (plus a trailing `/` where the tool expects one)
125
-
126
- Apps that use only relative paths (plain HTML) can skip the build with `hawk-app deploy <folder> --no-build`.
127
-
128
- ## Log in (once per machine)
129
-
130
- ```bash
131
- npx hawk-app sso # sign in with your Bridgeline account in the browser: the platform issues your own key
132
- # (--publisher <id>: another team's apps, e.g. qa-team; one saved key per platform)
133
- npx hawk-app login # or: paste a publisher key from Engineering (input hidden), checked and saved for every app
134
- npx hawk-app whoami # are you logged in? which platform and key apply here?
135
- npx hawk-app logout # remove the saved key from this machine
136
- ```
137
-
138
- **`sso`** opens the browser on the HawkSearch sign-in, which goes straight to Bridgeline's AD sign-in (Microsoft). Only
139
- Bridgeline AD accounts get a key; HawkSearch client logins can't. The platform then issues a key for you on this
140
- machine (`sso-<name>-<computer>`, in the Services team's publisher, so you work on the team's apps; `--publisher qa-team`
141
- for another team the platform allows, e.g. QA's own test apps) and the CLI saves it
142
- like `login` does. You're also added to Hawk-App-Developers in HawkSearch, so you can submit apps for approval. It
143
- works for 90 days; then commands say it was refused, and you run `sso` again. Signing in again on
144
- the same computer replaces that computer's key. Works on dev and prod (`npx hawk-app sso --env prod`). Pipelines keep using keys from the portal with `HAWK_APPS_PUBLISHER_KEY`.
145
-
146
- **`login`** takes a publisher key made in the Hawk Analytics portal. Those keys don't expire.
147
-
148
- `login` saves the key in your user profile, so every app on the machine uses it and no project files hold it:
149
- - **Windows:** `%APPDATA%\hawk-app\credentials.json`
150
- - **macOS / Linux:** `~/.config/hawk-app/credentials.json`, readable only by you
151
-
152
- It works the same in PowerShell, cmd and bash. The key is never shown, never stored in shell history, and never passed as a command-line argument.
153
-
154
- Every command works on **dev** unless you say otherwise. `--env prod` (on any command) picks prod:
155
- `npx hawk-app sso --env prod` signs you in to prod, and `npx hawk-app deploy --env prod` deploys there. Logging in
156
- never changes which environment later commands use, so prod is always chosen on purpose; `deploy` says
157
- when they go to prod. Keys are stored per environment, so a dev key is never sent to prod. (`--platform <url>` targets
158
- any other platform, e.g. one running locally.)
159
-
160
- **Which environment**, first match wins: `--env` / `--platform`, then `HAWK_APPS_ENV` (`dev` or `prod`) or
161
- `HAWK_APPS_PLATFORM_URL` in the environment (CI) or the project's `.env.local` / `.env`, else dev.
162
-
163
- **Which key**, first match wins:
164
- 1. The environment variable `HAWK_APPS_PUBLISHER_KEY`. CI pipelines set it as a secured variable.
165
- 2. The project's `.env.local` (git-ignored), for a per-project override. `npx hawk-app login --local` writes it for you.
166
- 3. The machine login from `hawk-app login` for that environment.
167
-
168
- ## Run locally without HawkSearch (`hawk-app dev`)
169
-
170
- ```bash
171
- npm run app:dev # = npx hawk-app dev; asks which client the first time
172
- npx hawk-app dev --client e3141 --engine 3141
173
- ```
174
-
175
- The first time, it asks for part of a client's name and lets you pick the client, then saves it in `.env.local` as
176
- `HAWK_APPS_DEV_CLIENT` (`--client <id>` uses another). A new app is registered on the platform the first time (not
177
- released: only Bridgeline staff see it). Queries and functions always run from the **deployed** version, even here:
178
- deploy after changing them.
179
-
180
- Starts your app (`npm run dev`) and a local stand-in for HawkSearch at `http://localhost:3200`, which embeds the app
181
- the way HawkSearch does (the bridge's own `<hawk-app-frame>`). The app gets **real tokens and real data**: the CLI asks
182
- the platform for an app token with your publisher key, for any client you name, as a Bridgeline user previewing your
183
- newest version (with the scopes waiting for review). The app's platform calls go through the local page, so nothing needs
184
- CORS setup, on dev or prod; so does the AI stream (`app.ai.stream`), which the page pipes through as it arrives.
185
-
186
- - The toolbar changes the client, site (engine), user (Bridgeline or client user), date range and theme. Toasts,
187
- titles and navigation requests from the app show in the page; a panel shows the token's claims and expiry.
188
- - `--url <dev server>`: use an app dev server that is already running (nothing is started). The default is where the
189
- template serves the app: `http://localhost:3100/<slug>/` for Next.js, `http://localhost:3100/` otherwise. `--no-start`: don't run `npm run dev`. `--port`: the local page's port (default 3200). `--client-user`: start
190
- as a client user. `HAWK_APPS_DEV_CLIENT` / `HAWK_APPS_DEV_ENGINE` in `.env.local` replace `--client` / `--engine`.
191
- - The HawkSearch dashboard API (`app.dashboard`, writes, and authenticated search) needs a HawkSearch key, which only a
192
- host has. Put yours in `.env.local` as `HAWKSEARCH_API_KEY` (and `HAWKSEARCH_DOMAIN`, default
193
- `https://dev.hawksearch.net`; `HAWKSEARCH_GROUP_KEY=true` for a group key): the platform seals it into the token. It is
194
- never written anywhere else.
195
- - Queries and functions run against your **current deployed version** (deploy first after changing them); the UI is your
196
- local code. Every dev token is audit-logged with your publisher key.
197
-
198
- ## Deploy
199
-
200
- **One app, one codebase.** Every developer of a publisher (team) can deploy any of its apps, so the CLI
201
- guards against mixing two apps under one slug: `create` and `init` refuse a slug that's already an app (saying who last
202
- changed it; `--yes` to set up a folder for that existing app on purpose), and `deploy` asks before going on
203
- when someone else's key made the app's last change ("`showcase` was last deployed by Jane Doe on … Deploy a new version
204
- of it? (y/N)"). `--yes` skips the question; in a pipeline (no terminal) it's a warning and the command goes on. Another
205
- publisher's slug is always refused (`409 slug_owned`).
206
-
207
- ```bash
208
- npx hawk-app deploy # build and upload a new version, with hawk-app.json
209
- npx hawk-app status # released or not, the newest version, the approval request, the aliases
210
- npx hawk-app open # the app in HawkSearch: review it and submit it for approval
211
- npx hawk-app clients add e3141 # give a client the app (see "Clients")
212
- npx hawk-app deploy --dry-run # what hawk-app.json would change, without a version (use this on pull requests)
213
- ```
214
-
215
- | Command | What it does |
216
- |---|---|
217
- | `init` | Creates `hawk-app.json` (never overwrites an existing one) and adds [AGENTS.md](#ai-coding-assistants-agentsmd). |
218
- | `validate [--build [<folder>]]` | Checks `hawk-app.json` locally, `queries/` and `functions/` if the app has them, and the project: the bridge is used and current, and there's a build script (or `--no-build` deploys). `--build` also builds the app with a stand-in base path (`/<slug>/v0`) and fails on absolute URLs outside it; the folder comes from the `app:deploy` script unless given. |
219
- | `deploy [<folder>] [--no-build] [--no-audit] [--alias <name>] [--dry-run]` | Sends hawk-app.json first (its scopes and settings go with every deploy), then reserves the next version number, runs `npm run build` with `HAWK_APP_BASE_PATH=/<slug>/v<n>` (when package.json has a `build` script, unless `--no-build`), then uploads every file in the build folder (the template's, `dist`, `out` or `public`, unless given) to `/<slug>/v<n>/` on the apps CDN, plus `queries/` and the bundled `functions` (if any) to the platform's private storage, and deploys the functions. Runs `npm audit` of the production dependencies first, asks before deploying with high or critical findings, and reports the counts to reviewers (see [Dependency check](#dependency-check); `--no-audit` skips it). Warns when the HTML loads absolute URLs outside `/<slug>/v<n>/`, which wouldn't load, and when the code calls a bridge feature whose scope `hawk-app.json` lacks (e.g. `app.ai.generate` without `ai:invoke`; `validate` and `doctor` check it too). Says whether the version is a draft (staff only until it's approved) or auto-approved, and when it cleared an approval request. `--alias staging` points the alias at the new version (see [Versions and aliases](#versions-and-aliases)). `--dry-run` only shows what hawk-app.json would change. |
220
- | `register` | Sends `hawk-app.json` without a new version (for scripts; `deploy` and `dev` already send it). It never releases anything and never changes the app's clients. On a released app, changes wait for review. |
221
- | `versions` | Every version with its status (draft, waiting, approved, rejected) and the aliases on it. |
222
- | `alias set <name> <version>` / `alias delete <name>` | Point an alias at a version that isn't approved (only Bridgeline staff in its clients see it), or delete one of yours with no clients on it. Approvers do the rest. |
223
- | `open [--alias <name>]` | Opens the app's page in HawkSearch (dev or prod, from `--env`) to review it and submit it for approval; `--alias` shows it as that alias's clients see it. Prints the link when there's no browser. |
224
- | `docs [<topic>]` | Opens the developer docs at developers.hawksearchapps.com, or a topic's page (`start`, `versions`, `bridge`, `api`, `cli`, `commands`, `functions`, …; `hawk-app help docs` lists them). Prints the link when there's no browser. |
225
- | `doctor` | Checks everything a developer needs: Node, the login and platform, `hawk-app.json`, the project (bridge, build script), the template, the scopes the code uses, the app on the platform and the MCP config. Each line says what to fix; exits 1 when something must be. |
226
- | `help [<command> \| all]` | The commands by task; one command's details; or every detail. |
227
- | `why <client id>` | What a client's users see of the app, and why (the client list, its alias, that version's status). |
228
- | `status` | Shows the automated review checks approvers will see (bundle, dependencies, origins, queries, …), the released version, the newest version and its status, settings waiting for review, the approval request (waiting for approval, or rejected and why), auto-approve, review emails (when an approver turned them off), the view cache (when Engineering shortened or stopped it), visibility, scopes, system parameters, config keys and clients. |
229
- | `clients` / `clients add <id> [<id> …] [--name "<name>"] [--alias <name>]` / `clients exclude <id> [--name "<name>"]` / `clients remove <id>` | Which clients have the app, and which version (alias) each gets (see [Clients](#clients)). |
230
- | `data tables` / `data describe <table>` / `data run <file.sql> --client <id> [--param name=value …]` | The tables app queries can use, and running a query on dev (see [Custom data](#custom-data-queries)). |
231
- | `secrets list` / `secrets set <NAME> [--value-from-env VAR]` / `secrets delete <NAME>` | The app's function secrets (see [Functions](#functions)). |
232
- | `logs <function> [--since 1h] [--follow]` | A function's logs (see [Functions](#functions)). |
233
- | `config list [--client <id>]` / `config set <key> <value> [--client <id>]` / `config unset <key> [--client <id>]` | The app's config values, global or for one client, without a deploy (see [App config](#app-config)). |
234
- | `sso [--publisher <id>]` | Sign in with your Bridgeline account; the platform issues your own key (see above). |
235
- | `login [--local]` / `whoami` / `logout` | Save a publisher key (CI, or without SSO), check which key and platform apply, or remove the saved key. |
236
- | `errors` / `usage` | The app's page errors, and its opens, users and clients per day (see [Errors and usage](#errors-and-usage-hawk-app-errors-hawk-app-usage)). |
237
- | `upgrade` / `templates` / `mcp` | Keep up with the template (see below); list the templates; the MCP server for AI assistants (see [MCP server](#mcp-server-hawk-app-mcp)). |
238
-
239
- Options: `--manifest <file>` to use a manifest other than `hawk-app.json`, `--env dev|prod` (default dev), and `--platform <url>` to target another platform. `deploy` never uploads hidden files such as `.env.local`.
240
-
241
- ## Keep up with the template (`hawk-app upgrade`)
242
-
243
- Templates move on (a new Next.js, a security fix, a config change). `hawk-app upgrade` brings an app's template-owned
244
- parts up to the template this CLI ships:
245
-
246
- ```bash
247
- npx hawk-app upgrade # shows what would change; changes nothing
248
- npx hawk-app upgrade --yes # applies it and runs npm install
249
- ```
250
-
251
- - **Dependencies:** the packages the template pins (e.g. `next`, `react`, `@mui/material`, the bridge) are raised to the
252
- template's version. Never lowered, and your own packages are never touched. A package the template gained since the
253
- app was made is added: a `react`, `preact`, `next` or `next-mui` app gets the UI kit
254
- (`@bridgeline-digital/hawk-app-ui`), and `upgrade` lists the code to change to start using it (next-mui: the
255
- provider; the others: the stylesheet and elements imports). A major version (Material UI 7 → 9) is called out, with
256
- what to check.
257
- - **Config files** (`next.config.ts`, `vite.config.ts`, `tsconfig.json`): replaced only if you never edited them (the
258
- CLI knows every version the templates have shipped). An edited one is left alone, with a diff of what the template
259
- changed so you can apply it yourself.
260
- - The vanilla template's CDN imports (the bridge and the UI kit, in `public/app.js` and `public/index.html`) are raised
261
- together to the template's minor version. A vanilla app made before the kit adds its two lines itself: see a new
262
- app's `public/index.html` and `public/app.js`.
263
- - **AGENTS.md:** the CLI's part is refreshed, and `CLAUDE.md` gets its `@AGENTS.md` import (see below).
264
- - **Never** your code (`src/`), `hawk-app.json` settings or any other file.
265
-
266
- It uses `"template"` in `hawk-app.json`, which `hawk-app create` writes; for older apps it guesses from the dependencies
267
- (and records it on `--yes`), or pass `--template <name>`. An app set up with `init` that follows no template gets only
268
- AGENTS.md refreshed. `hawk-app deploy` tells you when an app is behind its template. After upgrading: build, test
269
- (`npm run dev`) and deploy as usual. `--no-install` skips `npm install`.
270
-
271
- ## Versions and aliases
272
-
273
- Every deploy is a version with a status from its review: draft, waiting (submitted), approved or rejected. An **alias**
274
- points at a version and takes its status: on an approved version its clients' users see the app; on any other only
275
- Bridgeline staff in them do. **`default`** is the version released to everyone, for every client without another alias.
276
-
277
- ```bash
278
- npx hawk-app deploy --alias staging # staging → the new version (staff only until it's approved)
279
- npx hawk-app clients add e4 e5 --alias staging # new clients on staging: staff in them test it
280
- ```
281
-
282
- When an approver approves that version, they choose: release to everyone (`default` moves), or only to the aliases on it
283
- (staging's clients see it, everyone else stays on `default`). You can point aliases only at versions that aren't approved,
284
- and put on them only clients whose users don't see the app yet; anything users see is for approvers (the portal or
285
- HawkSearch). The full picture, with diagrams: docs/versions-and-aliases.md in the hawk-apps repository. `hawk-app status` shows the aliases, `hawk-app versions` every version with its status:
286
-
287
- ```
288
- released v7 (default: every client without another alias)
289
- aliases default v7 (all other clients) · pilot v8 (3 clients) · staging v9 (draft: Bridgeline staff only, 2 clients)
290
- ```
291
-
292
- ## Errors and usage (`hawk-app errors`, `hawk-app usage`)
293
-
294
- The bridge reports the app's page errors and one open per page load (platform/docs/telemetry.md):
295
-
296
- ```bash
297
- npx hawk-app errors # the last 7 days (--days 1-30): grouped, the most frequent first, with versions and clients
298
- npx hawk-app usage # the last 30 days (--days 1-400): opens, view opens, users and clients; staff apart
299
- ```
300
-
301
- ## Accessibility check
302
-
303
- `hawk-app deploy` runs axe-core (WCAG 2.1 A/AA and best practices) over up to 10 built pages and sends the result with
304
- the version: approvers see it as the `accessibility` review check (critical issues fail it; a force approve still goes
305
- through). `hawk-app validate --build` prints critical and serious issues before you deploy. It sees each page's HTML as
306
- built, not what the app draws later in the browser, and no layout (so no color contrast).
307
-
308
- ## Release channels: latest and dev
309
-
310
- The bridge, the UI kit and the CLI are published together, with the same version, on two npm tags:
311
- - **`latest`**, from `main`: stable releases, matched to the prod platform. What `npm install` and `npx hawk-app` get.
312
- - **`dev`**, from `dev`: pre-releases (`x.y.z-dev.<time>`) with features still in development, which may need the dev
313
- platform.
314
-
315
- To start an app on dev features, create it with the dev CLI. It puts the bridge, the UI kit and the CLI on the `dev` tag (the vanilla
316
- template's CDN imports too), so the app gets the dev bridge and not the release its template pins:
317
-
318
- ```bash
319
- npx @bridgeline-digital/hawk-app-cli@dev create my-app # a dev app (or --channel dev with any CLI)
320
- npx hawk-app upgrade --channel latest # once the features are released: ^ the latest versions (--yes applies)
321
- npx hawk-app upgrade --channel dev # move an existing app onto the dev builds (again: onto the newest ones)
322
- ```
323
-
324
- - `--channel` installs the packages it moves by name (`npm install <package>@dev`, or `@^<latest>`), so
325
- `package-lock.json` moves with them; npm then writes the version it installed (`^0.1.0-dev.<time>`), which is
326
- still the dev channel. Running `--channel dev` again moves to the newest dev builds.
327
- - A dev app's `npm update` follows the newest dev builds; its `package-lock.json` records the exact one, so commit it.
328
- - `hawk-app upgrade` keeps a dev app on `dev`; `--channel` is the only thing that moves it.
329
- - `hawk-app validate` warns when a dev app is set to use the prod platform (`--env prod`, `HAWK_APPS_ENV=prod`).
330
-
331
- ## AI coding assistants (AGENTS.md)
332
-
333
- `create` and `init` write these files for AI coding assistants, so they build Hawk Apps the way the platform expects
334
- instead of guessing:
335
-
336
- - **`AGENTS.md`**, read by GitHub Copilot, Cursor, Codex and others: the rules (no secrets, data only through the
337
- bridge, static files, the base path), the bridge's calls and their scopes, queries and functions, the commands, and
338
- what fails review. Its project section matches the app's template, or, for an app set up with `init`, says what the
339
- project has to do itself.
340
- - **`CLAUDE.md`** with `@AGENTS.md`, so Claude Code reads the same file.
341
-
342
- - **`.mcp.json`** (Claude Code) and **`.vscode/mcp.json`** (VS Code / Copilot), which start the CLI's MCP server (see
343
- below) from `node_modules`.
344
-
345
- The CLI owns the part of AGENTS.md between its `<!-- hawk-app:start … -->` and `<!-- hawk-app:end -->` markers, and
346
- `npx hawk-app upgrade` refreshes it when a new CLI changes it. Write your own notes outside the markers; they're kept.
347
- An existing AGENTS.md or CLAUDE.md is never replaced: the CLI's part (or the import) is added to it. An existing MCP
348
- settings file gets the `hawk-apps` server added (one with comments is left alone: add it yourself).
349
-
350
- ### MCP server (`hawk-app mcp`)
351
-
352
- `hawk-app mcp` serves the CLI's commands to AI coding assistants over stdio (the [Model Context
353
- Protocol](https://modelcontextprotocol.io)), so they can look things up and check their own work:
354
-
355
- | Tool | What it does |
356
- | --- | --- |
357
- | `docs` | Searches or reads the bridge and UI kit READMEs (the app's installed versions), this README, the `hawk-app.json` schema and the showcase app, whose code shows every feature working. |
358
- | `validate` | `hawk-app validate`, optionally with `--build`. |
359
- | `status` | `hawk-app status`, including the automated review checks approvers will see. |
360
- | `doctor` | `hawk-app doctor`: what's wrong with the setup, and how to fix it. |
361
- | `versions`, `why` | Every version with its status and aliases; what a client sees of the app, and why. |
362
- | `errors`, `usage` | The app's page errors; its opens, users and clients per day. |
363
- | `ui_components` | The UI kit's components for the app's template (next-mui: the provider, components and theme; the others: the stylesheet classes and elements), or what's said about one, from the kit's README, with a link to the live [gallery](https://developers.hawksearchapps.com/reference/ui-kit-gallery/). |
364
- | `analytics_reports` | The analytics API's reports for `app.analytics.report()`, with one report's columns, filters and an example call, read from the developer portal's dev spec (public; no client data). |
365
- | `hawksearch_api` | HawkSearch's API reference (developerdocs.hawksearch.com): an operation's parameters, request and response fields, the bridge call (`app.dashboard.*`, `app.hawksearch.*`) and its scope, or why apps can't call it (public; no client data). |
366
- | `data_tables`, `data_describe` | Table names and columns for queries. Never data: `data run` isn't a tool, because its rows would go to the assistant. |
367
- | `logs` | A function's logs. |
368
- | `init`, `upgrade` | `hawk-app init`; `hawk-app upgrade` (`apply` for `--yes`). |
369
- | `deploy` | `hawk-app deploy` (the folder is optional; `alias`, and `dryRun` to only show what hawk-app.json would change). Marked destructive, so the assistant asks you first. |
370
- | `clients`, `clients_add` | The app's clients, by id. Say "deploy to OTC": `clients_add` finds the matching clients and shows *you* a form to pick one; the assistant only hears whether a client was added. Marked destructive too. Removing clients isn't a tool. |
371
-
372
- It also has **prompts**, ready-made tasks that use the tools. Claude Code lists them as slash commands (type `/` and
373
- `hawk`):
374
-
375
- | Prompt | In Claude Code | What it does |
376
- | --- | --- | --- |
377
- | `check-before-submit` | `/mcp__hawk-apps__check-before-submit` | Validates (with the build check), reads the status and review checks, and explains how to fix what an approver would have to force approve. |
378
- | `add-feature` | `/mcp__hawk-apps__add-feature "<what to build>"` | Finds the bridge calls, scopes and showcase examples it needs, says what the review will see, makes the change and validates it. |
379
- | `explain-app` | `/mcp__hawk-apps__explain-app` | What the app does, its scopes (unused or missing ones too) and where it stands in review. |
380
- | `deploy-to-client` | `/mcp__hawk-apps__deploy-to-client "<client name>"` | Deploys to dev and gives a client the app; you pick the client from the matches for its name. |
381
-
382
- Each takes an optional `dir` for a workspace with several apps. In VS Code with Copilot they're under `/` too.
383
-
384
- - **Dev only.** Every tool runs with `--env dev`; nothing reaches prod. Approvals, secrets, removing clients and logins
385
- aren't tools.
386
- - **Your key stays put.** The tools use your saved login (or `.env.local`) like the CLI does; the key never passes
387
- through the assistant.
388
- - **No client names.** Tool output shows client ids only, and client names go to you, never to the assistant: the
389
- client picker is a form your assistant shows you (MCP elicitation). An assistant without forms tells you to run
390
- `npx hawk-app clients add --pick "<name>"` in a terminal instead.
391
- - **No silent go-aheads.** Where the CLI would ask (a teammate made the app's last change, or `npm audit` found high or
392
- critical vulnerabilities) the tool stops and tells the assistant, which has to ask you and call again with `force`.
393
- - **Several apps in one workspace?** Each tool takes `dir`, the app's folder relative to the workspace.
394
-
395
- `create` and `init` add it to `.mcp.json` and `.vscode/mcp.json`, started as
396
- `node node_modules/@bridgeline-digital/hawk-app-cli/bin/hawk-app.mjs mcp` (the CLI must be a dev dependency;
397
- `validate` warns when it isn't). For Cursor, copy the `mcpServers` entry of `.mcp.json` to `.cursor/mcp.json`.
398
-
399
- ## Clients
400
-
401
- Which clients have the app is not in `hawk-app.json`: the platform's registry holds it, and Engineering and approvers
402
- change it in the Hawk Analytics portal. Developers can change it with the CLI:
403
-
404
- ```bash
405
- npx hawk-app clients # the app's visibility and its clients
406
- npx hawk-app clients search outdoor # find a client's id by its name
407
- npx hawk-app clients add e3141 --name "Outdoor Demo" # give a client the app (visibility allowList)
408
- npx hawk-app clients add --pick "outdoor" # the same, picking the client from the matches
409
- npx hawk-app clients add e4 e5 --alias staging # on an alias: the version they get
410
- npx hawk-app clients add e4 --alias default # back to the default version
411
- npx hawk-app clients exclude e2002 # opt a client out (visibility all)
412
- npx hawk-app clients remove e3141 # take the client off the list
413
- ```
414
-
415
- `add` and `exclude` take only ids from the environment's HawkSearch client list (the Hawk Analytics portal's client
416
- picker): an id the dashboard doesn't use, such as `3141` for `e3141`, is refused with the id you probably meant. The
417
- client's name comes from that list unless you pass `--name`. `remove` takes any id on the app's list.
418
-
419
- `hawk-app clients` lists `+ e3141 (Outdoor Demo)` for a client that has the app and `- e2002 (Acme Retail)` for one
420
- that is opted out; `[on pilot]` after a client on an alias (`[on staging · draft: Bridgeline staff only]` when its version
421
- isn't approved). What the list means depends on `visibility` in `hawk-app.json`:
422
- - `allowList`: only the clients marked `+` have the app.
423
- - `all`: every client has it, except the ones marked `-`.
424
- - `internal`: Bridgeline users only; the list is ignored.
425
-
426
- Changes apply at once, released or not, without a deploy or an approval, and are recorded in the app's history.
427
- `--name` is a label for people (up to 100 characters); the client id is what grants access. Register the app before
428
- adding clients.
429
-
430
- `--alias` sets the version a client gets (see [Versions and aliases](#versions-and-aliases)). You can change it only for
431
- clients whose users don't see the app yet; after that, approvers do.
432
-
433
- ## Custom data (queries)
434
-
435
- Apps with the `data:read` scope can ship their own SQL, run by the platform against the current client's analytics
436
- tables. Put each query in `queries/` next to `hawk-app.json` (not in the build folder) as a pair of files:
437
-
438
- ```text
439
- my-app/
440
- hawk-app.json "scopes": ["data:read"]
441
- queries/
442
- top-searches.sql
443
- top-searches.json
444
- ```
445
-
446
- ```sql
447
- -- queries/top-searches.sql
448
- SELECT search_date, sum(search_count) AS searches
449
- FROM dailysearches
450
- WHERE search_date BETWEEN ? AND ? AND {{partitions(from, to)}}
451
- GROUP BY 1 ORDER BY 1
452
- ```
453
-
454
- ```json
455
- { "params": [{ "name": "from", "type": "date", "required": true }, { "name": "to", "type": "date", "required": true }] }
456
- ```
457
-
458
- The app calls it with `app.data.query('top-searches', { from, to })` (see the bridge README for types, the
459
- `{{partitions(from, to)}}` macro, the tables and the errors).
460
-
461
- **Step by step**
462
- 1. Add `"data:read"` to `scopes` in `hawk-app.json`.
463
- 2. Find your table and columns: `npx hawk-app data tables`, then `npx hawk-app data describe <table>`.
464
- 3. Write `queries/<name>.sql` and `queries/<name>.json`. Filter by a date range and add
465
- `{{partitions(from, to)}}`.
466
- 4. Try it on dev: `npx hawk-app data run queries/<name>.sql --client <id> --param from=… --param to=…`.
467
- 5. Call it from the app with `app.data.query('<name>', { … })`. Queries always run from a **deployed** version, even
468
- in dev mode (`?devUrl=`): the platform never reads your local `queries/` folder. So after adding or editing a
469
- query, deploy (step 6) before calling it from the app; use `data run` to iterate on the SQL itself.
470
- 6. `npm run app:deploy` (checks the queries and uploads them with the new version, with `hawk-app.json`).
471
- 7. Bridgeline staff review the version in HawkSearch: they see each added or changed query and its SQL diff.
472
- Once approved (or right away on a draft or auto-approve app), the app's users get the new queries.
473
-
474
- `hawk-app validate` and `hawk-app deploy` check the queries before anything is uploaded, with the same rules as the
475
- platform:
476
- - names are lowercase letters, digits and single dashes (up to 64), and every `.sql` has a `.json` (and vice versa);
477
- only `.sql` and `.json` files, no subfolders, at most 50 queries
478
- - each `.sql` is one `SELECT` or `WITH … SELECT` statement (no `;`), up to 64 KB
479
- - the `?` count equals the number of params (a `string[]` param is one `?`, written `IN ?`)
480
- - params have a `name` and a `type` (`string`, `int`, `decimal`, `date`, `timestamp`, `boolean`, `string[]`), and
481
- optionally `required`, `default`, `min`/`max` (numbers) and `maxItems` (`string[]`, up to 100)
482
- - `{{partitions(from, to)}}` (optionally `{{partitions(from, to, alias)}}`) uses params declared as `date`
483
- - `queries/` needs `data:read` in `scopes`.
484
-
485
- `deploy` uploads them with the version (`… 3 files, 12 KB from dist, 2 queries`). They're part of the version: staff
486
- review them with it, and each version runs its own queries.
487
-
488
- Try a query on dev before deploying it:
489
-
490
- ```bash
491
- npx hawk-app data tables # the tables queries can use (and which are partitioned)
492
- npx hawk-app data tables --app my-app # also: may my-app use the sensitive tables?
493
- npx hawk-app data describe dailysearches # a table's columns and types
494
- npx hawk-app data run queries/top-searches.sql --client e3141 --param from=2026-09-01 --param to=2026-09-30
495
- ```
496
-
497
- `data run` sends the SQL and its `.json` from your disk (nothing needs to be deployed), waits for the result, and prints
498
- up to 50 rows plus the bytes scanned. `--client` is the HawkSearch client id (the one in the app's context,
499
- `context.tenant.clientId`); on dev, use a demo or test client. `string[]` values are comma-separated (`--param engines=a,b`). It only works
500
- against the dev platform. When too many queries are running (429 `too_many_queries`) it waits and tries again.
501
-
502
- The sensitive tables (visitor-level data such as `summary`) are listed apart: only apps Engineering has opted in can
503
- query them. Ask Engineering if your app needs one.
504
-
505
-
506
- ## Functions
507
-
508
- Server-side code that ships with each version (AWS Lambda, Node 22), called from the app with
509
- `app.functions.call(name, input)`. The bridge README's **Functions** section covers `ctx`, errors and limits; this is
510
- the CLI side.
511
-
512
- **Before you start: Engineering must enable functions for your app** (they can reach the internet and hold secrets),
513
- and a version whose functions changed **always needs review**, even on an auto-approve app.
514
-
515
- 1. Add `"functions:invoke"` to `scopes`, and declare each function in `hawk-app.json`:
516
- ```jsonc
517
- "functions": {
518
- "sync-feed": { "timeoutSeconds": 20, "memoryMb": 512, "secrets": ["FEED_API_KEY"] },
519
- "echo": {}, // defaults: 10 s, 256 MB, no secrets
520
- "nightly-report": { "job": true, "timeoutSeconds": 600 } // a background job (see below)
521
- }
522
- ```
523
- 2. Write `functions/<name>.ts` (or `.mts`, `.js`, `.mjs`) exporting `handler`, with
524
- `@bridgeline-digital/hawk-app-bridge` (0.4.0 or later) installed in the app:
525
- ```ts
526
- import type { HawkFunction } from '@bridgeline-digital/hawk-app-bridge/server';
527
- export const handler: HawkFunction<{ name: string }, { greeting: string }> = async (input, ctx) => ({
528
- greeting: `Hello ${input.name} from ${ctx.client.id}`,
529
- });
530
- ```
531
- Shared code goes in subfolders (`functions/lib/…`) or anywhere else you import from (`src/`, npm packages).
532
- 3. Set its secrets once: `npx hawk-app secrets set FEED_API_KEY`.
533
- 4. `npx hawk-app validate`, then deploy as usual: `npm run app:deploy`.
534
- 5. Call it from the app: `await app.functions.call('sync-feed', { since: '2026-09-01' })`. Like queries, functions
535
- always run from a **deployed** version, also in dev mode (`?devUrl=`).
536
-
537
- `hawk-app validate` and `hawk-app deploy` check, with the platform's rules:
538
- - at most 10 functions; names are lowercase letters, digits and single dashes, up to 40 characters;
539
- - `job` true or false; `timeoutSeconds` 1–25 (1–900 for a job), `memoryMb` 128–1024, `secrets` up to 20 unique
540
- `UPPER_SNAKE_CASE` names, no other keys;
541
- - every declared function has its `functions/<name>.*` file, and every entry file in `functions/` is declared;
542
- - the `functions:invoke` scope, and the bridge installed with its `/server` entry.
543
-
544
- **What deploy does with them.** Before reserving a version, each function is bundled with esbuild (Node 22, ESM,
545
- every dependency included, your `handler` wrapped for Lambda) into `index.mjs`, zipped as `bundle.zip` (10 MB at
546
- most). `source.zip` (every file under `functions/` except `node_modules`, 5 MB at most) and `deps.json` (the npm
547
- packages in the bundle, with versions) go along so reviewers can see the code and its dependencies. After the upload
548
- the platform deploys each function and the CLI prints one line per function:
549
-
550
- ```
551
- Bundling 2 functions…
552
- ƒ echo 4 KB zipped, 1 package
553
- ƒ sync-feed 38 KB zipped, 2 packages
554
- Uploading v9 of my-app: 12 files, 210 KB from dist, 2 functions
555
- ✔ function echo (10 s, 256 MB) Lambda version 3
556
- ✔ function sync-feed (20 s, 512 MB, secrets FEED_API_KEY) Lambda version 5
557
- ✔ Deployed v9 of my-app (12 files, 2 functions). A draft: clients stay on v8 until it's approved in HawkSearch …
558
- ```
559
-
560
- Zips are built the same way every time, so a function whose code didn't change keeps the same code hash (and doesn't
561
- show as changed in review).
562
-
563
- **Secrets.** App-level, stored by the platform (encrypted, in AWS Systems Manager Parameter Store), never in the app or
564
- a version: a change applies to every version at once. A function receives only the secrets it declares, as
565
- `ctx.secrets.NAME`.
566
-
567
- ```bash
568
- npx hawk-app secrets list # names and when they were set; never values
569
- npx hawk-app secrets set FEED_API_KEY # type the value (hidden, never echoed)
570
- npx hawk-app secrets set FEED_API_KEY --value-from-env FEED # CI: read it from an environment variable
571
- npx hawk-app secrets delete FEED_API_KEY
572
- ```
573
-
574
- **Client secrets** are for values each client sets for itself, such as its email platform's API key. Declare them on
575
- the function (`"clientSecrets": ["MAILCHIMP_API_KEY"]`). The app sets them with `app.clientSecrets.set(...)` and the
576
- function reads `ctx.clientSecrets.MAILCHIMP_API_KEY`. The CLI never sees them.
577
-
578
- Values are never printed, logged or passed as command-line arguments, and are at most 4 KB each (UTF-8 bytes).
579
-
580
- **Logs.** `console.log` and `console.error` go to the function's log (kept 30 days):
581
-
582
- ```bash
583
- npx hawk-app logs sync-feed # recent events
584
- npx hawk-app logs sync-feed --since 1d # 15m, 1h, 1d or an ISO time
585
- npx hawk-app logs sync-feed --follow # keep polling every 2 s (Ctrl+C to stop)
586
- ```
587
-
588
- **When a call fails** the app gets a `HawkApiError`: `function_failed` (the handler threw: see the logs),
589
- `function_timeout`, `secret_missing` (run `secrets set`), `functions_not_enabled` (ask Engineering) or
590
- `function_not_found` (deploy the version with the function). The bridge README lists them all.
591
-
592
- ### Background jobs and schedules
593
-
594
- A function with `"job": true` is a **background job**: the app starts it with `app.functions.start(name, input)`
595
- (it can't be called with `call`, and a normal function can't be started), it runs up to 900 s (default 300), and the
596
- app reads its result later with `app.functions.wait(jobId)`. The bridge README's **Background jobs** section has the
597
- code and limits.
598
-
599
- `schedules` run job functions on a timer, for the clients where a user turns them on from the app
600
- (`app.schedules.enable(name)`); nothing runs until someone does:
601
-
602
- ```jsonc
603
- "functions": { "nightly-report": { "job": true, "timeoutSeconds": 600 } },
604
- "schedules": {
605
- "nightly": { "function": "nightly-report", "cron": "0 7 ? * MON-FRI *", "timezone": "America/Chicago" }
606
- }
607
- ```
608
-
609
- `hawk-app validate` checks: at most 5 schedules, named like functions; `function` is a job function of the same
610
- `hawk-app.json`; `cron` is an [EventBridge cron](https://docs.aws.amazon.com/scheduler/latest/UserGuide/schedule-types.html#cron-based)
611
- with 6 fields (`minutes hours day-of-month month day-of-week year`, no `cron( )` wrapper) that runs **at most hourly**
612
- (the minutes field is a single number, 0–59) with `?` for day-of-month or day-of-week (EventBridge needs one);
613
- `timezone` is an IANA name like `America/Chicago`, or `UTC` (the default); the `functions:invoke`
614
- scope. `schedules` is a setting sent with each deploy: on a released app a change waits for review like scopes,
615
- and `hawk-app status` lists the registered schedules.
616
-
617
- ## Email
618
-
619
- Apps with the `email:send` scope can send email with `app.email.send(...)` or, from a function,
620
- `ctx.platform.email.send(...)` (the bridge README's **Email** section has the details). There are no CLI commands for
621
- it: add `"email:send"` to `scopes` and deploy (without the scope, sends fail with `scope_missing`). Mail goes out from `noreply@hawksearch.com` with the app's name, to up to 10 recipients per
622
- send and 200 recipients per app and client per day.
623
-
624
- ## HawkSearch system parameters
625
-
626
- Apps with the `hawksearch:sysparams:write` scope can read and set the HawkSearch system parameters they list in
627
- `systemParameters`, for the user's current engine, with `app.hawksearch.systemParameters.list() / get(name) /
628
- set(name, value)` (or `ctx.platform.hawksearch.systemParameters` in a function; the bridge README's **HawkSearch
629
- system parameters** section has the details). There are no CLI commands for it:
630
-
631
- ```json
632
- {
633
- "scopes": ["hawksearch:sysparams:write"],
634
- "systemParameters": ["Timezone", "SearchPreviewURL"]
635
- }
636
- ```
637
-
638
- `hawk-app validate` checks the list: up to 50 names of 1-50 letters, digits, `_`, `.` or `-`, each listed once
639
- (names are not case-sensitive); the list needs the scope, and the scope needs a non-empty list. The list is a setting:
640
- on a released app a change waits for review like scopes. Any user of the app can set the listed parameters, including
641
- "AE only" ones; encrypted, global and read-only ones never can. Changes apply within about 3 minutes and only to the
642
- user's current engine (no group sync).
643
-
644
- ## App config
645
-
646
- Settings someone changes without a deploy (who gets a notification email, a threshold, a switch) go in `config`:
647
- declare each key with its type and an optional default, and read it in the app with `app.config.get(key)` /
648
- `app.config.all()` (`ctx.config` in a function; the bridge README's **App config** section has the details).
649
-
650
- ```json
651
- {
652
- "config": {
653
- "signupNotifyEmails": { "type": "emails", "label": "Sign-up notification recipients", "default": ["support@example.com"] },
654
- "connectorsRequestEmails": { "type": "emails", "label": "API Connections request recipients" }
655
- }
656
- }
657
- ```
658
-
659
- The values are set outside hawk-app.json, from the Hawk Analytics portal or the CLI, for the whole app (global) or for
660
- one client, and apply within about 30 seconds, with no deploy or approval. A client's value wins over the global one,
661
- which wins over the default. **Values are not secret**: they reach the browser, so keep keys in `hawk-app secrets`.
662
-
663
- ```bash
664
- npx hawk-app config list [--client e3141] # each key: type, default, global and client values
665
- npx hawk-app config set signupNotifyEmails a@example.com,b@example.com # global
666
- npx hawk-app config set signupNotifyEmails csd@example.com --client e3141 # one client
667
- npx hawk-app config unset signupNotifyEmails --client e3141
668
- ```
669
-
670
- `set` reads the value as the key's type in your hawk-app.json: `string` as is, `number`, `boolean` (`true`/`false`),
671
- `emails` comma-separated, `json` as JSON (quote it for your shell). `hawk-app validate` checks the declarations: at
672
- most 50 keys named `^[A-Za-z][A-Za-z0-9_]{0,63}$`, each with `type` (`string`, `number`, `boolean`, `emails`,
673
- `json`), optional `label` (up to 100 characters), `description` (up to 500) and `default` (a valid value of the type;
674
- `emails` up to 50 different plain addresses; every value at most 16 KB). The declarations are a setting: on a released
675
- app a change waits for review like scopes (values can already be set for a key waiting for review). `hawk-app status`
676
- lists the declared keys.
677
-
678
- ## AI
679
-
680
- Apps with the `ai:invoke` scope can ask Claude with `app.ai.generate(...)` / `app.ai.stream(...)` or, from a function,
681
- `ctx.platform.ai` (the bridge README's **AI** section has the details). There are no CLI commands for it: add
682
- `"ai:invoke"` to `scopes` and deploy. Two tiers, `fast` and `smart`, each with a daily token budget
683
- per app and client, plus a per-minute request limit; `app.ai.info()` shows today's use. The scope is elevated: an app
684
- with it is never auto-approved. Prompts and answers are never logged or stored.
685
-
686
- ## Review in HawkSearch
687
-
688
- - **Not released yet.** A new app's versions are drafts: only Bridgeline staff see it, only on the clients it's enabled
689
- for, on the Apps page in HawkSearch, always at its newest version. Developers can't release it; an approver does, on
690
- the Apps page (see [Versions and aliases](#versions-and-aliases)).
691
- - **Released apps.** Every deploy is a new version. Clients stay on the approved version until the new one is approved in
692
- HawkSearch (staff can open it there first). Changes to settings in `hawk-app.json` (name, description,
693
- icon, category, version, visibility, hosting, scopes, `systemParameters`, `schedules`, `config`, `externalOrigins`, `listed`, `minProtocolVersion`) wait for approval
694
- the same way. For `config` only the declarations are reviewed: its values are set at any time without review.
695
- - **Functions.** Engineering enables functions per app. A version whose functions changed (added, removed, new code,
696
- dependencies or settings) always waits for review, even with auto-approve; reviewers see each function's settings,
697
- dependency changes and a diff of its sources.
698
- - **Clients any time.** Client changes (`hawk-app clients` or the Hawk Analytics portal) apply at once, released or
699
- not, without review (see [Clients](#clients)).
700
- - **Auto-approve.** Approvers can set an app to auto-approve: new versions and setting changes are then released
701
- straight away.
702
- - **Approval workflow.** Bridgeline users submit a version or settings change for approval on the Apps page in
703
- HawkSearch (`npx hawk-app open`; with an optional note). Members of the **Approvers** group approve it (it's released), reject it with a
704
- reason, or force approve without a request (for example to roll back). The approvers get an email about each request
705
- (they can stop these with the link at the bottom), the submitter gets one when it's approved or rejected, and every
706
- step is posted to the #hawk-app-approvers Slack channel. A new main-line deploy or settings change clears a waiting request (deploy says so: submit it again in
707
- HawkSearch). `hawk-app status` shows `request waiting for approval (submitted by Jane Doe, 2026-10-08: <note>)`
708
- or `request rejected by Jane Doe on 2026-10-09: <reason>`.
709
- - **Elevated apps.** Apps using queries, functions, system parameters, dashboard writes, AI, external Lambdas, HawkSearch
710
- features or the index (rebuilds, items, management) are never auto-approved;
711
- approvers review their queries, functions and scopes like any other change.
712
- - **Rollback and retention.** The released version, the newest version and the 5 newest others are kept, so staff can
713
- roll back in HawkSearch; older versions are removed when you deploy.
714
-
715
- ## How deploy works
716
-
717
- 1. The CLI asks the platform for the next version number (`v<n>`) with your publisher key, and builds the app for
718
- `/<slug>/v<n>` (unless `--no-build`).
719
- 2. The CLI sends the list of files (paths and sizes). The platform checks them:
720
- - the file types are static web content
721
- - the sizes are within the limits
722
- - the paths can't leave your app's folder
723
- - you own the slug.
724
-
725
- It then returns one **short-lived upload link per file**. Each link allows exactly one upload, to exactly that file in the version's folder, and expires after about 15 minutes.
726
- 3. The CLI uploads each file straight to storage using its link.
727
- 4. The CLI tells the platform it's done, with the result of `npm audit` of the production dependencies (run before the version was reserved, see below). The platform checks that every file arrived and marks the version ready: an app not released yet shows it to Bridgeline staff at once, a released app holds it for review (or releases it, with auto-approve). Versions never change once uploaded, so there's no CDN refresh to wait for.
728
-
729
- The platform also sets content types and caching: hashed bundles are cached for a year, HTML is never cached, and everything else is cached for 5 minutes.
730
-
731
- ### Dependency check
732
-
733
- Before it reserves a version number, `deploy` runs `npm audit --json --omit=dev` in the app folder (next to
734
- `hawk-app.json`, 60 s at most), and it sends the vulnerability counts with the version. Reviewers see them with the other
735
- pre-approval checks in HawkSearch and the Hawk Analytics portal. High or critical vulnerabilities fail that check, so
736
- approvers can only force approve the version: when the audit finds any, `deploy` names the packages and asks before
737
- going on.
738
-
739
- ```
740
- dependencies 1 high, 1 moderate (npm audit)
741
- npm audit found 1 high, 1 moderate in the production dependencies (postcss). Approvers can only force approve this
742
- version. Deploy anyway? (Fix with npm audit fix or by updating the packages; npm run app:audit shows the details.) (y/N)
743
- ```
744
-
745
- No cancels without reserving anything. `--yes` goes on with a warning, and so does a pipeline (no terminal to ask in).
746
- Moderate and low findings are only reported. Check before deploying with `npm run app:audit` (in apps created from the
747
- templates: `npm audit --omit=dev --audit-level=high`, which exits non-zero on high or critical findings, so it also works
748
- as a CI step). Other outcomes:
749
-
750
- ```
751
- dependencies no known vulnerabilities
752
- dependencies not checked: no package-lock.json
753
- ```
754
-
755
- An app in an npm workspace is audited with the lockfile at the repository root. Without a `package-lock.json` (in the
756
- app folder or above it), when npm audit fails (for example a registry without audit support) or times out, the
757
- version is marked "not checked" with a short reason. `--no-audit` skips it (`not checked: skipped (--no-audit)`).
758
-
759
- ## In CI (example: Bitbucket Pipelines)
760
-
761
- ```yaml
762
- - step:
763
- name: Deploy Hawk App (dev)
764
- deployment: development # HAWK_APPS_ENV (dev or prod) and HAWK_APPS_PUBLISHER_KEY as secured variables
765
- script:
766
- - npm ci
767
- - npx hawk-app deploy # builds with the version's base path, then uploads (with hawk-app.json)
768
- ```
769
-
770
- Requires Node 20 or later. Dependencies: `esbuild` (bundles functions) and `fflate` (zips them).
1
+ # @bridgeline-digital/hawk-app-cli
2
+
3
+ Build, run and release **Hawk Apps**, that run inside HawkSearch. Sign in with your Bridgeline account (or a
4
+ **publisher key** in CI): no AWS account, no bucket names, no platform repo.
5
+
6
+ The full developer docs, with guides, the bridge API and every command, are at
7
+ **[developers.hawksearchapps.com](https://developers.hawksearchapps.com)** (sign in with your Bridgeline account;
8
+ `npx hawk-app docs` opens them).
9
+
10
+ ## Quickstart
11
+
12
+ ```bash
13
+ npx @bridgeline-digital/hawk-app-cli@latest sso # 1. once per machine: sign in with your Bridgeline account
14
+ npx @bridgeline-digital/hawk-app-cli@latest create my-app && cd my-app && npm install # 2. a new app
15
+ npm run app:dev # 3. run it locally inside a stand-in for HawkSearch
16
+ npm run app:deploy # 4. build and upload a new version (a draft)
17
+ npx hawk-app open # 5. review it in HawkSearch and submit it for approval
18
+ ```
19
+
20
+ Then `npx hawk-app status` shows where the app stands, and `npx hawk-app doctor` checks everything (Node, login,
21
+ `hawk-app.json`, the project, the scopes the code uses) and says what to fix. `npx hawk-app help` lists the commands by
22
+ task; `npx hawk-app help <command>` gives one command's details.
23
+
24
+ The templates add the CLI as a dev dependency; elsewhere: `npm i -D @bridgeline-digital/hawk-app-cli`.
25
+
26
+ ## Start a new app
27
+
28
+ ```bash
29
+ npx @bridgeline-digital/hawk-app-cli@latest create keyword-explorer # React + Vite + TypeScript
30
+ npx @bridgeline-digital/hawk-app-cli@latest create keyword-explorer --template preact # Preact + Vite + TypeScript (small bundle)
31
+ npx @bridgeline-digital/hawk-app-cli@latest create keyword-explorer --template next # Next.js (static export)
32
+ npx @bridgeline-digital/hawk-app-cli@latest create keyword-explorer --template next-mui # Next.js + Material UI (the UI kit's components)
33
+ npx @bridgeline-digital/hawk-app-cli@latest create keyword-explorer --template vanilla # plain HTML/JS, no build
34
+ ```
35
+
36
+ The folder name becomes the app's slug. Every template comes ready to run:
37
+ - the bridge set up, with the base path read from `HAWK_APP_BASE_PATH` (set by `hawk-app deploy`; `/<slug>/` in local dev)
38
+ - Spark's look in the host's colors and mode, from the UI kit
39
+ ([`@bridgeline-digital/hawk-app-ui`](../ui/README.md)): `next-mui` its MUI theme, provider and components;
40
+ every other template its stylesheet and elements (`<hawk-kpi>`, `<hawk-data-table>`, `<hawk-column-chart>`, …;
41
+ `vanilla` loads them from the CDN)
42
+ - a working analytics example (total searches for the selected engine and date range, per day)
43
+ - a `hawk-app.json` (new apps start as drafts)
44
+ - `app:dev` / `app:deploy` scripts and a README
45
+ - `AGENTS.md`, `CLAUDE.md` and the MCP server's settings for AI coding assistants (see [AI coding assistants](#ai-coding-assistants-agentsmd))
46
+
47
+ Add `--name "Keyword Explorer"` to set the display name, and `--install` to run `npm install` straight away.
48
+
49
+ ## Add to an existing app
50
+
51
+ ```bash
52
+ npx hawk-app init # creates hawk-app.json and AGENTS.md
53
+ npx hawk-app validate # checks hawk-app.json and the project (bridge, build script)
54
+ ```
55
+
56
+ `init` also writes [AGENTS.md](#ai-coding-assistants-agentsmd) with what a template would have done for you: the bridge,
57
+ the base path, the build folder and the dev server. `npx hawk-app validate --build` builds the app as deploy does and
58
+ checks that its HTML only loads URLs under the version's base path.
59
+
60
+ `hawk-app.json` decides the app's **visibility** and what data it can read (which clients have it is managed
61
+ separately, see [Clients](#clients)):
62
+
63
+ ```jsonc
64
+ {
65
+ "slug": "keyword-explorer", // unique; each deploy is served at <apps CDN>/keyword-explorer/v<n>/
66
+ "name": "Keyword Explorer",
67
+ "visibility": "allowList", // allowList: only the app's clients; all: every client except excluded ones;
68
+ // internal: Bridgeline users only; store: the app's clients, and every other
69
+ // client can request it from the store (needs "store", below) (clients: see "Clients")
70
+ "icon": "icon.svg", // the app's icon: an .svg (drawn in the host's color) or a square .png, next to this file
71
+ "scopes": ["reports:read"] // reports:read = analytics; hawksearch:read = dashboard API (read);
72
+ // hawksearch:write = create/update/delete synonyms, redirects, banners, …
73
+ // through the dashboard API (see the bridge's "Writing to HawkSearch");
74
+ // data:read = your own SQL queries in queries/ (see "Custom data");
75
+ // functions:invoke = server-side functions (see "Functions");
76
+ // email:send = send email (see "Email");
77
+ // storage = the app's own data and files per client (see the bridge's "Storage");
78
+ // hawksearch:sysparams:write = set HawkSearch system parameters
79
+ // (with "systemParameters", see "HawkSearch system parameters");
80
+ // hawksearch:search = HawkSearch search and autocomplete, live or preview;
81
+ // hawksearch:recommendations = the engine's recommendation widgets;
82
+ // hawksearch:index = rebuild the engine's index (where HawkSearch allows it)
83
+ // and read its indexes; hawksearch:index:items = add, update, delete items;
84
+ // hawksearch:index:manage = create, switch and delete indexes, hierarchy, mappings;
85
+ // hawksearch:features:write = HawkSearch features on and off;
86
+ // lambdas:invoke = Lambdas outside Hawk Apps (with "lambdas");
87
+ // ai:invoke = Claude through the platform (see "AI")
88
+ // "config": { "signupNotifyEmails": { "type": "emails", "default": ["a@example.com"] } }
89
+ // = non-secret settings the app reads with app.config, values set without a deploy (see "App config")
90
+ }
91
+ ```
92
+
93
+ Optional `"listed": false` keeps the app off the host's Apps landing page (the host also hides its own title and back
94
+ button for it), while it still opens by its direct URL. It only changes the listing, not who can open the app
95
+ (`visibility` and the app's clients still apply); the default is `true`, and `hawk-app status` shows `listed no` when it's off.
96
+
97
+ **Store apps.** `"visibility": "store"` offers the app to every client that doesn't have it, on the Store tab of the
98
+ Apps page in HawkSearch: a client user requests it, and a Hawk-App-Manager approves (the client is added to its
99
+ clients) or declines. It needs the listing:
100
+
101
+ ```jsonc
102
+ "store": {
103
+ "tagline": "Find and fix searches that return nothing", // one line, at most 120 characters
104
+ "highlights": ["Zero-result searches by volume"], // optional, up to 5
105
+ "screenshots": ["store/overview.png", "store/fix.png"] // 1-5 .png, .jpg or .webp next to hawk-app.json, at most 1 MB each
106
+ }
107
+ ```
108
+
109
+ A view with `"trial": true` (one at most, no params) is the live trial: the card runs it on the client's own data
110
+ before they have the app, with a read-only token (only the app's `reports:read`, `hawksearch:read`,
111
+ `hawksearch:search`, `hawksearch:recommendations` and `data:read`). There `app.context.trial` is true: hide what needs
112
+ other scopes (`validate` lists them); `navigate({ intent: 'app' })` opens the request dialog. Without a trial view, or
113
+ for a client without analytics data, the card shows the screenshots.
114
+
115
+ `validate` and `deploy` check the screenshots, and `deploy` uploads them with each version (`_hawk/store/1.png`…). The
116
+ listing is reviewed like the other settings; the price label is set by Bridgeline, not here. `hawk-app status` shows
117
+ whether the app is in the store, its price label and how many requests are open, approved and declined.
118
+
119
+ There is no `status` field any more: approvers release versions from the Apps page in HawkSearch (see [Review](#review-in-hawksearch)). An old `status` is ignored with a warning.
120
+
121
+ Every deploy is a new version served at `/<slug>/v<n>/`, so the build needs that base path. `hawk-app deploy` runs `npm run build` with `HAWK_APP_BASE_PATH=/<slug>/v<n>`; read it in your build config with `/<slug>` as the local fallback:
122
+ - Vite: ``base: `${process.env.HAWK_APP_BASE_PATH ?? '/keyword-explorer'}/` ``
123
+ - Next.js (`output: 'export'`): `basePath: process.env.HAWK_APP_BASE_PATH ?? '/keyword-explorer'`
124
+ - Other build tools: set the base href / public path from `HAWK_APP_BASE_PATH` the same way (plus a trailing `/` where the tool expects one)
125
+
126
+ Apps that use only relative paths (plain HTML) can skip the build with `hawk-app deploy <folder> --no-build`.
127
+
128
+ ## Log in (once per machine)
129
+
130
+ ```bash
131
+ npx hawk-app sso # sign in with your Bridgeline account in the browser: the platform issues your own key
132
+ # (--publisher <id>: another team's apps, e.g. qa-team; one saved key per platform)
133
+ npx hawk-app login # or: paste a publisher key from Engineering (input hidden), checked and saved for every app
134
+ npx hawk-app whoami # are you logged in? which platform and key apply here?
135
+ npx hawk-app logout # remove the saved key from this machine
136
+ ```
137
+
138
+ **`sso`** opens the browser on the HawkSearch sign-in, which goes straight to Bridgeline's AD sign-in (Microsoft). Only
139
+ Bridgeline AD accounts get a key; HawkSearch client logins can't. The platform then issues a key for you on this
140
+ machine (`sso-<name>-<computer>`, in the Services team's publisher, so you work on the team's apps; `--publisher qa-team`
141
+ for another team the platform allows, e.g. QA's own test apps) and the CLI saves it
142
+ like `login` does. You're also added to Hawk-App-Developers in HawkSearch, so you can submit apps for approval. It
143
+ works for 90 days; then commands say it was refused, and you run `sso` again. Signing in again on
144
+ the same computer replaces that computer's key. Works on dev and prod (`npx hawk-app sso --env prod`). Pipelines keep using keys from the portal with `HAWK_APPS_PUBLISHER_KEY`.
145
+
146
+ **`login`** takes a publisher key made in the Hawk Analytics portal. Those keys don't expire.
147
+
148
+ `login` saves the key in your user profile, so every app on the machine uses it and no project files hold it:
149
+ - **Windows:** `%APPDATA%\hawk-app\credentials.json`
150
+ - **macOS / Linux:** `~/.config/hawk-app/credentials.json`, readable only by you
151
+
152
+ It works the same in PowerShell, cmd and bash. The key is never shown, never stored in shell history, and never passed as a command-line argument.
153
+
154
+ Every command works on **dev** unless you say otherwise. `--env prod` (on any command) picks prod:
155
+ `npx hawk-app sso --env prod` signs you in to prod, and `npx hawk-app deploy --env prod` deploys there. Logging in
156
+ never changes which environment later commands use, so prod is always chosen on purpose; `deploy` says
157
+ when they go to prod. Keys are stored per environment, so a dev key is never sent to prod. (`--platform <url>` targets
158
+ any other platform, e.g. one running locally.)
159
+
160
+ **Which environment**, first match wins: `--env` / `--platform`, then `HAWK_APPS_ENV` (`dev` or `prod`) or
161
+ `HAWK_APPS_PLATFORM_URL` in the environment (CI) or the project's `.env.local` / `.env`, else dev.
162
+
163
+ **Which key**, first match wins:
164
+ 1. The environment variable `HAWK_APPS_PUBLISHER_KEY`. CI pipelines set it as a secured variable.
165
+ 2. The project's `.env.local` (git-ignored), for a per-project override. `npx hawk-app login --local` writes it for you.
166
+ 3. The machine login from `hawk-app login` for that environment.
167
+
168
+ ## Run locally without HawkSearch (`hawk-app dev`)
169
+
170
+ ```bash
171
+ npm run app:dev # = npx hawk-app dev; asks which client the first time
172
+ npx hawk-app dev --client e3141 --engine 3141
173
+ ```
174
+
175
+ The first time, it asks for part of a client's name and lets you pick the client, then saves it in `.env.local` as
176
+ `HAWK_APPS_DEV_CLIENT` (`--client <id>` uses another). A new app is registered on the platform the first time (not
177
+ released: only Bridgeline staff see it). Queries and functions always run from the **deployed** version, even here:
178
+ deploy after changing them.
179
+
180
+ Starts your app (`npm run dev`) and a local stand-in for HawkSearch at `http://localhost:3200`, which embeds the app
181
+ the way HawkSearch does (the bridge's own `<hawk-app-frame>`). The app gets **real tokens and real data**: the CLI asks
182
+ the platform for an app token with your publisher key, for any client you name, as a Bridgeline user previewing your
183
+ newest version (with the scopes waiting for review). The app's platform calls go through the local page, so nothing needs
184
+ CORS setup, on dev or prod; so does the AI stream (`app.ai.stream`), which the page pipes through as it arrives.
185
+
186
+ - The toolbar changes the client, site (engine), user (Bridgeline or client user), date range and theme. Toasts,
187
+ titles and navigation requests from the app show in the page; a panel shows the token's claims and expiry.
188
+ - `--url <dev server>`: use an app dev server that is already running (nothing is started). The default is where the
189
+ template serves the app: `http://localhost:3100/<slug>/` for Next.js, `http://localhost:3100/` otherwise. `--no-start`: don't run `npm run dev`. `--port`: the local page's port (default 3200). `--client-user`: start
190
+ as a client user. `HAWK_APPS_DEV_CLIENT` / `HAWK_APPS_DEV_ENGINE` in `.env.local` replace `--client` / `--engine`.
191
+ - The HawkSearch dashboard API (`app.dashboard`, writes, and authenticated search) needs a HawkSearch key, which only a
192
+ host has. Put yours in `.env.local` as `HAWKSEARCH_API_KEY` (and `HAWKSEARCH_DOMAIN`, default
193
+ `https://dev.hawksearch.net`; `HAWKSEARCH_GROUP_KEY=true` for a group key): the platform seals it into the token. It is
194
+ never written anywhere else.
195
+ - Queries and functions run against your **current deployed version** (deploy first after changing them); the UI is your
196
+ local code. Every dev token is audit-logged with your publisher key.
197
+
198
+ ## Deploy
199
+
200
+ **One app, one codebase.** Every developer of a publisher (team) can deploy any of its apps, so the CLI
201
+ guards against mixing two apps under one slug: `create` and `init` refuse a slug that's already an app (saying who last
202
+ changed it; `--yes` to set up a folder for that existing app on purpose), and `deploy` asks before going on
203
+ when someone else's key made the app's last change ("`showcase` was last deployed by Jane Doe on … Deploy a new version
204
+ of it? (y/N)"). `--yes` skips the question; in a pipeline (no terminal) it's a warning and the command goes on. Another
205
+ publisher's slug is always refused (`409 slug_owned`).
206
+
207
+ ```bash
208
+ npx hawk-app deploy # build and upload a new version, with hawk-app.json
209
+ npx hawk-app status # released or not, the newest version, the approval request, the aliases
210
+ npx hawk-app open # the app in HawkSearch: review it and submit it for approval
211
+ npx hawk-app clients add e3141 # give a client the app (see "Clients")
212
+ npx hawk-app deploy --dry-run # what hawk-app.json would change, without a version (use this on pull requests)
213
+ ```
214
+
215
+ | Command | What it does |
216
+ |---|---|
217
+ | `init` | Creates `hawk-app.json` (never overwrites an existing one) and adds [AGENTS.md](#ai-coding-assistants-agentsmd). |
218
+ | `validate [--build [<folder>]]` | Checks `hawk-app.json` locally, `queries/` and `functions/` if the app has them, and the project: the bridge is used and current, and there's a build script (or `--no-build` deploys). `--build` also builds the app with a stand-in base path (`/<slug>/v0`) and fails on absolute URLs outside it; the folder comes from the `app:deploy` script unless given. |
219
+ | `deploy [<folder>] [--no-build] [--no-audit] [--alias <name>] [--dry-run]` | Sends hawk-app.json first (its scopes and settings go with every deploy), then reserves the next version number, runs `npm run build` with `HAWK_APP_BASE_PATH=/<slug>/v<n>` (when package.json has a `build` script, unless `--no-build`), then uploads every file in the build folder (the template's, `dist`, `out` or `public`, unless given) to `/<slug>/v<n>/` on the apps CDN, plus `queries/` and the bundled `functions` (if any) to the platform's private storage, and deploys the functions. Runs `npm audit` of the production dependencies first, asks before deploying with high or critical findings, and reports the counts to reviewers (see [Dependency check](#dependency-check); `--no-audit` skips it). Warns when the HTML loads absolute URLs outside `/<slug>/v<n>/`, which wouldn't load, and when the code calls a bridge feature whose scope `hawk-app.json` lacks (e.g. `app.ai.generate` without `ai:invoke`; `validate` and `doctor` check it too). Says whether the version is a draft (staff only until it's approved) or auto-approved, and when it cleared an approval request. `--alias staging` points the alias at the new version (see [Versions and aliases](#versions-and-aliases)). `--dry-run` only shows what hawk-app.json would change. |
220
+ | `register` | Sends `hawk-app.json` without a new version (for scripts; `deploy` and `dev` already send it). It never releases anything and never changes the app's clients. On a released app, changes wait for review. |
221
+ | `versions` | Every version with its status (draft, waiting, approved, rejected) and the aliases on it. |
222
+ | `alias set <name> <version>` / `alias delete <name>` | Point an alias at a version that isn't approved (only Bridgeline staff in its clients see it), or delete one of yours with no clients on it. Approvers do the rest. |
223
+ | `open [--alias <name>]` | Opens the app's page in HawkSearch (dev or prod, from `--env`) to review it and submit it for approval; `--alias` shows it as that alias's clients see it. Prints the link when there's no browser. |
224
+ | `docs [<topic>]` | Opens the developer docs at developers.hawksearchapps.com, or a topic's page (`start`, `versions`, `bridge`, `api`, `cli`, `commands`, `functions`, …; `hawk-app help docs` lists them). Prints the link when there's no browser. |
225
+ | `doctor` | Checks everything a developer needs: Node, the login and platform, `hawk-app.json`, the project (bridge, build script), the template, the scopes the code uses, the app on the platform and the MCP config. Each line says what to fix; exits 1 when something must be. |
226
+ | `help [<command> \| all]` | The commands by task; one command's details; or every detail. |
227
+ | `why <client id>` | What a client's users see of the app, and why (the client list, its alias, that version's status). |
228
+ | `status` | Shows the automated review checks approvers will see (bundle, dependencies, origins, queries, …), the released version, the newest version and its status, settings waiting for review, the approval request (waiting for approval, or rejected and why), auto-approve, review emails (when an approver turned them off), the view cache (when Engineering shortened or stopped it), visibility, scopes, system parameters, config keys and clients. |
229
+ | `clients` / `clients add <id> [<id> …] [--name "<name>"] [--alias <name>]` / `clients exclude <id> [--name "<name>"]` / `clients remove <id>` | Which clients have the app, and which version (alias) each gets (see [Clients](#clients)). |
230
+ | `data tables` / `data describe <table>` / `data run <file.sql> --client <id> [--param name=value …]` | The tables app queries can use, and running a query on dev (see [Custom data](#custom-data-queries)). |
231
+ | `secrets list` / `secrets set <NAME> [--value-from-env VAR]` / `secrets delete <NAME>` | The app's function secrets (see [Functions](#functions)). |
232
+ | `logs <function> [--since 1h] [--follow]` | A function's logs (see [Functions](#functions)). |
233
+ | `config list [--client <id>]` / `config set <key> <value> [--client <id>]` / `config unset <key> [--client <id>]` | The app's config values, global or for one client, without a deploy (see [App config](#app-config)). |
234
+ | `sso [--publisher <id>]` | Sign in with your Bridgeline account; the platform issues your own key (see above). |
235
+ | `login [--local]` / `whoami` / `logout` | Save a publisher key (CI, or without SSO), check which key and platform apply, or remove the saved key. |
236
+ | `errors` / `usage` | The app's page errors, and its opens, users and clients per day (see [Errors and usage](#errors-and-usage-hawk-app-errors-hawk-app-usage)). |
237
+ | `upgrade` / `templates` / `mcp` | Keep up with the template (see below); list the templates; the MCP server for AI assistants (see [MCP server](#mcp-server-hawk-app-mcp)). |
238
+
239
+ Options: `--manifest <file>` to use a manifest other than `hawk-app.json`, `--env dev|prod` (default dev), and `--platform <url>` to target another platform. `deploy` never uploads hidden files such as `.env.local`.
240
+
241
+ ## Keep up with the template (`hawk-app upgrade`)
242
+
243
+ Templates move on (a new Next.js, a security fix, a config change). `hawk-app upgrade` brings an app's template-owned
244
+ parts up to the template this CLI ships:
245
+
246
+ ```bash
247
+ npx hawk-app upgrade # shows what would change; changes nothing
248
+ npx hawk-app upgrade --yes # applies it and runs npm install
249
+ ```
250
+
251
+ - **Dependencies:** the packages the template pins (e.g. `next`, `react`, `@mui/material`, the bridge) are raised to the
252
+ template's version. Never lowered, and your own packages are never touched. A package the template gained since the
253
+ app was made is added: a `react`, `preact`, `next` or `next-mui` app gets the UI kit
254
+ (`@bridgeline-digital/hawk-app-ui`), and `upgrade` lists the code to change to start using it (next-mui: the
255
+ provider; the others: the stylesheet and elements imports). A major version (Material UI 7 → 9) is called out, with
256
+ what to check.
257
+ - **Config files** (`next.config.ts`, `vite.config.ts`, `tsconfig.json`): replaced only if you never edited them (the
258
+ CLI knows every version the templates have shipped). An edited one is left alone, with a diff of what the template
259
+ changed so you can apply it yourself.
260
+ - The vanilla template's CDN imports (the bridge and the UI kit, in `public/app.js` and `public/index.html`) are raised
261
+ together to the template's minor version. A vanilla app made before the kit adds its two lines itself: see a new
262
+ app's `public/index.html` and `public/app.js`.
263
+ - **AGENTS.md:** the CLI's part is refreshed, and `CLAUDE.md` gets its `@AGENTS.md` import (see below).
264
+ - **Never** your code (`src/`), `hawk-app.json` settings or any other file.
265
+
266
+ It uses `"template"` in `hawk-app.json`, which `hawk-app create` writes; for older apps it guesses from the dependencies
267
+ (and records it on `--yes`), or pass `--template <name>`. An app set up with `init` that follows no template gets only
268
+ AGENTS.md refreshed. `hawk-app deploy` tells you when an app is behind its template. After upgrading: build, test
269
+ (`npm run dev`) and deploy as usual. `--no-install` skips `npm install`.
270
+
271
+ ## Versions and aliases
272
+
273
+ Every deploy is a version with a status from its review: draft, waiting (submitted), approved or rejected. An **alias**
274
+ points at a version and takes its status: on an approved version its clients' users see the app; on any other only
275
+ Bridgeline staff in them do. **`default`** is the version released to everyone, for every client without another alias.
276
+
277
+ ```bash
278
+ npx hawk-app deploy --alias staging # staging → the new version (staff only until it's approved)
279
+ npx hawk-app clients add e4 e5 --alias staging # new clients on staging: staff in them test it
280
+ ```
281
+
282
+ When an approver approves that version, they choose: release to everyone (`default` moves), or only to the aliases on it
283
+ (staging's clients see it, everyone else stays on `default`). You can point aliases only at versions that aren't approved,
284
+ and put on them only clients whose users don't see the app yet; anything users see is for approvers (the portal or
285
+ HawkSearch). The full picture, with diagrams: docs/versions-and-aliases.md in the hawk-apps repository. `hawk-app status` shows the aliases, `hawk-app versions` every version with its status:
286
+
287
+ ```
288
+ released v7 (default: every client without another alias)
289
+ aliases default v7 (all other clients) · pilot v8 (3 clients) · staging v9 (draft: Bridgeline staff only, 2 clients)
290
+ ```
291
+
292
+ ## Errors and usage (`hawk-app errors`, `hawk-app usage`)
293
+
294
+ The bridge reports the app's page errors and one open per page load (platform/docs/telemetry.md):
295
+
296
+ ```bash
297
+ npx hawk-app errors # the last 7 days (--days 1-30): grouped, the most frequent first, with versions and clients
298
+ npx hawk-app usage # the last 30 days (--days 1-400): opens, view opens, users and clients; staff apart
299
+ ```
300
+
301
+ ## Accessibility check
302
+
303
+ `hawk-app deploy` runs axe-core (WCAG 2.1 A/AA and best practices) over up to 10 built pages and sends the result with
304
+ the version: approvers see it as the `accessibility` review check (critical issues fail it; a force approve still goes
305
+ through). `hawk-app validate --build` prints critical and serious issues before you deploy. It sees each page's HTML as
306
+ built, not what the app draws later in the browser, and no layout (so no color contrast).
307
+
308
+ ## Release channels: latest and dev
309
+
310
+ The bridge, the UI kit and the CLI are published together, with the same version, on two npm tags:
311
+ - **`latest`**, from `main`: stable releases, matched to the prod platform. What `npm install` and `npx hawk-app` get.
312
+ - **`dev`**, from `dev`: pre-releases (`x.y.z-dev.<time>`) with features still in development, which may need the dev
313
+ platform.
314
+
315
+ To start an app on dev features, create it with the dev CLI. It puts the bridge, the UI kit and the CLI on the `dev` tag (the vanilla
316
+ template's CDN imports too), so the app gets the dev bridge and not the release its template pins:
317
+
318
+ ```bash
319
+ npx @bridgeline-digital/hawk-app-cli@dev create my-app # a dev app (or --channel dev with any CLI)
320
+ npx hawk-app upgrade --channel latest # once the features are released: ^ the latest versions (--yes applies)
321
+ npx hawk-app upgrade --channel dev # move an existing app onto the dev builds (again: onto the newest ones)
322
+ ```
323
+
324
+ - `--channel` installs the packages it moves by name (`npm install <package>@dev`, or `@^<latest>`), so
325
+ `package-lock.json` moves with them; npm then writes the version it installed (`^0.1.0-dev.<time>`), which is
326
+ still the dev channel. Running `--channel dev` again moves to the newest dev builds.
327
+ - A dev app's `npm update` follows the newest dev builds; its `package-lock.json` records the exact one, so commit it.
328
+ - `hawk-app upgrade` keeps a dev app on `dev`; `--channel` is the only thing that moves it.
329
+ - `hawk-app validate` warns when a dev app is set to use the prod platform (`--env prod`, `HAWK_APPS_ENV=prod`).
330
+
331
+ ## AI coding assistants (AGENTS.md)
332
+
333
+ `create` and `init` write these files for AI coding assistants, so they build Hawk Apps the way the platform expects
334
+ instead of guessing:
335
+
336
+ - **`AGENTS.md`**, read by GitHub Copilot, Cursor, Codex and others: the rules (no secrets, data only through the
337
+ bridge, static files, the base path), the bridge's calls and their scopes, queries and functions, the commands, and
338
+ what fails review. Its project section matches the app's template, or, for an app set up with `init`, says what the
339
+ project has to do itself.
340
+ - **`CLAUDE.md`** with `@AGENTS.md`, so Claude Code reads the same file.
341
+
342
+ - **`.mcp.json`** (Claude Code) and **`.vscode/mcp.json`** (VS Code / Copilot), which start the CLI's MCP server (see
343
+ below) from `node_modules`.
344
+
345
+ The CLI owns the part of AGENTS.md between its `<!-- hawk-app:start … -->` and `<!-- hawk-app:end -->` markers, and
346
+ `npx hawk-app upgrade` refreshes it when a new CLI changes it. Write your own notes outside the markers; they're kept.
347
+ An existing AGENTS.md or CLAUDE.md is never replaced: the CLI's part (or the import) is added to it. An existing MCP
348
+ settings file gets the `hawk-apps` server added (one with comments is left alone: add it yourself).
349
+
350
+ ### MCP server (`hawk-app mcp`)
351
+
352
+ `hawk-app mcp` serves the CLI's commands to AI coding assistants over stdio (the [Model Context
353
+ Protocol](https://modelcontextprotocol.io)), so they can look things up and check their own work:
354
+
355
+ | Tool | What it does |
356
+ | --- | --- |
357
+ | `docs` | Searches or reads the bridge and UI kit READMEs (the app's installed versions), this README, the `hawk-app.json` schema and the showcase app, whose code shows every feature working. |
358
+ | `validate` | `hawk-app validate`, optionally with `--build`. |
359
+ | `status` | `hawk-app status`, including the automated review checks approvers will see. |
360
+ | `doctor` | `hawk-app doctor`: what's wrong with the setup, and how to fix it. |
361
+ | `versions`, `why` | Every version with its status and aliases; what a client sees of the app, and why. |
362
+ | `errors`, `usage` | The app's page errors; its opens, users and clients per day. |
363
+ | `ui_components` | The UI kit's components for the app's template (next-mui: the provider, components and theme; the others: the stylesheet classes and elements), or what's said about one, from the kit's README, with a link to the live [gallery](https://developers.hawksearchapps.com/reference/ui-kit-gallery/). |
364
+ | `analytics_reports` | The analytics API's reports for `app.analytics.report()`, with one report's columns, filters and an example call, read from the developer portal's dev spec (public; no client data). |
365
+ | `hawksearch_api` | HawkSearch's API reference (developerdocs.hawksearch.com): an operation's parameters, request and response fields, the bridge call (`app.dashboard.*`, `app.hawksearch.*`) and its scope, or why apps can't call it (public; no client data). |
366
+ | `data_tables`, `data_describe` | Table names and columns for queries. Never data: `data run` isn't a tool, because its rows would go to the assistant. |
367
+ | `logs` | A function's logs. |
368
+ | `init`, `upgrade` | `hawk-app init`; `hawk-app upgrade` (`apply` for `--yes`). |
369
+ | `deploy` | `hawk-app deploy` (the folder is optional; `alias`, and `dryRun` to only show what hawk-app.json would change). Marked destructive, so the assistant asks you first. |
370
+ | `clients`, `clients_add` | The app's clients, by id. Say "deploy to OTC": `clients_add` finds the matching clients and shows *you* a form to pick one; the assistant only hears whether a client was added. Marked destructive too. Removing clients isn't a tool. |
371
+
372
+ It also has **prompts**, ready-made tasks that use the tools. Claude Code lists them as slash commands (type `/` and
373
+ `hawk`):
374
+
375
+ | Prompt | In Claude Code | What it does |
376
+ | --- | --- | --- |
377
+ | `check-before-submit` | `/mcp__hawk-apps__check-before-submit` | Validates (with the build check), reads the status and review checks, and explains how to fix what an approver would have to force approve. |
378
+ | `add-feature` | `/mcp__hawk-apps__add-feature "<what to build>"` | Finds the bridge calls, scopes and showcase examples it needs, says what the review will see, makes the change and validates it. |
379
+ | `explain-app` | `/mcp__hawk-apps__explain-app` | What the app does, its scopes (unused or missing ones too) and where it stands in review. |
380
+ | `deploy-to-client` | `/mcp__hawk-apps__deploy-to-client "<client name>"` | Deploys to dev and gives a client the app; you pick the client from the matches for its name. |
381
+
382
+ Each takes an optional `dir` for a workspace with several apps. In VS Code with Copilot they're under `/` too.
383
+
384
+ - **Dev only.** Every tool runs with `--env dev`; nothing reaches prod. Approvals, secrets, removing clients and logins
385
+ aren't tools.
386
+ - **Your key stays put.** The tools use your saved login (or `.env.local`) like the CLI does; the key never passes
387
+ through the assistant.
388
+ - **No client names.** Tool output shows client ids only, and client names go to you, never to the assistant: the
389
+ client picker is a form your assistant shows you (MCP elicitation). An assistant without forms tells you to run
390
+ `npx hawk-app clients add --pick "<name>"` in a terminal instead.
391
+ - **No silent go-aheads.** Where the CLI would ask (a teammate made the app's last change, or `npm audit` found high or
392
+ critical vulnerabilities) the tool stops and tells the assistant, which has to ask you and call again with `force`.
393
+ - **Several apps in one workspace?** Each tool takes `dir`, the app's folder relative to the workspace.
394
+
395
+ `create` and `init` add it to `.mcp.json` and `.vscode/mcp.json`, started as
396
+ `node node_modules/@bridgeline-digital/hawk-app-cli/bin/hawk-app.mjs mcp` (the CLI must be a dev dependency;
397
+ `validate` warns when it isn't). For Cursor, copy the `mcpServers` entry of `.mcp.json` to `.cursor/mcp.json`.
398
+
399
+ ## Clients
400
+
401
+ Which clients have the app is not in `hawk-app.json`: the platform's registry holds it, and Engineering and approvers
402
+ change it in the Hawk Analytics portal. Developers can change it with the CLI:
403
+
404
+ ```bash
405
+ npx hawk-app clients # the app's visibility and its clients
406
+ npx hawk-app clients search outdoor # find a client's id by its name
407
+ npx hawk-app clients add e3141 --name "Outdoor Demo" # give a client the app (visibility allowList)
408
+ npx hawk-app clients add --pick "outdoor" # the same, picking the client from the matches
409
+ npx hawk-app clients add e4 e5 --alias staging # on an alias: the version they get
410
+ npx hawk-app clients add e4 --alias default # back to the default version
411
+ npx hawk-app clients exclude e2002 # opt a client out (visibility all)
412
+ npx hawk-app clients remove e3141 # take the client off the list
413
+ ```
414
+
415
+ `add` and `exclude` take only ids from the environment's HawkSearch client list (the Hawk Analytics portal's client
416
+ picker): an id the dashboard doesn't use, such as `3141` for `e3141`, is refused with the id you probably meant. The
417
+ client's name comes from that list unless you pass `--name`. `remove` takes any id on the app's list.
418
+
419
+ `hawk-app clients` lists `+ e3141 (Outdoor Demo)` for a client that has the app and `- e2002 (Acme Retail)` for one
420
+ that is opted out; `[on pilot]` after a client on an alias (`[on staging · draft: Bridgeline staff only]` when its version
421
+ isn't approved). What the list means depends on `visibility` in `hawk-app.json`:
422
+ - `allowList`: only the clients marked `+` have the app.
423
+ - `all`: every client has it, except the ones marked `-`.
424
+ - `internal`: Bridgeline users only; the list is ignored.
425
+
426
+ Changes apply at once, released or not, without a deploy or an approval, and are recorded in the app's history.
427
+ `--name` is a label for people (up to 100 characters); the client id is what grants access. Register the app before
428
+ adding clients.
429
+
430
+ `--alias` sets the version a client gets (see [Versions and aliases](#versions-and-aliases)). You can change it only for
431
+ clients whose users don't see the app yet; after that, approvers do.
432
+
433
+ ## Custom data (queries)
434
+
435
+ Apps with the `data:read` scope can ship their own SQL, run by the platform against the current client's analytics
436
+ tables. Put each query in `queries/` next to `hawk-app.json` (not in the build folder) as a pair of files:
437
+
438
+ ```text
439
+ my-app/
440
+ hawk-app.json "scopes": ["data:read"]
441
+ queries/
442
+ top-searches.sql
443
+ top-searches.json
444
+ ```
445
+
446
+ ```sql
447
+ -- queries/top-searches.sql
448
+ SELECT search_date, sum(search_count) AS searches
449
+ FROM dailysearches
450
+ WHERE search_date BETWEEN ? AND ? AND {{partitions(from, to)}}
451
+ GROUP BY 1 ORDER BY 1
452
+ ```
453
+
454
+ ```json
455
+ { "params": [{ "name": "from", "type": "date", "required": true }, { "name": "to", "type": "date", "required": true }] }
456
+ ```
457
+
458
+ The app calls it with `app.data.query('top-searches', { from, to })` (see the bridge README for types, the
459
+ `{{partitions(from, to)}}` macro, the tables and the errors).
460
+
461
+ **Step by step**
462
+ 1. Add `"data:read"` to `scopes` in `hawk-app.json`.
463
+ 2. Find your table and columns: `npx hawk-app data tables`, then `npx hawk-app data describe <table>`.
464
+ 3. Write `queries/<name>.sql` and `queries/<name>.json`. Filter by a date range and add
465
+ `{{partitions(from, to)}}`.
466
+ 4. Try it on dev: `npx hawk-app data run queries/<name>.sql --client <id> --param from=… --param to=…`.
467
+ 5. Call it from the app with `app.data.query('<name>', { … })`. Queries always run from a **deployed** version, even
468
+ in dev mode (`?devUrl=`): the platform never reads your local `queries/` folder. So after adding or editing a
469
+ query, deploy (step 6) before calling it from the app; use `data run` to iterate on the SQL itself.
470
+ 6. `npm run app:deploy` (checks the queries and uploads them with the new version, with `hawk-app.json`).
471
+ 7. Bridgeline staff review the version in HawkSearch: they see each added or changed query and its SQL diff.
472
+ Once approved (or right away on a draft or auto-approve app), the app's users get the new queries.
473
+
474
+ `hawk-app validate` and `hawk-app deploy` check the queries before anything is uploaded, with the same rules as the
475
+ platform:
476
+ - names are lowercase letters, digits and single dashes (up to 64), and every `.sql` has a `.json` (and vice versa);
477
+ only `.sql` and `.json` files, no subfolders, at most 50 queries
478
+ - each `.sql` is one `SELECT` or `WITH … SELECT` statement (no `;`), up to 64 KB
479
+ - the `?` count equals the number of params (a `string[]` param is one `?`, written `IN ?`)
480
+ - params have a `name` and a `type` (`string`, `int`, `decimal`, `date`, `timestamp`, `boolean`, `string[]`), and
481
+ optionally `required`, `default`, `min`/`max` (numbers) and `maxItems` (`string[]`, up to 100)
482
+ - `{{partitions(from, to)}}` (optionally `{{partitions(from, to, alias)}}`) uses params declared as `date`
483
+ - `queries/` needs `data:read` in `scopes`.
484
+
485
+ `deploy` uploads them with the version (`… 3 files, 12 KB from dist, 2 queries`). They're part of the version: staff
486
+ review them with it, and each version runs its own queries.
487
+
488
+ Try a query on dev before deploying it:
489
+
490
+ ```bash
491
+ npx hawk-app data tables # the tables queries can use (and which are partitioned)
492
+ npx hawk-app data tables --app my-app # also: may my-app use the sensitive tables?
493
+ npx hawk-app data describe dailysearches # a table's columns and types
494
+ npx hawk-app data run queries/top-searches.sql --client e3141 --param from=2026-09-01 --param to=2026-09-30
495
+ ```
496
+
497
+ `data run` sends the SQL and its `.json` from your disk (nothing needs to be deployed), waits for the result, and prints
498
+ up to 50 rows plus the bytes scanned. `--client` is the HawkSearch client id (the one in the app's context,
499
+ `context.tenant.clientId`); on dev, use a demo or test client. `string[]` values are comma-separated (`--param engines=a,b`). It only works
500
+ against the dev platform. When too many queries are running (429 `too_many_queries`) it waits and tries again.
501
+
502
+ The sensitive tables (visitor-level data such as `summary`) are listed apart: only apps Engineering has opted in can
503
+ query them. Ask Engineering if your app needs one.
504
+
505
+
506
+ ## Functions
507
+
508
+ Server-side code that ships with each version (AWS Lambda, Node 22), called from the app with
509
+ `app.functions.call(name, input)`. The bridge README's **Functions** section covers `ctx`, errors and limits; this is
510
+ the CLI side.
511
+
512
+ **Before you start: Engineering must enable functions for your app** (they can reach the internet and hold secrets),
513
+ and a version whose functions changed **always needs review**, even on an auto-approve app.
514
+
515
+ 1. Add `"functions:invoke"` to `scopes`, and declare each function in `hawk-app.json`:
516
+ ```jsonc
517
+ "functions": {
518
+ "sync-feed": { "timeoutSeconds": 20, "memoryMb": 512, "secrets": ["FEED_API_KEY"] },
519
+ "echo": {}, // defaults: 10 s, 256 MB, no secrets
520
+ "nightly-report": { "job": true, "timeoutSeconds": 600 } // a background job (see below)
521
+ }
522
+ ```
523
+ 2. Write `functions/<name>.ts` (or `.mts`, `.js`, `.mjs`) exporting `handler`, with
524
+ `@bridgeline-digital/hawk-app-bridge` (0.4.0 or later) installed in the app:
525
+ ```ts
526
+ import type { HawkFunction } from '@bridgeline-digital/hawk-app-bridge/server';
527
+ export const handler: HawkFunction<{ name: string }, { greeting: string }> = async (input, ctx) => ({
528
+ greeting: `Hello ${input.name} from ${ctx.client.id}`,
529
+ });
530
+ ```
531
+ Shared code goes in subfolders (`functions/lib/…`) or anywhere else you import from (`src/`, npm packages).
532
+ 3. Set its secrets once: `npx hawk-app secrets set FEED_API_KEY`.
533
+ 4. `npx hawk-app validate`, then deploy as usual: `npm run app:deploy`.
534
+ 5. Call it from the app: `await app.functions.call('sync-feed', { since: '2026-09-01' })`. Like queries, functions
535
+ always run from a **deployed** version, also in dev mode (`?devUrl=`).
536
+
537
+ `hawk-app validate` and `hawk-app deploy` check, with the platform's rules:
538
+ - at most 10 functions; names are lowercase letters, digits and single dashes, up to 40 characters;
539
+ - `job` true or false; `timeoutSeconds` 1–25 (1–900 for a job), `memoryMb` 128–1024, `secrets` up to 20 unique
540
+ `UPPER_SNAKE_CASE` names, no other keys;
541
+ - every declared function has its `functions/<name>.*` file, and every entry file in `functions/` is declared;
542
+ - the `functions:invoke` scope, and the bridge installed with its `/server` entry.
543
+
544
+ **What deploy does with them.** Before reserving a version, each function is bundled with esbuild (Node 22, ESM,
545
+ every dependency included, your `handler` wrapped for Lambda) into `index.mjs`, zipped as `bundle.zip` (10 MB at
546
+ most). `source.zip` (every file under `functions/` except `node_modules`, 5 MB at most) and `deps.json` (the npm
547
+ packages in the bundle, with versions) go along so reviewers can see the code and its dependencies. After the upload
548
+ the platform deploys each function and the CLI prints one line per function:
549
+
550
+ ```
551
+ Bundling 2 functions…
552
+ ƒ echo 4 KB zipped, 1 package
553
+ ƒ sync-feed 38 KB zipped, 2 packages
554
+ Uploading v9 of my-app: 12 files, 210 KB from dist, 2 functions
555
+ ✔ function echo (10 s, 256 MB) Lambda version 3
556
+ ✔ function sync-feed (20 s, 512 MB, secrets FEED_API_KEY) Lambda version 5
557
+ ✔ Deployed v9 of my-app (12 files, 2 functions). A draft: clients stay on v8 until it's approved in HawkSearch …
558
+ ```
559
+
560
+ Zips are built the same way every time, so a function whose code didn't change keeps the same code hash (and doesn't
561
+ show as changed in review).
562
+
563
+ **Secrets.** App-level, stored by the platform (encrypted, in AWS Systems Manager Parameter Store), never in the app or
564
+ a version: a change applies to every version at once. A function receives only the secrets it declares, as
565
+ `ctx.secrets.NAME`.
566
+
567
+ ```bash
568
+ npx hawk-app secrets list # names and when they were set; never values
569
+ npx hawk-app secrets set FEED_API_KEY # type the value (hidden, never echoed)
570
+ npx hawk-app secrets set FEED_API_KEY --value-from-env FEED # CI: read it from an environment variable
571
+ npx hawk-app secrets delete FEED_API_KEY
572
+ ```
573
+
574
+ **Client secrets** are for values each client sets for itself, such as its email platform's API key. Declare them on
575
+ the function (`"clientSecrets": ["MAILCHIMP_API_KEY"]`). The app sets them with `app.clientSecrets.set(...)` and the
576
+ function reads `ctx.clientSecrets.MAILCHIMP_API_KEY`. The CLI never sees them.
577
+
578
+ Values are never printed, logged or passed as command-line arguments, and are at most 4 KB each (UTF-8 bytes).
579
+
580
+ **Logs.** `console.log` and `console.error` go to the function's log (kept 30 days):
581
+
582
+ ```bash
583
+ npx hawk-app logs sync-feed # recent events
584
+ npx hawk-app logs sync-feed --since 1d # 15m, 1h, 1d or an ISO time
585
+ npx hawk-app logs sync-feed --follow # keep polling every 2 s (Ctrl+C to stop)
586
+ ```
587
+
588
+ **When a call fails** the app gets a `HawkApiError`: `function_failed` (the handler threw: see the logs),
589
+ `function_timeout`, `secret_missing` (run `secrets set`), `functions_not_enabled` (ask Engineering) or
590
+ `function_not_found` (deploy the version with the function). The bridge README lists them all.
591
+
592
+ ### Background jobs and schedules
593
+
594
+ A function with `"job": true` is a **background job**: the app starts it with `app.functions.start(name, input)`
595
+ (it can't be called with `call`, and a normal function can't be started), it runs up to 900 s (default 300), and the
596
+ app reads its result later with `app.functions.wait(jobId)`. The bridge README's **Background jobs** section has the
597
+ code and limits.
598
+
599
+ `schedules` run job functions on a timer, for the clients where a user turns them on from the app
600
+ (`app.schedules.enable(name)`); nothing runs until someone does:
601
+
602
+ ```jsonc
603
+ "functions": { "nightly-report": { "job": true, "timeoutSeconds": 600 } },
604
+ "schedules": {
605
+ "nightly": { "function": "nightly-report", "cron": "0 7 ? * MON-FRI *", "timezone": "America/Chicago" }
606
+ }
607
+ ```
608
+
609
+ `hawk-app validate` checks: at most 5 schedules, named like functions; `function` is a job function of the same
610
+ `hawk-app.json`; `cron` is an [EventBridge cron](https://docs.aws.amazon.com/scheduler/latest/UserGuide/schedule-types.html#cron-based)
611
+ with 6 fields (`minutes hours day-of-month month day-of-week year`, no `cron( )` wrapper) that runs **at most hourly**
612
+ (the minutes field is a single number, 0–59) with `?` for day-of-month or day-of-week (EventBridge needs one);
613
+ `timezone` is an IANA name like `America/Chicago`, or `UTC` (the default); the `functions:invoke`
614
+ scope. `schedules` is a setting sent with each deploy: on a released app a change waits for review like scopes,
615
+ and `hawk-app status` lists the registered schedules.
616
+
617
+ ## Email
618
+
619
+ Apps with the `email:send` scope can send email with `app.email.send(...)` or, from a function,
620
+ `ctx.platform.email.send(...)` (the bridge README's **Email** section has the details). There are no CLI commands for
621
+ it: add `"email:send"` to `scopes` and deploy (without the scope, sends fail with `scope_missing`). Mail goes out from `noreply@hawksearch.com` with the app's name, to up to 10 recipients per
622
+ send and 200 recipients per app and client per day.
623
+
624
+ ## HawkSearch system parameters
625
+
626
+ Apps with the `hawksearch:sysparams:write` scope can read and set the HawkSearch system parameters they list in
627
+ `systemParameters`, for the user's current engine, with `app.hawksearch.systemParameters.list() / get(name) /
628
+ set(name, value)` (or `ctx.platform.hawksearch.systemParameters` in a function; the bridge README's **HawkSearch
629
+ system parameters** section has the details). There are no CLI commands for it:
630
+
631
+ ```json
632
+ {
633
+ "scopes": ["hawksearch:sysparams:write"],
634
+ "systemParameters": ["Timezone", "SearchPreviewURL"]
635
+ }
636
+ ```
637
+
638
+ `hawk-app validate` checks the list: up to 50 names of 1-50 letters, digits, `_`, `.` or `-`, each listed once
639
+ (names are not case-sensitive); the list needs the scope, and the scope needs a non-empty list. The list is a setting:
640
+ on a released app a change waits for review like scopes. Any user of the app can set the listed parameters, including
641
+ "AE only" ones; encrypted, global and read-only ones never can. Changes apply within about 3 minutes and only to the
642
+ user's current engine (no group sync).
643
+
644
+ ## App config
645
+
646
+ Settings someone changes without a deploy (who gets a notification email, a threshold, a switch) go in `config`:
647
+ declare each key with its type and an optional default, and read it in the app with `app.config.get(key)` /
648
+ `app.config.all()` (`ctx.config` in a function; the bridge README's **App config** section has the details).
649
+
650
+ ```json
651
+ {
652
+ "config": {
653
+ "signupNotifyEmails": { "type": "emails", "label": "Sign-up notification recipients", "default": ["support@example.com"] },
654
+ "connectorsRequestEmails": { "type": "emails", "label": "API Connections request recipients" }
655
+ }
656
+ }
657
+ ```
658
+
659
+ The values are set outside hawk-app.json, from the Hawk Analytics portal or the CLI, for the whole app (global) or for
660
+ one client, and apply within about 30 seconds, with no deploy or approval. A client's value wins over the global one,
661
+ which wins over the default. **Values are not secret**: they reach the browser, so keep keys in `hawk-app secrets`.
662
+
663
+ ```bash
664
+ npx hawk-app config list [--client e3141] # each key: type, default, global and client values
665
+ npx hawk-app config set signupNotifyEmails a@example.com,b@example.com # global
666
+ npx hawk-app config set signupNotifyEmails csd@example.com --client e3141 # one client
667
+ npx hawk-app config unset signupNotifyEmails --client e3141
668
+ ```
669
+
670
+ `set` reads the value as the key's type in your hawk-app.json: `string` as is, `number`, `boolean` (`true`/`false`),
671
+ `emails` comma-separated, `json` as JSON (quote it for your shell). `hawk-app validate` checks the declarations: at
672
+ most 50 keys named `^[A-Za-z][A-Za-z0-9_]{0,63}$`, each with `type` (`string`, `number`, `boolean`, `emails`,
673
+ `json`), optional `label` (up to 100 characters), `description` (up to 500) and `default` (a valid value of the type;
674
+ `emails` up to 50 different plain addresses; every value at most 16 KB). The declarations are a setting: on a released
675
+ app a change waits for review like scopes (values can already be set for a key waiting for review). `hawk-app status`
676
+ lists the declared keys.
677
+
678
+ ## AI
679
+
680
+ Apps with the `ai:invoke` scope can ask Claude with `app.ai.generate(...)` / `app.ai.stream(...)` or, from a function,
681
+ `ctx.platform.ai` (the bridge README's **AI** section has the details). There are no CLI commands for it: add
682
+ `"ai:invoke"` to `scopes` and deploy. Two tiers, `fast` and `smart`, each with a daily token budget
683
+ per app and client, plus a per-minute request limit; `app.ai.info()` shows today's use. The scope is elevated: an app
684
+ with it is never auto-approved. Prompts and answers are never logged or stored.
685
+
686
+ ## Review in HawkSearch
687
+
688
+ - **Not released yet.** A new app's versions are drafts: only Bridgeline staff see it, only on the clients it's enabled
689
+ for, on the Apps page in HawkSearch, always at its newest version. Developers can't release it; an approver does, on
690
+ the Apps page (see [Versions and aliases](#versions-and-aliases)).
691
+ - **Released apps.** Every deploy is a new version. Clients stay on the approved version until the new one is approved in
692
+ HawkSearch (staff can open it there first). Changes to settings in `hawk-app.json` (name, description,
693
+ icon, category, version, visibility, hosting, scopes, `systemParameters`, `schedules`, `config`, `externalOrigins`, `listed`, `minProtocolVersion`) wait for approval
694
+ the same way. For `config` only the declarations are reviewed: its values are set at any time without review.
695
+ - **Functions.** Engineering enables functions per app. A version whose functions changed (added, removed, new code,
696
+ dependencies or settings) always waits for review, even with auto-approve; reviewers see each function's settings,
697
+ dependency changes and a diff of its sources.
698
+ - **Clients any time.** Client changes (`hawk-app clients` or the Hawk Analytics portal) apply at once, released or
699
+ not, without review (see [Clients](#clients)).
700
+ - **Auto-approve.** Approvers can set an app to auto-approve: new versions and setting changes are then released
701
+ straight away.
702
+ - **Approval workflow.** Bridgeline users submit a version or settings change for approval on the Apps page in
703
+ HawkSearch (`npx hawk-app open`; with an optional note). Members of the **Approvers** group approve it (it's released), reject it with a
704
+ reason, or force approve without a request (for example to roll back). The approvers get an email about each request
705
+ (they can stop these with the link at the bottom), the submitter gets one when it's approved or rejected, and every
706
+ step is posted to the #hawk-app-approvers Slack channel. A new main-line deploy or settings change clears a waiting request (deploy says so: submit it again in
707
+ HawkSearch). `hawk-app status` shows `request waiting for approval (submitted by Jane Doe, 2026-10-08: <note>)`
708
+ or `request rejected by Jane Doe on 2026-10-09: <reason>`.
709
+ - **Elevated apps.** Apps using queries, functions, system parameters, dashboard writes, AI, external Lambdas, HawkSearch
710
+ features or the index (rebuilds, items, management) are never auto-approved;
711
+ approvers review their queries, functions and scopes like any other change.
712
+ - **Rollback and retention.** The released version, the newest version and the 5 newest others are kept, so staff can
713
+ roll back in HawkSearch; older versions are removed when you deploy.
714
+
715
+ ## How deploy works
716
+
717
+ 1. The CLI asks the platform for the next version number (`v<n>`) with your publisher key, and builds the app for
718
+ `/<slug>/v<n>` (unless `--no-build`).
719
+ 2. The CLI sends the list of files (paths and sizes). The platform checks them:
720
+ - the file types are static web content
721
+ - the sizes are within the limits
722
+ - the paths can't leave your app's folder
723
+ - you own the slug.
724
+
725
+ It then returns one **short-lived upload link per file**. Each link allows exactly one upload, to exactly that file in the version's folder, and expires after about 15 minutes.
726
+ 3. The CLI uploads each file straight to storage using its link.
727
+ 4. The CLI tells the platform it's done, with the result of `npm audit` of the production dependencies (run before the version was reserved, see below). The platform checks that every file arrived and marks the version ready: an app not released yet shows it to Bridgeline staff at once, a released app holds it for review (or releases it, with auto-approve). Versions never change once uploaded, so there's no CDN refresh to wait for.
728
+
729
+ The platform also sets content types and caching: hashed bundles are cached for a year, HTML is never cached, and everything else is cached for 5 minutes.
730
+
731
+ ### Dependency check
732
+
733
+ Before it reserves a version number, `deploy` runs `npm audit --json --omit=dev` in the app folder (next to
734
+ `hawk-app.json`, 60 s at most), and it sends the vulnerability counts with the version. Reviewers see them with the other
735
+ pre-approval checks in HawkSearch and the Hawk Analytics portal. High or critical vulnerabilities fail that check, so
736
+ approvers can only force approve the version: when the audit finds any, `deploy` names the packages and asks before
737
+ going on.
738
+
739
+ ```
740
+ dependencies 1 high, 1 moderate (npm audit)
741
+ npm audit found 1 high, 1 moderate in the production dependencies (postcss). Approvers can only force approve this
742
+ version. Deploy anyway? (Fix with npm audit fix or by updating the packages; npm run app:audit shows the details.) (y/N)
743
+ ```
744
+
745
+ No cancels without reserving anything. `--yes` goes on with a warning, and so does a pipeline (no terminal to ask in).
746
+ Moderate and low findings are only reported. Check before deploying with `npm run app:audit` (in apps created from the
747
+ templates: `npm audit --omit=dev --audit-level=high`, which exits non-zero on high or critical findings, so it also works
748
+ as a CI step). Other outcomes:
749
+
750
+ ```
751
+ dependencies no known vulnerabilities
752
+ dependencies not checked: no package-lock.json
753
+ ```
754
+
755
+ An app in an npm workspace is audited with the lockfile at the repository root. Without a `package-lock.json` (in the
756
+ app folder or above it), when npm audit fails (for example a registry without audit support) or times out, the
757
+ version is marked "not checked" with a short reason. `--no-audit` skips it (`not checked: skipped (--no-audit)`).
758
+
759
+ ## In CI (example: Bitbucket Pipelines)
760
+
761
+ ```yaml
762
+ - step:
763
+ name: Deploy Hawk App (dev)
764
+ deployment: development # HAWK_APPS_ENV (dev or prod) and HAWK_APPS_PUBLISHER_KEY as secured variables
765
+ script:
766
+ - npm ci
767
+ - npx hawk-app deploy # builds with the version's base path, then uploads (with hawk-app.json)
768
+ ```
769
+
770
+ Requires Node 20 or later. Dependencies: `esbuild` (bundles functions) and `fflate` (zips them).