@bridgeline-digital/hawk-app-cli 0.1.1 → 0.1.2-dev.20261027220454

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/README.md CHANGED
@@ -233,14 +233,14 @@ npx hawk-app deploy --dry-run # what hawk-app.json would change, without a v
233
233
  | `validate [--build [<folder>]]` | Checks `hawk-app.json` locally, `queries/` and `functions/` if the app has them, and the project: the bridge is used and current, the app says what its page shows ("Explain this page"), and there's a build script (or `--no-build` deploys). `--build` also builds the app with a stand-in base path (`/<slug>/v0`) and fails on absolute URLs outside it; the folder comes from the `app:deploy` script unless given. |
234
234
  | `deploy [<folder>] [--no-build] [--no-audit] [--alias <name>] [--dry-run]` | Sends hawk-app.json first (its scopes and settings go with every deploy), then reserves the next version number, runs `npm run build` with `HAWK_APP_BASE_PATH=/<slug>/v<n>` (when package.json has a `build` script, unless `--no-build`), then uploads every file in the build folder (the template's, `dist`, `out` or `public`, unless given) to `/<slug>/v<n>/` on the apps CDN, plus `queries/` and the bundled `functions` (if any) to the platform's private storage, and deploys the functions. Runs `npm audit` of the production dependencies first, asks before deploying with high or critical findings, and reports the counts to reviewers (see [Dependency check](#dependency-check); `--no-audit` skips it). Warns when the HTML loads absolute URLs outside `/<slug>/v<n>/`, which wouldn't load, and when the code calls a bridge feature whose scope `hawk-app.json` lacks (e.g. `app.ai.generate` without `ai:invoke`; `validate` and `doctor` check it too). Says whether the version is a draft (staff only until it's approved) or auto-approved, and when it cleared an approval request. `--alias staging` points the alias at the new version (see [Versions and aliases](#versions-and-aliases)). `--dry-run` only shows what hawk-app.json would change. |
235
235
  | `register` | Sends `hawk-app.json` without a new version (for scripts; `deploy` and `dev` already send it). It never releases anything and never changes the app's clients. On a released app, changes wait for review. |
236
- | `versions` | Every version with its status (draft, waiting, approved, rejected) and the aliases on it. |
236
+ | `versions` | Every version with its status (draft, waiting, approved, rejected), the aliases on it, and where it was deployed from: git commit, branch, uncommitted changes, pipeline build and who (Bitbucket Pipelines' variables win over git). |
237
237
  | `alias set <name> <version>` / `alias delete <name>` | Point an alias at a version that isn't approved (only Bridgeline staff in its clients see it), or delete one of yours with no clients on it. Approvers do the rest. |
238
238
  | `open [--alias <name>]` | Opens the app's page in HawkSearch (dev or prod, from `--env`) to review it and submit it for approval; `--alias` shows it as that alias's clients see it. Prints the link when there's no browser. |
239
239
  | `docs [<topic>]` | Opens the developer docs at developers.hawksearchapps.com, or a topic's page (`start`, `versions`, `bridge`, `api`, `cli`, `commands`, `functions`, …; `hawk-app help docs` lists them). Prints the link when there's no browser. |
240
240
  | `doctor` | Checks everything a developer needs: Node, the login and platform, `hawk-app.json`, the project (bridge, build script), the template, the scopes the code uses, the app on the platform and the MCP config. Each line says what to fix; exits 1 when something must be. |
241
241
  | `help [<command> \| all]` | The commands by task; one command's details; or every detail. |
242
242
  | `why <client id>` | What a client's users see of the app, and why (the client list, its alias, that version's status). |
243
- | `status` | Shows the automated review checks approvers will see (bundle, dependencies, origins, queries, …), the released version, the newest version and its status, settings waiting for review, the approval request (waiting for approval, or rejected and why), auto-approve, review emails (when an approver turned them off), the view cache (when Engineering shortened or stopped it), visibility, scopes, system parameters, config keys and clients. |
243
+ | `status` | Shows the automated review checks approvers will see (bundle, dependencies, origins, queries, …), the released version, the newest version and its status, where the last deploy came from (commit, branch, pipeline build), settings waiting for review, the approval request (waiting for approval, or rejected and why), auto-approve, review emails (when an approver turned them off), the view cache (when Engineering shortened or stopped it), visibility, scopes, system parameters, config keys and clients. |
244
244
  | `clients` / `clients add <id> [<id> …] [--name "<name>"] [--alias <name>]` / `clients exclude <id> [--name "<name>"]` / `clients remove <id>` | Which clients have the app, and which version (alias) each gets (see [Clients](#clients)). |
245
245
  | `data tables` / `data describe <table>` / `data run <file.sql> --client <id> [--param name=value …]` | The tables app queries can use, and running a query on dev (see [Custom data](#custom-data-queries)). |
246
246
  | `secrets list` / `secrets set <NAME> [--value-from-env VAR]` / `secrets delete <NAME>` | The app's function secrets (see [Functions](#functions)). |
package/agents/AGENTS.md CHANGED
@@ -27,7 +27,9 @@ client at a time. It gets its context and all of its data through the bridge,
27
27
  - **Ask for the fewest scopes.** Each one is in `hawk-app.json` and reviewed; elevated scopes are never auto-approved.
28
28
  - **List outside origins.** `externalOrigins` in `hawk-app.json` should list every https origin the app loads from or
29
29
  calls (`[]` when none). The default `["*"]` fails review. To show images from hosts you can't list (a client's
30
- product images), use `"img:*"`: images only, from any https origin, and it passes review.
30
+ product images), use `"img:*"`: images only, from any https origin, and it passes review. Add `"unsafe-eval"` only
31
+ when a library compiles code at run time (e.g. Rapid UI's Handlebars templates): the CSP blocks `eval`/`new Function`
32
+ otherwise, and review flags it.
31
33
  - **Handle a client without analytics.** When `app.context.tenant.analyticsProvisioned === false`, show a message
32
34
  instead of calling `app.analytics` or `app.data`.
33
35
  - Don't add a page heading: the host shows the app's name above the frame.
@@ -357,7 +357,7 @@ only, `LandingPage` has no list), with HawkSearch's models: a misspelled field o
357
357
  `get(name)` of such a resource is typed too, and `HawkSearchModel<'Synonym'>` (or `'RedirectModel'`, `'LandingPage'`,
358
358
  …) names a model in your own code. The resources, as HawkSearch spells them: `banner`, `bannerzone`, `boostbury`,
359
359
  `campaign`, `Facet`, `Field`, `FieldInfo`, `LandingPage`, `landingPageLayout`, `paginationset`,
360
- `recommendationvisibility`, `redirect`, `sortingset`, `spellcheck`, `Synonym`, `visibility`, `visitortarget` (paths
360
+ `recommendationvisibility`, `recommendationwidget`, `redirect`, `sortingset`, `spellcheck`, `Synonym`, `visibility`, `visitortarget` (paths
361
361
  ignore case). Other paths (`LandingPage/urls`, `reports/…`) stay on `get<T>(path)`. The types come from HawkSearch's dev
362
362
  API and are refreshed with each bridge release. To change data, see [Writing to HawkSearch](#writing-to-hawksearch-hawksearchwrite). For reporting, use
363
363
  analytics. Your assistant can look up any operation's fields, its bridge call and scope with the `hawk-apps` MCP
@@ -1744,7 +1744,8 @@ rejected, and why.
1744
1744
  (`fetch`, WebSockets) only the origins in `externalOrigins` (`"*"` allows any https origin, and fails the review
1745
1745
  check). List the origins it really uses: the apps CDN blocks everything else (a Content-Security-Policy), so a compromised npm dependency can't send
1746
1746
  data anywhere new. The platform API is always allowed. To show images from any site (for example a client's
1747
- product images) without opening anything else, use `"img:*"`. Dev mode on localhost has no such policy, so check a
1747
+ product images) without opening anything else, use `"img:*"`. A library that compiles code at run time (Rapid UI's
1748
+ Handlebars templates, `new Function`) needs `"unsafe-eval"`: no origin, and review flags it. Dev mode on localhost has no such policy, so check a
1748
1749
  deployed version in HawkSearch before it's released.
1749
1750
  - **Static files only.** Build to plain HTML, JS and CSS (`next build` with `output: 'export'`, `vite build`, …), with
1750
1751
  the base path read from `HAWK_APP_BASE_PATH` (`/<slug>/v<n>`, set by `hawk-app deploy`; the templates do this).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bridgeline-digital/hawk-app-cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.2-dev.20261027220454",
4
4
  "description": "For Bridgeline developers: create, deploy and register Hawk Apps (Services-built tools embedded in HawkSearch). Requires a Hawk Apps publisher key.",
5
5
  "author": "Bridgeline Digital",
6
6
  "license": "UNLICENSED",
@@ -289,7 +289,7 @@
289
289
  ]
290
290
  },
291
291
  "externalOrigins": {
292
- "description": "Outside origins the app may load scripts, styles, fonts and images from, embed, and call (fetch/XHR/WebSocket). [\"*\"] (the default when omitted) allows any https origin; a list allows only those; [] allows none. Entries starting with img: are for images only: \"img:*\" shows images from any https origin (e.g. a client's product images) and \"img:https://cdn.example.com\" from one, without allowing scripts, styles or calls there. Enforced by the apps CDN's Content-Security-Policy.",
292
+ "description": "Outside origins the app may load scripts, styles, fonts and images from, embed, and call (fetch/XHR/WebSocket). [\"*\"] (the default when omitted) allows any https origin; a list allows only those; [] allows none. Entries starting with img: are for images only: \"img:*\" shows images from any https origin (e.g. a client's product images) and \"img:https://cdn.example.com\" from one, without allowing scripts, styles or calls there. \"unsafe-eval\" is no origin: it lets the app's scripts compile code at run time, for libraries that need it (e.g. Rapid UI's Handlebars templates); review shows it. Enforced by the apps CDN's Content-Security-Policy.",
293
293
  "type": "array",
294
294
  "default": [
295
295
  "*"
@@ -302,6 +302,9 @@
302
302
  {
303
303
  "const": "*"
304
304
  },
305
+ {
306
+ "const": "unsafe-eval"
307
+ },
305
308
  {
306
309
  "pattern": "^(https|wss)://(\\*\\.)?([A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?\\.)*[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(:[0-9]{1,5})?/?$"
307
310
  },
package/src/cli.mjs CHANGED
@@ -10,6 +10,7 @@ import { upgrade, upgradeHint } from './upgrade.mjs';
10
10
  import { login, logout, whoami } from './login.mjs';
11
11
  import { sso } from './sso.mjs';
12
12
  import { checkNewSlug, confirmTakeover } from './slug-guard.mjs';
13
+ import { describeSource, readSource } from './source.mjs';
13
14
  import { MANIFEST_FILE, loadManifest, newManifest, validateManifest } from './manifest.mjs';
14
15
  import { DEFAULT_ICON_SVG, checkIcon } from './app-icon.mjs';
15
16
  import { checkScreenshots } from './store-listing.mjs';
@@ -526,6 +527,7 @@ export async function main(argv, { fetchImpl = fetch, log = console.log, runBuil
526
527
  client: client(),
527
528
  alias,
528
529
  manifest: settings,
530
+ source: readSource({ cwd: projectDir }),
529
531
  registerSettings: async () => printRegisterBrief(await client().register(manifest.slug, settings, false), log),
530
532
  beforeReserve: () => confirmTakeover({ client: client(), slug: manifest.slug, action: 'deploy', ...guard }),
531
533
  confirmAudit: (report) => confirmVulnerabilities(report, guard),
@@ -580,6 +582,8 @@ export async function main(argv, { fetchImpl = fetch, log = console.log, runBuil
580
582
  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)'}`);
581
583
  const newest = app.pendingVersion ?? (app.status !== 'Live' ? app.currentVersion : undefined);
582
584
  if (newest) log(` newest v${newest} ${describeNewest(app)}`);
585
+ const last = app.lastChange;
586
+ if (last?.kind === 'deploy' && last.source) log(` last deploy ${describeSource(last.source)} · ${last.name ?? 'unknown'}, ${last.at.slice(0, 10)}`);
583
587
  if (app.pending?.length) log(` settings waiting for review: ${app.pending.join(', ')}`);
584
588
  const review = describeReview(app);
585
589
  if (review) log(` request ${review}`);
package/src/deploy.mjs CHANGED
@@ -6,6 +6,7 @@ import { buildFunction, describeConfig, describeFunctions, zipSources } from './
6
6
  import { checkAccessibility, describeAccessibility } from './accessibility.mjs';
7
7
  import { checkBuildUrls, describeBuildUrls } from './project-checks.mjs';
8
8
  import { askYesNo } from './slug-guard.mjs';
9
+ import { describeSource } from './source.mjs';
9
10
  import { sourceMapIndex, splitSourceMaps } from './sourcemaps.mjs';
10
11
 
11
12
  const CONCURRENCY = 8;
@@ -43,8 +44,9 @@ export async function listFiles(dir) {
43
44
  /**
44
45
  * @param beforeReserve Runs after the local checks, just before a version number is taken (the slug check: slug-guard.mjs).
45
46
  * @param confirmAudit Called with the audit report when it has high or critical vulnerabilities; throws to stop the deploy.
47
+ * @param source Where it's deployed from (readSource in source.mjs), recorded on the version; null when unknown.
46
48
  */
47
- export async function deploy({ client, slug, dir, alias, manifest, registerSettings, iconFile = null, storeFiles = [], build = true, serverFiles = [], functions = [], projectDir = process.cwd(), audit = true, runBuild = npmRunBuild, runAudit = npmAudit, runAccessibility = checkAccessibility, beforeReserve, confirmAudit, fetchImpl = fetch, log = console.log }) {
49
+ export async function deploy({ client, slug, dir, alias, manifest, source = null, registerSettings, iconFile = null, storeFiles = [], build = true, serverFiles = [], functions = [], projectDir = process.cwd(), audit = true, runBuild = npmRunBuild, runAudit = npmAudit, runAccessibility = checkAccessibility, beforeReserve, confirmAudit, fetchImpl = fetch, log = console.log }) {
48
50
  // Without a build step the folder must already be there: check before reserving a version.
49
51
  if (!build) await checkFolder(dir, false);
50
52
 
@@ -152,10 +154,11 @@ export async function deploy({ client, slug, dir, alias, manifest, registerSetti
152
154
  built.map((fn) => fn.name),
153
155
  report,
154
156
  accessibility,
155
- alias ? { alias, manifest } : {},
157
+ { ...(alias ? { alias, manifest } : {}), ...(source ? { source } : {}) },
156
158
  );
157
159
  const n = completed.version ?? version;
158
160
  log(`✔ Deployed v${n} of ${slug} (${completed.files ?? files.length} files${queriesNote}).${describeState(completed)}`);
161
+ if (source) log(` from ${describeSource(source)}`);
159
162
  if (completed.entryUrl) log(` ${completed.entryUrl}`);
160
163
  if (built.length && completed.state === 'pending') log(' (Versions with functions always need review, even on auto-approve apps.)');
161
164
  if (completed.submissionCleared) log(` ! ${submissionClearedMessage('version')}`);
package/src/manifest.mjs CHANGED
@@ -369,7 +369,8 @@ export function validateManifest(manifest) {
369
369
  } else {
370
370
  const norm = (o) => o.trim().replace(/\/$/, '').toLowerCase();
371
371
  const isImage = (o) => norm(o).startsWith('img:');
372
- const bad = origins.filter((o) => o !== '*' && !isImage(o) && !ORIGIN.test(norm(o)));
372
+ // "unsafe-eval" is no origin: it lets the app's scripts compile code at run time (Rapid UI's templates).
373
+ const bad = origins.filter((o) => o !== '*' && norm(o) !== 'unsafe-eval' && !isImage(o) && !ORIGIN.test(norm(o)));
373
374
  const badImages = origins.filter((o) => isImage(o) && norm(o) !== 'img:*' && !(norm(o).startsWith('img:https://') && ORIGIN.test(norm(o).slice(4))));
374
375
  if (badImages.length) {
375
376
  errors.push(`externalOrigins: ${badImages.map((o) => `"${o}"`).join(', ')} must be image origins like img:https://cdn.example.com, or img:* for images from any https origin.`);
package/src/mcp-rules.mjs CHANGED
@@ -17,7 +17,7 @@ export const FALLBACK = {
17
17
  version: 1,
18
18
  dashboard: {
19
19
  apiVersion: 'v11',
20
- writeResources: ['banner', 'boostbury', 'campaign', 'visibility', 'visitortarget', 'redirect', 'synonym', 'spellcheck', 'paginationset', 'sortingset', 'recommendationvisibility', 'landingpagelayout', 'facet', 'landingpage'],
20
+ writeResources: ['banner', 'boostbury', 'campaign', 'visibility', 'visitortarget', 'redirect', 'synonym', 'spellcheck', 'paginationset', 'sortingset', 'recommendationvisibility', 'recommendationwidget', 'landingpagelayout', 'facet', 'landingpage'],
21
21
  maxBulkItems: 1000,
22
22
  maxWriteRequestBytes: 5 * 1024 * 1024,
23
23
  writesPerMinute: 120,
package/src/platform.mjs CHANGED
@@ -71,12 +71,16 @@ export function platformClient({ url = process.env.HAWK_APPS_PLATFORM_URL, key =
71
71
  /** Creates or updates the function's Lambda from the uploaded bundle: { name, lambdaVersion, codeSha256 }. */
72
72
  deployFunction: (slug, version, name, config) =>
73
73
  call('POST', `/publish/apps/${encodeURIComponent(slug)}/versions/${version}/functions/${encodeURIComponent(name)}`, config),
74
- /** `extra`: `{ alias, manifest }` for a deploy for an alias (the branch's hawk-app.json goes with it). */
74
+ /**
75
+ * `extra`: `{ alias, manifest }` for a deploy for an alias (the branch's hawk-app.json goes with it); `source`:
76
+ * where it was deployed from (source.mjs).
77
+ */
75
78
  completeVersion: (slug, version, files, serverFiles = [], functions = [], audit, accessibility, extra = {}) =>
76
79
  call('POST', `/publish/apps/${encodeURIComponent(slug)}/versions/${version}/complete`, {
77
80
  files,
78
81
  ...(extra.alias ? { alias: extra.alias } : {}),
79
82
  ...(extra.manifest ? { manifest: extra.manifest } : {}),
83
+ ...(extra.source ? { source: extra.source } : {}),
80
84
  ...(serverFiles.length ? { serverFiles } : {}),
81
85
  ...(functions.length ? { functions } : {}),
82
86
  ...(audit ? { audit } : {}),
@@ -4,6 +4,7 @@
4
4
  // - register / deploy: when someone else's key made the app's last change, ask before going on (--yes skips it; a
5
5
  // pipeline, with no terminal to ask in, gets a warning and goes on).
6
6
  import { createInterface } from 'node:readline';
7
+ import { describeSource } from './source.mjs';
7
8
 
8
9
  /**
9
10
  * What the platform says about a slug for the caller's key: free (404), another publisher's (409 slug_owned), or this
@@ -23,7 +24,8 @@ export async function slugStatus(client, slug) {
23
24
  const describeChange = (last) => {
24
25
  const who = last.name || last.keyId || 'a shared key';
25
26
  const when = last.at ? new Date(last.at).toLocaleString() : 'earlier';
26
- return `${last.kind === 'deploy' ? 'deployed' : 'registered'} by ${who} on ${when}`;
27
+ const from = describeSource(last.source);
28
+ return `${last.kind === 'deploy' ? 'deployed' : 'registered'} by ${who} on ${when}${from ? ` (${from})` : ''}`;
27
29
  };
28
30
 
29
31
  /** create / init: throws when the slug is taken; logs a note when it couldn't check. */
package/src/source.mjs ADDED
@@ -0,0 +1,43 @@
1
+ import { spawnSync } from 'node:child_process';
2
+
3
+ /**
4
+ * Where a deploy comes from, sent with the version so teammates can tell versions apart (`hawk-app versions`, and the
5
+ * question before deploying over someone else's change): the git commit and branch, whether the app's folder has
6
+ * uncommitted changes, and the pipeline build. Bitbucket Pipelines' variables win over git (a pipeline checks out a
7
+ * commit, not a branch). Null outside a git repo and outside CI.
8
+ */
9
+ export function readSource({ cwd = process.cwd(), env = process.env, run = git } = {}) {
10
+ const pipeline = env.BITBUCKET_BUILD_NUMBER ? 'bitbucket' : env.CI && env.CI !== 'false' ? 'ci' : null;
11
+ const commit = env.BITBUCKET_COMMIT || run(cwd, ['rev-parse', 'HEAD']);
12
+ if (!commit && !pipeline) return null;
13
+ const head = commit ? run(cwd, ['rev-parse', '--abbrev-ref', 'HEAD']) : null;
14
+ const branch = env.BITBUCKET_BRANCH || (head && head !== 'HEAD' ? head : null);
15
+ // Only the app's own folder: in a monorepo, other apps' changes don't make this version differ from the commit.
16
+ const dirty = commit ? Boolean(run(cwd, ['status', '--porcelain', '--', '.'])) : false;
17
+ return {
18
+ ...(commit ? { commit } : {}),
19
+ ...(branch ? { branch } : {}),
20
+ ...(dirty ? { dirty } : {}),
21
+ ...(pipeline ? { pipeline } : {}),
22
+ ...(pipeline === 'bitbucket' ? { build: env.BITBUCKET_BUILD_NUMBER } : {}),
23
+ };
24
+ }
25
+
26
+ /** "a1b2c3d on dev + uncommitted changes, bitbucket #123"; '' when nothing was reported. */
27
+ export function describeSource(source) {
28
+ if (!source) return '';
29
+ const parts = [];
30
+ if (source.commit) parts.push(source.commit.slice(0, 7));
31
+ if (source.branch) parts.push(`on ${source.branch}`);
32
+ let text = parts.join(' ');
33
+ if (source.dirty) text += ' + uncommitted changes';
34
+ if (source.pipeline) text += `${text ? ', ' : ''}${source.pipeline}${source.build ? ` #${source.build}` : ''}`;
35
+ return text;
36
+ }
37
+
38
+ /** A git command's trimmed output; null when git isn't there, this isn't a repo, or it printed nothing. */
39
+ function git(cwd, args) {
40
+ const result = spawnSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 5000 });
41
+ if (result.error || result.status !== 0) return null;
42
+ return result.stdout.trim() || null;
43
+ }
package/src/versions.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  import { DEFAULT_ALIAS, checkAlias } from './aliases.mjs';
2
+ import { describeSource } from './source.mjs';
2
3
 
3
4
  /**
4
5
  * Versions, aliases and "why": the developer's view of who sees which version.
@@ -28,6 +29,8 @@ export async function versionsCommand({ client, manifest, log = console.log }) {
28
29
  const note = STATUS_NOTE[v.status] ? ` (${STATUS_NOTE[v.status]})` : '';
29
30
  const branch = v.deployedFor ? ` deployed for ${v.deployedFor}` : '';
30
31
  log(` v${String(v.version).padEnd(4)} ${v.status.padEnd(9)}${aliases}${note}${branch}`);
32
+ const from = describeSource(v.source);
33
+ if (from) log(` from ${from}${v.createdByName ? ` · ${v.createdByName}` : ''}`);
31
34
  if (v.status === 'rejected' && v.rejectedReason) log(` rejected: ${v.rejectedReason}`);
32
35
  }
33
36
  log('Clients get the version of their alias (default without one); users see it only when it\'s approved.');