@bridgeline-digital/hawk-app-cli 0.1.0-dev.20261020202308 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +7 -7
- package/README.md +770 -770
- package/agents/AGENTS.md +192 -192
- package/agents/project/custom.md +21 -21
- package/agents/project/next-mui.md +21 -21
- package/agents/project/next.md +24 -24
- package/agents/project/vanilla.md +22 -22
- package/agents/project/vite.md +19 -19
- package/bin/hawk-app.mjs +0 -0
- package/docs/bridge/README.md +1315 -1315
- package/docs/showcase/README.md +47 -47
- package/docs/showcase/functions/echo.ts +12 -12
- package/docs/showcase/functions/long-task.ts +31 -31
- package/docs/showcase/functions/showcase.ts +186 -186
- package/docs/showcase/hawk-app.json +73 -73
- package/docs/showcase/queries/assistant-prompts-by-day.json +6 -6
- package/docs/showcase/queries/assistant-prompts-by-day.sql +8 -8
- package/docs/showcase/queries/searches-by-day.json +6 -6
- package/docs/showcase/queries/searches-by-day.sql +7 -7
- package/docs/showcase/src/App.tsx +1436 -1436
- package/docs/showcase/src/main.tsx +10 -10
- package/docs/showcase/src/styles.css +71 -71
- package/docs/showcase/src/useHawkApp.ts +44 -44
- package/docs/showcase/src/vite-env.d.ts +1 -1
- package/docs/ui/README.md +166 -166
- package/package.json +1 -1
- package/schema/hawk-app.schema.json +611 -611
- package/src/accessibility.mjs +105 -105
- package/src/agents.mjs +145 -145
- package/src/aliases.mjs +30 -30
- package/src/app-icon.mjs +82 -82
- package/src/channel.mjs +116 -116
- package/src/cli.mjs +825 -825
- package/src/clients.mjs +221 -221
- package/src/config.mjs +148 -148
- package/src/create.mjs +186 -186
- package/src/credentials.mjs +56 -56
- package/src/data.mjs +169 -169
- package/src/deploy.mjs +349 -349
- package/src/dev.mjs +459 -459
- package/src/doctor.mjs +176 -176
- package/src/env.mjs +140 -140
- package/src/functions.mjs +313 -313
- package/src/login.mjs +166 -166
- package/src/logs.mjs +64 -64
- package/src/manifest.mjs +425 -425
- package/src/mcp-docs.mjs +168 -168
- package/src/mcp-hawksearch.mjs +342 -342
- package/src/mcp-reports.mjs +130 -130
- package/src/mcp-rules.mjs +109 -109
- package/src/mcp.mjs +539 -539
- package/src/platform.mjs +116 -116
- package/src/project-checks.mjs +153 -153
- package/src/queries.mjs +289 -289
- package/src/scope-check.mjs +91 -91
- package/src/secrets.mjs +86 -86
- package/src/slug-guard.mjs +80 -80
- package/src/sso.mjs +182 -182
- package/src/telemetry.mjs +50 -50
- package/src/upgrade.mjs +406 -406
- package/src/versions.mjs +102 -102
- package/templates/next/next.config.ts +13 -13
- package/templates/next/package.json +26 -26
- package/templates/next/src/lib/useHawkApp.ts +46 -46
- package/templates/next-mui/next.config.ts +13 -13
- package/templates/next-mui/package.json +30 -30
- package/templates/next-mui/src/app/layout.tsx +19 -19
- package/templates/next-mui/src/app/page.tsx +78 -78
- package/templates/next-mui/src/app/providers.tsx +10 -10
- package/templates/preact/package.json +26 -26
- package/templates/preact/src/useHawkApp.ts +44 -44
- package/templates/preact/src/vite-env.d.ts +1 -1
- package/templates/preact/tsconfig.json +17 -17
- package/templates/preact/vite.config.ts +11 -11
- package/templates/react/package.json +29 -29
- package/templates/react/src/useHawkApp.ts +44 -44
- package/templates/react/src/vite-env.d.ts +1 -1
- package/templates/react/tsconfig.json +16 -16
- package/templates/react/vite.config.ts +11 -11
- package/templates/vanilla/package.json +15 -15
package/src/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
|
+
|