@bridgeline-digital/hawk-app-cli 0.1.0-dev.20261020202308 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/LICENSE +7 -7
  2. package/README.md +770 -770
  3. package/agents/AGENTS.md +192 -192
  4. package/agents/project/custom.md +21 -21
  5. package/agents/project/next-mui.md +21 -21
  6. package/agents/project/next.md +24 -24
  7. package/agents/project/vanilla.md +22 -22
  8. package/agents/project/vite.md +19 -19
  9. package/bin/hawk-app.mjs +0 -0
  10. package/docs/bridge/README.md +1315 -1315
  11. package/docs/showcase/README.md +47 -47
  12. package/docs/showcase/functions/echo.ts +12 -12
  13. package/docs/showcase/functions/long-task.ts +31 -31
  14. package/docs/showcase/functions/showcase.ts +186 -186
  15. package/docs/showcase/hawk-app.json +73 -73
  16. package/docs/showcase/queries/assistant-prompts-by-day.json +6 -6
  17. package/docs/showcase/queries/assistant-prompts-by-day.sql +8 -8
  18. package/docs/showcase/queries/searches-by-day.json +6 -6
  19. package/docs/showcase/queries/searches-by-day.sql +7 -7
  20. package/docs/showcase/src/App.tsx +1436 -1436
  21. package/docs/showcase/src/main.tsx +10 -10
  22. package/docs/showcase/src/styles.css +71 -71
  23. package/docs/showcase/src/useHawkApp.ts +44 -44
  24. package/docs/showcase/src/vite-env.d.ts +1 -1
  25. package/docs/ui/README.md +166 -166
  26. package/package.json +1 -1
  27. package/schema/hawk-app.schema.json +611 -611
  28. package/src/accessibility.mjs +105 -105
  29. package/src/agents.mjs +145 -145
  30. package/src/aliases.mjs +30 -30
  31. package/src/app-icon.mjs +82 -82
  32. package/src/channel.mjs +116 -116
  33. package/src/cli.mjs +825 -825
  34. package/src/clients.mjs +221 -221
  35. package/src/config.mjs +148 -148
  36. package/src/create.mjs +186 -186
  37. package/src/credentials.mjs +56 -56
  38. package/src/data.mjs +169 -169
  39. package/src/deploy.mjs +349 -349
  40. package/src/dev.mjs +459 -459
  41. package/src/doctor.mjs +176 -176
  42. package/src/env.mjs +140 -140
  43. package/src/functions.mjs +313 -313
  44. package/src/login.mjs +166 -166
  45. package/src/logs.mjs +64 -64
  46. package/src/manifest.mjs +425 -425
  47. package/src/mcp-docs.mjs +168 -168
  48. package/src/mcp-hawksearch.mjs +342 -342
  49. package/src/mcp-reports.mjs +130 -130
  50. package/src/mcp-rules.mjs +109 -109
  51. package/src/mcp.mjs +539 -539
  52. package/src/platform.mjs +116 -116
  53. package/src/project-checks.mjs +153 -153
  54. package/src/queries.mjs +289 -289
  55. package/src/scope-check.mjs +91 -91
  56. package/src/secrets.mjs +86 -86
  57. package/src/slug-guard.mjs +80 -80
  58. package/src/sso.mjs +182 -182
  59. package/src/telemetry.mjs +50 -50
  60. package/src/upgrade.mjs +406 -406
  61. package/src/versions.mjs +102 -102
  62. package/templates/next/next.config.ts +13 -13
  63. package/templates/next/package.json +26 -26
  64. package/templates/next/src/lib/useHawkApp.ts +46 -46
  65. package/templates/next-mui/next.config.ts +13 -13
  66. package/templates/next-mui/package.json +30 -30
  67. package/templates/next-mui/src/app/layout.tsx +19 -19
  68. package/templates/next-mui/src/app/page.tsx +78 -78
  69. package/templates/next-mui/src/app/providers.tsx +10 -10
  70. package/templates/preact/package.json +26 -26
  71. package/templates/preact/src/useHawkApp.ts +44 -44
  72. package/templates/preact/src/vite-env.d.ts +1 -1
  73. package/templates/preact/tsconfig.json +17 -17
  74. package/templates/preact/vite.config.ts +11 -11
  75. package/templates/react/package.json +29 -29
  76. package/templates/react/src/useHawkApp.ts +44 -44
  77. package/templates/react/src/vite-env.d.ts +1 -1
  78. package/templates/react/tsconfig.json +16 -16
  79. package/templates/react/vite.config.ts +11 -11
  80. package/templates/vanilla/package.json +15 -15
