@blinkhost/cli 2.0.0 → 2.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/README.md +44 -6
- package/dist/api.js +6 -3
- package/dist/auth.js +1 -1
- package/dist/cli.js +112 -58
- package/dist/credentials.d.ts +7 -0
- package/dist/credentials.js +20 -0
- package/dist/guidance.d.ts +27 -0
- package/dist/guidance.js +256 -0
- package/dist/remote.d.ts +1 -1
- package/dist/remote.js +9 -7
- package/dist/version.d.ts +4 -0
- package/dist/version.js +8 -0
- package/dist/workflows.d.ts +3 -3
- package/dist/workflows.js +25 -17
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -6,14 +6,23 @@ The BlinkHost CLI brings project setup, local development, source control, previ
|
|
|
6
6
|
|
|
7
7
|
Node.js 22.12 or newer is required.
|
|
8
8
|
|
|
9
|
-
Install the signed release archive directly from BlinkHost's public repository:
|
|
10
|
-
|
|
11
9
|
```bash
|
|
12
|
-
npm install --global
|
|
10
|
+
npm install --global @blinkhost/cli
|
|
13
11
|
blinkhost --version
|
|
14
12
|
```
|
|
15
13
|
|
|
16
|
-
|
|
14
|
+
Signed release archives, checksums, the CycloneDX SBOM, and Sigstore verification bundle are available at <https://github.com/blinkhost-ltd/blinkhost-cli/releases>.
|
|
15
|
+
|
|
16
|
+
## Start safely
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
blinkhost quickstart
|
|
20
|
+
blinkhost docs
|
|
21
|
+
blinkhost docs automation
|
|
22
|
+
blinkhost help create
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`quickstart` is local, read-only, and makes no network requests. It checks Node.js, Git, the operating-system credential service, the selected directory, and any existing BlinkHost manifest. It never signs in, uploads source, creates a cloud resource, or incurs usage. The bundled `docs` reference is offline and matches the installed CLI version; use the linked web documentation for longer guides.
|
|
17
26
|
|
|
18
27
|
## Connect an account
|
|
19
28
|
|
|
@@ -32,6 +41,8 @@ blinkhost profile use work
|
|
|
32
41
|
|
|
33
42
|
Revoke a device with `blinkhost auth revoke SESSION_ID` or disconnect all CLI sessions for the active profile with `blinkhost auth logout`.
|
|
34
43
|
|
|
44
|
+
Interactive credential storage uses macOS Keychain, Windows Password Vault, or Linux Secret Service (`secret-tool` with an available, unlocked session keyring). The CLI intentionally does not fall back to a plaintext refresh-token file. Headless systems should use a registered GitHub OIDC workload or a short-lived runtime access token.
|
|
45
|
+
|
|
35
46
|
## Create or adopt a project
|
|
36
47
|
|
|
37
48
|
```bash
|
|
@@ -42,10 +53,25 @@ blinkhost projects link PROJECT_ID
|
|
|
42
53
|
blinkhost dev
|
|
43
54
|
```
|
|
44
55
|
|
|
45
|
-
Supported frontends are Astro, HTML, React, Solid, Svelte, and Vue. Backend modules may use Go, Python, or Rust. `blinkhost init` detects supported metadata in an existing repository and creates `blinkhost.yaml` for review.
|
|
56
|
+
Supported frontends are Astro, HTML, React, Solid, Svelte, and Vue. Backend modules may use Go, Python, or Rust. `blinkhost init` detects supported frontend metadata in an existing repository and creates `blinkhost.yaml` for review. Preview the exact result without writing anything first:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
blinkhost init apps/storefront --dry-run --json
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`init` writes only `blinkhost.yaml`, refuses an existing manifest unless `--force` is explicitly supplied, and never rewrites application source. It does not guess backend services; add existing Go, Python, or Rust modules explicitly to the reviewed manifest. One manifest describes one deployable application. In a monorepo, select the application directory, or place the manifest at a build-context root that contains the app and its shared workspace packages; use separate BlinkHost projects for independently deployable apps.
|
|
46
63
|
|
|
47
64
|
Creation is atomic and refuses to replace an existing path. Dependency lifecycle scripts are disabled. Validation rejects unknown manifest fields, duplicate YAML keys, aliases, traversal, unsafe symbolic links, invalid cross-platform paths, duplicate module names, and missing declared inputs.
|
|
48
65
|
|
|
66
|
+
When using `--no-install`, enter the generated directory, run the selected package manager's install command, then run `blinkhost test .`. For example:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
blinkhost create study-circle --template astro --no-install
|
|
70
|
+
cd study-circle
|
|
71
|
+
npm install
|
|
72
|
+
blinkhost test .
|
|
73
|
+
```
|
|
74
|
+
|
|
49
75
|
## Remote workflows
|
|
50
76
|
|
|
51
77
|
Resource commands use the same pattern:
|
|
@@ -101,7 +127,7 @@ Support bundles are local, redacted JSON files with mode `0600`. They exclude to
|
|
|
101
127
|
|
|
102
128
|
## CI and automation
|
|
103
129
|
|
|
104
|
-
All commands support `--json`. Register one exact GitHub branch, tag, or protected environment as a workload identity, then grant the workflow `id-token: write`. The CLI exchanges GitHub's signed OIDC assertion for a ten-minute BlinkHost token automatically; no BlinkHost secret is stored in GitHub.
|
|
130
|
+
All commands support `--json`. In JSON mode, stdout contains exactly one response object on success or failure; subprocess and human diagnostics use stderr. Register one exact GitHub branch, tag, or protected environment as a workload identity, then grant the workflow `id-token: write`. The CLI exchanges GitHub's signed OIDC assertion for a ten-minute BlinkHost token automatically; no BlinkHost secret is stored in GitHub.
|
|
105
131
|
|
|
106
132
|
```bash
|
|
107
133
|
blinkhost workloads create --data @workload.json
|
|
@@ -129,6 +155,18 @@ blinkhost plugins run example -- arguments
|
|
|
129
155
|
|
|
130
156
|
Update checks never install software. Plugins require explicit local approval, are pinned to their executable SHA-256 digest, stop when the executable changes, and receive neither BlinkHost credentials nor the parent environment. A plugin is still third-party code running with your operating-system account; review it before adding it.
|
|
131
157
|
|
|
158
|
+
Pin or roll back explicitly with `npm install --global @blinkhost/cli@VERSION`. Tagged packages are published from the `blinkhost-ltd/blinkhost-cli` release workflow through npm Trusted Publishing with provenance. To verify a downloaded GitHub release, first run `sha256sum --check SHA256SUMS`, then verify its Sigstore bundle:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
cosign verify-blob \
|
|
162
|
+
--bundle SHA256SUMS.sigstore.json \
|
|
163
|
+
--certificate-identity "https://github.com/blinkhost-ltd/blinkhost-cli/.github/workflows/release.yml@refs/tags/v2.1.0" \
|
|
164
|
+
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
|
|
165
|
+
SHA256SUMS
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
The supported runtime is Node.js 22.12 or newer on Linux, macOS, and Windows, including x64 and arm64 environments where that Node release is available. Bash, Zsh, Fish, and PowerShell completion are generated by the CLI. For an offline installation, verify the package archive, checksum file, and Sigstore bundle on a connected machine, transfer them through an approved channel, then run `npm install --global ./blinkhost-cli-VERSION.tgz --offline`.
|
|
169
|
+
|
|
132
170
|
## Exit codes
|
|
133
171
|
|
|
134
172
|
| Code | Meaning |
|
package/dist/api.js
CHANGED
|
@@ -2,6 +2,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
2
2
|
import { activeProfile, writeConfig } from './config.js';
|
|
3
3
|
import { deleteRefreshCredential, getRefreshCredential, setRefreshCredential } from './credentials.js';
|
|
4
4
|
import { CliError, EXIT } from './errors.js';
|
|
5
|
+
import { VERSION } from './version.js';
|
|
5
6
|
const REQUEST_TIMEOUT_MS = 30_000;
|
|
6
7
|
function messageFrom(payload, fallback) {
|
|
7
8
|
if (payload && typeof payload === 'object') {
|
|
@@ -32,7 +33,7 @@ export async function publicRequest(apiOrigin, path, init = {}) {
|
|
|
32
33
|
...init,
|
|
33
34
|
redirect: 'error',
|
|
34
35
|
signal: controller.signal,
|
|
35
|
-
headers: { Accept: 'application/json', 'Content-Type': 'application/json', 'User-Agent':
|
|
36
|
+
headers: { Accept: 'application/json', 'Content-Type': 'application/json', 'User-Agent': `BlinkHost-CLI/${VERSION}`, ...Object.fromEntries(new Headers(init.headers).entries()) },
|
|
36
37
|
});
|
|
37
38
|
return { response, data: await parseResponse(response) };
|
|
38
39
|
}
|
|
@@ -105,13 +106,15 @@ export class ApiClient {
|
|
|
105
106
|
async request(path, init = {}) {
|
|
106
107
|
const headers = new Headers(init.headers);
|
|
107
108
|
headers.set('Authorization', `Bearer ${this.accessToken}`);
|
|
108
|
-
|
|
109
|
+
const requestId = headers.get('X-Request-ID') || randomUUID();
|
|
110
|
+
headers.set('X-Request-ID', requestId);
|
|
109
111
|
if (init.method && init.method !== 'GET' && !headers.has('Idempotency-Key'))
|
|
110
112
|
headers.set('Idempotency-Key', randomUUID());
|
|
111
113
|
const result = await publicRequest(this.profile.apiOrigin, path, { ...init, headers });
|
|
112
114
|
if (!result.response.ok) {
|
|
113
115
|
const exit = result.response.status === 401 || result.response.status === 403 ? EXIT.auth : result.response.status === 409 ? EXIT.conflict : EXIT.remote;
|
|
114
|
-
|
|
116
|
+
const responseId = result.response.headers.get('x-request-id') || requestId;
|
|
117
|
+
throw new CliError(messageFrom(result.data, `BlinkHost returned HTTP ${result.response.status}.`), exit, `api_${result.response.status}`, [`Request ID: ${responseId}`]);
|
|
115
118
|
}
|
|
116
119
|
return result.data;
|
|
117
120
|
}
|
package/dist/auth.js
CHANGED
|
@@ -5,7 +5,7 @@ import { activeProfile, validateApiOrigin, validateProfileName, writeConfig } fr
|
|
|
5
5
|
import { deleteRefreshCredential, setRefreshCredential } from './credentials.js';
|
|
6
6
|
import { ApiClient, publicRequest } from './api.js';
|
|
7
7
|
import { CliError, EXIT } from './errors.js';
|
|
8
|
-
|
|
8
|
+
import { VERSION } from './version.js';
|
|
9
9
|
const sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
10
10
|
function openBrowser(url) {
|
|
11
11
|
const command = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd.exe' : 'xdg-open';
|
package/dist/cli.js
CHANGED
|
@@ -13,45 +13,8 @@ import { login, logout } from './auth.js';
|
|
|
13
13
|
import { activeProfile, readConfig, validateProfileName, writeConfig } from './config.js';
|
|
14
14
|
import { openPreview, projectStatus, rawApi, readProjectLink, runRemote, runSecrets, syncProject, unlinkProject, uploadAsset, waitForRemote, writeProjectLink } from './remote.js';
|
|
15
15
|
import { checkForUpdate, ciCheck, completion, observability, runDev, runPlugins, supportBundle, testProject } from './workflows.js';
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
Usage:
|
|
20
|
-
blinkhost create <project-name> [--template react] [--package-manager npm]
|
|
21
|
-
[--module name:python] [--database APP_DB] [--no-install]
|
|
22
|
-
blinkhost init [path] [--force]
|
|
23
|
-
blinkhost validate [path]
|
|
24
|
-
blinkhost manifest [path]
|
|
25
|
-
blinkhost doctor [path]
|
|
26
|
-
blinkhost test [path]
|
|
27
|
-
blinkhost auth login|logout|status|sessions|revoke
|
|
28
|
-
blinkhost projects list|get|create|update|delete|action|link|current
|
|
29
|
-
blinkhost repositories|connections|previews|builds|deployments <action>
|
|
30
|
-
blinkhost modules|databases|bindings|assets|secrets <action>
|
|
31
|
-
blinkhost organizations|templates|approvals|handoffs|policies|workloads <action>
|
|
32
|
-
blinkhost dev [path] [--host HOST] [--port PORT]
|
|
33
|
-
blinkhost logs|metrics|analytics --project PROJECT_ID
|
|
34
|
-
blinkhost support bundle [--output PATH]
|
|
35
|
-
blinkhost completion bash|zsh|fish
|
|
36
|
-
blinkhost update check
|
|
37
|
-
blinkhost ci check
|
|
38
|
-
blinkhost plugins list|add|remove|verify|run
|
|
39
|
-
blinkhost api METHOD /api/customer/path/ [--data JSON_OR_@FILE]
|
|
40
|
-
|
|
41
|
-
Global options:
|
|
42
|
-
--json Return machine-readable output
|
|
43
|
-
--profile Use a named account and API profile
|
|
44
|
-
--quiet Suppress successful human-readable output
|
|
45
|
-
--verbose Include safe diagnostic detail in errors
|
|
46
|
-
--no-color Disable terminal colour (accepted for portable scripts)
|
|
47
|
-
--non-interactive Never open a browser or prompt
|
|
48
|
-
--help Show command help
|
|
49
|
-
--version Show the CLI version
|
|
50
|
-
|
|
51
|
-
Refresh credentials are stored only by the operating-system credential service.
|
|
52
|
-
BlinkHost remains authoritative for roles, plan limits, approvals, builds, releases,
|
|
53
|
-
deployments, and audit records. Secret values are accepted only through standard input.
|
|
54
|
-
`;
|
|
16
|
+
import { documentationIndex, documentationTopic, quickstart, renderTopHelp, renderTopic, searchDocumentation, TOP_LEVEL_COMMANDS } from './guidance.js';
|
|
17
|
+
import { VERSION, supportedNodeVersion } from './version.js';
|
|
55
18
|
let quietOutput = false;
|
|
56
19
|
let verboseOutput = false;
|
|
57
20
|
function terminalText(value) {
|
|
@@ -112,9 +75,13 @@ function parseModule(value) {
|
|
|
112
75
|
}
|
|
113
76
|
return { name, language: language };
|
|
114
77
|
}
|
|
115
|
-
async function runProcess(command, args, cwd) {
|
|
78
|
+
async function runProcess(command, args, cwd, json = false) {
|
|
116
79
|
return new Promise((resolve, reject) => {
|
|
117
|
-
const child = spawn(command, args, { cwd, shell: false, stdio: 'inherit', env: { ...process.env, npm_config_ignore_scripts: 'true' } });
|
|
80
|
+
const child = spawn(command, args, { cwd, shell: false, stdio: json ? ['inherit', 'pipe', 'pipe'] : 'inherit', env: { ...process.env, npm_config_ignore_scripts: 'true' } });
|
|
81
|
+
if (json) {
|
|
82
|
+
child.stdout?.on('data', (chunk) => process.stderr.write(chunk));
|
|
83
|
+
child.stderr?.on('data', (chunk) => process.stderr.write(chunk));
|
|
84
|
+
}
|
|
118
85
|
child.once('error', () => reject(new CliError(`The ${command} executable is not available.`, EXIT.filesystem, 'package_manager_unavailable')));
|
|
119
86
|
child.once('exit', (code) => resolve(code ?? 1));
|
|
120
87
|
});
|
|
@@ -141,7 +108,7 @@ async function commandCreate(args, json) {
|
|
|
141
108
|
const install = !noInstall;
|
|
142
109
|
assertNoUnknown(args);
|
|
143
110
|
if (!SUPPORTED_FRONTENDS.includes(framework))
|
|
144
|
-
throw new CliError(`Unsupported template: ${framework}.`, EXIT.usage, 'invalid_template');
|
|
111
|
+
throw new CliError(`Unsupported template: ${framework}. Choose ${SUPPORTED_FRONTENDS.join(', ')}.`, EXIT.usage, 'invalid_template');
|
|
145
112
|
if (!SUPPORTED_MANAGERS.includes(packageManager))
|
|
146
113
|
throw new CliError(`Unsupported package manager: ${packageManager}.`, EXIT.usage, 'invalid_package_manager');
|
|
147
114
|
if (database && !/^[A-Z][A-Z0-9_]{0,127}$/.test(database))
|
|
@@ -159,20 +126,45 @@ async function commandCreate(args, json) {
|
|
|
159
126
|
yarn: ['install', '--mode=skip-build'],
|
|
160
127
|
bun: ['install', '--ignore-scripts', '--no-progress'],
|
|
161
128
|
};
|
|
162
|
-
const code = await runProcess(packageManager, installArgs[packageManager], target);
|
|
129
|
+
const code = await runProcess(packageManager, installArgs[packageManager], target, json);
|
|
163
130
|
if (code !== 0)
|
|
164
131
|
throw new CliError(`Dependency installation exited with code ${code}. The project files were kept.`, EXIT.filesystem, 'install_failed');
|
|
165
132
|
}
|
|
166
|
-
|
|
133
|
+
const nextSteps = !install && framework !== 'html' ? [`cd ${name}`, `${packageManager} install`, 'blinkhost test .', 'blinkhost dev .'] : [`cd ${name}`, 'blinkhost test .', ...(framework === 'html' ? [] : ['blinkhost dev .'])];
|
|
134
|
+
emit({ ok: true, command: 'create', message: `Created ${name} at ${target}.`, data: { path: target, framework, package_manager: packageManager, modules: modules.length, generated_files: [...scaffold.files.keys(), 'blinkhost.yaml'].sort(), installed: install && framework !== 'html', next_steps: nextSteps } }, json);
|
|
135
|
+
}
|
|
136
|
+
function commandSuggestion(command) {
|
|
137
|
+
const distance = (left, right) => {
|
|
138
|
+
const row = Array.from({ length: right.length + 1 }, (_, index) => index);
|
|
139
|
+
for (let i = 1; i <= left.length; i += 1) {
|
|
140
|
+
let previous = row[0];
|
|
141
|
+
row[0] = i;
|
|
142
|
+
for (let j = 1; j <= right.length; j += 1) {
|
|
143
|
+
const before = row[j];
|
|
144
|
+
row[j] = Math.min(row[j] + 1, row[j - 1] + 1, previous + (left[i - 1] === right[j - 1] ? 0 : 1));
|
|
145
|
+
previous = before;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return row[right.length];
|
|
149
|
+
};
|
|
150
|
+
const ranked = TOP_LEVEL_COMMANDS.map((candidate) => ({ candidate, distance: distance(command, candidate) })).sort((a, b) => a.distance - b.distance);
|
|
151
|
+
return ranked[0] && ranked[0].distance <= Math.max(2, Math.floor(command.length / 3)) ? ranked[0].candidate : null;
|
|
167
152
|
}
|
|
168
153
|
async function commandInit(args, json) {
|
|
169
154
|
const force = takeFlag(args, '--force');
|
|
155
|
+
const dryRun = takeFlag(args, '--dry-run');
|
|
170
156
|
const path = args.shift();
|
|
171
157
|
assertNoUnknown(args);
|
|
158
|
+
if (force && dryRun)
|
|
159
|
+
throw new CliError('Choose either --force or --dry-run.', EXIT.usage, 'conflicting_options');
|
|
172
160
|
const root = resolveLocalPath(path);
|
|
173
161
|
const manifest = await detectManifest(root);
|
|
162
|
+
if (dryRun) {
|
|
163
|
+
emit({ ok: true, command: 'init', message: 'Detected a BlinkHost manifest without writing any files.', data: { path: root, framework: manifest.frontend.framework, written: false, manifest } }, json);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
174
166
|
await writeManifest(root, manifest, force);
|
|
175
|
-
emit({ ok: true, command: 'init', message: `Created blinkhost.yaml in ${root}.`, data: { path: root, framework: manifest.frontend.framework } }, json);
|
|
167
|
+
emit({ ok: true, command: 'init', message: `Created blinkhost.yaml in ${root}.`, data: { path: root, framework: manifest.frontend.framework, written: true, changed_files: ['blinkhost.yaml'] } }, json);
|
|
176
168
|
}
|
|
177
169
|
async function commandValidate(args, json) {
|
|
178
170
|
const path = args.shift();
|
|
@@ -235,18 +227,78 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
235
227
|
const profile = takeOption(args, '--profile');
|
|
236
228
|
const command = args.shift();
|
|
237
229
|
try {
|
|
238
|
-
|
|
239
|
-
|
|
230
|
+
const helpRequested = takeFlag(args, '--help');
|
|
231
|
+
if (!command || command === '--help') {
|
|
232
|
+
if (json)
|
|
233
|
+
emit({ ok: true, command: 'help', message: 'BlinkHost CLI command index.', data: documentationIndex() }, true);
|
|
234
|
+
else
|
|
235
|
+
process.stdout.write(renderTopHelp());
|
|
240
236
|
return EXIT.success;
|
|
241
237
|
}
|
|
242
238
|
if (command === '--version' || command === 'version') {
|
|
243
|
-
|
|
239
|
+
if (json)
|
|
240
|
+
emit({ ok: true, command: 'version', message: `BlinkHost CLI ${VERSION}.`, data: { version: VERSION } }, true);
|
|
241
|
+
else
|
|
242
|
+
process.stdout.write(`${VERSION}\n`);
|
|
244
243
|
return EXIT.success;
|
|
245
244
|
}
|
|
246
|
-
if (
|
|
247
|
-
|
|
245
|
+
if (command === 'help' || helpRequested) {
|
|
246
|
+
const requested = command === 'help' ? args.shift() : command;
|
|
247
|
+
assertNoUnknown(args);
|
|
248
|
+
if (!requested) {
|
|
249
|
+
if (json)
|
|
250
|
+
emit({ ok: true, command: 'help', message: 'BlinkHost CLI command index.', data: documentationIndex() }, true);
|
|
251
|
+
else
|
|
252
|
+
process.stdout.write(renderTopHelp());
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
const topic = documentationTopic(requested);
|
|
256
|
+
if (!topic)
|
|
257
|
+
throw new CliError(`No help is available for ${requested}. Run \`blinkhost docs\` to list topics.`, EXIT.usage, 'unknown_help_topic');
|
|
258
|
+
if (json)
|
|
259
|
+
emit({ ok: true, command: `help ${requested}`, message: `${topic.title}.`, data: { schema: 'blinkhost/cli-docs/v1', cli_version: VERSION, topic } }, true);
|
|
260
|
+
else
|
|
261
|
+
process.stdout.write(renderTopic(topic));
|
|
262
|
+
}
|
|
248
263
|
return EXIT.success;
|
|
249
264
|
}
|
|
265
|
+
if (command === 'docs') {
|
|
266
|
+
const search = takeOption(args, '--search');
|
|
267
|
+
const topicName = args.shift();
|
|
268
|
+
assertNoUnknown(args);
|
|
269
|
+
if (search && topicName)
|
|
270
|
+
throw new CliError('Choose a documentation topic or --search, not both.', EXIT.usage, 'conflicting_options');
|
|
271
|
+
if (search) {
|
|
272
|
+
const results = searchDocumentation(search);
|
|
273
|
+
if (json)
|
|
274
|
+
emit({ ok: true, command: 'docs search', message: `${results.length} documentation topic(s) matched.`, data: { schema: 'blinkhost/cli-docs/v1', cli_version: VERSION, query: search, results } }, true);
|
|
275
|
+
else
|
|
276
|
+
process.stdout.write(results.length ? results.map(renderTopic).join('\n') : `No offline documentation matched “${terminalText(search)}”.\n`);
|
|
277
|
+
}
|
|
278
|
+
else if (topicName) {
|
|
279
|
+
const topic = documentationTopic(topicName);
|
|
280
|
+
if (!topic)
|
|
281
|
+
throw new CliError(`Unknown documentation topic: ${topicName}. Run \`blinkhost docs\` to list topics.`, EXIT.usage, 'unknown_docs_topic');
|
|
282
|
+
if (json)
|
|
283
|
+
emit({ ok: true, command: `docs ${topicName}`, message: `${topic.title}.`, data: { schema: 'blinkhost/cli-docs/v1', cli_version: VERSION, topic } }, true);
|
|
284
|
+
else
|
|
285
|
+
process.stdout.write(renderTopic(topic));
|
|
286
|
+
}
|
|
287
|
+
else if (json)
|
|
288
|
+
emit({ ok: true, command: 'docs', message: 'Offline documentation index.', data: documentationIndex() }, true);
|
|
289
|
+
else
|
|
290
|
+
process.stdout.write(renderTopHelp());
|
|
291
|
+
return EXIT.success;
|
|
292
|
+
}
|
|
293
|
+
if (command === 'quickstart') {
|
|
294
|
+
const path = args.shift();
|
|
295
|
+
assertNoUnknown(args);
|
|
296
|
+
const data = await quickstart(path);
|
|
297
|
+
emit({ ok: true, command, message: 'Local BlinkHost readiness check completed. No remote changes were made.', data }, json);
|
|
298
|
+
return EXIT.success;
|
|
299
|
+
}
|
|
300
|
+
if (!supportedNodeVersion())
|
|
301
|
+
throw new CliError(`Node.js ${process.versions.node} is unsupported. Install Node.js 22.12 or newer. Help, docs and quickstart remain available.`, EXIT.filesystem, 'unsupported_node');
|
|
250
302
|
if (profile)
|
|
251
303
|
validateProfileName(profile);
|
|
252
304
|
if (command === 'create')
|
|
@@ -260,7 +312,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
260
312
|
else if (command === 'doctor')
|
|
261
313
|
await commandDoctor(args, json);
|
|
262
314
|
else if (command === 'test') {
|
|
263
|
-
const data = await testProject(args);
|
|
315
|
+
const data = await testProject(args, json);
|
|
264
316
|
emit({ ok: true, command, message: 'Project checks passed.', data }, json);
|
|
265
317
|
}
|
|
266
318
|
else if (command === 'auth') {
|
|
@@ -360,8 +412,8 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
360
412
|
}
|
|
361
413
|
else if (command === 'previews' && args[0] === 'open') {
|
|
362
414
|
args.shift();
|
|
363
|
-
const data = await openPreview(args, profile);
|
|
364
|
-
emit({ ok: true, command: 'previews open', message: 'Opened the preview in your browser.', data }, json);
|
|
415
|
+
const data = await openPreview(args, profile, !nonInteractive);
|
|
416
|
+
emit({ ok: true, command: 'previews open', message: nonInteractive ? 'Preview URL loaded without opening a browser.' : 'Opened the preview in your browser.', data }, json);
|
|
365
417
|
}
|
|
366
418
|
else if ((command === 'builds' || command === 'deployments' || command === 'previews') && args[0] === 'wait') {
|
|
367
419
|
args.shift();
|
|
@@ -382,7 +434,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
382
434
|
emit({ ok: true, command, message: 'Secret operation completed.', data }, json);
|
|
383
435
|
}
|
|
384
436
|
else if (command === 'dev') {
|
|
385
|
-
const data = await runDev(args);
|
|
437
|
+
const data = await runDev(args, json);
|
|
386
438
|
emit({ ok: true, command, message: 'Local development process finished.', data }, json);
|
|
387
439
|
}
|
|
388
440
|
else if (command === 'logs' || command === 'metrics' || command === 'analytics') {
|
|
@@ -412,21 +464,23 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
412
464
|
emit({ ok: true, command: 'ci check', message: 'CI identity and API capabilities are ready.', data }, json);
|
|
413
465
|
}
|
|
414
466
|
else if (command === 'plugins') {
|
|
415
|
-
const data = await runPlugins(args);
|
|
467
|
+
const data = await runPlugins(args, json);
|
|
416
468
|
emit({ ok: true, command, message: 'Plugin operation completed.', data }, json);
|
|
417
469
|
}
|
|
418
470
|
else if (command === 'api') {
|
|
419
471
|
const data = await rawApi(args, profile);
|
|
420
472
|
emit({ ok: true, command, message: 'API request completed.', data }, json);
|
|
421
473
|
}
|
|
422
|
-
else
|
|
423
|
-
|
|
474
|
+
else {
|
|
475
|
+
const suggestion = commandSuggestion(command);
|
|
476
|
+
throw new CliError(`Unknown command: ${command}.${suggestion ? ` Did you mean \`${suggestion}\`?` : ' Run `blinkhost docs` to list commands.'}`, EXIT.usage, 'unknown_command');
|
|
477
|
+
}
|
|
424
478
|
return EXIT.success;
|
|
425
479
|
}
|
|
426
480
|
catch (error) {
|
|
427
481
|
const failure = error instanceof CliError ? error : new CliError(error instanceof Error ? error.message : String(error), EXIT.internal, 'internal_error');
|
|
428
482
|
if (json)
|
|
429
|
-
process.
|
|
483
|
+
process.stdout.write(`${JSON.stringify({ ok: false, command: command ?? '', error: { code: failure.code, message: failure.message, details: failure.details } })}\n`);
|
|
430
484
|
else {
|
|
431
485
|
process.stderr.write(`Error: ${terminalText(failure.message)}\n`);
|
|
432
486
|
for (const detail of failure.details)
|
package/dist/credentials.d.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
export interface CredentialStoreStatus {
|
|
2
|
+
available: boolean;
|
|
3
|
+
provider: 'macos-keychain' | 'windows-password-vault' | 'linux-secret-service';
|
|
4
|
+
remediation: string | null;
|
|
5
|
+
}
|
|
6
|
+
/** Checks only whether the platform credential service can be invoked. */
|
|
7
|
+
export declare function credentialStoreStatus(): Promise<CredentialStoreStatus>;
|
|
1
8
|
export declare function getRefreshCredential(profile: string): Promise<string | null>;
|
|
2
9
|
export declare function setRefreshCredential(profile: string, token: string): Promise<void>;
|
|
3
10
|
export declare function deleteRefreshCredential(profile: string): Promise<void>;
|
package/dist/credentials.js
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { CliError, EXIT } from './errors.js';
|
|
3
3
|
const SERVICE = 'blinkhost-cli';
|
|
4
|
+
async function executableAvailable(command, args) {
|
|
5
|
+
return new Promise((resolve) => {
|
|
6
|
+
const child = spawn(command, args, { shell: false, stdio: 'ignore', windowsHide: true });
|
|
7
|
+
child.once('error', () => resolve(false));
|
|
8
|
+
child.once('exit', (code) => resolve(code === 0));
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
/** Checks only whether the platform credential service can be invoked. */
|
|
12
|
+
export async function credentialStoreStatus() {
|
|
13
|
+
if (process.platform === 'darwin') {
|
|
14
|
+
const available = await executableAvailable('security', ['help']);
|
|
15
|
+
return { available, provider: 'macos-keychain', remediation: available ? null : 'Restore the macOS security command before interactive sign-in.' };
|
|
16
|
+
}
|
|
17
|
+
if (process.platform === 'win32') {
|
|
18
|
+
const available = await executableAvailable('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', '[void][Windows.Security.Credentials.PasswordVault,Windows.Security.Credentials,ContentType=WindowsRuntime]']);
|
|
19
|
+
return { available, provider: 'windows-password-vault', remediation: available ? null : 'Use Windows with PowerShell and Password Vault available.' };
|
|
20
|
+
}
|
|
21
|
+
const available = await executableAvailable('secret-tool', ['--version']);
|
|
22
|
+
return { available, provider: 'linux-secret-service', remediation: available ? null : 'Install libsecret tools and start an unlocked Secret Service session, or use a short-lived workload identity in headless automation.' };
|
|
23
|
+
}
|
|
4
24
|
async function run(command, args, input, acceptMissing = false) {
|
|
5
25
|
return new Promise((resolve, reject) => {
|
|
6
26
|
const child = spawn(command, args, { shell: false, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface DocumentationTopic {
|
|
2
|
+
name: string;
|
|
3
|
+
title: string;
|
|
4
|
+
summary: string;
|
|
5
|
+
usage: string[];
|
|
6
|
+
details: string[];
|
|
7
|
+
examples: string[];
|
|
8
|
+
related: string[];
|
|
9
|
+
url: string;
|
|
10
|
+
}
|
|
11
|
+
export declare const TOP_LEVEL_COMMANDS: ("plugins" | "profile" | "modules" | "dev" | "databases" | "secrets" | "manifest" | "projects" | "repositories" | "connections" | "previews" | "builds" | "deployments" | "bindings" | "assets" | "organizations" | "templates" | "approvals" | "handoffs" | "policies" | "workloads" | "create" | "update" | "quickstart" | "init" | "doctor" | "auth" | "validate" | "test" | "ci" | "logs" | "support" | "metrics" | "analytics" | "completion" | "api" | "docs")[];
|
|
12
|
+
export declare function documentationIndex(): {
|
|
13
|
+
schema: string;
|
|
14
|
+
cli_version: string;
|
|
15
|
+
documentation_url: string;
|
|
16
|
+
topics: Array<{
|
|
17
|
+
name: string;
|
|
18
|
+
title: string;
|
|
19
|
+
summary: string;
|
|
20
|
+
url: string;
|
|
21
|
+
}>;
|
|
22
|
+
};
|
|
23
|
+
export declare function documentationTopic(name: string): DocumentationTopic | undefined;
|
|
24
|
+
export declare function searchDocumentation(query: string): DocumentationTopic[];
|
|
25
|
+
export declare function renderTopic(topic: DocumentationTopic): string;
|
|
26
|
+
export declare function renderTopHelp(): string;
|
|
27
|
+
export declare function quickstart(path?: string): Promise<Record<string, unknown>>;
|
package/dist/guidance.js
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { access } from 'node:fs/promises';
|
|
3
|
+
import { credentialStoreStatus } from './credentials.js';
|
|
4
|
+
import { SUPPORTED_FRONTENDS, SUPPORTED_MANAGERS, SUPPORTED_MODULES } from './manifest.js';
|
|
5
|
+
import { resolveLocalPath, validateProject } from './project.js';
|
|
6
|
+
import { DOCUMENTATION_URL, RELEASES_URL, VERSION, supportedNodeVersion } from './version.js';
|
|
7
|
+
const resource = (name, summary, extra = []) => ({
|
|
8
|
+
name,
|
|
9
|
+
title: `${name[0].toUpperCase()}${name.slice(1)} commands`,
|
|
10
|
+
summary,
|
|
11
|
+
usage: [
|
|
12
|
+
`blinkhost ${name} list [--query name=value]`,
|
|
13
|
+
`blinkhost ${name} get ID`,
|
|
14
|
+
`blinkhost ${name} create --data @request.json`,
|
|
15
|
+
`blinkhost ${name} update ID --data @request.json`,
|
|
16
|
+
`blinkhost ${name} delete ID --confirm ID`,
|
|
17
|
+
`blinkhost ${name} action ID ACTION --data @request.json`,
|
|
18
|
+
],
|
|
19
|
+
details: [
|
|
20
|
+
'List and get are read-only. Create, update, delete and action send remote requests to the active BlinkHost workspace.',
|
|
21
|
+
'Workspace roles, plan limits, protected-environment approvals, verified-artifact requirements and audit records remain enforced by BlinkHost.',
|
|
22
|
+
'Deletion requires the exact resource ID through --confirm. JSON request files must be regular files no larger than 1 MiB.',
|
|
23
|
+
...extra,
|
|
24
|
+
],
|
|
25
|
+
examples: [`blinkhost ${name} list --json`, `blinkhost ${name} get RESOURCE_ID --json`],
|
|
26
|
+
related: ['auth', 'automation', 'limits', 'troubleshooting'],
|
|
27
|
+
url: DOCUMENTATION_URL,
|
|
28
|
+
});
|
|
29
|
+
const TOPICS = {
|
|
30
|
+
quickstart: {
|
|
31
|
+
name: 'quickstart', title: 'Safe first steps', summary: 'Inspect local readiness and receive a non-destructive path to your first BlinkHost project.',
|
|
32
|
+
usage: ['blinkhost quickstart [path]', 'blinkhost quickstart [path] --json'],
|
|
33
|
+
details: ['Quickstart performs local checks only. It does not sign in, create cloud resources, deploy, upload source, or incur usage.', 'Use create for a new project or init to adopt an existing repository. Review blinkhost.yaml before linking a remote project.'],
|
|
34
|
+
examples: ['blinkhost quickstart', 'blinkhost create my-app --template astro --no-install', 'blinkhost init ./existing-app'], related: ['create', 'init', 'doctor', 'auth'], url: DOCUMENTATION_URL,
|
|
35
|
+
},
|
|
36
|
+
create: {
|
|
37
|
+
name: 'create', title: 'Create a local project', summary: 'Generate an atomic BlinkHost project scaffold without changing remote resources.',
|
|
38
|
+
usage: ['blinkhost create NAME [--template FRAMEWORK] [--package-manager MANAGER] [--module NAME:LANGUAGE] [--database BINDING] [--install|--no-install]'],
|
|
39
|
+
details: [`Frameworks: ${SUPPORTED_FRONTENDS.join(', ')}.`, `Package managers: ${SUPPORTED_MANAGERS.join(', ')}.`, `Backend module languages: ${SUPPORTED_MODULES.join(', ')}.`, 'The default template is react and the default package manager is npm. Existing paths are never overwritten. Dependency lifecycle scripts are disabled during installation.', 'Creation is local only; it does not create a BlinkHost project, database, preview, deployment, or billable resource.'],
|
|
40
|
+
examples: ['blinkhost create my-app --template astro', 'blinkhost create my-app --template react --module api:rust --module jobs:python --database APP_DB --no-install'], related: ['quickstart', 'validate', 'projects', 'manifest-reference'], url: DOCUMENTATION_URL,
|
|
41
|
+
},
|
|
42
|
+
init: {
|
|
43
|
+
name: 'init', title: 'Adopt an existing repository', summary: 'Detect supported project metadata and write a reviewable blinkhost.yaml.',
|
|
44
|
+
usage: ['blinkhost init [path] [--dry-run|--force]'],
|
|
45
|
+
details: ['Detection reads package.json dependencies and the pnpm, Yarn, Bun or npm lockfile at the selected application root. It recognizes Astro, React, Vue, Svelte and Solid; a directory without recognized framework metadata is described as static HTML.', '`--dry-run` returns the proposed manifest and writes nothing. Without it, init writes only blinkhost.yaml and refuses an existing manifest. `--force` replaces only an existing regular blinkhost.yaml after explicit use; application source and configuration are never rewritten.', 'Detection does not guess backend services. Review the proposal and add each Go, Python or Rust module explicitly before validation.', 'One manifest represents one deployable application. For a nested app, pass that directory. For a workspace app that imports shared packages, place the manifest at a root containing the complete build context and set frontend.root and frontend.dependency_root explicitly. Use separate application roots and BlinkHost projects for independently deployable monorepo applications.'],
|
|
46
|
+
examples: ['blinkhost init . --dry-run --json', 'blinkhost init apps/storefront', 'blinkhost docs manifest-reference'], related: ['validate', 'manifest-reference', 'projects'], url: DOCUMENTATION_URL,
|
|
47
|
+
},
|
|
48
|
+
validate: {
|
|
49
|
+
name: 'validate', title: 'Validate project compatibility', summary: 'Check the manifest, declared files, path safety and dependency metadata without building or uploading.',
|
|
50
|
+
usage: ['blinkhost validate [path]'],
|
|
51
|
+
details: ['Validation rejects unknown fields, duplicate YAML keys, aliases, traversal, unsafe symbolic links, reserved cross-platform paths, duplicate modules and missing declared inputs.', 'Warnings identify non-blocking portability or reproducibility concerns such as a missing lockfile.'],
|
|
52
|
+
examples: ['blinkhost validate', 'blinkhost validate apps/storefront --json'], related: ['manifest-reference', 'doctor', 'test'], url: DOCUMENTATION_URL,
|
|
53
|
+
},
|
|
54
|
+
manifest: {
|
|
55
|
+
name: 'manifest', title: 'Read the normalized project manifest', summary: 'Parse and print blinkhost.yaml after applying its strict schema rules.',
|
|
56
|
+
usage: ['blinkhost manifest [path]'], details: ['Use --json to wrap the normalized manifest in the stable CLI response envelope. Secret values are never valid manifest fields.'], examples: ['blinkhost manifest --json'], related: ['manifest-reference', 'validate'], url: DOCUMENTATION_URL,
|
|
57
|
+
},
|
|
58
|
+
doctor: {
|
|
59
|
+
name: 'doctor', title: 'Check the local toolchain', summary: 'Verify Node.js, Git, the selected directory and its BlinkHost manifest.',
|
|
60
|
+
usage: ['blinkhost doctor [path]'], details: ['Doctor is local and non-destructive. Quickstart additionally explains authentication prerequisites and next steps.'], examples: ['blinkhost doctor . --json'], related: ['quickstart', 'validate', 'portability'], url: DOCUMENTATION_URL,
|
|
61
|
+
},
|
|
62
|
+
test: {
|
|
63
|
+
name: 'test', title: 'Run project checks', summary: 'Validate the project and run its test script, or its build script when no test script exists.',
|
|
64
|
+
usage: ['blinkhost test [path]'], details: ['Static HTML projects require validation only. JavaScript dependencies must be installed first.', 'With --json, stdout contains exactly one JSON response; child-process diagnostics are sent to stderr.'], examples: ['blinkhost test .', 'blinkhost test . --json'], related: ['validate', 'automation', 'troubleshooting'], url: DOCUMENTATION_URL,
|
|
65
|
+
},
|
|
66
|
+
auth: {
|
|
67
|
+
name: 'auth', title: 'Authentication and sessions', summary: 'Connect a device through browser approval and manage its CLI sessions.',
|
|
68
|
+
usage: ['blinkhost auth login [--no-browser] [--api-origin URL]', 'blinkhost auth status', 'blinkhost auth sessions', 'blinkhost auth revoke SESSION_ID', 'blinkhost auth logout'],
|
|
69
|
+
details: ['Passwords are never entered in the terminal. Refresh credentials are stored only in macOS Keychain, Windows Password Vault, or Linux Secret Service through secret-tool.', 'Headless automation should use a registered GitHub OIDC workload or a short-lived BLINKHOST_ACCESS_TOKEN supplied by an approved CI secret service. Never persist it in source or CLI configuration.', '--non-interactive prevents browser authorization and fails closed when interaction would be required.'],
|
|
70
|
+
examples: ['blinkhost auth login', 'blinkhost auth login --no-browser', 'blinkhost auth sessions --json'], related: ['profile', 'ci', 'security', 'portability'], url: DOCUMENTATION_URL,
|
|
71
|
+
},
|
|
72
|
+
profile: {
|
|
73
|
+
name: 'profile', title: 'Account profiles', summary: 'Keep personal and work BlinkHost sessions separate.',
|
|
74
|
+
usage: ['blinkhost profile list', 'blinkhost profile use NAME'], details: ['--profile NAME selects a profile for one command. BLINKHOST_PROFILE selects a default for the current process. Profile names contain lowercase letters, numbers, hyphens or underscores.'], examples: ['blinkhost auth login --profile work', 'blinkhost profile use work'], related: ['auth', 'security'], url: DOCUMENTATION_URL,
|
|
75
|
+
},
|
|
76
|
+
projects: resource('projects', 'Manage projects and explicit local project links.', ['`projects link PROJECT_ID [path]` writes only a local .blinkhost/project.json pointer and does not change or deploy the remote project.', '`projects unlink [path]` removes only that local pointer. `projects pull|push CONNECTION_ID` synchronizes through a configured source connection.']),
|
|
77
|
+
repositories: resource('repositories', 'Inspect and manage connected source repositories.'),
|
|
78
|
+
connections: resource('connections', 'Manage project-to-repository source connections.'),
|
|
79
|
+
previews: resource('previews', 'Manage temporary preview environments.', ['`previews wait ID [--timeout 10..3600]` waits for a terminal state. `previews open ID` accepts only trusted BlinkHost HTTPS preview hosts. Preview operations can consume plan resources.']),
|
|
80
|
+
builds: resource('builds', 'Create, inspect and wait for verified builds.', ['`builds wait ID [--timeout 10..3600]` stops on success, failure, cancellation, expiry or timeout. Builds can consume plan resources.']),
|
|
81
|
+
deployments: resource('deployments', 'Manage production deployments and protected release actions.', ['Rollback uses `deployments action DEPLOYMENT_ID rollback --data @rollback.json`. Protected environments can require approval and a verified artifact. Deployment operations can consume plan resources.']),
|
|
82
|
+
modules: resource('modules', 'Manage Rust, Go and Python backend modules.'),
|
|
83
|
+
databases: resource('databases', 'Manage database resources.', ['Database list, get, create, update, delete and supported server actions are remote and may require plan capacity or protected-environment approval.', 'CLI v2.1 does not invent local backup, restore, credential-rotation or migration semantics. Use only actions returned by the active workspace API and consult the detailed database guide before production data changes. Deletion retains exact-ID confirmation and server authorization.']),
|
|
84
|
+
bindings: resource('bindings', 'Manage explicit application-to-resource bindings.'),
|
|
85
|
+
assets: resource('assets', 'Manage project assets.', ['Upload with `assets upload FILE --project PROJECT_ID [--parent ID] [--revision VALUE] [--replace ID]`. Uploads accept listed image, font, MP3 and MP4 types, reject symbolic links, verify SHA-256 and count against plan storage.']),
|
|
86
|
+
organizations: resource('organizations', 'Manage organizations within the permissions granted to your account.'),
|
|
87
|
+
templates: resource('templates', 'Inspect and manage verified source templates.'),
|
|
88
|
+
approvals: resource('approvals', 'Inspect and act on deployment approvals within your role.'),
|
|
89
|
+
handoffs: resource('handoffs', 'Manage explicit agency handoffs and their audit trail.'),
|
|
90
|
+
policies: resource('policies', 'Inspect and manage enterprise policies within authorized workspaces.'),
|
|
91
|
+
workloads: resource('workloads', 'Register narrowly scoped automation identities.', ['Use an exact GitHub branch, tag or protected-environment subject. Workload tokens are short-lived and do not replace workspace authorization.']),
|
|
92
|
+
secrets: {
|
|
93
|
+
name: 'secrets', title: 'Project secrets', summary: 'List, set, rotate and delete secret records without placing values in arguments.',
|
|
94
|
+
usage: ['blinkhost secrets list --project PROJECT_ID', "printf '%s' \"$VALUE\" | blinkhost secrets set NAME --project PROJECT_ID [--environment SCOPE] [--expires-in-days DAYS]", "printf '%s' \"$VALUE\" | blinkhost secrets rotate SECRET_ID", 'blinkhost secrets delete SECRET_ID --confirm SECRET_ID'],
|
|
95
|
+
details: ['Values are read only from standard input, limited to 64 KiB and never written to blinkhost.yaml. Disable shell tracing around secret input and use masked variables or an approved secret manager in CI.', 'List responses do not return secret values. Authorization and workspace scope are enforced by the API.'], examples: ["set +x; printf '%s' \"$API_TOKEN\" | blinkhost secrets set API_TOKEN --project PROJECT_ID"], related: ['security', 'automation'], url: DOCUMENTATION_URL,
|
|
96
|
+
},
|
|
97
|
+
dev: {
|
|
98
|
+
name: 'dev', title: 'Local development', summary: 'Run the manifest-declared frontend development process.', usage: ['blinkhost dev [path] [--host HOST] [--port PORT]'], details: ['This starts the local frontend process and sets BLINKHOST_LOCAL=1. It does not deploy a release. Static HTML projects should use a local static-file server.', 'Install dependencies first. Production bindings are not automatically exposed to local processes.'], examples: ['blinkhost dev . --host 127.0.0.1 --port 3000'], related: ['quickstart', 'test', 'previews'], url: DOCUMENTATION_URL,
|
|
99
|
+
},
|
|
100
|
+
logs: {
|
|
101
|
+
name: 'logs', title: 'Project observability', summary: 'Read authorized logs, metrics or analytics for a linked or explicit project.', usage: ['blinkhost logs [--project ID] [--since DURATION] [--limit COUNT]', 'blinkhost metrics [--project ID] [--since DURATION]', 'blinkhost analytics [--project ID]'], details: ['These commands are read-only and access is workspace-scoped.', 'Treat application logs as potentially sensitive. The CLI does not claim that arbitrary secrets written by application code can always be detected or redacted, so review output before sharing it. Use the redacted support bundle for platform diagnostics.', 'The service remains authoritative for retention, pagination, platform-managed filtering and audit access. Do not deliberately log credentials or personal data.'], examples: ['blinkhost logs --project PROJECT_ID --since 1h --limit 100 --json'], related: ['security', 'support', 'troubleshooting'], url: DOCUMENTATION_URL,
|
|
102
|
+
},
|
|
103
|
+
metrics: { name: 'metrics', title: 'Project metrics', summary: 'Read authorized metrics for a project.', usage: ['blinkhost metrics [--project ID] [--since DURATION]'], details: ['The command is read-only and accepts a linked project when --project is omitted.'], examples: ['blinkhost metrics --project PROJECT_ID --since 24h --json'], related: ['logs'], url: DOCUMENTATION_URL },
|
|
104
|
+
analytics: { name: 'analytics', title: 'Project analytics', summary: 'Read authorized analytics for a project.', usage: ['blinkhost analytics [--project ID]'], details: ['The command is read-only and accepts a linked project when --project is omitted.'], examples: ['blinkhost analytics --project PROJECT_ID --json'], related: ['logs'], url: DOCUMENTATION_URL },
|
|
105
|
+
support: {
|
|
106
|
+
name: 'support', title: 'Redacted support bundles', summary: 'Create a local diagnostic inventory for support without including source or credentials.', usage: ['blinkhost support bundle [--output PATH]'], details: ['The file is created with mode 0600 and refuses to overwrite an existing path. It excludes tokens, secret values, source code, filenames and repository URLs.', 'Review the generated file before sharing it. The default filename is blinkhost-support-<timestamp>.json in the current directory.'], examples: ['blinkhost support bundle', 'blinkhost support bundle --output ./support.json'], related: ['security', 'troubleshooting'], url: DOCUMENTATION_URL,
|
|
107
|
+
},
|
|
108
|
+
completion: {
|
|
109
|
+
name: 'completion', title: 'Shell completion', summary: 'Generate deterministic completion for Bash, Zsh, Fish or PowerShell.', usage: ['blinkhost completion bash|zsh|fish|powershell'], details: ['The command prints a script to standard output; source or install it using your shell configuration.'], examples: ['source <(blinkhost completion bash)', 'blinkhost completion zsh > ~/.zfunc/_blinkhost'], related: ['portability'], url: DOCUMENTATION_URL,
|
|
110
|
+
},
|
|
111
|
+
update: {
|
|
112
|
+
name: 'update', title: 'Check for CLI updates', summary: 'Compare this CLI version with the latest signed GitHub release.', usage: ['blinkhost update check'], details: ['The command reports an update but never installs one automatically. Pin a version with `npm install --global @blinkhost/cli@VERSION`; roll back with the same command and an earlier reviewed version.'], examples: ['blinkhost update check --json'], related: ['supply-chain'], url: DOCUMENTATION_URL,
|
|
113
|
+
},
|
|
114
|
+
ci: {
|
|
115
|
+
name: 'ci', title: 'CI readiness', summary: 'Verify the active workload identity and server-reported CLI capabilities.', usage: ['blinkhost ci check'], details: ['GitHub Actions should use an exact registered OIDC subject and id-token: write. No long-lived BlinkHost token is required.', 'Other CI systems may provide a short-lived BLINKHOST_ACCESS_TOKEN from an approved secret service; never commit or print it.'], examples: ['blinkhost ci check --non-interactive --json'], related: ['automation', 'auth', 'workloads'], url: DOCUMENTATION_URL,
|
|
116
|
+
},
|
|
117
|
+
plugins: {
|
|
118
|
+
name: 'plugins', title: 'Local CLI plugins', summary: 'Approve and run local executables with digest pinning and a restricted environment.', usage: ['blinkhost plugins list', 'blinkhost plugins add /absolute/path --name NAME', 'blinkhost plugins verify NAME', 'blinkhost plugins run NAME -- ARGS', 'blinkhost plugins remove NAME'], details: ['Plugins must be regular files, not symbolic links. BlinkHost records the SHA-256 digest and refuses changed executables until reviewed and added again.', 'Plugins receive only a small allowlist of process variables and never receive BlinkHost credentials. They still run with your operating-system account, so review them first.'], examples: ['blinkhost plugins verify formatter --json'], related: ['security'], url: DOCUMENTATION_URL,
|
|
119
|
+
},
|
|
120
|
+
api: {
|
|
121
|
+
name: 'api', title: 'Advanced customer API access', summary: 'Call supported customer API routes when a dedicated command is unavailable.', usage: ['blinkhost api GET /api/customer/path/', 'blinkhost api POST /api/customer/path/ --data @request.json'], details: ['Internal, staff, authentication and secret-bearing mutation routes are blocked. Absolute URLs, traversal, encoded paths and unsupported methods are rejected.', 'Prefer dedicated commands because they provide stronger validation and clearer safety boundaries.'], examples: ['blinkhost api GET /api/sites/ --json'], related: ['automation', 'security'], url: DOCUMENTATION_URL,
|
|
122
|
+
},
|
|
123
|
+
automation: {
|
|
124
|
+
name: 'automation', title: 'Automation contract', summary: 'Use deterministic output and short-lived identity safely from CI or coding agents.', usage: ['blinkhost COMMAND --json --non-interactive', 'blinkhost docs commands --json'], details: ['With --json, stdout is one JSON object for success or failure. Human and child-process diagnostics go to stderr. Exit codes, envelope fields and documented error codes remain stable within the v2 major line; service-specific data objects may add fields, so consumers must ignore unknown fields.', 'Mutating API requests carry a request ID and an idempotency key. A fresh key is generated for each CLI invocation. A timeout is not proof of failure: use the resource ID or request ID from output, list/get/status, and wait commands to reconcile state before starting a new mutation.', '`--non-interactive` prevents account authorization and preview commands from opening a browser. Never disable approvals, confirmation checks, workspace roles or plan controls in automation.'], examples: ['blinkhost validate . --json --non-interactive', 'blinkhost ci check --json --non-interactive'], related: ['exit-codes', 'ci', 'security', 'troubleshooting'], url: DOCUMENTATION_URL,
|
|
125
|
+
},
|
|
126
|
+
security: {
|
|
127
|
+
name: 'security', title: 'CLI security boundaries', summary: 'Understand credential, secret, plugin, path and remote-action safeguards.', usage: ['blinkhost docs security'], details: ['Passwords never enter the CLI. Refresh credentials require an operating-system credential service; CI uses short-lived identities.', 'Secret values enter only through stdin. Local project and payload files reject unsafe symbolic links and bounded inputs are enforced.', 'Remote roles, limits, approvals, verified artifacts and audit records remain server-enforced. CLI output must not be treated as a way around platform policy.'], examples: ['blinkhost auth sessions --json', 'blinkhost plugins verify NAME --json'], related: ['auth', 'secrets', 'plugins', 'supply-chain'], url: DOCUMENTATION_URL,
|
|
128
|
+
},
|
|
129
|
+
glossary: {
|
|
130
|
+
name: 'glossary', title: 'BlinkHost terminology', summary: 'Translate platform terms into the developer actions they represent.', usage: ['blinkhost docs glossary'],
|
|
131
|
+
details: ['Project: one deployable application and its resources. Module: a Go, Python or Rust backend service declared by that project. Binding: an explicit connection between application code and a managed resource.', 'Preview: a temporary environment for reviewing a change. Build: compilation and verification of source. Deployment: promotion of a verified artifact to an environment.', 'Profile: a local named account configuration. Workload: a narrowly scoped short-lived automation identity. Approval: a required authorized decision before a protected action. Handoff: an audited transfer of agency or team responsibility.'],
|
|
132
|
+
examples: ['blinkhost docs manifest-reference', 'blinkhost docs limits'], related: ['commands', 'manifest-reference'], url: DOCUMENTATION_URL,
|
|
133
|
+
},
|
|
134
|
+
portability: {
|
|
135
|
+
name: 'portability', title: 'Supported environments', summary: 'Prepare Linux, macOS, Windows and headless automation environments.', usage: ['blinkhost quickstart --json'], details: ['Node.js 22.12 or newer is required on Linux, macOS and Windows. npm may warn rather than block an unsupported Node version, so the CLI checks again before operational commands.', 'macOS uses Keychain through the security command. Windows uses Password Vault through PowerShell. Linux uses Secret Service through secret-tool and requires an available desktop or session keyring.', 'Headless CI should use a registered GitHub OIDC workload or a short-lived access token supplied at runtime. Plaintext refresh-token storage is intentionally unsupported.', 'For offline installation, download the package archive, SHA256SUMS and Sigstore bundle on a connected machine; verify them, transfer all files through an approved channel, then run `npm install --global ./blinkhost-cli-VERSION.tgz --offline`.'], examples: ['node --version', 'blinkhost quickstart --json'], related: ['auth', 'ci', 'completion', 'supply-chain'], url: DOCUMENTATION_URL,
|
|
136
|
+
},
|
|
137
|
+
'manifest-reference': {
|
|
138
|
+
name: 'manifest-reference', title: 'blinkhost.yaml reference', summary: 'Describe one application, frontend, backend modules and declared resources with schema blinkhost/v1.', usage: ['blinkhost manifest [path] --json', 'blinkhost validate [path] --json'], details: ['Top-level fields are schema, application, frontend, modules, resources, preview and ignore. Unknown fields are rejected.', `Frontend frameworks: ${SUPPORTED_FRONTENDS.join(', ')}. Package managers: ${SUPPORTED_MANAGERS.join(', ')}. Module languages: ${SUPPORTED_MODULES.join(', ')}.`, 'Paths are repository-relative POSIX paths. Values, aliases, traversal, symbolic-link escapes and platform-reserved segments are rejected. One manifest represents one BlinkHost application; use separate application roots and projects for multiple deployable applications in a monorepo.'], examples: ['blinkhost init apps/web', 'blinkhost validate apps/web --json'], related: ['init', 'validate', 'security'], url: DOCUMENTATION_URL,
|
|
139
|
+
},
|
|
140
|
+
limits: {
|
|
141
|
+
name: 'limits', title: 'Limits and cost boundaries', summary: 'Separate local CLI work from server-enforced plan usage.', usage: ['blinkhost docs limits'], details: ['quickstart, create, init, validate, manifest, doctor, local test and local dev do not create BlinkHost cloud resources.', 'Remote previews, builds, deployments, databases, assets and modules can consume plan capacity. Exact quotas and prices come from the active workspace and current pricing page; the CLI does not guess or hard-code them.', 'Use list/get/status commands to inspect existing state before a mutation. Protected operations may require approval.'], examples: ['blinkhost projects status --json', 'blinkhost auth status --json'], related: ['quickstart', 'automation', 'deployments'], url: 'https://app.blinkhost.me/pricing',
|
|
142
|
+
},
|
|
143
|
+
troubleshooting: {
|
|
144
|
+
name: 'troubleshooting', title: 'Failure and recovery guidance', summary: 'Recover safely from local, authentication, network and remote-operation failures.', usage: ['blinkhost doctor [path] --json', 'blinkhost support bundle'], details: ['Exit 2 means command usage, 3 validation, 4 local filesystem/tooling, 6 authentication/permission, 7 network/timeout, 8 remote failure, and 9 conflict or approval required.', 'The CLI does not automatically retry general remote mutations. Asset completion alone retries a temporary conflict for a bounded period. For a timeout, query the resource before retrying because the server may have completed the request; a new CLI invocation receives a new idempotency key.', 'Resume build, deployment or preview polling with `GROUP wait ID`; use list, get or status plus the request ID returned in an API error to reconcile uncertain state. There is no generic local resume for an interrupted upload or multi-resource sequence.', 'Missing dependencies: run the package manager install command, commit the lockfile, then retry test. Expired sessions: sign in again. Approval-required operations: complete the workspace approval rather than bypassing it.'], examples: ['blinkhost doctor . --json', 'blinkhost deployments get DEPLOYMENT_ID --json', 'blinkhost builds wait BUILD_ID --json'], related: ['support', 'automation', 'auth'], url: DOCUMENTATION_URL,
|
|
145
|
+
},
|
|
146
|
+
errors: {
|
|
147
|
+
name: 'errors', title: 'Error and compatibility contract', summary: 'Interpret stable CLI failures without parsing human prose.', usage: ['blinkhost COMMAND --json --non-interactive', 'blinkhost docs exit-codes'], details: ['Failure JSON has ok=false, command, and error with code, message and details. Details may gain entries; automation should branch on error.code and the process exit code, not message text.', 'Usage errors use exit 2, project/input validation 3, local tooling 4, unexpected CLI failures 5, authentication/authorization 6, network/timeouts 7, remote-service failures 8, and conflicts or required approvals 9.', 'HTTP failures include a request ID in details for support correlation. Secret values, authorization headers and refresh credentials are never intentionally added to CLI errors. Child-process output is sent to stderr in JSON mode and can contain whatever the child tool emits, so do not run untrusted scripts or print secrets from build scripts.'], examples: ['blinkhost creat --json', 'blinkhost validate . --json'], related: ['exit-codes', 'automation', 'security'], url: DOCUMENTATION_URL,
|
|
148
|
+
},
|
|
149
|
+
'supply-chain': {
|
|
150
|
+
name: 'supply-chain', title: 'Release verification', summary: 'Verify npm provenance or the signed GitHub release evidence.', usage: ['npm install --global @blinkhost/cli@VERSION'], details: ['Tagged releases publish from blinkhost-ltd/blinkhost-cli through the release.yml GitHub Actions workflow and npm Trusted Publishing.', 'Each GitHub release contains the package archive, CycloneDX SBOM, SHA256SUMS and SHA256SUMS.sigstore.json. Verify the checksum before installing an archive.', `Release evidence: ${RELEASES_URL}`], examples: ['sha256sum --check SHA256SUMS', `cosign verify-blob --bundle SHA256SUMS.sigstore.json --certificate-identity "https://github.com/blinkhost-ltd/blinkhost-cli/.github/workflows/release.yml@refs/tags/v${VERSION}" --certificate-oidc-issuer https://token.actions.githubusercontent.com SHA256SUMS`], related: ['update', 'security'], url: RELEASES_URL,
|
|
151
|
+
},
|
|
152
|
+
'exit-codes': {
|
|
153
|
+
name: 'exit-codes', title: 'Exit codes and JSON responses', summary: 'Build deterministic scripts around stable outcome categories.', usage: ['blinkhost COMMAND --json --non-interactive'], details: ['0 success; 2 invalid command or option; 3 project or input validation; 4 local filesystem or executable; 5 unexpected CLI failure; 6 authentication or permission; 7 network or timeout; 8 remote service failure; 9 state conflict or approval required.', 'JSON mode writes exactly one response object to stdout. The object contains ok, command, message and optional data or warnings on success, or ok, command and error with code, message and details on failure.'], examples: ['blinkhost validate . --json', 'blinkhost docs commands --json'], related: ['automation', 'troubleshooting'], url: DOCUMENTATION_URL,
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
const COMMANDS = [
|
|
157
|
+
'quickstart', 'docs', 'create', 'init', 'validate', 'manifest', 'doctor', 'test', 'auth', 'profile', 'projects',
|
|
158
|
+
'repositories', 'connections', 'previews', 'builds', 'deployments', 'modules', 'databases', 'bindings', 'assets',
|
|
159
|
+
'secrets', 'organizations', 'templates', 'approvals', 'handoffs', 'policies', 'workloads', 'dev', 'logs', 'metrics',
|
|
160
|
+
'analytics', 'support', 'completion', 'update', 'ci', 'plugins', 'api',
|
|
161
|
+
];
|
|
162
|
+
export const TOP_LEVEL_COMMANDS = [...COMMANDS];
|
|
163
|
+
export function documentationIndex() {
|
|
164
|
+
return {
|
|
165
|
+
schema: 'blinkhost/cli-docs/v1', cli_version: VERSION, documentation_url: DOCUMENTATION_URL,
|
|
166
|
+
topics: Object.values(TOPICS).map(({ name, title, summary, url }) => ({ name, title, summary, url })),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
export function documentationTopic(name) {
|
|
170
|
+
if (name === 'commands' || name === 'docs')
|
|
171
|
+
return {
|
|
172
|
+
name: 'commands', title: 'Command reference', summary: 'Discover every top-level command in this CLI release.',
|
|
173
|
+
usage: ['blinkhost COMMAND --help', 'blinkhost help COMMAND', 'blinkhost docs TOPIC', 'blinkhost docs --search TERM'],
|
|
174
|
+
details: [`Commands: ${COMMANDS.join(', ')}.`, 'Use --json for a versioned machine-readable response and --non-interactive in automation.'],
|
|
175
|
+
examples: ['blinkhost create --help', 'blinkhost docs automation --json'], related: ['quickstart', 'automation', 'exit-codes'], url: DOCUMENTATION_URL,
|
|
176
|
+
};
|
|
177
|
+
return TOPICS[name] ?? (['metrics', 'analytics'].includes(name) ? TOPICS.logs : undefined);
|
|
178
|
+
}
|
|
179
|
+
export function searchDocumentation(query) {
|
|
180
|
+
const normalized = query.trim().toLowerCase();
|
|
181
|
+
if (!normalized || normalized.length > 100 || /[\u0000-\u001f\u007f]/.test(normalized))
|
|
182
|
+
return [];
|
|
183
|
+
return Object.values(TOPICS).filter((topic) => JSON.stringify(topic).toLowerCase().includes(normalized));
|
|
184
|
+
}
|
|
185
|
+
export function renderTopic(topic) {
|
|
186
|
+
const block = [`BlinkHost CLI ${VERSION} — ${topic.title}`, '', topic.summary, '', 'Usage:', ...topic.usage.map((value) => ` ${value}`), '', ...topic.details, ''];
|
|
187
|
+
if (topic.examples.length)
|
|
188
|
+
block.push('Examples:', ...topic.examples.map((value) => ` ${value}`), '');
|
|
189
|
+
if (topic.related.length)
|
|
190
|
+
block.push(`Related topics: ${topic.related.join(', ')}`);
|
|
191
|
+
block.push(`Detailed documentation: ${topic.url}`, '');
|
|
192
|
+
return block.join('\n');
|
|
193
|
+
}
|
|
194
|
+
export function renderTopHelp() {
|
|
195
|
+
return `BlinkHost CLI ${VERSION}\n\nStart here:\n blinkhost quickstart [path] Check local readiness without changing anything\n blinkhost docs [topic] Read the version-matched offline reference\n blinkhost help COMMAND Show command-specific help\n\nCommands:\n ${COMMANDS.join('\n ')}\n\nGlobal options:\n --json Write one machine-readable response to stdout\n --profile NAME Use a named BlinkHost account profile\n --quiet Suppress successful human-readable output\n --verbose Include safe diagnostic detail in errors\n --no-color Disable terminal colour\n --non-interactive Never open a browser or prompt\n --help Show command-specific help\n --version Show the CLI version\n\nDocumentation: ${DOCUMENTATION_URL}\n`;
|
|
196
|
+
}
|
|
197
|
+
async function executable(command, args) {
|
|
198
|
+
return new Promise((resolve) => {
|
|
199
|
+
const child = spawn(command, args, { shell: false, stdio: 'ignore', windowsHide: true });
|
|
200
|
+
child.once('error', () => resolve(false));
|
|
201
|
+
child.once('exit', (code) => resolve(code === 0));
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
export async function quickstart(path) {
|
|
205
|
+
const root = resolveLocalPath(path);
|
|
206
|
+
const nodeOk = supportedNodeVersion();
|
|
207
|
+
const gitOk = await executable('git', ['--version']);
|
|
208
|
+
const credential = await credentialStoreStatus();
|
|
209
|
+
let directoryOk = true;
|
|
210
|
+
try {
|
|
211
|
+
await access(root);
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
directoryOk = false;
|
|
215
|
+
}
|
|
216
|
+
let project = { status: 'not_configured', path: root };
|
|
217
|
+
if (directoryOk) {
|
|
218
|
+
try {
|
|
219
|
+
const validation = await validateProject(root);
|
|
220
|
+
project = {
|
|
221
|
+
status: validation.errors.length ? 'invalid' : 'valid', path: root,
|
|
222
|
+
framework: validation.manifest.frontend.framework,
|
|
223
|
+
modules: validation.manifest.modules.length,
|
|
224
|
+
databases: validation.manifest.resources.databases.length,
|
|
225
|
+
errors: validation.errors, warnings: validation.warnings,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
const code = error.code;
|
|
230
|
+
project = { status: code === 'ENOENT' || (error instanceof Error && error.message.includes('No blinkhost.yaml')) ? 'not_configured' : 'unreadable', path: root };
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const nextSteps = project.status === 'valid'
|
|
234
|
+
? ['blinkhost test .', 'blinkhost auth login', 'blinkhost projects link PROJECT_ID', 'blinkhost dev .']
|
|
235
|
+
: ['blinkhost create my-app --template react --no-install', 'blinkhost init ./existing-app', 'blinkhost docs create', 'blinkhost docs init'];
|
|
236
|
+
return {
|
|
237
|
+
schema: 'blinkhost/cli-quickstart/v1', cli_version: VERSION,
|
|
238
|
+
mode: 'local_read_only', remote_changes: false, billable_resources_created: false,
|
|
239
|
+
checks: [
|
|
240
|
+
{ name: 'node', ok: nodeOk, detected: process.versions.node, required: '>=22.12.0', remediation: nodeOk ? null : 'Install Node.js 22.12 or newer.' },
|
|
241
|
+
{ name: 'git', ok: gitOk, remediation: gitOk ? null : 'Install Git and ensure it is available on PATH.' },
|
|
242
|
+
{ name: 'credential_service', ok: credential.available, provider: credential.provider, required_for: 'interactive account sessions', remediation: credential.remediation },
|
|
243
|
+
{ name: 'directory', ok: directoryOk, path: root },
|
|
244
|
+
],
|
|
245
|
+
project,
|
|
246
|
+
capabilities: {
|
|
247
|
+
frontends: SUPPORTED_FRONTENDS, package_managers: SUPPORTED_MANAGERS, backend_modules: SUPPORTED_MODULES,
|
|
248
|
+
local: ['create', 'init', 'validate', 'manifest', 'doctor', 'test', 'dev', 'docs'],
|
|
249
|
+
remote: ['projects', 'repositories', 'previews', 'builds', 'deployments', 'modules', 'databases', 'assets'],
|
|
250
|
+
remote_requirements: 'Authentication, workspace permission, plan capacity, and approval where applicable.',
|
|
251
|
+
},
|
|
252
|
+
next_steps: nextSteps,
|
|
253
|
+
documentation_url: DOCUMENTATION_URL,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
//# sourceMappingURL=guidance.js.map
|
package/dist/remote.d.ts
CHANGED
|
@@ -14,6 +14,6 @@ export declare function syncProject(kind: 'pull' | 'push', input: string[], prof
|
|
|
14
14
|
export declare function runSecrets(input: string[], profile?: string): Promise<unknown>;
|
|
15
15
|
export declare function rawApi(input: string[], profile?: string): Promise<unknown>;
|
|
16
16
|
export declare function waitForRemote(group: 'builds' | 'deployments' | 'previews', input: string[], profile?: string): Promise<unknown>;
|
|
17
|
-
export declare function openPreview(input: string[], profile?: string): Promise<unknown>;
|
|
17
|
+
export declare function openPreview(input: string[], profile?: string, launchBrowser?: boolean): Promise<unknown>;
|
|
18
18
|
export declare function uploadAsset(input: string[], profile?: string): Promise<unknown>;
|
|
19
19
|
export {};
|
package/dist/remote.js
CHANGED
|
@@ -293,7 +293,7 @@ export async function waitForRemote(group, input, profile) {
|
|
|
293
293
|
}
|
|
294
294
|
throw new CliError(`Timed out waiting for ${group.slice(0, -1)} ${id}.`, EXIT.network, 'wait_timeout');
|
|
295
295
|
}
|
|
296
|
-
export async function openPreview(input, profile) {
|
|
296
|
+
export async function openPreview(input, profile, launchBrowser = true) {
|
|
297
297
|
const args = [...input];
|
|
298
298
|
const id = safeIdentifier(args.shift());
|
|
299
299
|
noExtra(args);
|
|
@@ -306,12 +306,14 @@ export async function openPreview(input, profile) {
|
|
|
306
306
|
const trusted = url.protocol === 'https:' && (url.hostname === 'preview.blinkhost.me' || url.hostname.endsWith('.preview.blinkhost.me') || url.hostname.endsWith('.blinkhost.website'));
|
|
307
307
|
if (!trusted || url.username || url.password)
|
|
308
308
|
throw new CliError('BlinkHost returned an untrusted preview URL.', EXIT.remote, 'preview_url_untrusted');
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
309
|
+
if (launchBrowser) {
|
|
310
|
+
const command = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd.exe' : 'xdg-open';
|
|
311
|
+
const commandArgs = process.platform === 'win32' ? ['/d', '/s', '/c', 'start', '', url.toString()] : [url.toString()];
|
|
312
|
+
const child = spawn(command, commandArgs, { detached: true, shell: false, stdio: 'ignore' });
|
|
313
|
+
child.on('error', () => { });
|
|
314
|
+
child.unref();
|
|
315
|
+
}
|
|
316
|
+
return { id: decodeURIComponent(id), url: url.toString(), browser_opened: launchBrowser };
|
|
315
317
|
}
|
|
316
318
|
const ASSET_MEDIA_TYPES = {
|
|
317
319
|
'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif',
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare const VERSION = "2.1.0";
|
|
2
|
+
export declare const DOCUMENTATION_URL = "https://app.blinkhost.me/docs/source-control/cli";
|
|
3
|
+
export declare const RELEASES_URL = "https://github.com/blinkhost-ltd/blinkhost-cli/releases";
|
|
4
|
+
export declare function supportedNodeVersion(version?: string): boolean;
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const VERSION = '2.1.0';
|
|
2
|
+
export const DOCUMENTATION_URL = 'https://app.blinkhost.me/docs/source-control/cli';
|
|
3
|
+
export const RELEASES_URL = 'https://github.com/blinkhost-ltd/blinkhost-cli/releases';
|
|
4
|
+
export function supportedNodeVersion(version = process.versions.node) {
|
|
5
|
+
const [major = 0, minor = 0] = version.split('.').map(Number);
|
|
6
|
+
return major > 22 || major === 22 && minor >= 12;
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=version.js.map
|
package/dist/workflows.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
export declare function runDev(input: string[]): Promise<unknown>;
|
|
2
|
-
export declare function testProject(input: string[]): Promise<unknown>;
|
|
1
|
+
export declare function runDev(input: string[], json?: boolean): Promise<unknown>;
|
|
2
|
+
export declare function testProject(input: string[], json?: boolean): Promise<unknown>;
|
|
3
3
|
export declare function observability(kind: 'logs' | 'metrics' | 'analytics', input: string[], profile?: string): Promise<unknown>;
|
|
4
|
-
export declare function runPlugins(input: string[]): Promise<unknown>;
|
|
4
|
+
export declare function runPlugins(input: string[], json?: boolean): Promise<unknown>;
|
|
5
5
|
export declare function completion(shell: string | undefined): string;
|
|
6
6
|
export declare function checkForUpdate(): Promise<unknown>;
|
|
7
7
|
export declare function ciCheck(profile?: string): Promise<unknown>;
|
package/dist/workflows.js
CHANGED
|
@@ -8,6 +8,8 @@ import { readConfig, writeConfig } from './config.js';
|
|
|
8
8
|
import { CliError, EXIT } from './errors.js';
|
|
9
9
|
import { readProjectManifest, resolveLocalPath, validateProject } from './project.js';
|
|
10
10
|
import { readProjectLink } from './remote.js';
|
|
11
|
+
import { TOP_LEVEL_COMMANDS } from './guidance.js';
|
|
12
|
+
import { VERSION } from './version.js';
|
|
11
13
|
function takeOption(args, name) {
|
|
12
14
|
const index = args.indexOf(name);
|
|
13
15
|
if (index < 0)
|
|
@@ -22,9 +24,13 @@ function takeFlag(args, name) { const i = args.indexOf(name); if (i < 0)
|
|
|
22
24
|
return false; args.splice(i, 1); return true; }
|
|
23
25
|
function noExtra(args) { if (args.length)
|
|
24
26
|
throw new CliError(`Unexpected argument: ${args[0]}`, EXIT.usage, 'unexpected_argument'); }
|
|
25
|
-
async function spawnInherited(command, args, cwd, env) {
|
|
27
|
+
async function spawnInherited(command, args, cwd, env, json = false) {
|
|
26
28
|
return new Promise((resolve, reject) => {
|
|
27
|
-
const child = spawn(command, args, { cwd, env: env || process.env, shell: false, stdio: 'inherit', windowsHide: true });
|
|
29
|
+
const child = spawn(command, args, { cwd, env: env || process.env, shell: false, stdio: json ? ['inherit', 'pipe', 'pipe'] : 'inherit', windowsHide: true });
|
|
30
|
+
if (json) {
|
|
31
|
+
child.stdout?.on('data', (chunk) => process.stderr.write(chunk));
|
|
32
|
+
child.stderr?.on('data', (chunk) => process.stderr.write(chunk));
|
|
33
|
+
}
|
|
28
34
|
const forward = (signal) => child.kill(signal);
|
|
29
35
|
process.once('SIGINT', forward);
|
|
30
36
|
process.once('SIGTERM', forward);
|
|
@@ -32,7 +38,7 @@ async function spawnInherited(command, args, cwd, env) {
|
|
|
32
38
|
child.once('exit', (code) => { process.off('SIGINT', forward); process.off('SIGTERM', forward); resolve(code ?? 1); });
|
|
33
39
|
});
|
|
34
40
|
}
|
|
35
|
-
export async function runDev(input) {
|
|
41
|
+
export async function runDev(input, json = false) {
|
|
36
42
|
const args = [...input];
|
|
37
43
|
const root = resolveLocalPath(args.shift());
|
|
38
44
|
const host = takeOption(args, '--host');
|
|
@@ -51,12 +57,12 @@ export async function runDev(input) {
|
|
|
51
57
|
if (port)
|
|
52
58
|
managerArgs.push('--port', port);
|
|
53
59
|
const dependencyRoot = manifest.frontend.dependency_root === '.' ? root : join(root, manifest.frontend.dependency_root);
|
|
54
|
-
const exitCode = await spawnInherited(manager, managerArgs, dependencyRoot, { ...process.env, BLINKHOST_LOCAL: '1' });
|
|
60
|
+
const exitCode = await spawnInherited(manager, managerArgs, dependencyRoot, { ...process.env, BLINKHOST_LOCAL: '1' }, json);
|
|
55
61
|
if (exitCode !== 0)
|
|
56
|
-
throw new CliError(`The local development process exited with code ${exitCode}.`, EXIT.remote, 'dev_process_failed');
|
|
62
|
+
throw new CliError(`The local development process exited with code ${exitCode}.`, EXIT.remote, 'dev_process_failed', [`If dependencies are missing, run \`${manager} install\` in ${dependencyRoot}, then retry.`]);
|
|
57
63
|
return { exit_code: exitCode };
|
|
58
64
|
}
|
|
59
|
-
export async function testProject(input) {
|
|
65
|
+
export async function testProject(input, json = false) {
|
|
60
66
|
const args = [...input];
|
|
61
67
|
const root = resolveLocalPath(args.shift());
|
|
62
68
|
noExtra(args);
|
|
@@ -69,9 +75,9 @@ export async function testProject(input) {
|
|
|
69
75
|
const dependencyRoot = manifest.frontend.dependency_root === '.' ? root : join(root, manifest.frontend.dependency_root);
|
|
70
76
|
const packageJson = JSON.parse(await readFile(join(dependencyRoot, 'package.json'), 'utf8'));
|
|
71
77
|
const script = packageJson.scripts?.test ? 'test' : 'build';
|
|
72
|
-
const code = await spawnInherited(manifest.frontend.package_manager, ['run', script], dependencyRoot, { ...process.env, CI: '1' });
|
|
78
|
+
const code = await spawnInherited(manifest.frontend.package_manager, ['run', script], dependencyRoot, { ...process.env, CI: '1' }, json);
|
|
73
79
|
if (code !== 0)
|
|
74
|
-
throw new CliError(`Project ${script} exited with code ${code}.`, EXIT.validation, 'project_test_failed');
|
|
80
|
+
throw new CliError(`Project ${script} exited with code ${code}.`, EXIT.validation, 'project_test_failed', [`If dependencies are missing, run \`${manifest.frontend.package_manager} install\` in ${dependencyRoot}, then retry.`]);
|
|
75
81
|
return { validated: true, command: `${manifest.frontend.package_manager} run ${script}`, exit_code: code };
|
|
76
82
|
}
|
|
77
83
|
export async function observability(kind, input, profile) {
|
|
@@ -98,7 +104,7 @@ export async function observability(kind, input, profile) {
|
|
|
98
104
|
return client.request(`/api/sites/${encodeURIComponent(project)}/observability/${kind}/${suffix}`);
|
|
99
105
|
}
|
|
100
106
|
async function sha256File(path) { return createHash('sha256').update(await readFile(path)).digest('hex'); }
|
|
101
|
-
export async function runPlugins(input) {
|
|
107
|
+
export async function runPlugins(input, json = false) {
|
|
102
108
|
const args = [...input];
|
|
103
109
|
const action = args.shift() || 'list';
|
|
104
110
|
const config = await readConfig();
|
|
@@ -154,27 +160,29 @@ export async function runPlugins(input) {
|
|
|
154
160
|
for (const key of allowed)
|
|
155
161
|
if (process.env[key])
|
|
156
162
|
env[key] = process.env[key];
|
|
157
|
-
const code = await spawnInherited(plugin.executable, passthrough, process.cwd(), env);
|
|
163
|
+
const code = await spawnInherited(plugin.executable, passthrough, process.cwd(), env, json);
|
|
158
164
|
if (code !== 0)
|
|
159
165
|
throw new CliError(`Plugin ${name} exited with code ${code}.`, EXIT.remote, 'plugin_failed');
|
|
160
166
|
return { name, exit_code: code };
|
|
161
167
|
}
|
|
162
168
|
throw new CliError(`Unknown plugins action: ${action}.`, EXIT.usage, 'unknown_action');
|
|
163
169
|
}
|
|
170
|
+
const commandWords = TOP_LEVEL_COMMANDS.join(' ');
|
|
171
|
+
const powershellCommands = TOP_LEVEL_COMMANDS.map((command) => `'${command}'`).join(',');
|
|
164
172
|
const COMPLETIONS = {
|
|
165
|
-
bash: `complete -W "
|
|
166
|
-
zsh: `#compdef blinkhost\n_arguments '1:command:(
|
|
167
|
-
fish: `complete -c blinkhost -f -a "
|
|
168
|
-
powershell: `Register-ArgumentCompleter -Native -CommandName blinkhost -ScriptBlock { param($wordToComplete)
|
|
173
|
+
bash: `complete -W "${commandWords}" blinkhost`,
|
|
174
|
+
zsh: `#compdef blinkhost\n_arguments '1:command:(${commandWords})'`,
|
|
175
|
+
fish: `complete -c blinkhost -f -a "${commandWords}"`,
|
|
176
|
+
powershell: `Register-ArgumentCompleter -Native -CommandName blinkhost -ScriptBlock { param($wordToComplete) ${powershellCommands} | Where-Object { $_ -like "$wordToComplete*" } }`,
|
|
169
177
|
};
|
|
170
178
|
export function completion(shell) { if (!shell || !COMPLETIONS[shell])
|
|
171
179
|
throw new CliError('Choose bash, zsh, fish, or powershell.', EXIT.usage, 'invalid_shell'); return `${COMPLETIONS[shell]}\n`; }
|
|
172
180
|
export async function checkForUpdate() {
|
|
173
|
-
const response = await fetch('https://api.github.com/repos/blinkhost-ltd/blinkhost-cli/releases/latest', { headers: { Accept: 'application/vnd.github+json', 'User-Agent':
|
|
181
|
+
const response = await fetch('https://api.github.com/repos/blinkhost-ltd/blinkhost-cli/releases/latest', { headers: { Accept: 'application/vnd.github+json', 'User-Agent': `BlinkHost-CLI/${VERSION}` }, redirect: 'error' });
|
|
174
182
|
if (!response.ok)
|
|
175
183
|
throw new CliError('The release service could not be reached.', EXIT.network, 'update_check_failed');
|
|
176
184
|
const data = await response.json();
|
|
177
|
-
return { current_version:
|
|
185
|
+
return { current_version: VERSION, latest_version: data.tag_name?.replace(/^v/, '') || null, release_url: data.html_url || null, automatic_install: false };
|
|
178
186
|
}
|
|
179
187
|
export async function ciCheck(profile) {
|
|
180
188
|
const client = await ApiClient.create(profile);
|
|
@@ -201,7 +209,7 @@ export async function supportBundle(input, profile) {
|
|
|
201
209
|
catch (error) {
|
|
202
210
|
api = { reachable: false, error_code: error instanceof CliError ? error.code : 'unknown' };
|
|
203
211
|
}
|
|
204
|
-
const payload = { schema: 'blinkhost/support-bundle/v1', created_at: new Date().toISOString(), cli_version:
|
|
212
|
+
const payload = { schema: 'blinkhost/support-bundle/v1', created_at: new Date().toISOString(), cli_version: VERSION, system: { platform: platform(), release: release(), node: process.versions.node, device_hash: createHash('sha256').update(hostname()).digest('hex').slice(0, 16) }, project, api };
|
|
205
213
|
await mkdir(dirname(output), { recursive: true });
|
|
206
214
|
await writeFile(output, `${JSON.stringify(payload, null, 2)}\n`, { mode: 0o600, flag: 'wx' });
|
|
207
215
|
return { output, redacted: true };
|
package/package.json
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blinkhost/cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Secure local and remote developer workflows for BlinkHost",
|
|
5
|
+
"keywords": ["blinkhost", "deployment", "hosting", "cloud", "cli", "developer-tools"],
|
|
6
|
+
"homepage": "https://app.blinkhost.me/docs/source-control/cli",
|
|
7
|
+
"repository": { "type": "git", "url": "git+https://github.com/blinkhost-ltd/blinkhost-cli.git" },
|
|
8
|
+
"bugs": { "url": "https://github.com/blinkhost-ltd/blinkhost-cli/issues" },
|
|
5
9
|
"type": "module",
|
|
6
10
|
"bin": { "blinkhost": "dist/cli.js" },
|
|
7
11
|
"files": ["dist/*.js", "dist/*.d.ts", "README.md", "LICENSE"],
|