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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/LICENSE +7 -7
  2. package/README.md +770 -770
  3. package/agents/AGENTS.md +192 -192
  4. package/agents/project/custom.md +21 -21
  5. package/agents/project/next-mui.md +21 -21
  6. package/agents/project/next.md +24 -24
  7. package/agents/project/vanilla.md +22 -22
  8. package/agents/project/vite.md +19 -19
  9. package/bin/hawk-app.mjs +0 -0
  10. package/docs/bridge/README.md +1315 -1315
  11. package/docs/showcase/README.md +47 -47
  12. package/docs/showcase/functions/echo.ts +12 -12
  13. package/docs/showcase/functions/long-task.ts +31 -31
  14. package/docs/showcase/functions/showcase.ts +186 -186
  15. package/docs/showcase/hawk-app.json +73 -73
  16. package/docs/showcase/queries/assistant-prompts-by-day.json +6 -6
  17. package/docs/showcase/queries/assistant-prompts-by-day.sql +8 -8
  18. package/docs/showcase/queries/searches-by-day.json +6 -6
  19. package/docs/showcase/queries/searches-by-day.sql +7 -7
  20. package/docs/showcase/src/App.tsx +1436 -1436
  21. package/docs/showcase/src/main.tsx +10 -10
  22. package/docs/showcase/src/styles.css +71 -71
  23. package/docs/showcase/src/useHawkApp.ts +44 -44
  24. package/docs/showcase/src/vite-env.d.ts +1 -1
  25. package/docs/ui/README.md +166 -166
  26. package/package.json +1 -1
  27. package/schema/hawk-app.schema.json +611 -611
  28. package/src/accessibility.mjs +105 -105
  29. package/src/agents.mjs +145 -145
  30. package/src/aliases.mjs +30 -30
  31. package/src/app-icon.mjs +82 -82
  32. package/src/channel.mjs +116 -116
  33. package/src/cli.mjs +825 -825
  34. package/src/clients.mjs +221 -221
  35. package/src/config.mjs +148 -148
  36. package/src/create.mjs +186 -186
  37. package/src/credentials.mjs +56 -56
  38. package/src/data.mjs +169 -169
  39. package/src/deploy.mjs +349 -349
  40. package/src/dev.mjs +459 -459
  41. package/src/doctor.mjs +176 -176
  42. package/src/env.mjs +140 -140
  43. package/src/functions.mjs +313 -313
  44. package/src/login.mjs +166 -166
  45. package/src/logs.mjs +64 -64
  46. package/src/manifest.mjs +425 -425
  47. package/src/mcp-docs.mjs +168 -168
  48. package/src/mcp-hawksearch.mjs +342 -342
  49. package/src/mcp-reports.mjs +130 -130
  50. package/src/mcp-rules.mjs +109 -109
  51. package/src/mcp.mjs +539 -539
  52. package/src/platform.mjs +116 -116
  53. package/src/project-checks.mjs +153 -153
  54. package/src/queries.mjs +289 -289
  55. package/src/scope-check.mjs +91 -91
  56. package/src/secrets.mjs +86 -86
  57. package/src/slug-guard.mjs +80 -80
  58. package/src/sso.mjs +182 -182
  59. package/src/telemetry.mjs +50 -50
  60. package/src/upgrade.mjs +406 -406
  61. package/src/versions.mjs +102 -102
  62. package/templates/next/next.config.ts +13 -13
  63. package/templates/next/package.json +26 -26
  64. package/templates/next/src/lib/useHawkApp.ts +46 -46
  65. package/templates/next-mui/next.config.ts +13 -13
  66. package/templates/next-mui/package.json +30 -30
  67. package/templates/next-mui/src/app/layout.tsx +19 -19
  68. package/templates/next-mui/src/app/page.tsx +78 -78
  69. package/templates/next-mui/src/app/providers.tsx +10 -10
  70. package/templates/preact/package.json +26 -26
  71. package/templates/preact/src/useHawkApp.ts +44 -44
  72. package/templates/preact/src/vite-env.d.ts +1 -1
  73. package/templates/preact/tsconfig.json +17 -17
  74. package/templates/preact/vite.config.ts +11 -11
  75. package/templates/react/package.json +29 -29
  76. package/templates/react/src/useHawkApp.ts +44 -44
  77. package/templates/react/src/vite-env.d.ts +1 -1
  78. package/templates/react/tsconfig.json +16 -16
  79. package/templates/react/vite.config.ts +11 -11
  80. package/templates/vanilla/package.json +15 -15
