@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/login.mjs
CHANGED
|
@@ -1,166 +1,166 @@
|
|
|
1
|
-
import { spawnSync } from 'node:child_process';
|
|
2
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
-
import { appendFile, writeFile } from 'node:fs/promises';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
import { credentialsPath, removeCredential, saveCredential } from './credentials.mjs';
|
|
6
|
-
import { describePlatform, resolveSettings } from './env.mjs';
|
|
7
|
-
|
|
8
|
-
/** Reads a line from the terminal without echoing it. */
|
|
9
|
-
export function promptHidden(
|
|
10
|
-
question,
|
|
11
|
-
{
|
|
12
|
-
input = process.stdin,
|
|
13
|
-
output = process.stdout,
|
|
14
|
-
notInteractive = '`hawk-app login` needs an interactive terminal. In CI, set HAWK_APPS_PUBLISHER_KEY as a secured variable instead.',
|
|
15
|
-
} = {},
|
|
16
|
-
) {
|
|
17
|
-
if (!input.isTTY) {
|
|
18
|
-
return Promise.reject(new Error(notInteractive));
|
|
19
|
-
}
|
|
20
|
-
return new Promise((resolve, reject) => {
|
|
21
|
-
let value = '';
|
|
22
|
-
output.write(question);
|
|
23
|
-
input.setRawMode(true);
|
|
24
|
-
input.resume();
|
|
25
|
-
input.setEncoding('utf8');
|
|
26
|
-
const onData = (chunk) => {
|
|
27
|
-
for (const ch of chunk) {
|
|
28
|
-
if (ch === '\r' || ch === '\n') {
|
|
29
|
-
finish();
|
|
30
|
-
return resolve(value.trim());
|
|
31
|
-
}
|
|
32
|
-
if (ch === '\u0003') {
|
|
33
|
-
finish();
|
|
34
|
-
return reject(new Error('Cancelled.'));
|
|
35
|
-
}
|
|
36
|
-
if (ch === '\u007f' || ch === '\b') value = value.slice(0, -1);
|
|
37
|
-
else if (ch >= ' ') value += ch;
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
|
-
function finish() {
|
|
41
|
-
input.off('data', onData);
|
|
42
|
-
input.setRawMode(false);
|
|
43
|
-
input.pause();
|
|
44
|
-
output.write('\n');
|
|
45
|
-
}
|
|
46
|
-
input.on('data', onData);
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** Sets or replaces KEY=value lines in a .env file, keeping everything else (comments, other keys) as is. */
|
|
51
|
-
export function upsertEnv(text, values) {
|
|
52
|
-
const lines = text ? text.split(/\r?\n/) : [];
|
|
53
|
-
const pending = { ...values };
|
|
54
|
-
const updated = lines.map((line) => {
|
|
55
|
-
const match = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/.exec(line);
|
|
56
|
-
if (match && match[1] in pending) {
|
|
57
|
-
const next = `${match[1]}=${pending[match[1]]}`;
|
|
58
|
-
delete pending[match[1]];
|
|
59
|
-
return next;
|
|
60
|
-
}
|
|
61
|
-
return line;
|
|
62
|
-
});
|
|
63
|
-
while (updated.length && updated[updated.length - 1] === '') updated.pop();
|
|
64
|
-
for (const [key, value] of Object.entries(pending)) updated.push(`${key}=${value}`);
|
|
65
|
-
return updated.join('\n') + '\n';
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** Checks a key with the platform: 404 = accepted (no app by that name), 401 = rejected. */
|
|
69
|
-
export async function checkKey(url, key, fetchImpl = fetch) {
|
|
70
|
-
let response;
|
|
71
|
-
try {
|
|
72
|
-
response = await fetchImpl(`${url}/publish/apps/login-check`, { headers: { Authorization: `PublisherKey ${key}` } });
|
|
73
|
-
} catch (error) {
|
|
74
|
-
return { ok: false, reason: `Could not reach ${url}: ${error.message}.` };
|
|
75
|
-
}
|
|
76
|
-
if (response.status === 401) return { ok: false, reason: 'The platform rejected the key.' };
|
|
77
|
-
if (response.status !== 404 && !response.ok) return { ok: false, reason: `Unexpected response from the platform (${response.status}).` };
|
|
78
|
-
return { ok: true };
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const publisherOf = (key) => key.split('.')[0];
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Asks for the publisher key, checks it with the platform and saves it once for this machine, so every app
|
|
85
|
-
* uses it. With `local: true` it goes into this project's .env.local instead. The key never appears on
|
|
86
|
-
* screen, in shell history or as a command-line argument.
|
|
87
|
-
*/
|
|
88
|
-
export async function login({
|
|
89
|
-
dir = process.cwd(),
|
|
90
|
-
platform,
|
|
91
|
-
env,
|
|
92
|
-
local = false,
|
|
93
|
-
prompt = promptHidden,
|
|
94
|
-
fetchImpl = fetch,
|
|
95
|
-
log = console.log,
|
|
96
|
-
isIgnored = gitIgnored,
|
|
97
|
-
credentialsFile = credentialsPath(),
|
|
98
|
-
}) {
|
|
99
|
-
const { url } = resolveSettings({ flagPlatform: platform, flagEnv: env, dir, credentialsFile });
|
|
100
|
-
|
|
101
|
-
log(`Platform: ${describePlatform(url)}`);
|
|
102
|
-
const key = await prompt('Publisher key (input hidden): ');
|
|
103
|
-
if (!/^[a-z0-9-]+\.[A-Za-z0-9_-]{16,}$/.test(key)) throw new Error('That does not look like a publisher key ("<publisherId>.<secret>"). Nothing was saved.');
|
|
104
|
-
const check = await checkKey(url, key, fetchImpl);
|
|
105
|
-
if (!check.ok) throw new Error(`${check.reason} Nothing was saved.`);
|
|
106
|
-
|
|
107
|
-
if (!local) {
|
|
108
|
-
await saveCredential(credentialsFile, url, key);
|
|
109
|
-
log(`✔ Logged in to ${describePlatform(url)} as publisher "${publisherOf(key)}".`);
|
|
110
|
-
log(` Saved for every app on this machine: ${credentialsFile}`);
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const envPath = join(dir, '.env.local');
|
|
115
|
-
const existing = existsSync(envPath) ? readFileSync(envPath, 'utf8') : '';
|
|
116
|
-
await writeFile(envPath, upsertEnv(existing, { HAWK_APPS_PLATFORM_URL: url, HAWK_APPS_PUBLISHER_KEY: key }));
|
|
117
|
-
log(`✔ Logged in to ${url} as publisher "${publisherOf(key)}" for this project only (.env.local).`);
|
|
118
|
-
if (isIgnored(dir, '.env.local') === false || (isIgnored(dir, '.env.local') === undefined && !ignoredByGitignoreFile(dir))) {
|
|
119
|
-
const gitignore = join(dir, '.gitignore');
|
|
120
|
-
const needsNewline = existsSync(gitignore) && !readFileSync(gitignore, 'utf8').endsWith('\n');
|
|
121
|
-
await appendFile(gitignore, `${needsNewline ? '\n' : ''}.env.local\n`);
|
|
122
|
-
log(' Added .env.local to .gitignore so the key is never committed.');
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/** Shows which platform and key would be used here, and whether the platform accepts the key. */
|
|
127
|
-
export async function whoami({ dir = process.cwd(), platform, env, fetchImpl = fetch, log = console.log, credentialsFile = credentialsPath() }) {
|
|
128
|
-
const settings = resolveSettings({ flagPlatform: platform, flagEnv: env, dir, credentialsFile });
|
|
129
|
-
log(`Platform: ${describePlatform(settings.url)} (from ${settings.urlSource})`);
|
|
130
|
-
if (!settings.key) {
|
|
131
|
-
log('✖ Not logged in. Run `npx hawk-app sso` (Bridgeline account) or `npx hawk-app login` (publisher key) once on this machine.');
|
|
132
|
-
return 1;
|
|
133
|
-
}
|
|
134
|
-
const check = await checkKey(settings.url, settings.key, fetchImpl);
|
|
135
|
-
if (!check.ok) {
|
|
136
|
-
log(`✖ ${check.reason} Key from ${settings.keySource}. Run \`npx hawk-app sso\` or \`npx hawk-app login\` again.`);
|
|
137
|
-
return 1;
|
|
138
|
-
}
|
|
139
|
-
log(`✔ Logged in as publisher "${publisherOf(settings.key)}" (key from ${settings.keySource})`);
|
|
140
|
-
return 0;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/** Removes this machine's saved key for the platform. Keys in .env.local or the environment are not touched. */
|
|
144
|
-
export async function logout({ dir = process.cwd(), platform, env, log = console.log, credentialsFile = credentialsPath() }) {
|
|
145
|
-
const { url } = resolveSettings({ flagPlatform: platform, flagEnv: env, dir, credentialsFile });
|
|
146
|
-
if (await removeCredential(credentialsFile, url)) log(`✔ Logged out of ${url} on this machine.`);
|
|
147
|
-
else log(`Not logged in to ${url} on this machine.`);
|
|
148
|
-
const remaining = resolveSettings({ flagPlatform: platform, flagEnv: env, dir, credentialsFile });
|
|
149
|
-
if (remaining.key) log(` Note: a key is still set in ${remaining.keySource}; that one still applies here.`);
|
|
150
|
-
return 0;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function ignoredByGitignoreFile(dir) {
|
|
154
|
-
const path = join(dir, '.gitignore');
|
|
155
|
-
if (!existsSync(path)) return false;
|
|
156
|
-
return readFileSync(path, 'utf8')
|
|
157
|
-
.split(/\r?\n/)
|
|
158
|
-
.map((l) => l.trim())
|
|
159
|
-
.some((l) => l === '.env.local' || l === '.env*' || l === '.env.*' || l === '*.local');
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function gitIgnored(dir, file) {
|
|
163
|
-
const result = spawnSync('git', ['check-ignore', '-q', file], { cwd: dir, stdio: 'ignore' });
|
|
164
|
-
if (result.error || result.status === 128) return undefined;
|
|
165
|
-
return result.status === 0;
|
|
166
|
-
}
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { appendFile, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { credentialsPath, removeCredential, saveCredential } from './credentials.mjs';
|
|
6
|
+
import { describePlatform, resolveSettings } from './env.mjs';
|
|
7
|
+
|
|
8
|
+
/** Reads a line from the terminal without echoing it. */
|
|
9
|
+
export function promptHidden(
|
|
10
|
+
question,
|
|
11
|
+
{
|
|
12
|
+
input = process.stdin,
|
|
13
|
+
output = process.stdout,
|
|
14
|
+
notInteractive = '`hawk-app login` needs an interactive terminal. In CI, set HAWK_APPS_PUBLISHER_KEY as a secured variable instead.',
|
|
15
|
+
} = {},
|
|
16
|
+
) {
|
|
17
|
+
if (!input.isTTY) {
|
|
18
|
+
return Promise.reject(new Error(notInteractive));
|
|
19
|
+
}
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
let value = '';
|
|
22
|
+
output.write(question);
|
|
23
|
+
input.setRawMode(true);
|
|
24
|
+
input.resume();
|
|
25
|
+
input.setEncoding('utf8');
|
|
26
|
+
const onData = (chunk) => {
|
|
27
|
+
for (const ch of chunk) {
|
|
28
|
+
if (ch === '\r' || ch === '\n') {
|
|
29
|
+
finish();
|
|
30
|
+
return resolve(value.trim());
|
|
31
|
+
}
|
|
32
|
+
if (ch === '\u0003') {
|
|
33
|
+
finish();
|
|
34
|
+
return reject(new Error('Cancelled.'));
|
|
35
|
+
}
|
|
36
|
+
if (ch === '\u007f' || ch === '\b') value = value.slice(0, -1);
|
|
37
|
+
else if (ch >= ' ') value += ch;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
function finish() {
|
|
41
|
+
input.off('data', onData);
|
|
42
|
+
input.setRawMode(false);
|
|
43
|
+
input.pause();
|
|
44
|
+
output.write('\n');
|
|
45
|
+
}
|
|
46
|
+
input.on('data', onData);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Sets or replaces KEY=value lines in a .env file, keeping everything else (comments, other keys) as is. */
|
|
51
|
+
export function upsertEnv(text, values) {
|
|
52
|
+
const lines = text ? text.split(/\r?\n/) : [];
|
|
53
|
+
const pending = { ...values };
|
|
54
|
+
const updated = lines.map((line) => {
|
|
55
|
+
const match = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/.exec(line);
|
|
56
|
+
if (match && match[1] in pending) {
|
|
57
|
+
const next = `${match[1]}=${pending[match[1]]}`;
|
|
58
|
+
delete pending[match[1]];
|
|
59
|
+
return next;
|
|
60
|
+
}
|
|
61
|
+
return line;
|
|
62
|
+
});
|
|
63
|
+
while (updated.length && updated[updated.length - 1] === '') updated.pop();
|
|
64
|
+
for (const [key, value] of Object.entries(pending)) updated.push(`${key}=${value}`);
|
|
65
|
+
return updated.join('\n') + '\n';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Checks a key with the platform: 404 = accepted (no app by that name), 401 = rejected. */
|
|
69
|
+
export async function checkKey(url, key, fetchImpl = fetch) {
|
|
70
|
+
let response;
|
|
71
|
+
try {
|
|
72
|
+
response = await fetchImpl(`${url}/publish/apps/login-check`, { headers: { Authorization: `PublisherKey ${key}` } });
|
|
73
|
+
} catch (error) {
|
|
74
|
+
return { ok: false, reason: `Could not reach ${url}: ${error.message}.` };
|
|
75
|
+
}
|
|
76
|
+
if (response.status === 401) return { ok: false, reason: 'The platform rejected the key.' };
|
|
77
|
+
if (response.status !== 404 && !response.ok) return { ok: false, reason: `Unexpected response from the platform (${response.status}).` };
|
|
78
|
+
return { ok: true };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const publisherOf = (key) => key.split('.')[0];
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Asks for the publisher key, checks it with the platform and saves it once for this machine, so every app
|
|
85
|
+
* uses it. With `local: true` it goes into this project's .env.local instead. The key never appears on
|
|
86
|
+
* screen, in shell history or as a command-line argument.
|
|
87
|
+
*/
|
|
88
|
+
export async function login({
|
|
89
|
+
dir = process.cwd(),
|
|
90
|
+
platform,
|
|
91
|
+
env,
|
|
92
|
+
local = false,
|
|
93
|
+
prompt = promptHidden,
|
|
94
|
+
fetchImpl = fetch,
|
|
95
|
+
log = console.log,
|
|
96
|
+
isIgnored = gitIgnored,
|
|
97
|
+
credentialsFile = credentialsPath(),
|
|
98
|
+
}) {
|
|
99
|
+
const { url } = resolveSettings({ flagPlatform: platform, flagEnv: env, dir, credentialsFile });
|
|
100
|
+
|
|
101
|
+
log(`Platform: ${describePlatform(url)}`);
|
|
102
|
+
const key = await prompt('Publisher key (input hidden): ');
|
|
103
|
+
if (!/^[a-z0-9-]+\.[A-Za-z0-9_-]{16,}$/.test(key)) throw new Error('That does not look like a publisher key ("<publisherId>.<secret>"). Nothing was saved.');
|
|
104
|
+
const check = await checkKey(url, key, fetchImpl);
|
|
105
|
+
if (!check.ok) throw new Error(`${check.reason} Nothing was saved.`);
|
|
106
|
+
|
|
107
|
+
if (!local) {
|
|
108
|
+
await saveCredential(credentialsFile, url, key);
|
|
109
|
+
log(`✔ Logged in to ${describePlatform(url)} as publisher "${publisherOf(key)}".`);
|
|
110
|
+
log(` Saved for every app on this machine: ${credentialsFile}`);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const envPath = join(dir, '.env.local');
|
|
115
|
+
const existing = existsSync(envPath) ? readFileSync(envPath, 'utf8') : '';
|
|
116
|
+
await writeFile(envPath, upsertEnv(existing, { HAWK_APPS_PLATFORM_URL: url, HAWK_APPS_PUBLISHER_KEY: key }));
|
|
117
|
+
log(`✔ Logged in to ${url} as publisher "${publisherOf(key)}" for this project only (.env.local).`);
|
|
118
|
+
if (isIgnored(dir, '.env.local') === false || (isIgnored(dir, '.env.local') === undefined && !ignoredByGitignoreFile(dir))) {
|
|
119
|
+
const gitignore = join(dir, '.gitignore');
|
|
120
|
+
const needsNewline = existsSync(gitignore) && !readFileSync(gitignore, 'utf8').endsWith('\n');
|
|
121
|
+
await appendFile(gitignore, `${needsNewline ? '\n' : ''}.env.local\n`);
|
|
122
|
+
log(' Added .env.local to .gitignore so the key is never committed.');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Shows which platform and key would be used here, and whether the platform accepts the key. */
|
|
127
|
+
export async function whoami({ dir = process.cwd(), platform, env, fetchImpl = fetch, log = console.log, credentialsFile = credentialsPath() }) {
|
|
128
|
+
const settings = resolveSettings({ flagPlatform: platform, flagEnv: env, dir, credentialsFile });
|
|
129
|
+
log(`Platform: ${describePlatform(settings.url)} (from ${settings.urlSource})`);
|
|
130
|
+
if (!settings.key) {
|
|
131
|
+
log('✖ Not logged in. Run `npx hawk-app sso` (Bridgeline account) or `npx hawk-app login` (publisher key) once on this machine.');
|
|
132
|
+
return 1;
|
|
133
|
+
}
|
|
134
|
+
const check = await checkKey(settings.url, settings.key, fetchImpl);
|
|
135
|
+
if (!check.ok) {
|
|
136
|
+
log(`✖ ${check.reason} Key from ${settings.keySource}. Run \`npx hawk-app sso\` or \`npx hawk-app login\` again.`);
|
|
137
|
+
return 1;
|
|
138
|
+
}
|
|
139
|
+
log(`✔ Logged in as publisher "${publisherOf(settings.key)}" (key from ${settings.keySource})`);
|
|
140
|
+
return 0;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Removes this machine's saved key for the platform. Keys in .env.local or the environment are not touched. */
|
|
144
|
+
export async function logout({ dir = process.cwd(), platform, env, log = console.log, credentialsFile = credentialsPath() }) {
|
|
145
|
+
const { url } = resolveSettings({ flagPlatform: platform, flagEnv: env, dir, credentialsFile });
|
|
146
|
+
if (await removeCredential(credentialsFile, url)) log(`✔ Logged out of ${url} on this machine.`);
|
|
147
|
+
else log(`Not logged in to ${url} on this machine.`);
|
|
148
|
+
const remaining = resolveSettings({ flagPlatform: platform, flagEnv: env, dir, credentialsFile });
|
|
149
|
+
if (remaining.key) log(` Note: a key is still set in ${remaining.keySource}; that one still applies here.`);
|
|
150
|
+
return 0;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function ignoredByGitignoreFile(dir) {
|
|
154
|
+
const path = join(dir, '.gitignore');
|
|
155
|
+
if (!existsSync(path)) return false;
|
|
156
|
+
return readFileSync(path, 'utf8')
|
|
157
|
+
.split(/\r?\n/)
|
|
158
|
+
.map((l) => l.trim())
|
|
159
|
+
.some((l) => l === '.env.local' || l === '.env*' || l === '.env.*' || l === '*.local');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function gitIgnored(dir, file) {
|
|
163
|
+
const result = spawnSync('git', ['check-ignore', '-q', file], { cwd: dir, stdio: 'ignore' });
|
|
164
|
+
if (result.error || result.status === 128) return undefined;
|
|
165
|
+
return result.status === 0;
|
|
166
|
+
}
|
package/src/logs.mjs
CHANGED
|
@@ -1,64 +1,64 @@
|
|
|
1
|
-
export const FOLLOW_INTERVAL_MS = 2_000;
|
|
2
|
-
const SINCE = /^\d+[mhd]$/;
|
|
3
|
-
const USAGE = 'Usage: hawk-app logs <function> [--since 15m|1h|1d|<ISO time>] [--follow]';
|
|
4
|
-
|
|
5
|
-
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* `hawk-app logs <function>`: prints the function's log events (all versions), page by page. With --follow it keeps
|
|
9
|
-
* polling every 2 s (with the platform's nextToken, or from the last event's time) until Ctrl+C or `signal` aborts.
|
|
10
|
-
*/
|
|
11
|
-
export async function logsCommand({ client, manifest, name, flags = {}, sleep = wait, signal, log = console.log }) {
|
|
12
|
-
if (!name) throw new Error(USAGE);
|
|
13
|
-
const declared = manifest.functions && typeof manifest.functions === 'object' ? Object.keys(manifest.functions) : [];
|
|
14
|
-
if (!declared.includes(name)) {
|
|
15
|
-
throw new Error(`"${name}" is not a function of ${manifest.slug}${declared.length ? ` (functions: ${declared.join(', ')})` : ' (hawk-app.json declares no functions)'}.`);
|
|
16
|
-
}
|
|
17
|
-
let since = flags.since;
|
|
18
|
-
if (since !== undefined && (typeof since !== 'string' || (!SINCE.test(since) && Number.isNaN(Date.parse(since))))) {
|
|
19
|
-
throw new Error(`--since takes a duration (15m, 1h, 1d) or an ISO time, not "${since}".`);
|
|
20
|
-
}
|
|
21
|
-
const follow = !!flags.follow;
|
|
22
|
-
const platform = client();
|
|
23
|
-
|
|
24
|
-
let nextToken;
|
|
25
|
-
let printed = 0;
|
|
26
|
-
// Events already printed at the latest timestamp, so polling again from that time doesn't repeat them.
|
|
27
|
-
let lastTime;
|
|
28
|
-
let seenAtLast = new Set();
|
|
29
|
-
if (follow) log(`Following ${name} logs (Ctrl+C to stop)…`);
|
|
30
|
-
|
|
31
|
-
while (!signal?.aborted) {
|
|
32
|
-
const page = await platform.functionLogs(manifest.slug, name, { since, nextToken });
|
|
33
|
-
for (const event of page.events ?? []) {
|
|
34
|
-
const time = toIso(event.timestamp);
|
|
35
|
-
const key = `${time}\n${event.message}`;
|
|
36
|
-
if (time === lastTime && seenAtLast.has(key)) continue;
|
|
37
|
-
if (time !== lastTime) {
|
|
38
|
-
lastTime = time;
|
|
39
|
-
seenAtLast = new Set();
|
|
40
|
-
}
|
|
41
|
-
seenAtLast.add(key);
|
|
42
|
-
log(`${time} ${String(event.message ?? '').replace(/\s+$/, '')}`);
|
|
43
|
-
printed += 1;
|
|
44
|
-
}
|
|
45
|
-
if (page.nextToken) {
|
|
46
|
-
nextToken = page.nextToken;
|
|
47
|
-
// More pages now; when following, the platform may also hand back a token to poll with.
|
|
48
|
-
if (page.events?.length || !follow) continue;
|
|
49
|
-
} else {
|
|
50
|
-
nextToken = undefined;
|
|
51
|
-
if (!follow) break;
|
|
52
|
-
if (lastTime) since = lastTime;
|
|
53
|
-
}
|
|
54
|
-
await sleep(FOLLOW_INTERVAL_MS);
|
|
55
|
-
}
|
|
56
|
-
if (!follow && !printed) log(`No log events for ${name}${since ? ` since ${since}` : ''}.`);
|
|
57
|
-
return 0;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function toIso(timestamp) {
|
|
61
|
-
if (typeof timestamp === 'number') return new Date(timestamp).toISOString();
|
|
62
|
-
const parsed = Date.parse(timestamp);
|
|
63
|
-
return Number.isNaN(parsed) ? String(timestamp ?? '') : new Date(parsed).toISOString();
|
|
64
|
-
}
|
|
1
|
+
export const FOLLOW_INTERVAL_MS = 2_000;
|
|
2
|
+
const SINCE = /^\d+[mhd]$/;
|
|
3
|
+
const USAGE = 'Usage: hawk-app logs <function> [--since 15m|1h|1d|<ISO time>] [--follow]';
|
|
4
|
+
|
|
5
|
+
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `hawk-app logs <function>`: prints the function's log events (all versions), page by page. With --follow it keeps
|
|
9
|
+
* polling every 2 s (with the platform's nextToken, or from the last event's time) until Ctrl+C or `signal` aborts.
|
|
10
|
+
*/
|
|
11
|
+
export async function logsCommand({ client, manifest, name, flags = {}, sleep = wait, signal, log = console.log }) {
|
|
12
|
+
if (!name) throw new Error(USAGE);
|
|
13
|
+
const declared = manifest.functions && typeof manifest.functions === 'object' ? Object.keys(manifest.functions) : [];
|
|
14
|
+
if (!declared.includes(name)) {
|
|
15
|
+
throw new Error(`"${name}" is not a function of ${manifest.slug}${declared.length ? ` (functions: ${declared.join(', ')})` : ' (hawk-app.json declares no functions)'}.`);
|
|
16
|
+
}
|
|
17
|
+
let since = flags.since;
|
|
18
|
+
if (since !== undefined && (typeof since !== 'string' || (!SINCE.test(since) && Number.isNaN(Date.parse(since))))) {
|
|
19
|
+
throw new Error(`--since takes a duration (15m, 1h, 1d) or an ISO time, not "${since}".`);
|
|
20
|
+
}
|
|
21
|
+
const follow = !!flags.follow;
|
|
22
|
+
const platform = client();
|
|
23
|
+
|
|
24
|
+
let nextToken;
|
|
25
|
+
let printed = 0;
|
|
26
|
+
// Events already printed at the latest timestamp, so polling again from that time doesn't repeat them.
|
|
27
|
+
let lastTime;
|
|
28
|
+
let seenAtLast = new Set();
|
|
29
|
+
if (follow) log(`Following ${name} logs (Ctrl+C to stop)…`);
|
|
30
|
+
|
|
31
|
+
while (!signal?.aborted) {
|
|
32
|
+
const page = await platform.functionLogs(manifest.slug, name, { since, nextToken });
|
|
33
|
+
for (const event of page.events ?? []) {
|
|
34
|
+
const time = toIso(event.timestamp);
|
|
35
|
+
const key = `${time}\n${event.message}`;
|
|
36
|
+
if (time === lastTime && seenAtLast.has(key)) continue;
|
|
37
|
+
if (time !== lastTime) {
|
|
38
|
+
lastTime = time;
|
|
39
|
+
seenAtLast = new Set();
|
|
40
|
+
}
|
|
41
|
+
seenAtLast.add(key);
|
|
42
|
+
log(`${time} ${String(event.message ?? '').replace(/\s+$/, '')}`);
|
|
43
|
+
printed += 1;
|
|
44
|
+
}
|
|
45
|
+
if (page.nextToken) {
|
|
46
|
+
nextToken = page.nextToken;
|
|
47
|
+
// More pages now; when following, the platform may also hand back a token to poll with.
|
|
48
|
+
if (page.events?.length || !follow) continue;
|
|
49
|
+
} else {
|
|
50
|
+
nextToken = undefined;
|
|
51
|
+
if (!follow) break;
|
|
52
|
+
if (lastTime) since = lastTime;
|
|
53
|
+
}
|
|
54
|
+
await sleep(FOLLOW_INTERVAL_MS);
|
|
55
|
+
}
|
|
56
|
+
if (!follow && !printed) log(`No log events for ${name}${since ? ` since ${since}` : ''}.`);
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function toIso(timestamp) {
|
|
61
|
+
if (typeof timestamp === 'number') return new Date(timestamp).toISOString();
|
|
62
|
+
const parsed = Date.parse(timestamp);
|
|
63
|
+
return Number.isNaN(parsed) ? String(timestamp ?? '') : new Date(parsed).toISOString();
|
|
64
|
+
}
|