package/src/cli.mjs CHANGED
@@ -1,825 +1,825 @@
1
- import { existsSync, readFileSync } from 'node:fs';
2
- import { writeFile } from 'node:fs/promises';
3
- import { basename, dirname, join, resolve } from 'node:path';
4
- import { TEMPLATES, createProject, titleCase, toSlug } from './create.mjs';
5
- import { addAgentFiles } from './agents.mjs';
6
- import { confirmVulnerabilities, deploy, npmRunBuild, submissionClearedMessage } from './deploy.mjs';
7
- import { buildTarget, checkBuildUrls, checkProject, describeBuildUrls } from './project-checks.mjs';
8
- import { loadEnv, PLATFORMS, resolveSettings } from './env.mjs';
9
- import { upgrade, upgradeHint } from './upgrade.mjs';
10
- import { login, logout, whoami } from './login.mjs';
11
- import { sso } from './sso.mjs';
12
- import { checkNewSlug, confirmTakeover } from './slug-guard.mjs';
13
- import { MANIFEST_FILE, loadManifest, newManifest, validateManifest } from './manifest.mjs';
14
- import { DEFAULT_ICON_SVG, checkIcon } from './app-icon.mjs';
15
- import { checkScreenshots } from './store-listing.mjs';
16
- import { platformClient } from './platform.mjs';
17
- import { dataCommand } from './data.mjs';
18
- import { DATA_SCOPE, QUERIES_DIR, loadQueries } from './queries.mjs';
19
- import { FUNCTIONS_DIR, checkBridge, describeFunctions, loadFunctions } from './functions.mjs';
20
- import { secretsCommand } from './secrets.mjs';
21
- import { configCommand } from './config.mjs';
22
- import { clientsCommand, pickDevClient, rememberDevClient } from './clients.mjs';
23
- import { DEFAULT_ALIAS, checkAlias, describeAlias, describeClientAlias } from './aliases.mjs';
24
- import { aliasCommand, versionsCommand, whyCommand } from './versions.mjs';
25
- import { describeMissingScopes, missingScopes } from './scope-check.mjs';
26
- import { docsCommand, doctorCommand, openCommand } from './doctor.mjs';
27
- import { logsCommand } from './logs.mjs';
28
- import { devCommand } from './dev.mjs';
29
- import { appChannel } from './channel.mjs';
30
- import { accessibilityWarnings, checkAccessibility } from './accessibility.mjs';
31
- import { errorsCommand, usageCommand } from './telemetry.mjs';
32
-
33
- /** The short help: what each command is for, by task. `hawk-app help <command>` shows a command's details. */
34
- const HELP = `hawk-app: build, run and release Hawk Apps
35
-
36
- Start
37
- create <folder> Start a new app from a template (react, preact, next, next-mui, vanilla)
38
- init Make an existing project a Hawk App
39
- sso Sign in with your Bridgeline account (once per machine; CI: login with a publisher key)
40
- doctor Check everything: Node, login, hawk-app.json, project, scopes, template, MCP
41
- docs [topic] The developer docs: guides, the bridge API, every command (Bridgeline sign-in)
42
-
43
- Develop
44
- dev Run the app locally inside a stand-in for HawkSearch, with a client's real data
45
- validate Check hawk-app.json, queries/, functions/ and the project (--build: the build too)
46
- upgrade Bring the app up to the template this CLI ships
47
-
48
- Release
49
- deploy Build and upload a new version, with hawk-app.json (--alias staging: off the main line;
50
- --dry-run: only show what hawk-app.json would change)
51
- status Released or not, the newest version, the approval request, the aliases
52
- versions Every version with its status (draft, waiting, approved, rejected) and aliases
53
- open The app in HawkSearch, to review it and submit it for approval (--alias: as its clients)
54
- alias set|delete Point an alias at a version that isn't approved, or delete one of yours
55
- why <client id> What a client's users see of the app, and why
56
-
57
- Clients
58
- clients Who has the app: add, exclude, remove, search (also in the Hawk Analytics portal)
59
- config The app's config values, global or per client: list, set, unset (no deploy)
60
-
61
- Data and functions
62
- data The tables app queries can use, and running a query
63
- secrets, logs Function secrets and logs
64
-
65
- Monitor
66
- errors, usage The app's page errors, and its opens, users and clients per day
67
-
68
- Account
69
- login, whoami, logout A publisher key (for CI), and which key and platform apply here
70
- templates The templates create can use
71
- mcp The MCP server for AI coding assistants
72
-
73
- hawk-app help <command> shows a command's details; hawk-app help all, everything.
74
- Options: --env dev|prod (default dev), --manifest <file>, --platform <url>.
75
- Docs: https://developers.hawksearchapps.com (hawk-app docs).
76
- `;
77
-
78
- /** Every command's details (hawk-app help <command>, hawk-app help all). */
79
- const DETAILS = `hawk-app: build, run and release Hawk Apps
80
-
81
- Usage:
82
- hawk-app create <folder> [--template react|preact|next|next-mui|vanilla] [--name "<name>"] [--install]
83
- [--channel latest|dev]
84
- Start a new app from a template (default: react).
85
- Refuses a slug that's already an app (--yes: use it anyway).
86
- --channel: the bridge and CLI builds it follows; default this
87
- CLI's own (npx @bridgeline-digital/hawk-app-cli@dev create
88
- makes a dev app, on the dev tag)
89
- hawk-app login [--local] Save your publisher key once for this machine (input hidden;
90
- --local: this project's .env.local only)
91
- hawk-app sso [--publisher <id>] Sign in with your Bridgeline account in the browser instead: the
92
- platform issues your own key (it expires; sign in again then)
93
- and saves it like login. Bridgeline AD accounts only.
94
- --publisher: the team whose apps you work on (default: services)
95
- hawk-app whoami Show whether you're logged in, and which key/platform apply here
96
- hawk-app logout Remove this machine's saved key
97
- hawk-app init [--slug <slug>] [--name "<name>"] [--yes]
98
- Make an existing project (no template) a Hawk App: writes
99
- hawk-app.json and AGENTS.md (refuses a slug that's already an
100
- app unless --yes)
101
- hawk-app templates List the templates create can use
102
- hawk-app validate [--build [<build-folder>]] Check hawk-app.json, queries/ and functions/ (if any) and the
103
- project (bridge, build script) locally. --build also builds
104
- the app as deploy does and checks its URLs use the base path
105
- hawk-app upgrade [--yes] [--template <name>] [--no-install] [--channel latest|dev]
106
- Bring the app up to the template this CLI ships: the
107
- template's package versions (never lowered), config files
108
- you haven't edited, and the CLI's part of AGENTS.md (also for
109
- apps without a template). Shows the changes; --yes applies them.
110
- --channel first moves the bridge and CLI to the dev tag (dev) or
111
- to the released versions (latest: ^ npm's latest)
112
- hawk-app deploy [<build-folder>] [--no-build] [--no-audit] [--alias <name>] [--dry-run]
113
- Build the app and upload it as a new version (v1, v2, …),
114
- with hawk-app.json. The folder comes from the template (dist,
115
- out, public) unless given. Runs \`npm run build\` with
116
- HAWK_APP_BASE_PATH=/<slug>/v<n> when package.json has a build
117
- script; --no-build uploads the folder as it is.
118
- queries/ and functions/ next to hawk-app.json are checked and
119
- deployed with it (functions are bundled with esbuild).
120
- Reports \`npm audit\` of the production dependencies to
121
- reviewers (never blocks; --no-audit skips it).
122
- Asks first when a teammate made the app's last change
123
- (--yes skips the question; CI only warns). --alias points
124
- an alias (e.g. staging) at the new version: only Bridgeline
125
- staff in its clients see it until it's approved.
126
- --dry-run shows what hawk-app.json would change, and deploys
127
- nothing (use it on pull requests)
128
- hawk-app register Send hawk-app.json without a new version (scripts; deploy
129
- already sends it). Changes wait for review in HawkSearch
130
- hawk-app status Show what is released, the newest version, the approval request,
131
- the aliases and the automated checks approvers will see
132
- hawk-app open [--alias <name>] Open the app's page in HawkSearch (dev or prod): review it and
133
- submit it for approval there; --alias shows it as that alias's
134
- clients see it
135
- hawk-app docs [<topic>] Open the developer docs (developers.hawksearchapps.com, Bridgeline
136
- sign-in), or a topic's page: start, concepts, versions, bridge,
137
- api, cli, commands, manifest, platform, data, functions, email,
138
- storage, ai, search, review, whats-new
139
- hawk-app doctor Check everything a developer needs: Node, login and platform,
140
- hawk-app.json, the project, scopes the code uses, the template,
141
- the app on the platform and the MCP config
142
- hawk-app versions Every version with its status (draft, waiting, approved,
143
- rejected) and the aliases on it
144
- hawk-app alias set <name> <version> Point an alias at a version that isn't approved (Bridgeline
145
- staff only); approvers do the rest. alias delete <name>
146
- deletes one of yours (no clients on it)
147
- hawk-app why <client id> What a client's users see of the app, and why
148
- hawk-app mcp Run the MCP server for AI coding assistants (stdio; dev platform
149
- only). create and init add it to .mcp.json and .vscode/mcp.json
150
- hawk-app dev [--client <id>] [--engine <id>] [--url <app dev URL>] [--port 3200] [--client-user] [--no-start]
151
- Run the app locally in a stand-in for HawkSearch, with real
152
- tokens and data for any client (runs \`npm run dev\` unless
153
- --url/--no-start; HAWKSEARCH_API_KEY adds the dashboard API).
154
- Without --client it asks which client and remembers it in
155
- .env.local (HAWK_APPS_DEV_CLIENT). Queries and functions run
156
- from the deployed version: deploy after changing them
157
-
158
- Clients (who has the app; also managed in the Hawk Analytics portal, not in hawk-app.json):
159
- hawk-app clients Show the app's visibility and its clients
160
- hawk-app clients search <name or id> Find a client's id by its name (e.g. "outdoor")
161
- hawk-app clients add <id> [<id> …] [--name "<name>"] [--alias <name>]
162
- Give clients the app (visibility allowList). --alias: the
163
- version they get (an alias; default: the default one). On an
164
- alias whose version isn't approved, only Bridgeline staff in
165
- them see the app
166
- hawk-app clients add --pick "<name or id>" The same, picking the client from the matches by name
167
- hawk-app clients exclude <id> [--name "<name>"] Opt a client out (visibility all)
168
- hawk-app clients remove <id> Remove a client from the list
169
- Changes apply at once, without a deploy or approval, and are recorded in the app's history.
170
-
171
- App config (values for the keys declared in "config" in hawk-app.json; also in the Hawk Analytics portal):
172
- hawk-app config list [--client <id>] Each key: its type, default, global value and client values
173
- (--client: only that client's), marking invalid and undeclared ones
174
- hawk-app config set <key> <value> [--client <id>] Set the global value, or one client's. The value is read as the
175
- key's type: emails comma-separated, json as JSON
176
- hawk-app config unset <key> [--client <id>] Clear the global value, or one client's
177
- Values apply within about 30 seconds, without a deploy or approval. They are not secret: they reach the browser
178
- (app.config.get / ctx.config); keep keys in secrets.
179
-
180
- Monitor:
181
- hawk-app errors [--days 7] The app's errors in its pages (uncaught errors, rejections and
182
- app.reportError), grouped, the most frequent first (up to 30 days)
183
- hawk-app usage [--days 30] Opens of the app and its views, users and clients per day (up to
184
- 400 days; Bridgeline staff counted apart)
185
-
186
- Custom data (app queries, scope data:read):
187
- hawk-app data tables [--app <slug>] List the tables queries can use (--app: is the app
188
- opted in to the sensitive tables?)
189
- hawk-app data describe <table> Show a table's columns and types
190
- hawk-app data run <queries/name.sql> --client <id> [--param name=value …]
191
- Run a query against a client's data (dev platform only);
192
- string[] values are comma-separated
193
-
194
- Functions (server-side code, scope functions:invoke):
195
- hawk-app secrets list List the app's secrets (names only, never values)
196
- hawk-app secrets set <NAME> [--value-from-env <VAR>]
197
- Set a secret: type the value (hidden), or read it from an
198
- environment variable (CI). Applies to every version at once
199
- hawk-app secrets delete <NAME> Delete a secret
200
- hawk-app logs <function> [--since 1h] [--follow] Show a function's logs (--since 15m|1h|1d|<ISO time>;
201
- --follow keeps polling every 2 s)
202
- Background jobs: "job": true on a function (up to 900 s), started with app.functions.start. "schedules" in
203
- hawk-app.json run job functions on an EventBridge cron (at most hourly) for clients where a user turns them on.
204
-
205
- Email (scope email:send, no commands): app.email.send / ctx.platform.email.send from the bridge.
206
- HawkSearch system parameters (scope hawksearch:sysparams:write + "systemParameters" in hawk-app.json, no commands):
207
- app.hawksearch.systemParameters.list/get/set from the bridge, for the user's current engine.
208
- HawkSearch features (scope hawksearch:features:write, no commands): app.hawksearch.features.list/set/setFacet from the
209
- bridge, HawkSearch's Feature Configuration for the user's current engine, one feature code at a time.
210
- HawkSearch index (scope hawksearch:index, no commands): app.hawksearch.index.rebuild/status/wait from the bridge, a full
211
- rebuild of the user's engine's index from its feed, where the engine has EnableRebuildIndex on; one at a time; and
212
- the Indexing API's reads (indexes, current). Item writes: scope hawksearch:index:items (app.hawksearch.index.items).
213
- Creating, switching and deleting indexes, hierarchy and mappings: scope hawksearch:index:manage.
214
- HawkSearch recommendations (scope hawksearch:recommendations, no commands): app.hawksearch.recommendations from the
215
- bridge, the engine's recommendation widgets, never tracked in the client's recommendation reports.
216
- Storage (scope storage, no commands): app.storage / ctx.platform.storage get, put, delete, list, batch and usage from
217
- the bridge, the app's own key-value data per client (JSON values up to 350 KB, optional expiry, version checks), and
218
- storage.files upload, url, download, list and delete for its files (up to 100 MB each).
219
- AI (scope ai:invoke, no commands): app.ai / ctx.platform.ai generate, stream and info from the bridge.
220
- External Lambdas (scope lambdas:invoke + "lambdas" in hawk-app.json, no commands): app.lambdas.invoke /
221
- ctx.platform.lambdas.invoke from the bridge, for Lambdas Bridgeline Engineering allows on the platform.
222
-
223
- Options:
224
- --manifest <file> Manifest to use (default: hawk-app.json)
225
- --env <dev|prod> The environment to use (default: dev). hawk-app sso --env prod signs you in to prod.
226
- --platform <url> A platform URL instead of --env (e.g. a local platform)
227
-
228
- Which environment (first match wins): --env / --platform, then HAWK_APPS_ENV / HAWK_APPS_PLATFORM_URL in the
229
- environment (CI) or the project's .env.local / .env, else dev. Logging in never changes it, so prod is always chosen
230
- on purpose.
231
- Which key (first match wins):
232
- 1. Environment variable HAWK_APPS_PUBLISHER_KEY (CI secrets)
233
- 2. The project's .env.local / .env (git-ignored)
234
- 3. hawk-app login (saved once per machine in your user profile, per environment)
235
-
236
- Typical pipeline:
237
- hawk-app deploy --dry-run (pull requests: what hawk-app.json would change)
238
- hawk-app deploy (main branch: a new version, with hawk-app.json)
239
- hawk-app deploy --alias preview-<branch> (other branches: off the main line, Bridgeline staff only)
240
-
241
- Review: every deploy is a new version, a draft that only Bridgeline staff see. Bridgeline staff submit a version for
242
- approval in HawkSearch (hawk-app open), and members of the Approvers group approve or reject it (unless the app is set
243
- to auto-approve); a newer main-line deploy clears a waiting request. Once a version is approved and released, clients
244
- get it (their alias's version; default without one). Client changes (hawk-app clients or the Hawk Analytics portal) apply straight away.
245
- `;
246
-
247
- /**
248
- * The details of one command: its lines in DETAILS (the usage line and its continuation lines), plus the section of
249
- * its group when it has one (e.g. Clients). Null for an unknown command.
250
- */
251
- function commandHelp(name) {
252
- const lines = DETAILS.split('\n');
253
- const picked = [];
254
- for (let i = 0; i < lines.length; i++) {
255
- if (!new RegExp(`^ hawk-app ${name}(\\s|$)`).test(lines[i])) continue;
256
- picked.push(lines[i]);
257
- for (let j = i + 1; j < lines.length && /^ {4,}\S/.test(lines[j]); j++) picked.push(lines[j]);
258
- }
259
- return picked.length ? picked.join('\n') : null;
260
- }
261
-
262
- // clientNames: false prints client ids without their names (the MCP server, whose output goes to an AI assistant).
263
- /** A dev-channel app (bridge or CLI on dev builds) set to use the prod platform: its dev features may not be there yet. */
264
- export function devAppOnProd(projectDir, url) {
265
- if (url !== PLATFORMS.prod) return null;
266
- const pkgFile = join(projectDir, 'package.json');
267
- const pkg = existsSync(pkgFile) ? JSON.parse(readFileSync(pkgFile, 'utf8')) : null;
268
- return appChannel(pkg) === 'dev'
269
- ? 'This app is on the dev channel (dev builds of the bridge or CLI) but set to use the prod platform: their features may not be on prod yet. Move it to the released versions first: npx hawk-app upgrade --channel latest --yes.'
270
- : null;
271
- }
272
-
273
- export async function main(argv, { fetchImpl = fetch, log = console.log, runBuild, runAudit, runInstall, sleep, prompt, ask, confirm, choose, clientNames = true, env, signal, openUrl } = {}) {
274
- const { command, positional, flags } = parse(argv);
275
- if (['deploy', 'register', 'status', 'versions', 'alias', 'why', 'open', 'doctor', 'clients', 'data', 'secrets', 'config', 'logs', 'dev'].includes(command)) {
276
- for (const warning of loadEnv().warnings) log(`! ${warning}`);
277
- }
278
- const manifestFile = flags.manifest ?? MANIFEST_FILE;
279
- const client = () => {
280
- const { url, key } = resolveSettings({ flagPlatform: flags.platform, flagEnv: flags.env });
281
- return platformClient({ url, key, fetchImpl });
282
- };
283
- // For the slug checks of create / init, which also work without a key (they just can't check then).
284
- const clientIfLoggedIn = () => {
285
- const { url, key } = resolveSettings({ flagPlatform: flags.platform, flagEnv: flags.env });
286
- return url && key ? platformClient({ url, key, fetchImpl }) : null;
287
- };
288
- // Deploys and registers say when they go to prod (only ever on purpose: see resolveSettings).
289
- const notePlatform = () => {
290
- const { url, urlSource } = resolveSettings({ flagPlatform: flags.platform, flagEnv: flags.env });
291
- if (url === PLATFORMS.prod) log(`! Platform: prod (${url}, from ${urlSource})`);
292
- };
293
- const guard = { yes: !!flags.yes, log, ...(confirm ? { confirm } : {}) };
294
-
295
- switch (command) {
296
- case undefined:
297
- case 'help':
298
- case '--help':
299
- case '-h': {
300
- const topic = command === 'help' ? positional[0] : undefined;
301
- if (!topic) {
302
- log(HELP);
303
- return 0;
304
- }
305
- if (topic === 'all') {
306
- log(DETAILS);
307
- return 0;
308
- }
309
- const details = commandHelp(topic);
310
- if (!details) throw new Error(`No command "${topic}". \`hawk-app help\` lists them.`);
311
- log(details);
312
- return 0;
313
- }
314
-
315
- case 'open':
316
- return openCommand({ manifestFile, flags, ...(openUrl ? { openUrl } : {}), log });
317
-
318
- case 'doctor':
319
- return doctorCommand({ manifestFile, flags, fetchImpl, log });
320
-
321
- case 'docs':
322
- return docsCommand({ topic: positional[0], ...(openUrl ? { openUrl } : {}), log });
323
-
324
- case '--version':
325
- case '-v':
326
- log(JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version);
327
- return 0;
328
-
329
- case 'create': {
330
- await createProject({
331
- target: positional[0],
332
- template: flags.template ?? 'react',
333
- name: flags.name,
334
- install: !!flags.install,
335
- channel: flags.channel,
336
- checkSlug: (slug) => checkNewSlug({ client: clientIfLoggedIn(), slug, ...guard }),
337
- log,
338
- });
339
- return 0;
340
- }
341
-
342
- case 'login':
343
- await login({ platform: flags.platform, env: flags.env, local: !!flags.local, fetchImpl, log });
344
- return 0;
345
-
346
- case 'sso':
347
- await sso({ platform: flags.platform, env: flags.env, publisher: flags.publisher, fetchImpl, log, ...(openUrl ? { openUrl } : {}) });
348
- return 0;
349
-
350
- case 'whoami':
351
- return whoami({ platform: flags.platform, env: flags.env, fetchImpl, log });
352
-
353
- case 'logout':
354
- return logout({ platform: flags.platform, env: flags.env, log });
355
-
356
- case 'mcp': {
357
- // Loaded only here: the MCP SDK isn't needed by any other command.
358
- const { serveMcp } = await import('./mcp.mjs');
359
- return serveMcp({ version: JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version });
360
- }
361
-
362
- case 'templates':
363
- for (const [key, spec] of Object.entries(TEMPLATES)) log(` ${key.padEnd(8)} ${spec.description}`);
364
- return 0;
365
-
366
- case 'init': {
367
- if (existsSync(manifestFile)) throw new Error(`${manifestFile} already exists.`);
368
- const slug = flags.slug ?? toSlug(packageName() ?? basename(process.cwd()));
369
- await checkNewSlug({ client: clientIfLoggedIn(), slug, ...guard });
370
- const manifest = newManifest({ slug, name: flags.name ?? titleCase(slug) });
371
- await writeFile(manifestFile, JSON.stringify(manifest, null, 2) + '\n');
372
- log(`✔ Created ${manifestFile} for "${slug}".`);
373
- const iconFile = join(dirname(resolve(manifestFile)), manifest.icon);
374
- if (!existsSync(iconFile)) {
375
- await writeFile(iconFile, DEFAULT_ICON_SVG);
376
- log(`✔ Added a default ${manifest.icon} (replace it with your app's icon: an .svg in one color, or a square .png).`);
377
- }
378
- await addAgentFiles(dirname(resolve(manifestFile)), { slug, name: manifest.name }, log);
379
- for (const warning of (await checkProject(dirname(resolve(manifestFile)))).warnings) log(`! ${warning}`);
380
- log(' Next: `npx hawk-app dev` (run it locally inside a stand-in for HawkSearch) and `npx hawk-app deploy` (a new version).');
381
- log(' Versions are drafts that only Bridgeline staff see until an approver releases one (`npx hawk-app open`).');
382
- log(' (First time on this machine? Run `npx hawk-app sso` once. `npx hawk-app doctor` checks the setup.)');
383
- return 0;
384
- }
385
-
386
- case 'validate': {
387
- const manifest = await loadManifest(manifestFile);
388
- const result = validateManifest(manifest);
389
- const queries = await checkQueries(manifest, manifestFile);
390
- const functions = await checkFunctions(manifest, manifestFile);
391
- const projectDir = dirname(resolve(manifestFile));
392
- const project = await checkProject(projectDir);
393
- const { url } = resolveSettings({ flagPlatform: flags.platform, flagEnv: flags.env });
394
- const channelWarning = devAppOnProd(projectDir, url);
395
- if (channelWarning) project.warnings.push(channelWarning);
396
- // Bridge calls whose scope hawk-app.json lacks (they'd fail with 403 scope_missing).
397
- const scopeWarning = describeMissingScopes(await missingScopes(projectDir, manifest.scopes));
398
- if (scopeWarning) project.warnings.push(scopeWarning);
399
- const icon = result.errors.length ? { errors: [], warnings: [] } : await checkIcon(projectDir, manifest.icon);
400
- project.warnings.push(...icon.warnings);
401
- const screenshots = result.errors.length ? { errors: [] } : await checkScreenshots(projectDir, manifest.store);
402
- const { errors: buildErrors, warnings: buildWarnings } = flags.build
403
- ? await checkBuild({ projectDir, manifest, folder: positional[0], runBuild, log })
404
- : { errors: [], warnings: [] };
405
- const ok = `✔ ${manifestFile} looks good.${queries ? ` ${QUERIES_DIR}/: ${describeQueryCount(queries.queries.length)}.` : ''}${functions ? ` ${FUNCTIONS_DIR}/: ${describeFunctions(functions.functions.length)}.` : ''}${flags.build ? ' The build\'s URLs are under its base path.' : ''}`;
406
- return report(
407
- { errors: [...result.errors, ...icon.errors, ...screenshots.errors, ...(queries?.errors ?? []), ...(functions?.errors ?? []), ...buildErrors], warnings: [...result.warnings, ...project.warnings, ...buildWarnings] },
408
- ok,
409
- log,
410
- );
411
- }
412
-
413
- case 'deploy': {
414
- notePlatform();
415
- const manifest = await loadManifest(manifestFile);
416
- const projectDir = dirname(resolve(manifestFile));
417
- // A nudge when the app's dependencies are behind its template (never stops a deploy).
418
- try {
419
- const hint = upgradeHint(dirname(resolve(manifestFile)), manifest);
420
- if (hint) log(hint);
421
- } catch {
422
- // An app the CLI can't match to a template: no hint.
423
- }
424
- const checked = validateManifest(manifest);
425
- failOnErrors(checked, log);
426
- for (const warning of checked.warnings) log(`! ${warning}`);
427
- const icon = await checkIcon(projectDir, manifest.icon);
428
- for (const warning of icon.warnings) log(`! ${warning}`);
429
- if (icon.errors.length) {
430
- for (const error of icon.errors) log(`✖ ${error}`);
431
- throw new Error('Fix the icon first.');
432
- }
433
- const screenshots = await checkScreenshots(projectDir, manifest.store);
434
- if (screenshots.errors.length) {
435
- for (const error of screenshots.errors) log(`✖ ${error}`);
436
- throw new Error('Fix the store screenshots first.');
437
- }
438
- const scopeWarning = describeMissingScopes(await missingScopes(dirname(resolve(manifestFile)), manifest.scopes));
439
- if (scopeWarning) log(`! ${scopeWarning}`);
440
- const queries = await checkQueries(manifest, manifestFile);
441
- if (queries?.errors.length) {
442
- for (const error of queries.errors) log(`✖ ${error}`);
443
- throw new Error(`Fix ${QUERIES_DIR}/ first.`);
444
- }
445
- const functions = await checkFunctions(manifest, manifestFile);
446
- if (functions?.errors.length) {
447
- for (const error of functions.errors) log(`✖ ${error}`);
448
- throw new Error(`Fix the functions first.`);
449
- }
450
- const alias = flags.alias === undefined ? undefined : checkAlias(flags.alias);
451
- // hawk-app.json as the platform takes it ($schema and template are for local tools).
452
- const { $schema: _schema, template: _template, ...settings } = manifest;
453
- if (flags['dry-run']) {
454
- // Pull requests: what hawk-app.json would change, without a version.
455
- printRegister(await client().register(manifest.slug, settings, true), log);
456
- return 0;
457
- }
458
- const target = buildTarget(projectDir, manifest, positional[0]);
459
- if (!target) throw new Error('Which folder does the build go to? hawk-app deploy <build-folder> (e.g. dist, out, build)');
460
- await deploy({
461
- client: client(),
462
- alias,
463
- manifest: settings,
464
- registerSettings: async () => printRegisterBrief(await client().register(manifest.slug, settings, false), log),
465
- beforeReserve: () => confirmTakeover({ client: client(), slug: manifest.slug, action: 'deploy', ...guard }),
466
- confirmAudit: (report) => confirmVulnerabilities(report, guard),
467
- slug: manifest.slug,
468
- iconFile: icon.file,
469
- storeFiles: screenshots.files,
470
- dir: positional[0] ?? resolve(projectDir, target.folder),
471
- build: target.build && !flags['no-build'],
472
- serverFiles: queries?.files ?? [],
473
- functions: functions?.functions ?? [],
474
- projectDir,
475
- audit: !flags['no-audit'],
476
- ...(runBuild ? { runBuild } : {}),
477
- ...(runAudit ? { runAudit } : {}),
478
- fetchImpl,
479
- log,
480
- });
481
- return 0;
482
- }
483
-
484
- case 'register': {
485
- notePlatform();
486
- const manifest = await loadManifest(manifestFile);
487
- failOnErrors(validateManifest(manifest), log);
488
- // $schema and template are for local tools (the editor, hawk-app upgrade), not the platform.
489
- const { $schema, template, ...body } = manifest;
490
- if (!flags['dry-run']) await confirmTakeover({ client: client(), slug: manifest.slug, action: 'register', ...guard });
491
- const result = await client().register(manifest.slug, body, !!flags['dry-run']);
492
- printRegister(result, log);
493
- return 0;
494
- }
495
-
496
- case 'upgrade': {
497
- await upgrade({ manifestFile, template: flags.template, channel: flags.channel, yes: !!flags.yes, install: !flags['no-install'], ...(runInstall ? { runInstall } : {}), log });
498
- return 0;
499
- }
500
-
501
- case 'status': {
502
- const manifest = await loadManifest(manifestFile);
503
- const app = await client().status(manifest.slug).catch((error) => {
504
- if (error.status === 404) return null;
505
- throw error;
506
- });
507
- if (!app) {
508
- log(`${manifest.slug} isn't on the platform yet: \`npm run app:dev\` or \`npm run app:deploy\` adds it.`);
509
- return 0;
510
- }
511
- log(`${app.slug}: ${app.name}`);
512
- if (app.adminLocked) log(' LOCKED by an administrator: nobody sees it');
513
- else if (app.status === 'Disabled') log(' disabled nobody sees it');
514
- // Released: the default alias, what every client without another alias gets.
515
- log(` released ${app.status === 'Live' && app.liveVersion ? `v${app.liveVersion} (default: every client without another alias)` : 'not yet (only Bridgeline staff see it; an approver releases it)'}`);
516
- const newest = app.pendingVersion ?? (app.status !== 'Live' ? app.currentVersion : undefined);
517
- if (newest) log(` newest v${newest} ${describeNewest(app)}`);
518
- if (app.pending?.length) log(` settings waiting for review: ${app.pending.join(', ')}`);
519
- const review = describeReview(app);
520
- if (review) log(` request ${review}`);
521
- const staged = (app.aliases ?? []).filter((a) => a.name !== DEFAULT_ALIAS);
522
- if (staged.length) log(` aliases ${(app.aliases ?? []).map(describeAlias).join(' · ')}`);
523
- log(' (hawk-app versions lists every version with its status; hawk-app why <client> explains what a client sees.)');
524
- if (app.checks?.length) {
525
- log(' checks what approvers will see (a failed one needs a force approve):');
526
- for (const c of app.checks) log(` ${CHECK_MARKS[c.level] ?? '·'} ${c.title}: ${c.detail}`);
527
- }
528
- if (typeof app.autoApprove === 'boolean') log(` auto-approve ${app.autoApprove ? 'on (new versions and changes are released without review)' : 'off'}`);
529
- if (app.reviewEmails === false) log(' review emails off (approval requests, approvals and rejections aren\'t emailed; approvers change it in the Hawk Analytics portal)');
530
- if (app.viewCacheMinutes === 0) log(' view cache off (Engineering stopped it in the Hawk Analytics portal; the views\' cacheMinutes are ignored)');
531
- else if (Number.isInteger(app.viewCacheMinutes)) log(` view cache ${app.viewCacheMinutes} min for views that declare cacheMinutes (set by Engineering in the Hawk Analytics portal)`);
532
- log(` visibility ${app.visibility}`);
533
- if (app.listed === false) log(' listed no (not on the Apps landing page; opens by its direct URL)');
534
- if (app.store) log(` store ${describeStore(app.store)}`);
535
- if (app.hosting) log(` hosting ${app.hosting}`);
536
- log(` entry URL ${app.entryUrl}`);
537
- if (app.publishedAt) log(` published ${app.publishedAt.slice(0, 10)} by ${app.publishedByName ?? app.publishedBy ?? 'unknown'}`);
538
- log(` scopes ${app.scopes.join(' ') || '(none)'}`);
539
- log(` origins ${describeOrigins(app.externalOrigins)}`);
540
- if (app.systemParameters?.length) log(` sysparams ${app.systemParameters.join(' ')}`);
541
- if (app.lambdas?.length) log(` lambdas ${app.lambdas.join(' ')}`);
542
- const configKeys = Object.keys(app.config ?? {});
543
- if (configKeys.length) log(` config ${configKeys.join(' ')}`);
544
- const schedules = Object.entries(app.schedules ?? {});
545
- if (schedules.length) {
546
- log(` schedules ${schedules.length} (each runs only for clients where a user turns it on in the app)`);
547
- for (const [name, s] of schedules) log(` ${name} ${s.function} "${s.cron}" ${s.timezone ?? 'UTC'}`);
548
- }
549
- log(` clients ${app.clients.length}`);
550
- for (const c of app.clients) {
551
- const alias = c.alias ? (app.aliases ?? []).find((a) => a.name === c.alias) : undefined;
552
- log(` ${c.enabled ? '+' : '-'} ${c.id}${c.name && clientNames ? ` (${c.name})` : ''}${describeClientAlias({ alias: c.alias, aliasStatus: alias?.status })}`);
553
- }
554
- return 0;
555
- }
556
-
557
- case 'dev': {
558
- const manifest = await loadManifest(manifestFile);
559
- const projectDir = dirname(resolve(manifestFile));
560
- if (!flags.client && !(env ?? process.env).HAWK_APPS_DEV_CLIENT) {
561
- // No client yet: ask which one (by name), and remember it in .env.local for next time.
562
- const id = await pickDevClient({ client, slug: manifest.slug, ...(choose ? { choose } : {}), ...(ask ? { ask } : {}), log });
563
- await rememberDevClient(projectDir, id);
564
- log(`✔ Using client ${id} (saved as HAWK_APPS_DEV_CLIENT in .env.local; --client <id> uses another).`);
565
- flags.client = id;
566
- }
567
- // A new app isn't on the platform yet: register it (a draft, so only Bridgeline staff see it) to run it.
568
- const known = await client().status(manifest.slug).then(() => true, (error) => (error.status === 404 ? false : true));
569
- if (!known) {
570
- const { $schema: _schema, template: _template, ...settings } = manifest;
571
- await client().register(manifest.slug, settings, false);
572
- log(`✔ Registered ${manifest.slug} on the platform (not released: only Bridgeline staff see it).`);
573
- }
574
- return devCommand({ client: client(), manifest, flags, projectDir, ...(env ? { env } : {}), signal, log });
575
- }
576
-
577
- case 'data':
578
- return dataCommand({ client, sub: positional[0], args: positional.slice(1), flags, ...(sleep ? { sleep } : {}), log });
579
-
580
- case 'secrets': {
581
- const manifest = await loadManifest(manifestFile);
582
- return secretsCommand({ client, manifest, sub: positional[0], args: positional.slice(1), flags, ...(prompt ? { prompt } : {}), ...(env ? { env } : {}), log });
583
- }
584
-
585
- case 'config': {
586
- const manifest = await loadManifest(manifestFile);
587
- return configCommand({ client, manifest, sub: positional[0], args: positional.slice(1), flags, log });
588
- }
589
-
590
- case 'clients': {
591
- // search reads the environment's client list: no app needed.
592
- if (positional[0] === 'search') return clientsCommand({ client, sub: 'search', args: positional.slice(1), log });
593
- const manifest = await loadManifest(manifestFile);
594
- return clientsCommand({ client, manifest, sub: positional[0], args: positional.slice(1), flags, names: clientNames, ...(choose ? { choose } : {}), log });
595
- }
596
-
597
- case 'versions':
598
- return versionsCommand({ client: client(), manifest: await loadManifest(manifestFile), log });
599
-
600
- case 'alias':
601
- return aliasCommand({ client: client(), manifest: await loadManifest(manifestFile), sub: positional[0], args: positional.slice(1), log });
602
-
603
- case 'why':
604
- return whyCommand({ client: client(), manifest: await loadManifest(manifestFile), clientId: positional[0], log });
605
-
606
- case 'errors': {
607
- return errorsCommand({ client: client(), manifest: await loadManifest(manifestFile), flags, log });
608
- }
609
-
610
- case 'usage': {
611
- return usageCommand({ client: client(), manifest: await loadManifest(manifestFile), flags, log });
612
- }
613
-
614
- case 'logs': {
615
- const manifest = await loadManifest(manifestFile);
616
- const controller = new AbortController();
617
- const stop = () => controller.abort();
618
- signal?.addEventListener('abort', stop, { once: true });
619
- if (flags.follow) process.once('SIGINT', stop);
620
- try {
621
- return await logsCommand({ client, manifest, name: positional[0], flags, ...(sleep ? { sleep } : {}), signal: controller.signal, log });
622
- } finally {
623
- process.off('SIGINT', stop);
624
- signal?.removeEventListener('abort', stop);
625
- }
626
- }
627
-
628
- default:
629
- log(HELP);
630
- throw new Error(`Unknown command "${command}".`);
631
- }
632
- }
633
-
634
- /** What `hawk-app deploy` says about the hawk-app.json it sent first (the full summary is `hawk-app register`'s). */
635
- function printRegisterBrief(result, log) {
636
- log(`✔ hawk-app.json sent${result.pending?.length ? `: ${result.pending.join(', ')} waiting for review` : ''}`);
637
- for (const warning of result.warnings ?? []) log(` ! ${warning}`);
638
- if (result.submissionCleared) log(` ! ${submissionClearedMessage('settings')}`);
639
- }
640
-
641
- function printRegister(result, log) {
642
- log(`${result.dryRun ? 'Dry run, nothing was changed:' : '✔ Sent hawk-app.json for'} ${result.slug}`);
643
- log(` status ${result.status}${result.adminLocked ? ' (LOCKED by an administrator: hidden from everyone)' : ''}`);
644
- log(` visibility ${result.visibility}`);
645
- if (result.hosting) log(` hosting ${result.hosting}`);
646
- log(` entry URL ${result.entryUrl}`);
647
- log(` scopes ${result.scopes.join(' ') || '(none)'}`);
648
- if (result.pending?.length) {
649
- log(` settings waiting for review: ${result.pending.join(', ')}`);
650
- log(' (approvers approve them in HawkSearch, with the next version)');
651
- }
652
- if (typeof result.autoApprove === 'boolean') log(` auto-approve ${result.autoApprove ? 'on (changes are released without review)' : 'off'}`);
653
- if (result.externalOrigins) {
654
- const o = result.externalOrigins;
655
- log(` origins ${describeOrigins(o.current)}${o.restamped ? ` (applied to ${o.restamped} deployed page${o.restamped === 1 ? '' : 's'})` : ''}`);
656
- for (const origin of o.added) log(` + ${origin} (added)`);
657
- for (const origin of o.removed) log(` - ${origin} (removed)`);
658
- }
659
- log(' clients manage with `hawk-app clients` or in the Hawk Analytics portal');
660
- for (const warning of result.warnings ?? []) log(` ! ${warning}`);
661
- if (result.submissionCleared && !result.dryRun) log(` ! ${submissionClearedMessage('settings')}`);
662
- }
663
-
664
- const CHECK_MARKS = { pass: '✔', fail: '✖', info: 'i' };
665
-
666
- /** The newest version's status, from the approval request: waiting, rejected, or a draft. */
667
- function describeNewest({ reviewState }) {
668
- if (reviewState === 'waiting_for_approval') return 'waiting for approval';
669
- if (reviewState === 'rejected') return 'rejected (see request)';
670
- return "draft: only Bridgeline staff see it until it's approved";
671
- }
672
-
673
- const day = (iso) => (typeof iso === 'string' ? iso.slice(0, 10) : 'unknown date');
674
- const person = (user) => user?.name ?? user?.email ?? 'unknown';
675
-
676
- /**
677
- * The app's place in the approval workflow (null when the platform doesn't say):
678
- * "waiting for approval (submitted by Jane Doe, 2026-10-08: <note>)", "rejected by Jane Doe on 2026-10-08: <reason>".
679
- */
680
- export function describeReview({ reviewState, submission, lastDecision }) {
681
- switch (reviewState) {
682
- case 'waiting_for_approval': {
683
- if (!submission) return 'waiting for approval';
684
- const note = submission.note ? `: ${submission.note}` : '';
685
- return `waiting for approval (submitted by ${person(submission.submittedBy)}, ${day(submission.submittedAt)}${note})`;
686
- }
687
- case 'rejected': {
688
- if (!lastDecision) return 'rejected';
689
- return `rejected by ${person(lastDecision.by)} on ${day(lastDecision.at)}${lastDecision.reason ? `: ${lastDecision.reason}` : ''}`;
690
- }
691
- case 'pending':
692
- return 'not submitted yet (Bridgeline staff submit it for approval in HawkSearch)';
693
- case 'draft':
694
- return 'draft (Bridgeline staff submit it for approval in HawkSearch)';
695
- case 'live':
696
- return 'nothing waiting for approval';
697
- default:
698
- return null;
699
- }
700
- }
701
-
702
- /** What the app may load from and call, as the CDN's CSP will allow it. */
703
- /** A store app's line in `status`: "in the store, $99/month; requests 2 open, 5 approved, 1 declined". */
704
- function describeStore(store) {
705
- const where = store.offered
706
- ? 'in the store'
707
- : store.listed === false
708
- ? 'out of the store (a Hawk-App-Manager took it out)'
709
- : 'not in the store yet (clients see it once it\'s released)';
710
- const price = store.price ? `, ${store.price}` : '';
711
- return `${where}${price}; requests ${store.openRequests} open, ${store.approvedRequests} approved, ${store.declinedRequests} declined`;
712
- }
713
-
714
- function describeOrigins(origins) {
715
- if (!origins) return 'any https origin';
716
- if (origins.length === 1 && origins[0] === '*') return 'any https origin (*)';
717
- return origins.length ? origins.join(' ') : 'none (own files and the platform only)';
718
- }
719
-
720
- /**
721
- * Checks queries/ next to the manifest (not in the build folder). Null when the app has none. Queries need the
722
- * data:read scope.
723
- */
724
- async function checkQueries(manifest, manifestFile) {
725
- const result = await loadQueries(dirname(resolve(manifestFile)));
726
- if (!result) return null;
727
- if (!(manifest.scopes ?? []).includes(DATA_SCOPE)) {
728
- result.errors.unshift(`${QUERIES_DIR}/ needs the "${DATA_SCOPE}" scope: add it to "scopes" in hawk-app.json (or remove ${QUERIES_DIR}/).`);
729
- }
730
- return result;
731
- }
732
-
733
- /**
734
- * Checks functions/ next to the manifest against its "functions" block. Null when the app has neither. Also checks
735
- * that the app has a bridge with the /server entry, which the bundles are built with.
736
- */
737
- async function checkFunctions(manifest, manifestFile) {
738
- const projectDir = dirname(resolve(manifestFile));
739
- const result = await loadFunctions(projectDir, manifest);
740
- if (!result) return null;
741
- if (result.functions.length) {
742
- const problem = checkBridge(projectDir);
743
- if (problem) result.errors.push(problem);
744
- }
745
- return result;
746
- }
747
-
748
- /**
749
- * `validate --build [<folder>]`: builds the app as deploy would (with a stand-in base path, /<slug>/v0) and checks
750
- * that its HTML only loads URLs under it. The folder comes from the argument or the app:deploy script.
751
- */
752
- async function checkBuild({ projectDir, manifest, folder, runBuild = npmRunBuild, log }) {
753
- const found = buildTarget(projectDir, manifest, folder);
754
- if (!found) return { errors: ['--build needs the build folder: hawk-app validate --build <folder> (e.g. dist, out).'], warnings: [] };
755
- const target = found.folder;
756
- const basePath = `/${manifest.slug}/v0`;
757
- if (found.build) {
758
- log(`Building with HAWK_APP_BASE_PATH=${basePath}…`);
759
- await runBuild({ basePath, cwd: projectDir });
760
- }
761
- const dir = resolve(projectDir, target);
762
- if (!existsSync(dir)) return { errors: [`The build folder "${target}" doesn't exist.`], warnings: [] };
763
- const outside = await checkBuildUrls(dir, basePath);
764
- // The accessibility check approvers will see (critical and serious issues).
765
- const warnings = accessibilityWarnings(await checkAccessibility(dir));
766
- return { errors: outside.length ? [describeBuildUrls(outside, basePath)] : [], warnings };
767
- }
768
-
769
-
770
- function describeQueryCount(count) {
771
- return `${count} quer${count === 1 ? 'y' : 'ies'}`;
772
- }
773
-
774
- function report({ errors, warnings }, ok, log) {
775
- for (const warning of warnings) log(`! ${warning}`);
776
- if (errors.length) {
777
- for (const error of errors) log(`✖ ${error}`);
778
- return 1;
779
- }
780
- log(ok);
781
- return 0;
782
- }
783
-
784
- function failOnErrors(result, log) {
785
- if (result.errors.length) {
786
- report(result, '', log);
787
- throw new Error('Fix hawk-app.json first.');
788
- }
789
- }
790
-
791
- export function parse(argv) {
792
- const flags = {};
793
- const positional = [];
794
- let command;
795
- for (let i = 0; i < argv.length; i++) {
796
- const arg = argv[i];
797
- if (arg.startsWith('--') && arg.length > 2 && command !== undefined) {
798
- const eq = arg.indexOf('=');
799
- const name = eq === -1 ? arg.slice(2) : arg.slice(2, eq);
800
- const inline = eq === -1 ? undefined : arg.slice(eq + 1);
801
- if (name === 'param') {
802
- // Repeatable, and the value keeps its own "=": --param from=2026-09-01 --param to=2026-09-30
803
- flags.param = [...(flags.param ?? []), inline ?? argv[++i]];
804
- continue;
805
- }
806
- const takesValue = ['manifest', 'platform', 'slug', 'name', 'template', 'client', 'app', 'since', 'value-from-env', 'engine', 'url', 'port', 'env', 'publisher', 'pick', 'channel', 'days', 'alias'].includes(name);
807
- flags[name] = takesValue ? inline ?? argv[++i] : true;
808
- } else if (command === undefined) {
809
- command = arg;
810
- } else {
811
- positional.push(arg);
812
- }
813
- }
814
- return { command, positional, flags };
815
- }
816
-
817
- function packageName() {
818
- try {
819
- return JSON.parse(readFileSync('package.json', 'utf8')).name?.replace(/^@[^/]+\//, '');
820
- } catch {
821
- return undefined;
822
- }
823
- }
824
-
825
-
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { writeFile } from 'node:fs/promises';
3
+ import { basename, dirname, join, resolve } from 'node:path';
4
+ import { TEMPLATES, createProject, titleCase, toSlug } from './create.mjs';
5
+ import { addAgentFiles } from './agents.mjs';
6
+ import { confirmVulnerabilities, deploy, npmRunBuild, submissionClearedMessage } from './deploy.mjs';
7
+ import { buildTarget, checkBuildUrls, checkProject, describeBuildUrls } from './project-checks.mjs';
8
+ import { loadEnv, PLATFORMS, resolveSettings } from './env.mjs';
9
+ import { upgrade, upgradeHint } from './upgrade.mjs';
10
+ import { login, logout, whoami } from './login.mjs';
11
+ import { sso } from './sso.mjs';
12
+ import { checkNewSlug, confirmTakeover } from './slug-guard.mjs';
13
+ import { MANIFEST_FILE, loadManifest, newManifest, validateManifest } from './manifest.mjs';
14
+ import { DEFAULT_ICON_SVG, checkIcon } from './app-icon.mjs';
15
+ import { checkScreenshots } from './store-listing.mjs';
16
+ import { platformClient } from './platform.mjs';
17
+ import { dataCommand } from './data.mjs';
18
+ import { DATA_SCOPE, QUERIES_DIR, loadQueries } from './queries.mjs';
19
+ import { FUNCTIONS_DIR, checkBridge, describeFunctions, loadFunctions } from './functions.mjs';
20
+ import { secretsCommand } from './secrets.mjs';
21
+ import { configCommand } from './config.mjs';
22
+ import { clientsCommand, pickDevClient, rememberDevClient } from './clients.mjs';
23
+ import { DEFAULT_ALIAS, checkAlias, describeAlias, describeClientAlias } from './aliases.mjs';
24
+ import { aliasCommand, versionsCommand, whyCommand } from './versions.mjs';
25
+ import { describeMissingScopes, missingScopes } from './scope-check.mjs';
26
+ import { docsCommand, doctorCommand, openCommand } from './doctor.mjs';
27
+ import { logsCommand } from './logs.mjs';
28
+ import { devCommand } from './dev.mjs';
29
+ import { appChannel } from './channel.mjs';
30
+ import { accessibilityWarnings, checkAccessibility } from './accessibility.mjs';
31
+ import { errorsCommand, usageCommand } from './telemetry.mjs';
32
+
33
+ /** The short help: what each command is for, by task. `hawk-app help <command>` shows a command's details. */
34
+ const HELP = `hawk-app: build, run and release Hawk Apps
35
+
36
+ Start
37
+ create <folder> Start a new app from a template (react, preact, next, next-mui, vanilla)
38
+ init Make an existing project a Hawk App
39
+ sso Sign in with your Bridgeline account (once per machine; CI: login with a publisher key)
40
+ doctor Check everything: Node, login, hawk-app.json, project, scopes, template, MCP
41
+ docs [topic] The developer docs: guides, the bridge API, every command (Bridgeline sign-in)
42
+
43
+ Develop
44
+ dev Run the app locally inside a stand-in for HawkSearch, with a client's real data
45
+ validate Check hawk-app.json, queries/, functions/ and the project (--build: the build too)
46
+ upgrade Bring the app up to the template this CLI ships
47
+
48
+ Release
49
+ deploy Build and upload a new version, with hawk-app.json (--alias staging: off the main line;
50
+ --dry-run: only show what hawk-app.json would change)
51
+ status Released or not, the newest version, the approval request, the aliases
52
+ versions Every version with its status (draft, waiting, approved, rejected) and aliases
53
+ open The app in HawkSearch, to review it and submit it for approval (--alias: as its clients)
54
+ alias set|delete Point an alias at a version that isn't approved, or delete one of yours
55
+ why <client id> What a client's users see of the app, and why
56
+
57
+ Clients
58
+ clients Who has the app: add, exclude, remove, search (also in the Hawk Analytics portal)
59
+ config The app's config values, global or per client: list, set, unset (no deploy)
60
+
61
+ Data and functions
62
+ data The tables app queries can use, and running a query
63
+ secrets, logs Function secrets and logs
64
+
65
+ Monitor
66
+ errors, usage The app's page errors, and its opens, users and clients per day
67
+
68
+ Account
69
+ login, whoami, logout A publisher key (for CI), and which key and platform apply here
70
+ templates The templates create can use
71
+ mcp The MCP server for AI coding assistants
72
+
73
+ hawk-app help <command> shows a command's details; hawk-app help all, everything.
74
+ Options: --env dev|prod (default dev), --manifest <file>, --platform <url>.
75
+ Docs: https://developers.hawksearchapps.com (hawk-app docs).
76
+ `;
77
+
78
+ /** Every command's details (hawk-app help <command>, hawk-app help all). */
79
+ const DETAILS = `hawk-app: build, run and release Hawk Apps
80
+
81
+ Usage:
82
+ hawk-app create <folder> [--template react|preact|next|next-mui|vanilla] [--name "<name>"] [--install]
83
+ [--channel latest|dev]
84
+ Start a new app from a template (default: react).
85
+ Refuses a slug that's already an app (--yes: use it anyway).
86
+ --channel: the bridge and CLI builds it follows; default this
87
+ CLI's own (npx @bridgeline-digital/hawk-app-cli@dev create
88
+ makes a dev app, on the dev tag)
89
+ hawk-app login [--local] Save your publisher key once for this machine (input hidden;
90
+ --local: this project's .env.local only)
91
+ hawk-app sso [--publisher <id>] Sign in with your Bridgeline account in the browser instead: the
92
+ platform issues your own key (it expires; sign in again then)
93
+ and saves it like login. Bridgeline AD accounts only.
94
+ --publisher: the team whose apps you work on (default: services)
95
+ hawk-app whoami Show whether you're logged in, and which key/platform apply here
96
+ hawk-app logout Remove this machine's saved key
97
+ hawk-app init [--slug <slug>] [--name "<name>"] [--yes]
98
+ Make an existing project (no template) a Hawk App: writes
99
+ hawk-app.json and AGENTS.md (refuses a slug that's already an
100
+ app unless --yes)
101
+ hawk-app templates List the templates create can use
102
+ hawk-app validate [--build [<build-folder>]] Check hawk-app.json, queries/ and functions/ (if any) and the
103
+ project (bridge, build script) locally. --build also builds
104
+ the app as deploy does and checks its URLs use the base path
105
+ hawk-app upgrade [--yes] [--template <name>] [--no-install] [--channel latest|dev]
106
+ Bring the app up to the template this CLI ships: the
107
+ template's package versions (never lowered), config files
108
+ you haven't edited, and the CLI's part of AGENTS.md (also for
109
+ apps without a template). Shows the changes; --yes applies them.
110
+ --channel first moves the bridge and CLI to the dev tag (dev) or
111
+ to the released versions (latest: ^ npm's latest)
112
+ hawk-app deploy [<build-folder>] [--no-build] [--no-audit] [--alias <name>] [--dry-run]
113
+ Build the app and upload it as a new version (v1, v2, …),
114
+ with hawk-app.json. The folder comes from the template (dist,
115
+ out, public) unless given. Runs \`npm run build\` with
116
+ HAWK_APP_BASE_PATH=/<slug>/v<n> when package.json has a build
117
+ script; --no-build uploads the folder as it is.
118
+ queries/ and functions/ next to hawk-app.json are checked and
119
+ deployed with it (functions are bundled with esbuild).
120
+ Reports \`npm audit\` of the production dependencies to
121
+ reviewers (never blocks; --no-audit skips it).
122
+ Asks first when a teammate made the app's last change
123
+ (--yes skips the question; CI only warns). --alias points
124
+ an alias (e.g. staging) at the new version: only Bridgeline
125
+ staff in its clients see it until it's approved.
126
+ --dry-run shows what hawk-app.json would change, and deploys
127
+ nothing (use it on pull requests)
128
+ hawk-app register Send hawk-app.json without a new version (scripts; deploy
129
+ already sends it). Changes wait for review in HawkSearch
130
+ hawk-app status Show what is released, the newest version, the approval request,
131
+ the aliases and the automated checks approvers will see
132
+ hawk-app open [--alias <name>] Open the app's page in HawkSearch (dev or prod): review it and
133
+ submit it for approval there; --alias shows it as that alias's
134
+ clients see it
135
+ hawk-app docs [<topic>] Open the developer docs (developers.hawksearchapps.com, Bridgeline
136
+ sign-in), or a topic's page: start, concepts, versions, bridge,
137
+ api, cli, commands, manifest, platform, data, functions, email,
138
+ storage, ai, search, review, whats-new
139
+ hawk-app doctor Check everything a developer needs: Node, login and platform,
140
+ hawk-app.json, the project, scopes the code uses, the template,
141
+ the app on the platform and the MCP config
142
+ hawk-app versions Every version with its status (draft, waiting, approved,
143
+ rejected) and the aliases on it
144
+ hawk-app alias set <name> <version> Point an alias at a version that isn't approved (Bridgeline
145
+ staff only); approvers do the rest. alias delete <name>
146
+ deletes one of yours (no clients on it)
147
+ hawk-app why <client id> What a client's users see of the app, and why
148
+ hawk-app mcp Run the MCP server for AI coding assistants (stdio; dev platform
149
+ only). create and init add it to .mcp.json and .vscode/mcp.json
150
+ hawk-app dev [--client <id>] [--engine <id>] [--url <app dev URL>] [--port 3200] [--client-user] [--no-start]
151
+ Run the app locally in a stand-in for HawkSearch, with real
152
+ tokens and data for any client (runs \`npm run dev\` unless
153
+ --url/--no-start; HAWKSEARCH_API_KEY adds the dashboard API).
154
+ Without --client it asks which client and remembers it in
155
+ .env.local (HAWK_APPS_DEV_CLIENT). Queries and functions run
156
+ from the deployed version: deploy after changing them
157
+
158
+ Clients (who has the app; also managed in the Hawk Analytics portal, not in hawk-app.json):
159
+ hawk-app clients Show the app's visibility and its clients
160
+ hawk-app clients search <name or id> Find a client's id by its name (e.g. "outdoor")
161
+ hawk-app clients add <id> [<id> …] [--name "<name>"] [--alias <name>]
162
+ Give clients the app (visibility allowList). --alias: the
163
+ version they get (an alias; default: the default one). On an
164
+ alias whose version isn't approved, only Bridgeline staff in
165
+ them see the app
166
+ hawk-app clients add --pick "<name or id>" The same, picking the client from the matches by name
167
+ hawk-app clients exclude <id> [--name "<name>"] Opt a client out (visibility all)
168
+ hawk-app clients remove <id> Remove a client from the list
169
+ Changes apply at once, without a deploy or approval, and are recorded in the app's history.
170
+
171
+ App config (values for the keys declared in "config" in hawk-app.json; also in the Hawk Analytics portal):
172
+ hawk-app config list [--client <id>] Each key: its type, default, global value and client values
173
+ (--client: only that client's), marking invalid and undeclared ones
174
+ hawk-app config set <key> <value> [--client <id>] Set the global value, or one client's. The value is read as the
175
+ key's type: emails comma-separated, json as JSON
176
+ hawk-app config unset <key> [--client <id>] Clear the global value, or one client's
177
+ Values apply within about 30 seconds, without a deploy or approval. They are not secret: they reach the browser
178
+ (app.config.get / ctx.config); keep keys in secrets.
179
+
180
+ Monitor:
181
+ hawk-app errors [--days 7] The app's errors in its pages (uncaught errors, rejections and
182
+ app.reportError), grouped, the most frequent first (up to 30 days)
183
+ hawk-app usage [--days 30] Opens of the app and its views, users and clients per day (up to
184
+ 400 days; Bridgeline staff counted apart)
185
+
186
+ Custom data (app queries, scope data:read):
187
+ hawk-app data tables [--app <slug>] List the tables queries can use (--app: is the app
188
+ opted in to the sensitive tables?)
189
+ hawk-app data describe <table> Show a table's columns and types
190
+ hawk-app data run <queries/name.sql> --client <id> [--param name=value …]
191
+ Run a query against a client's data (dev platform only);
192
+ string[] values are comma-separated
193
+
194
+ Functions (server-side code, scope functions:invoke):
195
+ hawk-app secrets list List the app's secrets (names only, never values)
196
+ hawk-app secrets set <NAME> [--value-from-env <VAR>]
197
+ Set a secret: type the value (hidden), or read it from an
198
+ environment variable (CI). Applies to every version at once
199
+ hawk-app secrets delete <NAME> Delete a secret
200
+ hawk-app logs <function> [--since 1h] [--follow] Show a function's logs (--since 15m|1h|1d|<ISO time>;
201
+ --follow keeps polling every 2 s)
202
+ Background jobs: "job": true on a function (up to 900 s), started with app.functions.start. "schedules" in
203
+ hawk-app.json run job functions on an EventBridge cron (at most hourly) for clients where a user turns them on.
204
+
205
+ Email (scope email:send, no commands): app.email.send / ctx.platform.email.send from the bridge.
206
+ HawkSearch system parameters (scope hawksearch:sysparams:write + "systemParameters" in hawk-app.json, no commands):
207
+ app.hawksearch.systemParameters.list/get/set from the bridge, for the user's current engine.
208
+ HawkSearch features (scope hawksearch:features:write, no commands): app.hawksearch.features.list/set/setFacet from the
209
+ bridge, HawkSearch's Feature Configuration for the user's current engine, one feature code at a time.
210
+ HawkSearch index (scope hawksearch:index, no commands): app.hawksearch.index.rebuild/status/wait from the bridge, a full
211
+ rebuild of the user's engine's index from its feed, where the engine has EnableRebuildIndex on; one at a time; and
212
+ the Indexing API's reads (indexes, current). Item writes: scope hawksearch:index:items (app.hawksearch.index.items).
213
+ Creating, switching and deleting indexes, hierarchy and mappings: scope hawksearch:index:manage.
214
+ HawkSearch recommendations (scope hawksearch:recommendations, no commands): app.hawksearch.recommendations from the
215
+ bridge, the engine's recommendation widgets, never tracked in the client's recommendation reports.
216
+ Storage (scope storage, no commands): app.storage / ctx.platform.storage get, put, delete, list, batch and usage from
217
+ the bridge, the app's own key-value data per client (JSON values up to 350 KB, optional expiry, version checks), and
218
+ storage.files upload, url, download, list and delete for its files (up to 100 MB each).
219
+ AI (scope ai:invoke, no commands): app.ai / ctx.platform.ai generate, stream and info from the bridge.
220
+ External Lambdas (scope lambdas:invoke + "lambdas" in hawk-app.json, no commands): app.lambdas.invoke /
221
+ ctx.platform.lambdas.invoke from the bridge, for Lambdas Bridgeline Engineering allows on the platform.
222
+
223
+ Options:
224
+ --manifest <file> Manifest to use (default: hawk-app.json)
225
+ --env <dev|prod> The environment to use (default: dev). hawk-app sso --env prod signs you in to prod.
226
+ --platform <url> A platform URL instead of --env (e.g. a local platform)
227
+
228
+ Which environment (first match wins): --env / --platform, then HAWK_APPS_ENV / HAWK_APPS_PLATFORM_URL in the
229
+ environment (CI) or the project's .env.local / .env, else dev. Logging in never changes it, so prod is always chosen
230
+ on purpose.
231
+ Which key (first match wins):
232
+ 1. Environment variable HAWK_APPS_PUBLISHER_KEY (CI secrets)
233
+ 2. The project's .env.local / .env (git-ignored)
234
+ 3. hawk-app login (saved once per machine in your user profile, per environment)
235
+
236
+ Typical pipeline:
237
+ hawk-app deploy --dry-run (pull requests: what hawk-app.json would change)
238
+ hawk-app deploy (main branch: a new version, with hawk-app.json)
239
+ hawk-app deploy --alias preview-<branch> (other branches: off the main line, Bridgeline staff only)
240
+
241
+ Review: every deploy is a new version, a draft that only Bridgeline staff see. Bridgeline staff submit a version for
242
+ approval in HawkSearch (hawk-app open), and members of the Approvers group approve or reject it (unless the app is set
243
+ to auto-approve); a newer main-line deploy clears a waiting request. Once a version is approved and released, clients
244
+ get it (their alias's version; default without one). Client changes (hawk-app clients or the Hawk Analytics portal) apply straight away.
245
+ `;
246
+
247
+ /**
248
+ * The details of one command: its lines in DETAILS (the usage line and its continuation lines), plus the section of
249
+ * its group when it has one (e.g. Clients). Null for an unknown command.
250
+ */
251
+ function commandHelp(name) {
252
+ const lines = DETAILS.split('\n');
253
+ const picked = [];
254
+ for (let i = 0; i < lines.length; i++) {
255
+ if (!new RegExp(`^ hawk-app ${name}(\\s|$)`).test(lines[i])) continue;
256
+ picked.push(lines[i]);
257
+ for (let j = i + 1; j < lines.length && /^ {4,}\S/.test(lines[j]); j++) picked.push(lines[j]);
258
+ }
259
+ return picked.length ? picked.join('\n') : null;
260
+ }
261
+
262
+ // clientNames: false prints client ids without their names (the MCP server, whose output goes to an AI assistant).
263
+ /** A dev-channel app (bridge or CLI on dev builds) set to use the prod platform: its dev features may not be there yet. */
264
+ export function devAppOnProd(projectDir, url) {
265
+ if (url !== PLATFORMS.prod) return null;
266
+ const pkgFile = join(projectDir, 'package.json');
267
+ const pkg = existsSync(pkgFile) ? JSON.parse(readFileSync(pkgFile, 'utf8')) : null;
268
+ return appChannel(pkg) === 'dev'
269
+ ? 'This app is on the dev channel (dev builds of the bridge or CLI) but set to use the prod platform: their features may not be on prod yet. Move it to the released versions first: npx hawk-app upgrade --channel latest --yes.'
270
+ : null;
271
+ }
272
+
273
+ export async function main(argv, { fetchImpl = fetch, log = console.log, runBuild, runAudit, runInstall, sleep, prompt, ask, confirm, choose, clientNames = true, env, signal, openUrl } = {}) {
274
+ const { command, positional, flags } = parse(argv);
275
+ if (['deploy', 'register', 'status', 'versions', 'alias', 'why', 'open', 'doctor', 'clients', 'data', 'secrets', 'config', 'logs', 'dev'].includes(command)) {
276
+ for (const warning of loadEnv().warnings) log(`! ${warning}`);
277
+ }
278
+ const manifestFile = flags.manifest ?? MANIFEST_FILE;
279
+ const client = () => {
280
+ const { url, key } = resolveSettings({ flagPlatform: flags.platform, flagEnv: flags.env });
281
+ return platformClient({ url, key, fetchImpl });
282
+ };
283
+ // For the slug checks of create / init, which also work without a key (they just can't check then).
284
+ const clientIfLoggedIn = () => {
285
+ const { url, key } = resolveSettings({ flagPlatform: flags.platform, flagEnv: flags.env });
286
+ return url && key ? platformClient({ url, key, fetchImpl }) : null;
287
+ };
288
+ // Deploys and registers say when they go to prod (only ever on purpose: see resolveSettings).
289
+ const notePlatform = () => {
290
+ const { url, urlSource } = resolveSettings({ flagPlatform: flags.platform, flagEnv: flags.env });
291
+ if (url === PLATFORMS.prod) log(`! Platform: prod (${url}, from ${urlSource})`);
292
+ };
293
+ const guard = { yes: !!flags.yes, log, ...(confirm ? { confirm } : {}) };
294
+
295
+ switch (command) {
296
+ case undefined:
297
+ case 'help':
298
+ case '--help':
299
+ case '-h': {
300
+ const topic = command === 'help' ? positional[0] : undefined;
301
+ if (!topic) {
302
+ log(HELP);
303
+ return 0;
304
+ }
305
+ if (topic === 'all') {
306
+ log(DETAILS);
307
+ return 0;
308
+ }
309
+ const details = commandHelp(topic);
310
+ if (!details) throw new Error(`No command "${topic}". \`hawk-app help\` lists them.`);
311
+ log(details);
312
+ return 0;
313
+ }
314
+
315
+ case 'open':
316
+ return openCommand({ manifestFile, flags, ...(openUrl ? { openUrl } : {}), log });
317
+
318
+ case 'doctor':
319
+ return doctorCommand({ manifestFile, flags, fetchImpl, log });
320
+
321
+ case 'docs':
322
+ return docsCommand({ topic: positional[0], ...(openUrl ? { openUrl } : {}), log });
323
+
324
+ case '--version':
325
+ case '-v':
326
+ log(JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version);
327
+ return 0;
328
+
329
+ case 'create': {
330
+ await createProject({
331
+ target: positional[0],
332
+ template: flags.template ?? 'react',
333
+ name: flags.name,
334
+ install: !!flags.install,
335
+ channel: flags.channel,
336
+ checkSlug: (slug) => checkNewSlug({ client: clientIfLoggedIn(), slug, ...guard }),
337
+ log,
338
+ });
339
+ return 0;
340
+ }
341
+
342
+ case 'login':
343
+ await login({ platform: flags.platform, env: flags.env, local: !!flags.local, fetchImpl, log });
344
+ return 0;
345
+
346
+ case 'sso':
347
+ await sso({ platform: flags.platform, env: flags.env, publisher: flags.publisher, fetchImpl, log, ...(openUrl ? { openUrl } : {}) });
348
+ return 0;
349
+
350
+ case 'whoami':
351
+ return whoami({ platform: flags.platform, env: flags.env, fetchImpl, log });
352
+
353
+ case 'logout':
354
+ return logout({ platform: flags.platform, env: flags.env, log });
355
+
356
+ case 'mcp': {
357
+ // Loaded only here: the MCP SDK isn't needed by any other command.
358
+ const { serveMcp } = await import('./mcp.mjs');
359
+ return serveMcp({ version: JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version });
360
+ }
361
+
362
+ case 'templates':
363
+ for (const [key, spec] of Object.entries(TEMPLATES)) log(` ${key.padEnd(8)} ${spec.description}`);
364
+ return 0;
365
+
366
+ case 'init': {
367
+ if (existsSync(manifestFile)) throw new Error(`${manifestFile} already exists.`);
368
+ const slug = flags.slug ?? toSlug(packageName() ?? basename(process.cwd()));
369
+ await checkNewSlug({ client: clientIfLoggedIn(), slug, ...guard });
370
+ const manifest = newManifest({ slug, name: flags.name ?? titleCase(slug) });
371
+ await writeFile(manifestFile, JSON.stringify(manifest, null, 2) + '\n');
372
+ log(`✔ Created ${manifestFile} for "${slug}".`);
373
+ const iconFile = join(dirname(resolve(manifestFile)), manifest.icon);
374
+ if (!existsSync(iconFile)) {
375
+ await writeFile(iconFile, DEFAULT_ICON_SVG);
376
+ log(`✔ Added a default ${manifest.icon} (replace it with your app's icon: an .svg in one color, or a square .png).`);
377
+ }
378
+ await addAgentFiles(dirname(resolve(manifestFile)), { slug, name: manifest.name }, log);
379
+ for (const warning of (await checkProject(dirname(resolve(manifestFile)))).warnings) log(`! ${warning}`);
380
+ log(' Next: `npx hawk-app dev` (run it locally inside a stand-in for HawkSearch) and `npx hawk-app deploy` (a new version).');
381
+ log(' Versions are drafts that only Bridgeline staff see until an approver releases one (`npx hawk-app open`).');
382
+ log(' (First time on this machine? Run `npx hawk-app sso` once. `npx hawk-app doctor` checks the setup.)');
383
+ return 0;
384
+ }
385
+
386
+ case 'validate': {
387
+ const manifest = await loadManifest(manifestFile);
388
+ const result = validateManifest(manifest);
389
+ const queries = await checkQueries(manifest, manifestFile);
390
+ const functions = await checkFunctions(manifest, manifestFile);
391
+ const projectDir = dirname(resolve(manifestFile));
392
+ const project = await checkProject(projectDir);
393
+ const { url } = resolveSettings({ flagPlatform: flags.platform, flagEnv: flags.env });
394
+ const channelWarning = devAppOnProd(projectDir, url);
395
+ if (channelWarning) project.warnings.push(channelWarning);
396
+ // Bridge calls whose scope hawk-app.json lacks (they'd fail with 403 scope_missing).
397
+ const scopeWarning = describeMissingScopes(await missingScopes(projectDir, manifest.scopes));
398
+ if (scopeWarning) project.warnings.push(scopeWarning);
399
+ const icon = result.errors.length ? { errors: [], warnings: [] } : await checkIcon(projectDir, manifest.icon);
400
+ project.warnings.push(...icon.warnings);
401
+ const screenshots = result.errors.length ? { errors: [] } : await checkScreenshots(projectDir, manifest.store);
402
+ const { errors: buildErrors, warnings: buildWarnings } = flags.build
403
+ ? await checkBuild({ projectDir, manifest, folder: positional[0], runBuild, log })
404
+ : { errors: [], warnings: [] };
405
+ const ok = `✔ ${manifestFile} looks good.${queries ? ` ${QUERIES_DIR}/: ${describeQueryCount(queries.queries.length)}.` : ''}${functions ? ` ${FUNCTIONS_DIR}/: ${describeFunctions(functions.functions.length)}.` : ''}${flags.build ? ' The build\'s URLs are under its base path.' : ''}`;
406
+ return report(
407
+ { errors: [...result.errors, ...icon.errors, ...screenshots.errors, ...(queries?.errors ?? []), ...(functions?.errors ?? []), ...buildErrors], warnings: [...result.warnings, ...project.warnings, ...buildWarnings] },
408
+ ok,
409
+ log,
410
+ );
411
+ }
412
+
413
+ case 'deploy': {
414
+ notePlatform();
415
+ const manifest = await loadManifest(manifestFile);
416
+ const projectDir = dirname(resolve(manifestFile));
417
+ // A nudge when the app's dependencies are behind its template (never stops a deploy).
418
+ try {
419
+ const hint = upgradeHint(dirname(resolve(manifestFile)), manifest);
420
+ if (hint) log(hint);
421
+ } catch {
422
+ // An app the CLI can't match to a template: no hint.
423
+ }
424
+ const checked = validateManifest(manifest);
425
+ failOnErrors(checked, log);
426
+ for (const warning of checked.warnings) log(`! ${warning}`);
427
+ const icon = await checkIcon(projectDir, manifest.icon);
428
+ for (const warning of icon.warnings) log(`! ${warning}`);
429
+ if (icon.errors.length) {
430
+ for (const error of icon.errors) log(`✖ ${error}`);
431
+ throw new Error('Fix the icon first.');
432
+ }
433
+ const screenshots = await checkScreenshots(projectDir, manifest.store);
434
+ if (screenshots.errors.length) {
435
+ for (const error of screenshots.errors) log(`✖ ${error}`);
436
+ throw new Error('Fix the store screenshots first.');
437
+ }
438
+ const scopeWarning = describeMissingScopes(await missingScopes(dirname(resolve(manifestFile)), manifest.scopes));
439
+ if (scopeWarning) log(`! ${scopeWarning}`);
440
+ const queries = await checkQueries(manifest, manifestFile);
441
+ if (queries?.errors.length) {
442
+ for (const error of queries.errors) log(`✖ ${error}`);
443
+ throw new Error(`Fix ${QUERIES_DIR}/ first.`);
444
+ }
445
+ const functions = await checkFunctions(manifest, manifestFile);
446
+ if (functions?.errors.length) {
447
+ for (const error of functions.errors) log(`✖ ${error}`);
448
+ throw new Error(`Fix the functions first.`);
449
+ }
450
+ const alias = flags.alias === undefined ? undefined : checkAlias(flags.alias);
451
+ // hawk-app.json as the platform takes it ($schema and template are for local tools).
452
+ const { $schema: _schema, template: _template, ...settings } = manifest;
453
+ if (flags['dry-run']) {
454
+ // Pull requests: what hawk-app.json would change, without a version.
455
+ printRegister(await client().register(manifest.slug, settings, true), log);
456
+ return 0;
457
+ }
458
+ const target = buildTarget(projectDir, manifest, positional[0]);
459
+ if (!target) throw new Error('Which folder does the build go to? hawk-app deploy <build-folder> (e.g. dist, out, build)');
460
+ await deploy({
461
+ client: client(),
462
+ alias,
463
+ manifest: settings,
464
+ registerSettings: async () => printRegisterBrief(await client().register(manifest.slug, settings, false), log),
465
+ beforeReserve: () => confirmTakeover({ client: client(), slug: manifest.slug, action: 'deploy', ...guard }),
466
+ confirmAudit: (report) => confirmVulnerabilities(report, guard),
467
+ slug: manifest.slug,
468
+ iconFile: icon.file,
469
+ storeFiles: screenshots.files,
470
+ dir: positional[0] ?? resolve(projectDir, target.folder),
471
+ build: target.build && !flags['no-build'],
472
+ serverFiles: queries?.files ?? [],
473
+ functions: functions?.functions ?? [],
474
+ projectDir,
475
+ audit: !flags['no-audit'],
476
+ ...(runBuild ? { runBuild } : {}),
477
+ ...(runAudit ? { runAudit } : {}),
478
+ fetchImpl,
479
+ log,
480
+ });
481
+ return 0;
482
+ }
483
+
484
+ case 'register': {
485
+ notePlatform();
486
+ const manifest = await loadManifest(manifestFile);
487
+ failOnErrors(validateManifest(manifest), log);
488
+ // $schema and template are for local tools (the editor, hawk-app upgrade), not the platform.
489
+ const { $schema, template, ...body } = manifest;
490
+ if (!flags['dry-run']) await confirmTakeover({ client: client(), slug: manifest.slug, action: 'register', ...guard });
491
+ const result = await client().register(manifest.slug, body, !!flags['dry-run']);
492
+ printRegister(result, log);
493
+ return 0;
494
+ }
495
+
496
+ case 'upgrade': {
497
+ await upgrade({ manifestFile, template: flags.template, channel: flags.channel, yes: !!flags.yes, install: !flags['no-install'], ...(runInstall ? { runInstall } : {}), log });
498
+ return 0;
499
+ }
500
+
501
+ case 'status': {
502
+ const manifest = await loadManifest(manifestFile);
503
+ const app = await client().status(manifest.slug).catch((error) => {
504
+ if (error.status === 404) return null;
505
+ throw error;
506
+ });
507
+ if (!app) {
508
+ log(`${manifest.slug} isn't on the platform yet: \`npm run app:dev\` or \`npm run app:deploy\` adds it.`);
509
+ return 0;
510
+ }
511
+ log(`${app.slug}: ${app.name}`);
512
+ if (app.adminLocked) log(' LOCKED by an administrator: nobody sees it');
513
+ else if (app.status === 'Disabled') log(' disabled nobody sees it');
514
+ // Released: the default alias, what every client without another alias gets.
515
+ log(` released ${app.status === 'Live' && app.liveVersion ? `v${app.liveVersion} (default: every client without another alias)` : 'not yet (only Bridgeline staff see it; an approver releases it)'}`);
516
+ const newest = app.pendingVersion ?? (app.status !== 'Live' ? app.currentVersion : undefined);
517
+ if (newest) log(` newest v${newest} ${describeNewest(app)}`);
518
+ if (app.pending?.length) log(` settings waiting for review: ${app.pending.join(', ')}`);
519
+ const review = describeReview(app);
520
+ if (review) log(` request ${review}`);
521
+ const staged = (app.aliases ?? []).filter((a) => a.name !== DEFAULT_ALIAS);
522
+ if (staged.length) log(` aliases ${(app.aliases ?? []).map(describeAlias).join(' · ')}`);
523
+ log(' (hawk-app versions lists every version with its status; hawk-app why <client> explains what a client sees.)');
524
+ if (app.checks?.length) {
525
+ log(' checks what approvers will see (a failed one needs a force approve):');
526
+ for (const c of app.checks) log(` ${CHECK_MARKS[c.level] ?? '·'} ${c.title}: ${c.detail}`);
527
+ }
528
+ if (typeof app.autoApprove === 'boolean') log(` auto-approve ${app.autoApprove ? 'on (new versions and changes are released without review)' : 'off'}`);
529
+ if (app.reviewEmails === false) log(' review emails off (approval requests, approvals and rejections aren\'t emailed; approvers change it in the Hawk Analytics portal)');
530
+ if (app.viewCacheMinutes === 0) log(' view cache off (Engineering stopped it in the Hawk Analytics portal; the views\' cacheMinutes are ignored)');
531
+ else if (Number.isInteger(app.viewCacheMinutes)) log(` view cache ${app.viewCacheMinutes} min for views that declare cacheMinutes (set by Engineering in the Hawk Analytics portal)`);
532
+ log(` visibility ${app.visibility}`);
533
+ if (app.listed === false) log(' listed no (not on the Apps landing page; opens by its direct URL)');
534
+ if (app.store) log(` store ${describeStore(app.store)}`);
535
+ if (app.hosting) log(` hosting ${app.hosting}`);
536
+ log(` entry URL ${app.entryUrl}`);
537
+ if (app.publishedAt) log(` published ${app.publishedAt.slice(0, 10)} by ${app.publishedByName ?? app.publishedBy ?? 'unknown'}`);
538
+ log(` scopes ${app.scopes.join(' ') || '(none)'}`);
539
+ log(` origins ${describeOrigins(app.externalOrigins)}`);
540
+ if (app.systemParameters?.length) log(` sysparams ${app.systemParameters.join(' ')}`);
541
+ if (app.lambdas?.length) log(` lambdas ${app.lambdas.join(' ')}`);
542
+ const configKeys = Object.keys(app.config ?? {});
543
+ if (configKeys.length) log(` config ${configKeys.join(' ')}`);
544
+ const schedules = Object.entries(app.schedules ?? {});
545
+ if (schedules.length) {
546
+ log(` schedules ${schedules.length} (each runs only for clients where a user turns it on in the app)`);
547
+ for (const [name, s] of schedules) log(` ${name} ${s.function} "${s.cron}" ${s.timezone ?? 'UTC'}`);
548
+ }
549
+ log(` clients ${app.clients.length}`);
550
+ for (const c of app.clients) {
551
+ const alias = c.alias ? (app.aliases ?? []).find((a) => a.name === c.alias) : undefined;
552
+ log(` ${c.enabled ? '+' : '-'} ${c.id}${c.name && clientNames ? ` (${c.name})` : ''}${describeClientAlias({ alias: c.alias, aliasStatus: alias?.status })}`);
553
+ }
554
+ return 0;
555
+ }
556
+
557
+ case 'dev': {
558
+ const manifest = await loadManifest(manifestFile);
559
+ const projectDir = dirname(resolve(manifestFile));
560
+ if (!flags.client && !(env ?? process.env).HAWK_APPS_DEV_CLIENT) {
561
+ // No client yet: ask which one (by name), and remember it in .env.local for next time.
562
+ const id = await pickDevClient({ client, slug: manifest.slug, ...(choose ? { choose } : {}), ...(ask ? { ask } : {}), log });
563
+ await rememberDevClient(projectDir, id);
564
+ log(`✔ Using client ${id} (saved as HAWK_APPS_DEV_CLIENT in .env.local; --client <id> uses another).`);
565
+ flags.client = id;
566
+ }
567
+ // A new app isn't on the platform yet: register it (a draft, so only Bridgeline staff see it) to run it.
568
+ const known = await client().status(manifest.slug).then(() => true, (error) => (error.status === 404 ? false : true));
569
+ if (!known) {
570
+ const { $schema: _schema, template: _template, ...settings } = manifest;
571
+ await client().register(manifest.slug, settings, false);
572
+ log(`✔ Registered ${manifest.slug} on the platform (not released: only Bridgeline staff see it).`);
573
+ }
574
+ return devCommand({ client: client(), manifest, flags, projectDir, ...(env ? { env } : {}), signal, log });
575
+ }
576
+
577
+ case 'data':
578
+ return dataCommand({ client, sub: positional[0], args: positional.slice(1), flags, ...(sleep ? { sleep } : {}), log });
579
+
580
+ case 'secrets': {
581
+ const manifest = await loadManifest(manifestFile);
582
+ return secretsCommand({ client, manifest, sub: positional[0], args: positional.slice(1), flags, ...(prompt ? { prompt } : {}), ...(env ? { env } : {}), log });
583
+ }
584
+
585
+ case 'config': {
586
+ const manifest = await loadManifest(manifestFile);
587
+ return configCommand({ client, manifest, sub: positional[0], args: positional.slice(1), flags, log });
588
+ }
589
+
590
+ case 'clients': {
591
+ // search reads the environment's client list: no app needed.
592
+ if (positional[0] === 'search') return clientsCommand({ client, sub: 'search', args: positional.slice(1), log });
593
+ const manifest = await loadManifest(manifestFile);
594
+ return clientsCommand({ client, manifest, sub: positional[0], args: positional.slice(1), flags, names: clientNames, ...(choose ? { choose } : {}), log });
595
+ }
596
+
597
+ case 'versions':
598
+ return versionsCommand({ client: client(), manifest: await loadManifest(manifestFile), log });
599
+
600
+ case 'alias':
601
+ return aliasCommand({ client: client(), manifest: await loadManifest(manifestFile), sub: positional[0], args: positional.slice(1), log });
602
+
603
+ case 'why':
604
+ return whyCommand({ client: client(), manifest: await loadManifest(manifestFile), clientId: positional[0], log });
605
+
606
+ case 'errors': {
607
+ return errorsCommand({ client: client(), manifest: await loadManifest(manifestFile), flags, log });
608
+ }
609
+
610
+ case 'usage': {
611
+ return usageCommand({ client: client(), manifest: await loadManifest(manifestFile), flags, log });
612
+ }
613
+
614
+ case 'logs': {
615
+ const manifest = await loadManifest(manifestFile);
616
+ const controller = new AbortController();
617
+ const stop = () => controller.abort();
618
+ signal?.addEventListener('abort', stop, { once: true });
619
+ if (flags.follow) process.once('SIGINT', stop);
620
+ try {
621
+ return await logsCommand({ client, manifest, name: positional[0], flags, ...(sleep ? { sleep } : {}), signal: controller.signal, log });
622
+ } finally {
623
+ process.off('SIGINT', stop);
624
+ signal?.removeEventListener('abort', stop);
625
+ }
626
+ }
627
+
628
+ default:
629
+ log(HELP);
630
+ throw new Error(`Unknown command "${command}".`);
631
+ }
632
+ }
633
+
634
+ /** What `hawk-app deploy` says about the hawk-app.json it sent first (the full summary is `hawk-app register`'s). */
635
+ function printRegisterBrief(result, log) {
636
+ log(`✔ hawk-app.json sent${result.pending?.length ? `: ${result.pending.join(', ')} waiting for review` : ''}`);
637
+ for (const warning of result.warnings ?? []) log(` ! ${warning}`);
638
+ if (result.submissionCleared) log(` ! ${submissionClearedMessage('settings')}`);
639
+ }
640
+
641
+ function printRegister(result, log) {
642
+ log(`${result.dryRun ? 'Dry run, nothing was changed:' : '✔ Sent hawk-app.json for'} ${result.slug}`);
643
+ log(` status ${result.status}${result.adminLocked ? ' (LOCKED by an administrator: hidden from everyone)' : ''}`);
644
+ log(` visibility ${result.visibility}`);
645
+ if (result.hosting) log(` hosting ${result.hosting}`);
646
+ log(` entry URL ${result.entryUrl}`);
647
+ log(` scopes ${result.scopes.join(' ') || '(none)'}`);
648
+ if (result.pending?.length) {
649
+ log(` settings waiting for review: ${result.pending.join(', ')}`);
650
+ log(' (approvers approve them in HawkSearch, with the next version)');
651
+ }
652
+ if (typeof result.autoApprove === 'boolean') log(` auto-approve ${result.autoApprove ? 'on (changes are released without review)' : 'off'}`);
653
+ if (result.externalOrigins) {
654
+ const o = result.externalOrigins;
655
+ log(` origins ${describeOrigins(o.current)}${o.restamped ? ` (applied to ${o.restamped} deployed page${o.restamped === 1 ? '' : 's'})` : ''}`);
656
+ for (const origin of o.added) log(` + ${origin} (added)`);
657
+ for (const origin of o.removed) log(` - ${origin} (removed)`);
658
+ }
659
+ log(' clients manage with `hawk-app clients` or in the Hawk Analytics portal');
660
+ for (const warning of result.warnings ?? []) log(` ! ${warning}`);
661
+ if (result.submissionCleared && !result.dryRun) log(` ! ${submissionClearedMessage('settings')}`);
662
+ }
663
+
664
+ const CHECK_MARKS = { pass: '✔', fail: '✖', info: 'i' };
665
+
666
+ /** The newest version's status, from the approval request: waiting, rejected, or a draft. */
667
+ function describeNewest({ reviewState }) {
668
+ if (reviewState === 'waiting_for_approval') return 'waiting for approval';
669
+ if (reviewState === 'rejected') return 'rejected (see request)';
670
+ return "draft: only Bridgeline staff see it until it's approved";
671
+ }
672
+
673
+ const day = (iso) => (typeof iso === 'string' ? iso.slice(0, 10) : 'unknown date');
674
+ const person = (user) => user?.name ?? user?.email ?? 'unknown';
675
+
676
+ /**
677
+ * The app's place in the approval workflow (null when the platform doesn't say):
678
+ * "waiting for approval (submitted by Jane Doe, 2026-10-08: <note>)", "rejected by Jane Doe on 2026-10-08: <reason>".
679
+ */
680
+ export function describeReview({ reviewState, submission, lastDecision }) {
681
+ switch (reviewState) {
682
+ case 'waiting_for_approval': {
683
+ if (!submission) return 'waiting for approval';
684
+ const note = submission.note ? `: ${submission.note}` : '';
685
+ return `waiting for approval (submitted by ${person(submission.submittedBy)}, ${day(submission.submittedAt)}${note})`;
686
+ }
687
+ case 'rejected': {
688
+ if (!lastDecision) return 'rejected';
689
+ return `rejected by ${person(lastDecision.by)} on ${day(lastDecision.at)}${lastDecision.reason ? `: ${lastDecision.reason}` : ''}`;
690
+ }
691
+ case 'pending':
692
+ return 'not submitted yet (Bridgeline staff submit it for approval in HawkSearch)';
693
+ case 'draft':
694
+ return 'draft (Bridgeline staff submit it for approval in HawkSearch)';
695
+ case 'live':
696
+ return 'nothing waiting for approval';
697
+ default:
698
+ return null;
699
+ }
700
+ }
701
+
702
+ /** What the app may load from and call, as the CDN's CSP will allow it. */
703
+ /** A store app's line in `status`: "in the store, $99/month; requests 2 open, 5 approved, 1 declined". */
704
+ function describeStore(store) {
705
+ const where = store.offered
706
+ ? 'in the store'
707
+ : store.listed === false
708
+ ? 'out of the store (a Hawk-App-Manager took it out)'
709
+ : 'not in the store yet (clients see it once it\'s released)';
710
+ const price = store.price ? `, ${store.price}` : '';
711
+ return `${where}${price}; requests ${store.openRequests} open, ${store.approvedRequests} approved, ${store.declinedRequests} declined`;
712
+ }
713
+
714
+ function describeOrigins(origins) {
715
+ if (!origins) return 'any https origin';
716
+ if (origins.length === 1 && origins[0] === '*') return 'any https origin (*)';
717
+ return origins.length ? origins.join(' ') : 'none (own files and the platform only)';
718
+ }
719
+
720
+ /**
721
+ * Checks queries/ next to the manifest (not in the build folder). Null when the app has none. Queries need the
722
+ * data:read scope.
723
+ */
724
+ async function checkQueries(manifest, manifestFile) {
725
+ const result = await loadQueries(dirname(resolve(manifestFile)));
726
+ if (!result) return null;
727
+ if (!(manifest.scopes ?? []).includes(DATA_SCOPE)) {
728
+ result.errors.unshift(`${QUERIES_DIR}/ needs the "${DATA_SCOPE}" scope: add it to "scopes" in hawk-app.json (or remove ${QUERIES_DIR}/).`);
729
+ }
730
+ return result;
731
+ }
732
+
733
+ /**
734
+ * Checks functions/ next to the manifest against its "functions" block. Null when the app has neither. Also checks
735
+ * that the app has a bridge with the /server entry, which the bundles are built with.
736
+ */
737
+ async function checkFunctions(manifest, manifestFile) {
738
+ const projectDir = dirname(resolve(manifestFile));
739
+ const result = await loadFunctions(projectDir, manifest);
740
+ if (!result) return null;
741
+ if (result.functions.length) {
742
+ const problem = checkBridge(projectDir);
743
+ if (problem) result.errors.push(problem);
744
+ }
745
+ return result;
746
+ }
747
+
748
+ /**
749
+ * `validate --build [<folder>]`: builds the app as deploy would (with a stand-in base path, /<slug>/v0) and checks
750
+ * that its HTML only loads URLs under it. The folder comes from the argument or the app:deploy script.
751
+ */
752
+ async function checkBuild({ projectDir, manifest, folder, runBuild = npmRunBuild, log }) {
753
+ const found = buildTarget(projectDir, manifest, folder);
754
+ if (!found) return { errors: ['--build needs the build folder: hawk-app validate --build <folder> (e.g. dist, out).'], warnings: [] };
755
+ const target = found.folder;
756
+ const basePath = `/${manifest.slug}/v0`;
757
+ if (found.build) {
758
+ log(`Building with HAWK_APP_BASE_PATH=${basePath}…`);
759
+ await runBuild({ basePath, cwd: projectDir });
760
+ }
761
+ const dir = resolve(projectDir, target);
762
+ if (!existsSync(dir)) return { errors: [`The build folder "${target}" doesn't exist.`], warnings: [] };
763
+ const outside = await checkBuildUrls(dir, basePath);
764
+ // The accessibility check approvers will see (critical and serious issues).
765
+ const warnings = accessibilityWarnings(await checkAccessibility(dir));
766
+ return { errors: outside.length ? [describeBuildUrls(outside, basePath)] : [], warnings };
767
+ }
768
+
769
+
770
+ function describeQueryCount(count) {
771
+ return `${count} quer${count === 1 ? 'y' : 'ies'}`;
772
+ }
773
+
774
+ function report({ errors, warnings }, ok, log) {
775
+ for (const warning of warnings) log(`! ${warning}`);
776
+ if (errors.length) {
777
+ for (const error of errors) log(`✖ ${error}`);
778
+ return 1;
779
+ }
780
+ log(ok);
781
+ return 0;
782
+ }
783
+
784
+ function failOnErrors(result, log) {
785
+ if (result.errors.length) {
786
+ report(result, '', log);
787
+ throw new Error('Fix hawk-app.json first.');
788
+ }
789
+ }
790
+
791
+ export function parse(argv) {
792
+ const flags = {};
793
+ const positional = [];
794
+ let command;
795
+ for (let i = 0; i < argv.length; i++) {
796
+ const arg = argv[i];
797
+ if (arg.startsWith('--') && arg.length > 2 && command !== undefined) {
798
+ const eq = arg.indexOf('=');
799
+ const name = eq === -1 ? arg.slice(2) : arg.slice(2, eq);
800
+ const inline = eq === -1 ? undefined : arg.slice(eq + 1);
801
+ if (name === 'param') {
802
+ // Repeatable, and the value keeps its own "=": --param from=2026-09-01 --param to=2026-09-30
803
+ flags.param = [...(flags.param ?? []), inline ?? argv[++i]];
804
+ continue;
805
+ }
806
+ const takesValue = ['manifest', 'platform', 'slug', 'name', 'template', 'client', 'app', 'since', 'value-from-env', 'engine', 'url', 'port', 'env', 'publisher', 'pick', 'channel', 'days', 'alias'].includes(name);
807
+ flags[name] = takesValue ? inline ?? argv[++i] : true;
808
+ } else if (command === undefined) {
809
+ command = arg;
810
+ } else {
811
+ positional.push(arg);
812
+ }
813
+ }
814
+ return { command, positional, flags };
815
+ }
816
+
817
+ function packageName() {
818
+ try {
819
+ return JSON.parse(readFileSync('package.json', 'utf8')).name?.replace(/^@[^/]+\//, '');
820
+ } catch {
821
+ return undefined;
822
+ }
823
+ }
824
+
825
+