package/src/doctor.mjs CHANGED
@@ -1,176 +1,176 @@
1
- import { existsSync, readFileSync } from 'node:fs';
2
- import { dirname, join, resolve } from 'node:path';
3
- import { describePlatform, PLATFORMS, resolveSettings } from './env.mjs';
4
- import { checkKey } from './login.mjs';
5
- import { loadManifest, validateManifest } from './manifest.mjs';
6
- import { checkProject } from './project-checks.mjs';
7
- import { describeMissingScopes, missingScopes } from './scope-check.mjs';
8
- import { upgradeHint } from './upgrade.mjs';
9
- import { openBrowser } from './sso.mjs';
10
-
11
- /** The HawkSearch dashboard of each platform environment (where apps are reviewed and opened). */
12
- export const DASHBOARDS = {
13
- dev: 'https://e360.dev.bridgeline.com',
14
- prod: 'https://app.hawksearch.com',
15
- };
16
-
17
- const envOf = (url) => Object.entries(PLATFORMS).find(([, u]) => u.replace(/\/+$/, '') === String(url).replace(/\/+$/, ''))?.[0];
18
-
19
- /** The app's page in HawkSearch (optionally shown as an alias's clients see it); null for an unknown platform. */
20
- export function appUrl(platformUrl, slug, alias) {
21
- const dashboard = DASHBOARDS[envOf(platformUrl)];
22
- if (!dashboard) return null;
23
- return `${dashboard}/admin/apps/${encodeURIComponent(slug)}${alias ? `?alias=${encodeURIComponent(alias)}` : ''}`;
24
- }
25
-
26
- /** The developer docs (built from the hawk-apps repo's markdown; sign in with a Bridgeline account). */
27
- export const DOCS_URL = 'https://developers.hawksearchapps.com';
28
-
29
- /**
30
- * `hawk-app docs <topic>`: the page for each topic. Developers' topics open the developer docs (a feature's section of
31
- * the bridge reference); `platform` opens the engineering docs, which the site keeps out of its navigation and search.
32
- */
33
- export const DOCS_TOPICS = {
34
- start: '/start/',
35
- claude: '/start/with-claude/',
36
- concepts: '/concepts/how-it-works/',
37
- versions: '/concepts/versions-and-aliases/',
38
- aliases: '/concepts/versions-and-aliases/',
39
- bridge: '/reference/bridge/',
40
- api: '/reference/api/',
41
- cli: '/reference/cli/',
42
- commands: '/reference/cli-commands/',
43
- manifest: '/reference/hawk-app-json/',
44
- 'hawk-app.json': '/reference/hawk-app-json/',
45
- queries: '/reference/bridge/#custom-data-queries-dataread',
46
- data: '/reference/bridge/#data',
47
- functions: '/reference/bridge/#functions-functionsinvoke',
48
- email: '/reference/bridge/#email-emailsend',
49
- storage: '/reference/bridge/#storage-storage',
50
- ai: '/reference/bridge/#ai-aiinvoke',
51
- search: '/reference/bridge/#hawksearch-search-hawksearchsearch',
52
- review: '/reference/cli/#review-in-hawksearch',
53
- 'whats-new': '/whats-new/cli/',
54
- platform: '/engineering/',
55
- };
56
-
57
- /** `hawk-app docs [topic]`: opens the developer docs (a topic's page), and prints the link. */
58
- export async function docsCommand({ topic, openUrl = openBrowser, log = console.log }) {
59
- const key = typeof topic === 'string' ? topic.toLowerCase() : undefined;
60
- if (key !== undefined && !(key in DOCS_TOPICS)) {
61
- throw new Error(`No docs topic "${topic}". Topics: ${Object.keys(DOCS_TOPICS).join(', ')}.`);
62
- }
63
- const url = `${DOCS_URL}${key ? DOCS_TOPICS[key] : '/'}`;
64
- log(`Hawk Apps developer docs: ${url}`);
65
- log(' Sign in with your Bridgeline account. The site has search (top of every page).');
66
- try {
67
- await openUrl(url);
68
- } catch {
69
- // No browser (CI, SSH): the link above is enough.
70
- }
71
- return 0;
72
- }
73
-
74
- /** `hawk-app open [--alias <name>]`: the app's page in HawkSearch, to review it, submit it for approval, or try an alias. */
75
- export async function openCommand({ manifestFile, flags = {}, openUrl = openBrowser, log = console.log }) {
76
- const manifest = await loadManifest(manifestFile);
77
- const { url } = resolveSettings({ flagPlatform: flags.platform, flagEnv: flags.env, dir: dirname(resolve(manifestFile)) });
78
- const alias = typeof flags.alias === 'string' ? flags.alias : undefined;
79
- const page = appUrl(url, manifest.slug, alias);
80
- if (!page) throw new Error(`No HawkSearch dashboard is known for ${describePlatform(url)}. Open the Apps page there and pick ${manifest.slug}.`);
81
- log(`${manifest.slug} in HawkSearch${alias ? ` (as ${alias}'s clients see it)` : ''}: ${page}`);
82
- log(' Bridgeline staff: review it there and submit it for approval (More review actions). Clients\' users see it once it\'s released.');
83
- try {
84
- await openUrl(page);
85
- } catch {
86
- // No browser (CI, SSH): the link above is enough.
87
- }
88
- return 0;
89
- }
90
-
91
- /**
92
- * `hawk-app doctor`: everything a developer needs working, in one go: Node, the login and platform, hawk-app.json, the
93
- * project (bridge, build script, template), scopes the code uses, the app on the platform and the MCP config. Each line
94
- * says what to do when it isn't right. Exit 1 when something must be fixed.
95
- */
96
- export async function doctorCommand({ manifestFile, flags = {}, fetchImpl = fetch, log = console.log, nodeVersion = process.versions.node }) {
97
- let failed = 0;
98
- const ok = (text) => log(`✔ ${text}`);
99
- const warn = (text) => log(`! ${text}`);
100
- const fail = (text) => {
101
- failed += 1;
102
- log(`✖ ${text}`);
103
- };
104
-
105
- // Node.
106
- const major = Number(String(nodeVersion).split('.')[0]);
107
- if (major >= 20) ok(`Node ${nodeVersion}`);
108
- else if (major >= 18) warn(`Node ${nodeVersion}: works, but the templates are tested on Node 20 or later.`);
109
- else fail(`Node ${nodeVersion}: install Node 20 or later (https://nodejs.org).`);
110
-
111
- // Login and platform.
112
- const projectDir = dirname(resolve(manifestFile));
113
- const settings = resolveSettings({ flagPlatform: flags.platform, flagEnv: flags.env, dir: projectDir });
114
- if (!settings.key) {
115
- fail(`Not logged in to ${describePlatform(settings.url)}: run \`npx hawk-app sso\` (Bridgeline account) once on this machine.`);
116
- } else {
117
- const check = await checkKey(settings.url, settings.key, fetchImpl);
118
- if (check.ok) ok(`Logged in to ${describePlatform(settings.url)} (key from ${settings.keySource})`);
119
- else fail(`${check.reason} Run \`npx hawk-app sso\` again.`);
120
- }
121
-
122
- // The app.
123
- if (!existsSync(manifestFile)) {
124
- fail(`No ${manifestFile} here: run this in the app's folder, or start one with \`npx hawk-app create <folder>\` (\`init\` for an existing project).`);
125
- return failed ? 1 : 0;
126
- }
127
- let manifest;
128
- try {
129
- manifest = await loadManifest(manifestFile);
130
- } catch (error) {
131
- fail(error.message);
132
- return 1;
133
- }
134
- const checked = validateManifest(manifest);
135
- if (checked.errors.length) fail(`${manifestFile}: ${checked.errors.join(' ')} (details: npx hawk-app validate)`);
136
- else ok(`${manifestFile}: ${manifest.slug}`);
137
- for (const warning of checked.warnings) warn(warning);
138
-
139
- const project = await checkProject(projectDir);
140
- for (const warning of project.warnings) warn(warning);
141
- if (!project.warnings.length) ok('Project: the bridge and a build script are set up');
142
- try {
143
- const hint = upgradeHint(projectDir, manifest);
144
- if (hint) warn(hint.replace(/^! /, ''));
145
- else ok('Up to date with its template');
146
- } catch {
147
- // An app that follows no template: nothing to compare.
148
- }
149
- const scopes = describeMissingScopes(await missingScopes(projectDir, manifest.scopes));
150
- if (scopes) warn(scopes);
151
- else ok('Scopes: hawk-app.json has the ones the code uses');
152
-
153
- // The app on the platform.
154
- if (settings.key) {
155
- try {
156
- const response = await fetchImpl(`${settings.url}/publish/apps/${encodeURIComponent(manifest.slug)}`, { headers: { Authorization: `PublisherKey ${settings.key}` } });
157
- if (response.status === 404) warn(`${manifest.slug} isn't on ${describePlatform(settings.url)} yet: \`npm run app:dev -- --client <id>\` or \`npm run app:deploy\` registers it.`);
158
- else if (response.status === 409) fail(`${manifest.slug} belongs to another team: pick another slug in ${manifestFile}.`);
159
- else if (response.ok) {
160
- const app = await response.json();
161
- ok(`On the platform: ${app.status === 'Live' && app.liveVersion ? `released v${app.liveVersion}` : 'not released yet'} (npx hawk-app status)`);
162
- }
163
- } catch {
164
- // Already reported with the login check.
165
- }
166
- }
167
-
168
- // AI assistants.
169
- const mcp = join(projectDir, '.mcp.json');
170
- if (existsSync(mcp) && readFileSync(mcp, 'utf8').includes('hawk-apps')) ok('MCP server set up for AI assistants (.mcp.json)');
171
- else warn('No MCP config for AI assistants: `npx hawk-app upgrade --yes` adds .mcp.json and .vscode/mcp.json.');
172
-
173
- log(failed ? `\n${failed} thing${failed === 1 ? '' : 's'} to fix above.` : '\nAll set.');
174
- log(`Docs: ${DOCS_URL} (npx hawk-app docs)`);
175
- return failed ? 1 : 0;
176
- }
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ import { describePlatform, PLATFORMS, resolveSettings } from './env.mjs';
4
+ import { checkKey } from './login.mjs';
5
+ import { loadManifest, validateManifest } from './manifest.mjs';
6
+ import { checkProject } from './project-checks.mjs';
7
+ import { describeMissingScopes, missingScopes } from './scope-check.mjs';
8
+ import { upgradeHint } from './upgrade.mjs';
9
+ import { openBrowser } from './sso.mjs';
10
+
11
+ /** The HawkSearch dashboard of each platform environment (where apps are reviewed and opened). */
12
+ export const DASHBOARDS = {
13
+ dev: 'https://e360.dev.bridgeline.com',
14
+ prod: 'https://app.hawksearch.com',
15
+ };
16
+
17
+ const envOf = (url) => Object.entries(PLATFORMS).find(([, u]) => u.replace(/\/+$/, '') === String(url).replace(/\/+$/, ''))?.[0];
18
+
19
+ /** The app's page in HawkSearch (optionally shown as an alias's clients see it); null for an unknown platform. */
20
+ export function appUrl(platformUrl, slug, alias) {
21
+ const dashboard = DASHBOARDS[envOf(platformUrl)];
22
+ if (!dashboard) return null;
23
+ return `${dashboard}/admin/apps/${encodeURIComponent(slug)}${alias ? `?alias=${encodeURIComponent(alias)}` : ''}`;
24
+ }
25
+
26
+ /** The developer docs (built from the hawk-apps repo's markdown; sign in with a Bridgeline account). */
27
+ export const DOCS_URL = 'https://developers.hawksearchapps.com';
28
+
29
+ /**
30
+ * `hawk-app docs <topic>`: the page for each topic. Developers' topics open the developer docs (a feature's section of
31
+ * the bridge reference); `platform` opens the engineering docs, which the site keeps out of its navigation and search.
32
+ */
33
+ export const DOCS_TOPICS = {
34
+ start: '/start/',
35
+ claude: '/start/with-claude/',
36
+ concepts: '/concepts/how-it-works/',
37
+ versions: '/concepts/versions-and-aliases/',
38
+ aliases: '/concepts/versions-and-aliases/',
39
+ bridge: '/reference/bridge/',
40
+ api: '/reference/api/',
41
+ cli: '/reference/cli/',
42
+ commands: '/reference/cli-commands/',
43
+ manifest: '/reference/hawk-app-json/',
44
+ 'hawk-app.json': '/reference/hawk-app-json/',
45
+ queries: '/reference/bridge/#custom-data-queries-dataread',
46
+ data: '/reference/bridge/#data',
47
+ functions: '/reference/bridge/#functions-functionsinvoke',
48
+ email: '/reference/bridge/#email-emailsend',
49
+ storage: '/reference/bridge/#storage-storage',
50
+ ai: '/reference/bridge/#ai-aiinvoke',
51
+ search: '/reference/bridge/#hawksearch-search-hawksearchsearch',
52
+ review: '/reference/cli/#review-in-hawksearch',
53
+ 'whats-new': '/whats-new/cli/',
54
+ platform: '/engineering/',
55
+ };
56
+
57
+ /** `hawk-app docs [topic]`: opens the developer docs (a topic's page), and prints the link. */
58
+ export async function docsCommand({ topic, openUrl = openBrowser, log = console.log }) {
59
+ const key = typeof topic === 'string' ? topic.toLowerCase() : undefined;
60
+ if (key !== undefined && !(key in DOCS_TOPICS)) {
61
+ throw new Error(`No docs topic "${topic}". Topics: ${Object.keys(DOCS_TOPICS).join(', ')}.`);
62
+ }
63
+ const url = `${DOCS_URL}${key ? DOCS_TOPICS[key] : '/'}`;
64
+ log(`Hawk Apps developer docs: ${url}`);
65
+ log(' Sign in with your Bridgeline account. The site has search (top of every page).');
66
+ try {
67
+ await openUrl(url);
68
+ } catch {
69
+ // No browser (CI, SSH): the link above is enough.
70
+ }
71
+ return 0;
72
+ }
73
+
74
+ /** `hawk-app open [--alias <name>]`: the app's page in HawkSearch, to review it, submit it for approval, or try an alias. */
75
+ export async function openCommand({ manifestFile, flags = {}, openUrl = openBrowser, log = console.log }) {
76
+ const manifest = await loadManifest(manifestFile);
77
+ const { url } = resolveSettings({ flagPlatform: flags.platform, flagEnv: flags.env, dir: dirname(resolve(manifestFile)) });
78
+ const alias = typeof flags.alias === 'string' ? flags.alias : undefined;
79
+ const page = appUrl(url, manifest.slug, alias);
80
+ if (!page) throw new Error(`No HawkSearch dashboard is known for ${describePlatform(url)}. Open the Apps page there and pick ${manifest.slug}.`);
81
+ log(`${manifest.slug} in HawkSearch${alias ? ` (as ${alias}'s clients see it)` : ''}: ${page}`);
82
+ log(' Bridgeline staff: review it there and submit it for approval (More review actions). Clients\' users see it once it\'s released.');
83
+ try {
84
+ await openUrl(page);
85
+ } catch {
86
+ // No browser (CI, SSH): the link above is enough.
87
+ }
88
+ return 0;
89
+ }
90
+
91
+ /**
92
+ * `hawk-app doctor`: everything a developer needs working, in one go: Node, the login and platform, hawk-app.json, the
93
+ * project (bridge, build script, template), scopes the code uses, the app on the platform and the MCP config. Each line
94
+ * says what to do when it isn't right. Exit 1 when something must be fixed.
95
+ */
96
+ export async function doctorCommand({ manifestFile, flags = {}, fetchImpl = fetch, log = console.log, nodeVersion = process.versions.node }) {
97
+ let failed = 0;
98
+ const ok = (text) => log(`✔ ${text}`);
99
+ const warn = (text) => log(`! ${text}`);
100
+ const fail = (text) => {
101
+ failed += 1;
102
+ log(`✖ ${text}`);
103
+ };
104
+
105
+ // Node.
106
+ const major = Number(String(nodeVersion).split('.')[0]);
107
+ if (major >= 20) ok(`Node ${nodeVersion}`);
108
+ else if (major >= 18) warn(`Node ${nodeVersion}: works, but the templates are tested on Node 20 or later.`);
109
+ else fail(`Node ${nodeVersion}: install Node 20 or later (https://nodejs.org).`);
110
+
111
+ // Login and platform.
112
+ const projectDir = dirname(resolve(manifestFile));
113
+ const settings = resolveSettings({ flagPlatform: flags.platform, flagEnv: flags.env, dir: projectDir });
114
+ if (!settings.key) {
115
+ fail(`Not logged in to ${describePlatform(settings.url)}: run \`npx hawk-app sso\` (Bridgeline account) once on this machine.`);
116
+ } else {
117
+ const check = await checkKey(settings.url, settings.key, fetchImpl);
118
+ if (check.ok) ok(`Logged in to ${describePlatform(settings.url)} (key from ${settings.keySource})`);
119
+ else fail(`${check.reason} Run \`npx hawk-app sso\` again.`);
120
+ }
121
+
122
+ // The app.
123
+ if (!existsSync(manifestFile)) {
124
+ fail(`No ${manifestFile} here: run this in the app's folder, or start one with \`npx hawk-app create <folder>\` (\`init\` for an existing project).`);
125
+ return failed ? 1 : 0;
126
+ }
127
+ let manifest;
128
+ try {
129
+ manifest = await loadManifest(manifestFile);
130
+ } catch (error) {
131
+ fail(error.message);
132
+ return 1;
133
+ }
134
+ const checked = validateManifest(manifest);
135
+ if (checked.errors.length) fail(`${manifestFile}: ${checked.errors.join(' ')} (details: npx hawk-app validate)`);
136
+ else ok(`${manifestFile}: ${manifest.slug}`);
137
+ for (const warning of checked.warnings) warn(warning);
138
+
139
+ const project = await checkProject(projectDir);
140
+ for (const warning of project.warnings) warn(warning);
141
+ if (!project.warnings.length) ok('Project: the bridge and a build script are set up');
142
+ try {
143
+ const hint = upgradeHint(projectDir, manifest);
144
+ if (hint) warn(hint.replace(/^! /, ''));
145
+ else ok('Up to date with its template');
146
+ } catch {
147
+ // An app that follows no template: nothing to compare.
148
+ }
149
+ const scopes = describeMissingScopes(await missingScopes(projectDir, manifest.scopes));
150
+ if (scopes) warn(scopes);
151
+ else ok('Scopes: hawk-app.json has the ones the code uses');
152
+
153
+ // The app on the platform.
154
+ if (settings.key) {
155
+ try {
156
+ const response = await fetchImpl(`${settings.url}/publish/apps/${encodeURIComponent(manifest.slug)}`, { headers: { Authorization: `PublisherKey ${settings.key}` } });
157
+ if (response.status === 404) warn(`${manifest.slug} isn't on ${describePlatform(settings.url)} yet: \`npm run app:dev -- --client <id>\` or \`npm run app:deploy\` registers it.`);
158
+ else if (response.status === 409) fail(`${manifest.slug} belongs to another team: pick another slug in ${manifestFile}.`);
159
+ else if (response.ok) {
160
+ const app = await response.json();
161
+ ok(`On the platform: ${app.status === 'Live' && app.liveVersion ? `released v${app.liveVersion}` : 'not released yet'} (npx hawk-app status)`);
162
+ }
163
+ } catch {
164
+ // Already reported with the login check.
165
+ }
166
+ }
167
+
168
+ // AI assistants.
169
+ const mcp = join(projectDir, '.mcp.json');
170
+ if (existsSync(mcp) && readFileSync(mcp, 'utf8').includes('hawk-apps')) ok('MCP server set up for AI assistants (.mcp.json)');
171
+ else warn('No MCP config for AI assistants: `npx hawk-app upgrade --yes` adds .mcp.json and .vscode/mcp.json.');
172
+
173
+ log(failed ? `\n${failed} thing${failed === 1 ? '' : 's'} to fix above.` : '\nAll set.');
174
+ log(`Docs: ${DOCS_URL} (npx hawk-app docs)`);
175
+ return failed ? 1 : 0;
176
+ }
package/src/env.mjs CHANGED
@@ -1,140 +1,140 @@
1
- import { spawnSync } from 'node:child_process';
2
- import { existsSync, readFileSync } from 'node:fs';
3
- import { join } from 'node:path';
4
- import { credentialsPath, normalizeUrl, readCredentials } from './credentials.mjs';
5
-
6
- export const ENV_FILES = ['.env.local', '.env'];
7
-
8
- /** The platform of each environment (`--env` / HAWK_APPS_ENV). */
9
- export const PLATFORMS = {
10
- dev: 'https://api-dev.hawksearchapps.com',
11
- prod: 'https://api.hawksearchapps.com',
12
- };
13
-
14
- /** Dev, unless a command, the environment or the project says otherwise: prod is never a default. */
15
- export const DEFAULT_PLATFORM_URL = PLATFORMS.dev;
16
-
17
- /** The URL of an environment name; throws for an unknown one (a typo must not fall back to dev or prod). */
18
- export function platformForEnv(name, source) {
19
- if (!name) return undefined;
20
- const url = PLATFORMS[String(name).trim().toLowerCase()];
21
- if (!url) throw new Error(`Unknown environment "${name}" (${source}): use ${Object.keys(PLATFORMS).join(' or ')}.`);
22
- return url;
23
- }
24
-
25
- /** "prod (https://…)" for a known platform, else the URL. */
26
- export function describePlatform(url) {
27
- const name = Object.entries(PLATFORMS).find(([, u]) => normalizeUrl(u) === normalizeUrl(url))?.[0];
28
- return name ? `${name} (${url})` : String(url);
29
- }
30
- const PREFIX = 'HAWK_APPS_';
31
-
32
- /** Parses KEY=VALUE lines; supports comments, blank lines, `export KEY=...` and single/double quotes. */
33
- export function parseEnv(text) {
34
- const values = {};
35
- for (const raw of text.split(/\r?\n/)) {
36
- const line = raw.trim();
37
- if (!line || line.startsWith('#')) continue;
38
- const match = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/.exec(line);
39
- if (!match) continue;
40
- let value = match[2].trim();
41
- if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
42
- value = value.slice(1, -1);
43
- } else {
44
- value = value.replace(/\s+#.*$/, '');
45
- }
46
- values[match[1]] = value;
47
- }
48
- return values;
49
- }
50
-
51
- /**
52
- * Loads HAWK_APPS_* settings from .env.local / .env in `dir`. Variables already set in the environment win,
53
- * so CI secrets always take precedence. Only HAWK_APPS_* keys are applied.
54
- * @returns {{ loaded: string[], warnings: string[] }}
55
- */
56
- export function loadEnv(dir = process.cwd(), env = process.env, isIgnored = gitIgnored) {
57
- const loaded = [];
58
- const warnings = [];
59
- for (const file of ENV_FILES) {
60
- const path = join(dir, file);
61
- if (!existsSync(path)) continue;
62
- const values = parseEnv(readFileSync(path, 'utf8'));
63
- for (const [key, value] of Object.entries(values)) {
64
- if (!key.startsWith(PREFIX) || !value || env[key] !== undefined) continue;
65
- env[key] = value;
66
- loaded.push(`${key} (${file})`);
67
- }
68
- if (values.HAWK_APPS_PUBLISHER_KEY && isIgnored(dir, file) === false) {
69
- warnings.push(`${file} contains your publisher key but is not git-ignored. Add it to .gitignore so the key is never committed.`);
70
- }
71
- }
72
- return { loaded, warnings };
73
- }
74
-
75
- /**
76
- * Where the platform URL and publisher key come from, highest priority first:
77
- * URL: --platform / --env, HAWK_APPS_PLATFORM_URL / HAWK_APPS_ENV, project .env.local / .env (the same two), dev.
78
- * `hawk-app login` never changes it: logging in to prod doesn't make later commands use prod.
79
- * Key: HAWK_APPS_PUBLISHER_KEY, project .env.local / .env, the key saved by `hawk-app login` for that URL.
80
- */
81
- // credentialsFile defaults to this machine's `hawk-app login` file, so every command sees the saved login (not just whoami).
82
- export function resolveSettings({ flagPlatform, flagEnv, env = process.env, dir = process.cwd(), credentialsFile = credentialsPath(env) } = {}) {
83
- if (flagPlatform && flagEnv) throw new Error('Use --env or --platform, not both.');
84
- const fileValues = {};
85
- const fileSource = {};
86
- for (const file of [...ENV_FILES].reverse()) {
87
- const path = join(dir, file);
88
- if (!existsSync(path)) continue;
89
- for (const [key, value] of Object.entries(parseEnv(readFileSync(path, 'utf8')))) {
90
- if (key.startsWith(PREFIX) && value) {
91
- fileValues[key] = value;
92
- fileSource[key] = file;
93
- }
94
- }
95
- }
96
- const credentials = readCredentials(credentialsFile);
97
-
98
- const [url, urlSource] = first([
99
- [flagPlatform, '--platform'],
100
- [platformForEnv(flagEnv, '--env'), '--env'],
101
- [env.HAWK_APPS_PLATFORM_URL, 'environment'],
102
- [platformForEnv(env.HAWK_APPS_ENV, 'HAWK_APPS_ENV'), 'HAWK_APPS_ENV'],
103
- [fileValues.HAWK_APPS_PLATFORM_URL, fileSource.HAWK_APPS_PLATFORM_URL],
104
- [platformForEnv(fileValues.HAWK_APPS_ENV, fileSource.HAWK_APPS_ENV), fileSource.HAWK_APPS_ENV],
105
- [DEFAULT_PLATFORM_URL, 'default (dev)'],
106
- ]);
107
- const platform = normalizeUrl(url);
108
- const [key, keySource] = first([
109
- [env.HAWK_APPS_PUBLISHER_KEY, 'environment'],
110
- [fileValues.HAWK_APPS_PUBLISHER_KEY, fileSource.HAWK_APPS_PUBLISHER_KEY],
111
- [credentials.platforms[platform]?.publisherKey, credentialsFile],
112
- ]);
113
- return { url: platform, urlSource, key, keySource, credentialsFile };
114
- }
115
-
116
- function first(candidates) {
117
- return candidates.find(([value]) => value) ?? [undefined, undefined];
118
- }
119
-
120
- /** true / false inside a git repo; undefined when git isn't available or this isn't a repo. */
121
- function gitIgnored(dir, file) {
122
- const result = spawnSync('git', ['check-ignore', '-q', file], { cwd: dir, stdio: 'ignore' });
123
- if (result.error || result.status === 128) return undefined;
124
- return result.status === 0;
125
- }
126
-
127
- export const ENV_EXAMPLE = `# Optional per-project overrides. Normally you don't need this file: run \`npx hawk-app sso\` once per machine.
128
- # To override for this project only, copy to .env.local (git-ignored) and uncomment.
129
- # Environment variables (e.g. CI secrets) take precedence over both.
130
-
131
- # The environment this project deploys to (dev unless set; or pass --env prod to a command).
132
- # HAWK_APPS_ENV=prod
133
-
134
- # Publisher key for this project only. Prefer \`npx hawk-app login --local\` so it isn't typed into a file. Never commit it.
135
- # HAWK_APPS_PUBLISHER_KEY=
136
-
137
- # The client (and site) \`hawk-app dev\` runs the app with; it asks the first time and saves the client here.
138
- # HAWK_APPS_DEV_CLIENT=
139
- # HAWK_APPS_DEV_ENGINE=
140
- `;
1
+ import { spawnSync } from 'node:child_process';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { credentialsPath, normalizeUrl, readCredentials } from './credentials.mjs';
5
+
6
+ export const ENV_FILES = ['.env.local', '.env'];
7
+
8
+ /** The platform of each environment (`--env` / HAWK_APPS_ENV). */
9
+ export const PLATFORMS = {
10
+ dev: 'https://api-dev.hawksearchapps.com',
11
+ prod: 'https://api.hawksearchapps.com',
12
+ };
13
+
14
+ /** Dev, unless a command, the environment or the project says otherwise: prod is never a default. */
15
+ export const DEFAULT_PLATFORM_URL = PLATFORMS.dev;
16
+
17
+ /** The URL of an environment name; throws for an unknown one (a typo must not fall back to dev or prod). */
18
+ export function platformForEnv(name, source) {
19
+ if (!name) return undefined;
20
+ const url = PLATFORMS[String(name).trim().toLowerCase()];
21
+ if (!url) throw new Error(`Unknown environment "${name}" (${source}): use ${Object.keys(PLATFORMS).join(' or ')}.`);
22
+ return url;
23
+ }
24
+
25
+ /** "prod (https://…)" for a known platform, else the URL. */
26
+ export function describePlatform(url) {
27
+ const name = Object.entries(PLATFORMS).find(([, u]) => normalizeUrl(u) === normalizeUrl(url))?.[0];
28
+ return name ? `${name} (${url})` : String(url);
29
+ }
30
+ const PREFIX = 'HAWK_APPS_';
31
+
32
+ /** Parses KEY=VALUE lines; supports comments, blank lines, `export KEY=...` and single/double quotes. */
33
+ export function parseEnv(text) {
34
+ const values = {};
35
+ for (const raw of text.split(/\r?\n/)) {
36
+ const line = raw.trim();
37
+ if (!line || line.startsWith('#')) continue;
38
+ const match = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/.exec(line);
39
+ if (!match) continue;
40
+ let value = match[2].trim();
41
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
42
+ value = value.slice(1, -1);
43
+ } else {
44
+ value = value.replace(/\s+#.*$/, '');
45
+ }
46
+ values[match[1]] = value;
47
+ }
48
+ return values;
49
+ }
50
+
51
+ /**
52
+ * Loads HAWK_APPS_* settings from .env.local / .env in `dir`. Variables already set in the environment win,
53
+ * so CI secrets always take precedence. Only HAWK_APPS_* keys are applied.
54
+ * @returns {{ loaded: string[], warnings: string[] }}
55
+ */
56
+ export function loadEnv(dir = process.cwd(), env = process.env, isIgnored = gitIgnored) {
57
+ const loaded = [];
58
+ const warnings = [];
59
+ for (const file of ENV_FILES) {
60
+ const path = join(dir, file);
61
+ if (!existsSync(path)) continue;
62
+ const values = parseEnv(readFileSync(path, 'utf8'));
63
+ for (const [key, value] of Object.entries(values)) {
64
+ if (!key.startsWith(PREFIX) || !value || env[key] !== undefined) continue;
65
+ env[key] = value;
66
+ loaded.push(`${key} (${file})`);
67
+ }
68
+ if (values.HAWK_APPS_PUBLISHER_KEY && isIgnored(dir, file) === false) {
69
+ warnings.push(`${file} contains your publisher key but is not git-ignored. Add it to .gitignore so the key is never committed.`);
70
+ }
71
+ }
72
+ return { loaded, warnings };
73
+ }
74
+
75
+ /**
76
+ * Where the platform URL and publisher key come from, highest priority first:
77
+ * URL: --platform / --env, HAWK_APPS_PLATFORM_URL / HAWK_APPS_ENV, project .env.local / .env (the same two), dev.
78
+ * `hawk-app login` never changes it: logging in to prod doesn't make later commands use prod.
79
+ * Key: HAWK_APPS_PUBLISHER_KEY, project .env.local / .env, the key saved by `hawk-app login` for that URL.
80
+ */
81
+ // credentialsFile defaults to this machine's `hawk-app login` file, so every command sees the saved login (not just whoami).
82
+ export function resolveSettings({ flagPlatform, flagEnv, env = process.env, dir = process.cwd(), credentialsFile = credentialsPath(env) } = {}) {
83
+ if (flagPlatform && flagEnv) throw new Error('Use --env or --platform, not both.');
84
+ const fileValues = {};
85
+ const fileSource = {};
86
+ for (const file of [...ENV_FILES].reverse()) {
87
+ const path = join(dir, file);
88
+ if (!existsSync(path)) continue;
89
+ for (const [key, value] of Object.entries(parseEnv(readFileSync(path, 'utf8')))) {
90
+ if (key.startsWith(PREFIX) && value) {
91
+ fileValues[key] = value;
92
+ fileSource[key] = file;
93
+ }
94
+ }
95
+ }
96
+ const credentials = readCredentials(credentialsFile);
97
+
98
+ const [url, urlSource] = first([
99
+ [flagPlatform, '--platform'],
100
+ [platformForEnv(flagEnv, '--env'), '--env'],
101
+ [env.HAWK_APPS_PLATFORM_URL, 'environment'],
102
+ [platformForEnv(env.HAWK_APPS_ENV, 'HAWK_APPS_ENV'), 'HAWK_APPS_ENV'],
103
+ [fileValues.HAWK_APPS_PLATFORM_URL, fileSource.HAWK_APPS_PLATFORM_URL],
104
+ [platformForEnv(fileValues.HAWK_APPS_ENV, fileSource.HAWK_APPS_ENV), fileSource.HAWK_APPS_ENV],
105
+ [DEFAULT_PLATFORM_URL, 'default (dev)'],
106
+ ]);
107
+ const platform = normalizeUrl(url);
108
+ const [key, keySource] = first([
109
+ [env.HAWK_APPS_PUBLISHER_KEY, 'environment'],
110
+ [fileValues.HAWK_APPS_PUBLISHER_KEY, fileSource.HAWK_APPS_PUBLISHER_KEY],
111
+ [credentials.platforms[platform]?.publisherKey, credentialsFile],
112
+ ]);
113
+ return { url: platform, urlSource, key, keySource, credentialsFile };
114
+ }
115
+
116
+ function first(candidates) {
117
+ return candidates.find(([value]) => value) ?? [undefined, undefined];
118
+ }
119
+
120
+ /** true / false inside a git repo; undefined when git isn't available or this isn't a repo. */
121
+ function gitIgnored(dir, file) {
122
+ const result = spawnSync('git', ['check-ignore', '-q', file], { cwd: dir, stdio: 'ignore' });
123
+ if (result.error || result.status === 128) return undefined;
124
+ return result.status === 0;
125
+ }
126
+
127
+ export const ENV_EXAMPLE = `# Optional per-project overrides. Normally you don't need this file: run \`npx hawk-app sso\` once per machine.
128
+ # To override for this project only, copy to .env.local (git-ignored) and uncomment.
129
+ # Environment variables (e.g. CI secrets) take precedence over both.
130
+
131
+ # The environment this project deploys to (dev unless set; or pass --env prod to a command).
132
+ # HAWK_APPS_ENV=prod
133
+
134
+ # Publisher key for this project only. Prefer \`npx hawk-app login --local\` so it isn't typed into a file. Never commit it.
135
+ # HAWK_APPS_PUBLISHER_KEY=
136
+
137
+ # The client (and site) \`hawk-app dev\` runs the app with; it asks the first time and saves the client here.
138
+ # HAWK_APPS_DEV_CLIENT=
139
+ # HAWK_APPS_DEV_ENGINE=
140
+ `;