@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/app-icon.mjs
CHANGED
|
@@ -1,82 +1,82 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
2
|
-
import { readFile } from 'node:fs/promises';
|
|
3
|
-
import { extname, resolve } from 'node:path';
|
|
4
|
-
|
|
5
|
-
// The app's icon: an .svg or .png file next to hawk-app.json ("icon": "icon.svg"). Deploy uploads it into every
|
|
6
|
-
// version as _hawk/icon.svg or _hawk/icon.png, so it's reviewed and released with the version. Hosts draw an SVG in
|
|
7
|
-
// their own text color (as a CSS mask: only its shape counts, not its colors) and show a PNG as is. Without one, hosts
|
|
8
|
-
// show their default icon.
|
|
9
|
-
|
|
10
|
-
/** A relative path to an .svg or .png file: letters, digits, . _ - and /, no "..", at most 200 characters. */
|
|
11
|
-
export const ICON_PATH = /^(?!.*\.\.)[A-Za-z0-9_-][A-Za-z0-9_.-]*(\/[A-Za-z0-9_-][A-Za-z0-9_.-]*)*\.(svg|png)$/i;
|
|
12
|
-
export const ICON_PATH_HINT = 'an .svg or .png file next to hawk-app.json, e.g. "icon.svg"';
|
|
13
|
-
|
|
14
|
-
export const MAX_SVG_BYTES = 32 * 1024;
|
|
15
|
-
export const MAX_PNG_BYTES = 256 * 1024;
|
|
16
|
-
export const MIN_PNG_SIZE = 64;
|
|
17
|
-
export const MAX_PNG_SIZE = 1024;
|
|
18
|
-
|
|
19
|
-
/** The icon `create` and `init` add: a neutral app glyph (four rounded squares), one color, square. */
|
|
20
|
-
export const DEFAULT_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
|
21
|
-
<rect x="3" y="3" width="8" height="8" rx="2"/>
|
|
22
|
-
<rect x="13" y="3" width="8" height="8" rx="2"/>
|
|
23
|
-
<rect x="3" y="13" width="8" height="8" rx="2"/>
|
|
24
|
-
<rect x="13" y="13" width="8" height="8" rx="2"/>
|
|
25
|
-
</svg>
|
|
26
|
-
`;
|
|
27
|
-
|
|
28
|
-
/** Where deploy puts the icon in a version: _hawk/icon.svg or _hawk/icon.png. */
|
|
29
|
-
export function iconUploadPath(icon) {
|
|
30
|
-
return `_hawk/icon${extname(icon).toLowerCase()}`;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Checks the icon file hawk-app.json names (when it names one): there, the right type and size, square. Returns the
|
|
35
|
-
* file to upload ({ path: _hawk/icon.*, size, data }), with errors and warnings.
|
|
36
|
-
*/
|
|
37
|
-
export async function checkIcon(projectDir, icon) {
|
|
38
|
-
const errors = [];
|
|
39
|
-
const warnings = [];
|
|
40
|
-
if (icon === undefined || icon === null || icon === '') return { errors, warnings, file: null };
|
|
41
|
-
if (typeof icon !== 'string' || !ICON_PATH.test(icon)) {
|
|
42
|
-
return { errors: [`icon must be ${ICON_PATH_HINT}.`], warnings, file: null };
|
|
43
|
-
}
|
|
44
|
-
const full = resolve(projectDir, icon);
|
|
45
|
-
if (!existsSync(full)) {
|
|
46
|
-
return { errors: [`icon: ${icon} doesn't exist next to hawk-app.json. Add it, or leave "icon" out for the hosts' default icon.`], warnings, file: null };
|
|
47
|
-
}
|
|
48
|
-
const data = await readFile(full);
|
|
49
|
-
if (extname(icon).toLowerCase() === '.svg') {
|
|
50
|
-
if (data.length > MAX_SVG_BYTES) errors.push(`icon: ${icon} is ${Math.ceil(data.length / 1024)} KB; an SVG icon can be at most ${MAX_SVG_BYTES / 1024} KB.`);
|
|
51
|
-
const text = data.toString('utf8');
|
|
52
|
-
const root = /<svg\b[^>]*>/i.exec(text)?.[0];
|
|
53
|
-
if (!root) {
|
|
54
|
-
errors.push(`icon: ${icon} isn't an SVG (no <svg> element).`);
|
|
55
|
-
} else {
|
|
56
|
-
const box = /\bviewBox\s*=\s*["']\s*[-\d.]+[\s,]+[-\d.]+[\s,]+([\d.]+)[\s,]+([\d.]+)\s*["']/i.exec(root);
|
|
57
|
-
if (!box) warnings.push(`icon: ${icon} has no viewBox: hosts may draw it at the wrong size. Give it a square one, e.g. viewBox="0 0 24 24".`);
|
|
58
|
-
else if (Math.abs(Number(box[1]) - Number(box[2])) > 0.001) warnings.push(`icon: ${icon} isn't square (viewBox ${box[1]}×${box[2]}): hosts fit it in a square.`);
|
|
59
|
-
}
|
|
60
|
-
if (/<(image|foreignObject)\b/i.test(text)) {
|
|
61
|
-
warnings.push(`icon: ${icon} contains <image> or <foreignObject>: hosts draw only an SVG icon's shape, in their own color. For a picture, use a .png icon.`);
|
|
62
|
-
}
|
|
63
|
-
} else {
|
|
64
|
-
if (data.length > MAX_PNG_BYTES) errors.push(`icon: ${icon} is ${Math.ceil(data.length / 1024)} KB; a PNG icon can be at most ${MAX_PNG_BYTES / 1024} KB.`);
|
|
65
|
-
const size = pngSize(data);
|
|
66
|
-
if (!size) {
|
|
67
|
-
errors.push(`icon: ${icon} isn't a PNG.`);
|
|
68
|
-
} else if (size.width !== size.height) {
|
|
69
|
-
errors.push(`icon: ${icon} is ${size.width}×${size.height}; a PNG icon must be square.`);
|
|
70
|
-
} else if (size.width < MIN_PNG_SIZE || size.width > MAX_PNG_SIZE) {
|
|
71
|
-
errors.push(`icon: ${icon} is ${size.width}×${size.height}; a PNG icon must be ${MIN_PNG_SIZE}–${MAX_PNG_SIZE} px (256×256 is a good size).`);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
return { errors, warnings, file: errors.length ? null : { path: iconUploadPath(icon), size: data.length, data } };
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** A PNG's width and height from its IHDR chunk; null when it isn't a PNG. */
|
|
78
|
-
export function pngSize(data) {
|
|
79
|
-
const signature = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
80
|
-
if (data.length < 24 || !data.subarray(0, 8).equals(signature) || data.toString('ascii', 12, 16) !== 'IHDR') return null;
|
|
81
|
-
return { width: data.readUInt32BE(16), height: data.readUInt32BE(20) };
|
|
82
|
-
}
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { extname, resolve } from 'node:path';
|
|
4
|
+
|
|
5
|
+
// The app's icon: an .svg or .png file next to hawk-app.json ("icon": "icon.svg"). Deploy uploads it into every
|
|
6
|
+
// version as _hawk/icon.svg or _hawk/icon.png, so it's reviewed and released with the version. Hosts draw an SVG in
|
|
7
|
+
// their own text color (as a CSS mask: only its shape counts, not its colors) and show a PNG as is. Without one, hosts
|
|
8
|
+
// show their default icon.
|
|
9
|
+
|
|
10
|
+
/** A relative path to an .svg or .png file: letters, digits, . _ - and /, no "..", at most 200 characters. */
|
|
11
|
+
export const ICON_PATH = /^(?!.*\.\.)[A-Za-z0-9_-][A-Za-z0-9_.-]*(\/[A-Za-z0-9_-][A-Za-z0-9_.-]*)*\.(svg|png)$/i;
|
|
12
|
+
export const ICON_PATH_HINT = 'an .svg or .png file next to hawk-app.json, e.g. "icon.svg"';
|
|
13
|
+
|
|
14
|
+
export const MAX_SVG_BYTES = 32 * 1024;
|
|
15
|
+
export const MAX_PNG_BYTES = 256 * 1024;
|
|
16
|
+
export const MIN_PNG_SIZE = 64;
|
|
17
|
+
export const MAX_PNG_SIZE = 1024;
|
|
18
|
+
|
|
19
|
+
/** The icon `create` and `init` add: a neutral app glyph (four rounded squares), one color, square. */
|
|
20
|
+
export const DEFAULT_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
|
21
|
+
<rect x="3" y="3" width="8" height="8" rx="2"/>
|
|
22
|
+
<rect x="13" y="3" width="8" height="8" rx="2"/>
|
|
23
|
+
<rect x="3" y="13" width="8" height="8" rx="2"/>
|
|
24
|
+
<rect x="13" y="13" width="8" height="8" rx="2"/>
|
|
25
|
+
</svg>
|
|
26
|
+
`;
|
|
27
|
+
|
|
28
|
+
/** Where deploy puts the icon in a version: _hawk/icon.svg or _hawk/icon.png. */
|
|
29
|
+
export function iconUploadPath(icon) {
|
|
30
|
+
return `_hawk/icon${extname(icon).toLowerCase()}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Checks the icon file hawk-app.json names (when it names one): there, the right type and size, square. Returns the
|
|
35
|
+
* file to upload ({ path: _hawk/icon.*, size, data }), with errors and warnings.
|
|
36
|
+
*/
|
|
37
|
+
export async function checkIcon(projectDir, icon) {
|
|
38
|
+
const errors = [];
|
|
39
|
+
const warnings = [];
|
|
40
|
+
if (icon === undefined || icon === null || icon === '') return { errors, warnings, file: null };
|
|
41
|
+
if (typeof icon !== 'string' || !ICON_PATH.test(icon)) {
|
|
42
|
+
return { errors: [`icon must be ${ICON_PATH_HINT}.`], warnings, file: null };
|
|
43
|
+
}
|
|
44
|
+
const full = resolve(projectDir, icon);
|
|
45
|
+
if (!existsSync(full)) {
|
|
46
|
+
return { errors: [`icon: ${icon} doesn't exist next to hawk-app.json. Add it, or leave "icon" out for the hosts' default icon.`], warnings, file: null };
|
|
47
|
+
}
|
|
48
|
+
const data = await readFile(full);
|
|
49
|
+
if (extname(icon).toLowerCase() === '.svg') {
|
|
50
|
+
if (data.length > MAX_SVG_BYTES) errors.push(`icon: ${icon} is ${Math.ceil(data.length / 1024)} KB; an SVG icon can be at most ${MAX_SVG_BYTES / 1024} KB.`);
|
|
51
|
+
const text = data.toString('utf8');
|
|
52
|
+
const root = /<svg\b[^>]*>/i.exec(text)?.[0];
|
|
53
|
+
if (!root) {
|
|
54
|
+
errors.push(`icon: ${icon} isn't an SVG (no <svg> element).`);
|
|
55
|
+
} else {
|
|
56
|
+
const box = /\bviewBox\s*=\s*["']\s*[-\d.]+[\s,]+[-\d.]+[\s,]+([\d.]+)[\s,]+([\d.]+)\s*["']/i.exec(root);
|
|
57
|
+
if (!box) warnings.push(`icon: ${icon} has no viewBox: hosts may draw it at the wrong size. Give it a square one, e.g. viewBox="0 0 24 24".`);
|
|
58
|
+
else if (Math.abs(Number(box[1]) - Number(box[2])) > 0.001) warnings.push(`icon: ${icon} isn't square (viewBox ${box[1]}×${box[2]}): hosts fit it in a square.`);
|
|
59
|
+
}
|
|
60
|
+
if (/<(image|foreignObject)\b/i.test(text)) {
|
|
61
|
+
warnings.push(`icon: ${icon} contains <image> or <foreignObject>: hosts draw only an SVG icon's shape, in their own color. For a picture, use a .png icon.`);
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
if (data.length > MAX_PNG_BYTES) errors.push(`icon: ${icon} is ${Math.ceil(data.length / 1024)} KB; a PNG icon can be at most ${MAX_PNG_BYTES / 1024} KB.`);
|
|
65
|
+
const size = pngSize(data);
|
|
66
|
+
if (!size) {
|
|
67
|
+
errors.push(`icon: ${icon} isn't a PNG.`);
|
|
68
|
+
} else if (size.width !== size.height) {
|
|
69
|
+
errors.push(`icon: ${icon} is ${size.width}×${size.height}; a PNG icon must be square.`);
|
|
70
|
+
} else if (size.width < MIN_PNG_SIZE || size.width > MAX_PNG_SIZE) {
|
|
71
|
+
errors.push(`icon: ${icon} is ${size.width}×${size.height}; a PNG icon must be ${MIN_PNG_SIZE}–${MAX_PNG_SIZE} px (256×256 is a good size).`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return { errors, warnings, file: errors.length ? null : { path: iconUploadPath(icon), size: data.length, data } };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** A PNG's width and height from its IHDR chunk; null when it isn't a PNG. */
|
|
78
|
+
export function pngSize(data) {
|
|
79
|
+
const signature = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
80
|
+
if (data.length < 24 || !data.subarray(0, 8).equals(signature) || data.toString('ascii', 12, 16) !== 'IHDR') return null;
|
|
81
|
+
return { width: data.readUInt32BE(16), height: data.readUInt32BE(20) };
|
|
82
|
+
}
|
package/src/channel.mjs
CHANGED
|
@@ -1,116 +1,116 @@
|
|
|
1
|
-
// Release channels of the Hawk Apps packages. `latest` (from main) is stable and matches the prod platform; `dev`
|
|
2
|
-
// (prereleases from the dev branch, e.g. 0.7.0-dev.20261014…) has features that may need the dev platform.
|
|
3
|
-
// - `hawk-app create` from a dev CLI (npx …-cli@dev create) makes a dev app: the Hawk packages on the `dev` dist-tag
|
|
4
|
-
// (the vanilla template's CDN import too), so it gets the dev bridge, not the latest one the template pins.
|
|
5
|
-
// - `hawk-app upgrade --channel latest|dev` moves an app between channels.
|
|
6
|
-
import { spawnSync } from 'node:child_process';
|
|
7
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
8
|
-
import { readFile, writeFile } from 'node:fs/promises';
|
|
9
|
-
import { join } from 'node:path';
|
|
10
|
-
|
|
11
|
-
export const BRIDGE = '@bridgeline-digital/hawk-app-bridge';
|
|
12
|
-
export const CLI = '@bridgeline-digital/hawk-app-cli';
|
|
13
|
-
/** The UI kit (next-mui apps): released with the bridge and CLI, so it follows the same channel. */
|
|
14
|
-
export const UI = '@bridgeline-digital/hawk-app-ui';
|
|
15
|
-
export const HAWK_PACKAGES = [BRIDGE, UI, CLI];
|
|
16
|
-
export const CHANNELS = ['latest', 'dev'];
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* The vanilla template's imports from the CDN, the bridge and the UI kit (one version: they're released together): by
|
|
20
|
-
* minor (`@0.6/`) or by dist-tag (`@dev/`).
|
|
21
|
-
*/
|
|
22
|
-
export const CDN_BRIDGE = /(hawk-app-(?:bridge|ui)@)([0-9]+\.[0-9]+|dev)(\/)/;
|
|
23
|
-
|
|
24
|
-
/** The vanilla template's files that load from the CDN: the bridge and the kit's elements (app.js), the kit's stylesheet (index.html). */
|
|
25
|
-
export const CDN_FILES = [join('public', 'app.js'), join('public', 'index.html')];
|
|
26
|
-
|
|
27
|
-
/** `text` with every CDN import of the bridge and the UI kit at `version` (a minor or a dist-tag). */
|
|
28
|
-
export const pointCdn = (text, version) => text.replace(new RegExp(CDN_BRIDGE.source, 'g'), `$1${version}$3`);
|
|
29
|
-
|
|
30
|
-
/** Points the app's CDN files (those that exist) at `version`; returns the files changed. */
|
|
31
|
-
export async function writeCdnFiles(dir, version) {
|
|
32
|
-
const changed = [];
|
|
33
|
-
for (const file of CDN_FILES) {
|
|
34
|
-
const path = join(dir, file);
|
|
35
|
-
if (!existsSync(path)) continue;
|
|
36
|
-
const text = await readFile(path, 'utf8');
|
|
37
|
-
const next = pointCdn(text, version);
|
|
38
|
-
if (next !== text) {
|
|
39
|
-
await writeFile(path, next);
|
|
40
|
-
changed.push(file);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
return changed;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const isPrerelease = (version) => /^\d+\.\d+\.\d+-/.test(String(version ?? '').trim());
|
|
47
|
-
|
|
48
|
-
/** This CLI's version, e.g. 0.7.0 or 0.8.0-dev.20261014120000. */
|
|
49
|
-
export const cliVersion = () => JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
50
|
-
|
|
51
|
-
/** The channel this CLI came from: a prerelease is a dev build. */
|
|
52
|
-
export const cliChannel = (version = cliVersion()) => (isPrerelease(version) ? 'dev' : 'latest');
|
|
53
|
-
|
|
54
|
-
/** A package.json version spec on the dev channel: the `dev` dist-tag or a prerelease range. */
|
|
55
|
-
export const isDevSpec = (spec) => {
|
|
56
|
-
const text = String(spec ?? '').trim();
|
|
57
|
-
return text === 'dev' || isPrerelease(text.replace(/^[\^~>=v\s]+/, ''));
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
/** The app's channel from its package.json (dev when any Hawk package is on dev), or null without them. */
|
|
61
|
-
export function appChannel(pkg) {
|
|
62
|
-
const specs = HAWK_PACKAGES.map((name) => pkg?.dependencies?.[name] ?? pkg?.devDependencies?.[name]).filter(Boolean);
|
|
63
|
-
if (!specs.length) return null;
|
|
64
|
-
return specs.some(isDevSpec) ? 'dev' : 'latest';
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** The version npm's `latest` tag points at, e.g. "0.6.0"; null when npm can't be asked (offline, no npm). */
|
|
68
|
-
export function npmLatest(name, run = spawnSync) {
|
|
69
|
-
const result = run('npm', ['view', `${name}@latest`, 'version'], { encoding: 'utf8', shell: process.platform === 'win32' });
|
|
70
|
-
const version = String(result?.stdout ?? '').trim().split(/\s+/).pop();
|
|
71
|
-
return result?.status === 0 && /^\d+\.\d+\.\d+$/.test(version) ? version : null;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* The changes that put a package.json (and the vanilla CDN import) on `channel`: [{ name, section, from, to }] and
|
|
76
|
-
* { from, to } for the CDN. `latest` uses npm's latest version (`fallback` when npm can't be asked: the template's).
|
|
77
|
-
*/
|
|
78
|
-
export function planChannel({ pkg, appJs, channel, latestVersion }) {
|
|
79
|
-
if (!CHANNELS.includes(channel)) throw new Error(`--channel must be latest or dev, not "${channel}".`);
|
|
80
|
-
const dependencies = [];
|
|
81
|
-
for (const name of HAWK_PACKAGES) {
|
|
82
|
-
const section = ['dependencies', 'devDependencies'].find((s) => pkg?.[s]?.[name] !== undefined);
|
|
83
|
-
if (!section) continue;
|
|
84
|
-
const from = pkg[section][name];
|
|
85
|
-
let to;
|
|
86
|
-
if (channel === 'dev') {
|
|
87
|
-
to = 'dev';
|
|
88
|
-
} else {
|
|
89
|
-
const version = latestVersion(name);
|
|
90
|
-
if (!version) throw new Error(`Couldn't find the latest version of ${name} on npm. Check your connection and try again.`);
|
|
91
|
-
to = `^${version}`;
|
|
92
|
-
}
|
|
93
|
-
if (from !== to) dependencies.push({ name, section, from, to });
|
|
94
|
-
}
|
|
95
|
-
let cdn = null;
|
|
96
|
-
const current = appJs ? CDN_BRIDGE.exec(appJs)?.[2] : null;
|
|
97
|
-
if (current) {
|
|
98
|
-
const version = channel === 'dev' ? 'dev' : latestVersion(BRIDGE)?.split('.').slice(0, 2).join('.');
|
|
99
|
-
if (!version) throw new Error(`Couldn't find the latest version of ${BRIDGE} on npm. Check your connection and try again.`);
|
|
100
|
-
if (current !== version) cdn = { from: current, to: version };
|
|
101
|
-
}
|
|
102
|
-
return { channel, dependencies, cdn };
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/** Applies planChannel's changes to the package.json object and app.js text; returns the new app.js text. */
|
|
106
|
-
export function applyChannel(plan, pkg, appJs) {
|
|
107
|
-
for (const { name, section, to } of plan.dependencies) pkg[section][name] = to;
|
|
108
|
-
return plan.cdn && appJs ? pointCdn(appJs, plan.cdn.to) : appJs;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/** What a dev app needs to know (create and upgrade print it). */
|
|
112
|
-
export const DEV_CHANNEL_NOTE = [
|
|
113
|
-
'This app is on the dev channel: the bridge, the UI kit and the CLI follow their newest dev builds (the `dev` tag on npm).',
|
|
114
|
-
"Dev features may need the dev platform (the default): don't deploy it to prod until they're released.",
|
|
115
|
-
'When they are, move it to the released versions: npx hawk-app upgrade --channel latest --yes',
|
|
116
|
-
];
|
|
1
|
+
// Release channels of the Hawk Apps packages. `latest` (from main) is stable and matches the prod platform; `dev`
|
|
2
|
+
// (prereleases from the dev branch, e.g. 0.7.0-dev.20261014…) has features that may need the dev platform.
|
|
3
|
+
// - `hawk-app create` from a dev CLI (npx …-cli@dev create) makes a dev app: the Hawk packages on the `dev` dist-tag
|
|
4
|
+
// (the vanilla template's CDN import too), so it gets the dev bridge, not the latest one the template pins.
|
|
5
|
+
// - `hawk-app upgrade --channel latest|dev` moves an app between channels.
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
8
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
|
|
11
|
+
export const BRIDGE = '@bridgeline-digital/hawk-app-bridge';
|
|
12
|
+
export const CLI = '@bridgeline-digital/hawk-app-cli';
|
|
13
|
+
/** The UI kit (next-mui apps): released with the bridge and CLI, so it follows the same channel. */
|
|
14
|
+
export const UI = '@bridgeline-digital/hawk-app-ui';
|
|
15
|
+
export const HAWK_PACKAGES = [BRIDGE, UI, CLI];
|
|
16
|
+
export const CHANNELS = ['latest', 'dev'];
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The vanilla template's imports from the CDN, the bridge and the UI kit (one version: they're released together): by
|
|
20
|
+
* minor (`@0.6/`) or by dist-tag (`@dev/`).
|
|
21
|
+
*/
|
|
22
|
+
export const CDN_BRIDGE = /(hawk-app-(?:bridge|ui)@)([0-9]+\.[0-9]+|dev)(\/)/;
|
|
23
|
+
|
|
24
|
+
/** The vanilla template's files that load from the CDN: the bridge and the kit's elements (app.js), the kit's stylesheet (index.html). */
|
|
25
|
+
export const CDN_FILES = [join('public', 'app.js'), join('public', 'index.html')];
|
|
26
|
+
|
|
27
|
+
/** `text` with every CDN import of the bridge and the UI kit at `version` (a minor or a dist-tag). */
|
|
28
|
+
export const pointCdn = (text, version) => text.replace(new RegExp(CDN_BRIDGE.source, 'g'), `$1${version}$3`);
|
|
29
|
+
|
|
30
|
+
/** Points the app's CDN files (those that exist) at `version`; returns the files changed. */
|
|
31
|
+
export async function writeCdnFiles(dir, version) {
|
|
32
|
+
const changed = [];
|
|
33
|
+
for (const file of CDN_FILES) {
|
|
34
|
+
const path = join(dir, file);
|
|
35
|
+
if (!existsSync(path)) continue;
|
|
36
|
+
const text = await readFile(path, 'utf8');
|
|
37
|
+
const next = pointCdn(text, version);
|
|
38
|
+
if (next !== text) {
|
|
39
|
+
await writeFile(path, next);
|
|
40
|
+
changed.push(file);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return changed;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const isPrerelease = (version) => /^\d+\.\d+\.\d+-/.test(String(version ?? '').trim());
|
|
47
|
+
|
|
48
|
+
/** This CLI's version, e.g. 0.7.0 or 0.8.0-dev.20261014120000. */
|
|
49
|
+
export const cliVersion = () => JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
50
|
+
|
|
51
|
+
/** The channel this CLI came from: a prerelease is a dev build. */
|
|
52
|
+
export const cliChannel = (version = cliVersion()) => (isPrerelease(version) ? 'dev' : 'latest');
|
|
53
|
+
|
|
54
|
+
/** A package.json version spec on the dev channel: the `dev` dist-tag or a prerelease range. */
|
|
55
|
+
export const isDevSpec = (spec) => {
|
|
56
|
+
const text = String(spec ?? '').trim();
|
|
57
|
+
return text === 'dev' || isPrerelease(text.replace(/^[\^~>=v\s]+/, ''));
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/** The app's channel from its package.json (dev when any Hawk package is on dev), or null without them. */
|
|
61
|
+
export function appChannel(pkg) {
|
|
62
|
+
const specs = HAWK_PACKAGES.map((name) => pkg?.dependencies?.[name] ?? pkg?.devDependencies?.[name]).filter(Boolean);
|
|
63
|
+
if (!specs.length) return null;
|
|
64
|
+
return specs.some(isDevSpec) ? 'dev' : 'latest';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The version npm's `latest` tag points at, e.g. "0.6.0"; null when npm can't be asked (offline, no npm). */
|
|
68
|
+
export function npmLatest(name, run = spawnSync) {
|
|
69
|
+
const result = run('npm', ['view', `${name}@latest`, 'version'], { encoding: 'utf8', shell: process.platform === 'win32' });
|
|
70
|
+
const version = String(result?.stdout ?? '').trim().split(/\s+/).pop();
|
|
71
|
+
return result?.status === 0 && /^\d+\.\d+\.\d+$/.test(version) ? version : null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The changes that put a package.json (and the vanilla CDN import) on `channel`: [{ name, section, from, to }] and
|
|
76
|
+
* { from, to } for the CDN. `latest` uses npm's latest version (`fallback` when npm can't be asked: the template's).
|
|
77
|
+
*/
|
|
78
|
+
export function planChannel({ pkg, appJs, channel, latestVersion }) {
|
|
79
|
+
if (!CHANNELS.includes(channel)) throw new Error(`--channel must be latest or dev, not "${channel}".`);
|
|
80
|
+
const dependencies = [];
|
|
81
|
+
for (const name of HAWK_PACKAGES) {
|
|
82
|
+
const section = ['dependencies', 'devDependencies'].find((s) => pkg?.[s]?.[name] !== undefined);
|
|
83
|
+
if (!section) continue;
|
|
84
|
+
const from = pkg[section][name];
|
|
85
|
+
let to;
|
|
86
|
+
if (channel === 'dev') {
|
|
87
|
+
to = 'dev';
|
|
88
|
+
} else {
|
|
89
|
+
const version = latestVersion(name);
|
|
90
|
+
if (!version) throw new Error(`Couldn't find the latest version of ${name} on npm. Check your connection and try again.`);
|
|
91
|
+
to = `^${version}`;
|
|
92
|
+
}
|
|
93
|
+
if (from !== to) dependencies.push({ name, section, from, to });
|
|
94
|
+
}
|
|
95
|
+
let cdn = null;
|
|
96
|
+
const current = appJs ? CDN_BRIDGE.exec(appJs)?.[2] : null;
|
|
97
|
+
if (current) {
|
|
98
|
+
const version = channel === 'dev' ? 'dev' : latestVersion(BRIDGE)?.split('.').slice(0, 2).join('.');
|
|
99
|
+
if (!version) throw new Error(`Couldn't find the latest version of ${BRIDGE} on npm. Check your connection and try again.`);
|
|
100
|
+
if (current !== version) cdn = { from: current, to: version };
|
|
101
|
+
}
|
|
102
|
+
return { channel, dependencies, cdn };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Applies planChannel's changes to the package.json object and app.js text; returns the new app.js text. */
|
|
106
|
+
export function applyChannel(plan, pkg, appJs) {
|
|
107
|
+
for (const { name, section, to } of plan.dependencies) pkg[section][name] = to;
|
|
108
|
+
return plan.cdn && appJs ? pointCdn(appJs, plan.cdn.to) : appJs;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** What a dev app needs to know (create and upgrade print it). */
|
|
112
|
+
export const DEV_CHANNEL_NOTE = [
|
|
113
|
+
'This app is on the dev channel: the bridge, the UI kit and the CLI follow their newest dev builds (the `dev` tag on npm).',
|
|
114
|
+
"Dev features may need the dev platform (the default): don't deploy it to prod until they're released.",
|
|
115
|
+
'When they are, move it to the released versions: npx hawk-app upgrade --channel latest --yes',
|
|
116
|
+
];
|