@hypequery/cli 1.10.4 → 1.12.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 +135 -17
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +38 -8
- package/dist/commands/deploy.d.ts +18 -3
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +130 -8
- package/dist/commands/deployment.d.ts +10 -1
- package/dist/commands/deployment.d.ts.map +1 -1
- package/dist/commands/deployment.js +39 -20
- package/dist/commands/login.d.ts +21 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +241 -0
- package/dist/utils/cloud-credential-store.d.ts +28 -0
- package/dist/utils/cloud-credential-store.d.ts.map +1 -0
- package/dist/utils/cloud-credential-store.js +293 -0
- package/dist/utils/deployment-upload.d.ts.map +1 -1
- package/dist/utils/deployment-upload.js +13 -2
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -25,6 +25,13 @@ Or install it once:
|
|
|
25
25
|
npm install -D @hypequery/cli
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
+
Ship it to Hypequery Cloud:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npx @hypequery/cli login
|
|
32
|
+
npx @hypequery/cli deploy analytics/api.ts
|
|
33
|
+
```
|
|
34
|
+
|
|
28
35
|
## Commands
|
|
29
36
|
|
|
30
37
|
### `hypequery init`
|
|
@@ -140,7 +147,9 @@ semantic keys such as `dataset:orders`.
|
|
|
140
147
|
|
|
141
148
|
Builds a closed deployment bundle for an exported HypeQuery API. The bundle
|
|
142
149
|
contains canonical deployment metadata, every referenced runtime artifact, and
|
|
143
|
-
a manifest that binds their exact bytes and identities.
|
|
150
|
+
a manifest that binds their exact bytes and identities. `hypequery deploy` runs
|
|
151
|
+
this step for you; reach for it directly in CI pipelines that stage artifacts,
|
|
152
|
+
or when you need the runtime options below.
|
|
144
153
|
|
|
145
154
|
```bash
|
|
146
155
|
npx hypequery deployment:build analytics/api.ts
|
|
@@ -182,11 +191,112 @@ are still accepted for metadata-only validation.
|
|
|
182
191
|
npx hypequery deployment:validate analytics/hypequery-deployment
|
|
183
192
|
```
|
|
184
193
|
|
|
194
|
+
### `hypequery login`
|
|
195
|
+
|
|
196
|
+
Authorizes the local CLI through your existing Hypequery Cloud browser
|
|
197
|
+
session. The command uses an S256 PKCE loopback flow, then stores the
|
|
198
|
+
target-scoped deployment token in the operating-system credential vault.
|
|
199
|
+
Tokens expire after 12 hours.
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
npx hypequery login
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Use `--cloud-url <origin>` or `HYPEQUERY_CLOUD_URL` for a self-hosted or local
|
|
206
|
+
Cloud instance. HTTPS is required except for loopback development origins.
|
|
207
|
+
|
|
208
|
+
Once logged in, `hypequery deploy` automatically uses the stored endpoint and
|
|
209
|
+
token, and resolves the project and environment from the same stored profile.
|
|
210
|
+
The deployment endpoint is returned by Cloud during the token exchange; it is
|
|
211
|
+
the authenticated upload API, not the browser login endpoint. For
|
|
212
|
+
non-interactive CI usage, set both `HYPEQUERY_API_TOKEN` and
|
|
213
|
+
`HYPEQUERY_DEPLOYMENT_ENDPOINT` (or pass `--endpoint`).
|
|
214
|
+
|
|
215
|
+
Because the CLI also loads a project `.env`, a `HYPEQUERY_API_TOKEN` left there
|
|
216
|
+
takes precedence over the login flow and is rejected unless it is paired with an
|
|
217
|
+
endpoint. Unset it to use `hypequery login`.
|
|
218
|
+
|
|
219
|
+
Token storage requires an operating-system credential vault (Keychain,
|
|
220
|
+
Credential Manager, or a Secret Service implementation such as
|
|
221
|
+
gnome-keyring/KWallet). Headless environments without one should use
|
|
222
|
+
`HYPEQUERY_API_TOKEN` instead of `hypequery login`.
|
|
223
|
+
|
|
224
|
+
### `hypequery logout`
|
|
225
|
+
|
|
226
|
+
Revokes the current Cloud token and removes it from the operating-system
|
|
227
|
+
credential vault:
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
npx hypequery logout
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
If the stored profile is unreadable, `logout` still removes the local state and
|
|
234
|
+
reports that the token was not revoked; it expires on its own.
|
|
235
|
+
|
|
236
|
+
### `hypequery deploy`
|
|
237
|
+
|
|
238
|
+
Builds a deployment bundle from an API module, prepares a target-bound release,
|
|
239
|
+
and submits both. This is the primary Cloud workflow: sign in once, then deploy
|
|
240
|
+
with one command.
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
npx hypequery login
|
|
244
|
+
npx hypequery deploy analytics/api.ts
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
The bundle is written to `analytics/hypequery-deployment` and the release to
|
|
248
|
+
`analytics/hypequery-deployment.release.json`. The target comes from the stored
|
|
249
|
+
Cloud profile unless you override it:
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
npx hypequery deploy analytics/api.ts \
|
|
253
|
+
--project my-project \
|
|
254
|
+
--environment production
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
The deployment endpoint and token are resolved before the build, so a missing
|
|
258
|
+
or expired login fails immediately rather than after bundling. The credential
|
|
259
|
+
rules are the same as `hypequery deployment:submit` below.
|
|
260
|
+
|
|
261
|
+
The three steps remain available individually as `deployment:build`,
|
|
262
|
+
`deployment:release`, and `deployment:submit` for CI pipelines that stage
|
|
263
|
+
artifacts, and for the cases `deploy` does not cover: `deploy` always builds the
|
|
264
|
+
runtime artifact itself, so Python deployments and prebuilt runtime artifacts
|
|
265
|
+
(`--runtime`, `--runtime-artifact`, `--runtime-file`) need `deployment:build`.
|
|
266
|
+
|
|
267
|
+
Options:
|
|
268
|
+
|
|
269
|
+
- `--project <project>`: target project identifier; defaults to the logged-in
|
|
270
|
+
target and must be paired with `--environment`
|
|
271
|
+
- `--environment <environment>`: target environment identifier; defaults to the
|
|
272
|
+
logged-in target and must be paired with `--project`
|
|
273
|
+
- `--bundle-output <directory>`: bundle directory, default
|
|
274
|
+
`analytics/hypequery-deployment`
|
|
275
|
+
- `--release-output <path>`: release JSON path, default beside the bundle
|
|
276
|
+
- `--endpoint <url>`: HTTPS submission endpoint; requires `HYPEQUERY_API_TOKEN`
|
|
277
|
+
|
|
278
|
+
> **Deprecated:** `hypequery deploy <bundle> --release <file>` still submits a
|
|
279
|
+
> prebuilt bundle and prints a deprecation warning. Use
|
|
280
|
+
> `hypequery deployment:submit` instead. `--release` cannot be combined with
|
|
281
|
+
> `--project`, `--environment`, `--bundle-output`, or `--release-output`.
|
|
282
|
+
|
|
185
283
|
### `hypequery deployment:release`
|
|
186
284
|
|
|
187
285
|
Prepares a deterministic release request from a verified deployment bundle and
|
|
188
|
-
|
|
189
|
-
|
|
286
|
+
a project/environment target. This command does not upload, authorize, or
|
|
287
|
+
execute the release.
|
|
288
|
+
|
|
289
|
+
After `hypequery login` the target is read from the stored Cloud profile, and
|
|
290
|
+
the resolved target is printed alongside the release identity:
|
|
291
|
+
|
|
292
|
+
```bash
|
|
293
|
+
npx hypequery deployment:release analytics/hypequery-deployment
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
To target something other than the logged-in project and environment, pass both
|
|
297
|
+
flags. A half-specified override is rejected rather than completed from the
|
|
298
|
+
stored profile, so an unset shell variable fails loudly instead of silently
|
|
299
|
+
retargeting the release:
|
|
190
300
|
|
|
191
301
|
```bash
|
|
192
302
|
npx hypequery deployment:release analytics/hypequery-deployment \
|
|
@@ -200,28 +310,36 @@ invalidate bundle verification.
|
|
|
200
310
|
|
|
201
311
|
Options:
|
|
202
312
|
|
|
203
|
-
- `--project <project>`:
|
|
204
|
-
|
|
313
|
+
- `--project <project>`: target project identifier; defaults to the logged-in
|
|
314
|
+
target and must be paired with `--environment`
|
|
315
|
+
- `--environment <environment>`: target environment identifier; defaults to the
|
|
316
|
+
logged-in target and must be paired with `--project`
|
|
205
317
|
- `--output <path>`: release JSON path, default beside the bundle
|
|
206
318
|
|
|
207
|
-
### `hypequery
|
|
319
|
+
### `hypequery deployment:submit`
|
|
208
320
|
|
|
209
321
|
Submits a verified deployment bundle with an already-prepared target-bound
|
|
210
322
|
release. The command verifies both inputs again, requires their bundle
|
|
211
|
-
identities to match, and streams only the files declared by the bundle.
|
|
323
|
+
identities to match, and streams only the files declared by the bundle. Use it
|
|
324
|
+
when the bundle and release were produced by an earlier pipeline step; for the
|
|
325
|
+
ordinary path, use `hypequery deploy`.
|
|
212
326
|
|
|
213
327
|
```bash
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
--release analytics/hypequery-deployment.release.json \
|
|
217
|
-
--endpoint https://deploy.example.com/v1/releases
|
|
328
|
+
npx hypequery deployment:submit analytics/hypequery-deployment \
|
|
329
|
+
--release analytics/hypequery-deployment.release.json
|
|
218
330
|
```
|
|
219
331
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
`
|
|
223
|
-
|
|
224
|
-
|
|
332
|
+
For local development, run `hypequery login` first; the CLI reads the endpoint
|
|
333
|
+
and token from its secure Cloud profile. For CI and manual credentials, set
|
|
334
|
+
`HYPEQUERY_API_TOKEN` together with either `--endpoint` or
|
|
335
|
+
`HYPEQUERY_DEPLOYMENT_ENDPOINT`. The CLI never combines one explicit value with
|
|
336
|
+
the other value from the stored profile. Tokens are never accepted as
|
|
337
|
+
command-line arguments, keeping them out of shell history. The submission
|
|
338
|
+
endpoint must not contain credentials or a URL fragment, and must use HTTPS
|
|
339
|
+
except for `127.0.0.1`/`localhost`, which is permitted for local Cloud
|
|
340
|
+
development and warns that the token is sent in cleartext. The release identity
|
|
341
|
+
is sent as the idempotency key, so an unchanged release can be submitted safely
|
|
342
|
+
again.
|
|
225
343
|
|
|
226
344
|
This command submits immutable deployment inputs. Activation, status changes,
|
|
227
345
|
promotion, and rollback remain control-plane operations.
|
|
@@ -229,7 +347,7 @@ promotion, and rollback remain control-plane operations.
|
|
|
229
347
|
Options:
|
|
230
348
|
|
|
231
349
|
- `--release <path>`: required target-bound release JSON
|
|
232
|
-
- `--endpoint <url>`: HTTPS submission endpoint;
|
|
350
|
+
- `--endpoint <url>`: HTTPS submission endpoint; requires `HYPEQUERY_API_TOKEN`
|
|
233
351
|
|
|
234
352
|
## Non-interactive Setup
|
|
235
353
|
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAyBpC,QAAA,MAAM,OAAO,SAAgB,CAAC;AAa9B,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;EAKpE;AAsND,OAAO,EAAE,OAAO,EAAE,CAAC"}
|
package/dist/cli.js
CHANGED
|
@@ -7,7 +7,8 @@ import { generateCommand } from './commands/generate.js';
|
|
|
7
7
|
import { generateDatasetsCommand } from './commands/generate-datasets.js';
|
|
8
8
|
import { generateManifestCommand } from './commands/generate-manifest.js';
|
|
9
9
|
import { buildDeploymentCommand, prepareDeploymentReleaseCommand, validateDeploymentCommand, } from './commands/deployment.js';
|
|
10
|
-
import { deployCommand, } from './commands/deploy.js';
|
|
10
|
+
import { deployCommand, submitDeploymentCommand, } from './commands/deploy.js';
|
|
11
|
+
import { loginCommand, logoutCommand, } from './commands/login.js';
|
|
11
12
|
const program = new Command();
|
|
12
13
|
function getCliVersion() {
|
|
13
14
|
try {
|
|
@@ -28,6 +29,19 @@ program
|
|
|
28
29
|
.name('hypequery')
|
|
29
30
|
.description('Type-safe analytics layer for ClickHouse')
|
|
30
31
|
.version(getCliVersion());
|
|
32
|
+
program
|
|
33
|
+
.command('login')
|
|
34
|
+
.description('Authorize this machine with Hypequery Cloud')
|
|
35
|
+
.option('--cloud-url <url>', 'Cloud origin (or HYPEQUERY_CLOUD_URL)')
|
|
36
|
+
.action(runCommand(async (options) => {
|
|
37
|
+
await loginCommand(options);
|
|
38
|
+
}));
|
|
39
|
+
program
|
|
40
|
+
.command('logout')
|
|
41
|
+
.description('Revoke and remove the local Hypequery Cloud credential')
|
|
42
|
+
.action(runCommand(async () => {
|
|
43
|
+
await logoutCommand();
|
|
44
|
+
}));
|
|
31
45
|
function runCommand(action) {
|
|
32
46
|
return async (...args) => {
|
|
33
47
|
try {
|
|
@@ -133,19 +147,31 @@ program
|
|
|
133
147
|
program
|
|
134
148
|
.command('deployment:release <bundle>')
|
|
135
149
|
.description('Prepare a target-bound release from a verified deployment bundle')
|
|
136
|
-
.
|
|
137
|
-
.
|
|
150
|
+
.option('--project <project>', 'Target project identifier (advanced override)')
|
|
151
|
+
.option('--environment <environment>', 'Target environment identifier (advanced override)')
|
|
138
152
|
.option('-o, --output <path>', 'Release JSON path (default: beside the bundle)')
|
|
139
153
|
.action(runCommand(async (bundle, options) => {
|
|
140
154
|
await prepareDeploymentReleaseCommand(bundle, options);
|
|
141
155
|
}));
|
|
142
156
|
program
|
|
143
|
-
.command('
|
|
144
|
-
.description('Submit a
|
|
157
|
+
.command('deployment:submit <bundle>')
|
|
158
|
+
.description('Submit a prebuilt deployment bundle and release')
|
|
145
159
|
.requiredOption('--release <path>', 'Target-bound release JSON path')
|
|
146
|
-
.option('--endpoint <url>', 'HTTPS submission endpoint
|
|
160
|
+
.option('--endpoint <url>', 'HTTPS submission endpoint; requires HYPEQUERY_API_TOKEN')
|
|
147
161
|
.action(runCommand(async (bundle, options) => {
|
|
148
|
-
await
|
|
162
|
+
await submitDeploymentCommand(bundle, options);
|
|
163
|
+
}));
|
|
164
|
+
program
|
|
165
|
+
.command('deploy <source>')
|
|
166
|
+
.description('Build, prepare, and deploy a Hypequery API module')
|
|
167
|
+
.option('--bundle-output <directory>', 'Bundle directory (default: analytics/hypequery-deployment)')
|
|
168
|
+
.option('--release-output <path>', 'Release JSON path (default: beside the bundle)')
|
|
169
|
+
.option('--project <project>', 'Target project identifier (advanced override)')
|
|
170
|
+
.option('--environment <environment>', 'Target environment identifier (advanced override)')
|
|
171
|
+
.option('--release <path>', 'Submit a prebuilt bundle with this release (legacy)')
|
|
172
|
+
.option('--endpoint <url>', 'HTTPS submission endpoint; requires HYPEQUERY_API_TOKEN')
|
|
173
|
+
.action(runCommand(async (source, options) => {
|
|
174
|
+
await deployCommand(source, options);
|
|
149
175
|
}));
|
|
150
176
|
// Help command
|
|
151
177
|
program
|
|
@@ -170,6 +196,8 @@ program
|
|
|
170
196
|
program.on('--help', () => {
|
|
171
197
|
console.log('');
|
|
172
198
|
console.log('Examples:');
|
|
199
|
+
console.log(' hypequery login');
|
|
200
|
+
console.log(' hypequery logout');
|
|
173
201
|
console.log(' hypequery init');
|
|
174
202
|
console.log(' hypequery dev');
|
|
175
203
|
console.log(' hypequery dev --port 3000');
|
|
@@ -177,10 +205,12 @@ program.on('--help', () => {
|
|
|
177
205
|
console.log(' hypequery generate:types --output analytics/schema.ts');
|
|
178
206
|
console.log(' hypequery generate:datasets');
|
|
179
207
|
console.log(' hypequery generate:manifest analytics/api.ts --output analytics/hypequery-manifest.json');
|
|
208
|
+
console.log(' hypequery deploy analytics/api.ts');
|
|
209
|
+
console.log(' hypequery deploy analytics/api.ts --project my-project --environment production');
|
|
180
210
|
console.log(' hypequery deployment:build analytics/api.ts');
|
|
181
211
|
console.log(' hypequery deployment:validate analytics/hypequery-deployment');
|
|
182
212
|
console.log(' hypequery deployment:release analytics/hypequery-deployment --project my-project --environment production');
|
|
183
|
-
console.log(' hypequery
|
|
213
|
+
console.log(' hypequery deployment:submit analytics/hypequery-deployment --release analytics/hypequery-deployment.release.json');
|
|
184
214
|
console.log('');
|
|
185
215
|
console.log('Docs: https://hypequery.com/docs');
|
|
186
216
|
});
|
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
import { createHttpDeploymentUploadTransport, type DeploymentSubmissionResponse } from '../utils/deployment-upload.js';
|
|
2
|
-
|
|
2
|
+
import { type StoredCloudCredential } from '../utils/cloud-credential-store.js';
|
|
3
|
+
import { buildDeploymentCommand, prepareDeploymentReleaseCommand } from './deployment.js';
|
|
4
|
+
export interface SubmitDeploymentOptions {
|
|
3
5
|
release?: string;
|
|
4
6
|
endpoint?: string;
|
|
5
7
|
}
|
|
6
|
-
export interface
|
|
8
|
+
export interface DeployOptions extends SubmitDeploymentOptions {
|
|
9
|
+
project?: string;
|
|
10
|
+
environment?: string;
|
|
11
|
+
bundleOutput?: string;
|
|
12
|
+
releaseOutput?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface SubmitDeploymentDependencies {
|
|
7
15
|
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
8
16
|
readonly createTransport?: typeof createHttpDeploymentUploadTransport;
|
|
17
|
+
readonly loadCredential?: () => Promise<StoredCloudCredential | null>;
|
|
18
|
+
}
|
|
19
|
+
export interface DeployDependencies extends SubmitDeploymentDependencies {
|
|
20
|
+
readonly buildDeployment?: typeof buildDeploymentCommand;
|
|
21
|
+
readonly prepareDeploymentRelease?: typeof prepareDeploymentReleaseCommand;
|
|
22
|
+
readonly submitDeployment?: typeof submitDeploymentCommand;
|
|
9
23
|
}
|
|
10
|
-
export declare function
|
|
24
|
+
export declare function submitDeploymentCommand(bundlePath: string | undefined, options?: SubmitDeploymentOptions, dependencies?: SubmitDeploymentDependencies): Promise<DeploymentSubmissionResponse>;
|
|
25
|
+
export declare function deployCommand(sourcePath: string | undefined, options?: DeployOptions, dependencies?: DeployDependencies): Promise<DeploymentSubmissionResponse>;
|
|
11
26
|
//# sourceMappingURL=deploy.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../src/commands/deploy.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../src/commands/deploy.ts"],"names":[],"mappings":"AASA,OAAO,EACL,mCAAmC,EAEnC,KAAK,4BAA4B,EAElC,MAAM,+BAA+B,CAAC;AAEvC,OAAO,EAEL,KAAK,qBAAqB,EAC3B,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EACL,sBAAsB,EACtB,+BAA+B,EAChC,MAAM,iBAAiB,CAAC;AAMzB,MAAM,WAAW,uBAAuB;IACtC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,aAAc,SAAQ,uBAAuB;IAC5D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,CAAC,GAAG,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;IAC5D,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,mCAAmC,CAAC;IACtE,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAC,CAAC;CACvE;AAED,MAAM,WAAW,kBAAmB,SAAQ,4BAA4B;IACtE,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,sBAAsB,CAAC;IACzD,QAAQ,CAAC,wBAAwB,CAAC,EAAE,OAAO,+BAA+B,CAAC;IAC3E,QAAQ,CAAC,gBAAgB,CAAC,EAAE,OAAO,uBAAuB,CAAC;CAC5D;AA8GD,wBAAsB,uBAAuB,CAC3C,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,OAAO,GAAE,uBAA4B,EACrC,YAAY,GAAE,4BAAiC,GAC9C,OAAO,CAAC,4BAA4B,CAAC,CAwDvC;AAmDD,wBAAsB,aAAa,CACjC,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,OAAO,GAAE,aAAkB,EAC3B,YAAY,GAAE,kBAAuB,GACpC,OAAO,CAAC,4BAA4B,CAAC,CAmDvC"}
|
package/dist/commands/deploy.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import { constants, open } from 'node:fs/promises';
|
|
1
|
+
import { access, constants, open } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
2
3
|
import { prepareProtocolDeploymentReleaseEnvelope, } from '@hypequery/protocol';
|
|
3
|
-
import { verifyDeploymentBundle } from '../utils/deployment-bundle.js';
|
|
4
|
+
import { DEPLOYMENT_BUNDLE_MANIFEST, verifyDeploymentBundle, } from '../utils/deployment-bundle.js';
|
|
4
5
|
import { createHttpDeploymentUploadTransport, DeploymentUploadError, } from '../utils/deployment-upload.js';
|
|
5
6
|
import { logger } from '../utils/logger.js';
|
|
7
|
+
import { loadCloudCredential, } from '../utils/cloud-credential-store.js';
|
|
8
|
+
import { buildDeploymentCommand, prepareDeploymentReleaseCommand, } from './deployment.js';
|
|
6
9
|
const MAX_RELEASE_FILE_BYTES = 16 * 1024;
|
|
10
|
+
const DEFAULT_BUNDLE_PATH = 'analytics/hypequery-deployment';
|
|
7
11
|
const utf8Decoder = new TextDecoder('utf-8', { fatal: true });
|
|
8
12
|
async function readReleaseFile(releasePath) {
|
|
9
13
|
let handle;
|
|
@@ -60,16 +64,47 @@ function requiredConfiguration(value, message) {
|
|
|
60
64
|
throw new Error(message);
|
|
61
65
|
return value;
|
|
62
66
|
}
|
|
63
|
-
|
|
67
|
+
/**
|
|
68
|
+
* Resolves the submission endpoint and token. Kept separate from
|
|
69
|
+
* `submitDeploymentCommand` so `deployCommand` can run it as a preflight
|
|
70
|
+
* before the bundle build.
|
|
71
|
+
*/
|
|
72
|
+
async function resolveDeploymentCredential(endpointOption, dependencies) {
|
|
73
|
+
const env = dependencies.env ?? process.env;
|
|
74
|
+
let deploymentEndpoint = endpointOption ?? env.HYPEQUERY_DEPLOYMENT_ENDPOINT;
|
|
75
|
+
let token = env.HYPEQUERY_API_TOKEN;
|
|
76
|
+
const hasExplicitEndpoint = Boolean(deploymentEndpoint);
|
|
77
|
+
const hasExplicitToken = Boolean(token);
|
|
78
|
+
if (hasExplicitEndpoint !== hasExplicitToken) {
|
|
79
|
+
throw new Error(hasExplicitEndpoint
|
|
80
|
+
? 'An explicit deployment endpoint requires HYPEQUERY_API_TOKEN.'
|
|
81
|
+
: 'HYPEQUERY_API_TOKEN requires --endpoint or HYPEQUERY_DEPLOYMENT_ENDPOINT.\n\n'
|
|
82
|
+
+ 'If you meant to use `hypequery login`, unset HYPEQUERY_API_TOKEN — '
|
|
83
|
+
+ 'the CLI also reads it from a project .env file.');
|
|
84
|
+
}
|
|
85
|
+
if (!hasExplicitEndpoint) {
|
|
86
|
+
const credential = await (dependencies.loadCredential ?? loadCloudCredential)();
|
|
87
|
+
if (credential) {
|
|
88
|
+
if (Date.parse(credential.expiresAt) <= Date.now()) {
|
|
89
|
+
throw new Error('The stored Cloud credential has expired. Run `hypequery login` again.');
|
|
90
|
+
}
|
|
91
|
+
deploymentEndpoint = credential.deploymentEndpoint;
|
|
92
|
+
token = credential.token;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
endpoint: requiredConfiguration(deploymentEndpoint, 'Missing deployment endpoint. Run `hypequery login`, pass --endpoint, or set HYPEQUERY_DEPLOYMENT_ENDPOINT.'),
|
|
97
|
+
token: requiredConfiguration(token, 'Missing deployment credential. Run `hypequery login` or set HYPEQUERY_API_TOKEN.'),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
export async function submitDeploymentCommand(bundlePath, options = {}, dependencies = {}) {
|
|
64
101
|
if (!bundlePath) {
|
|
65
102
|
throw new Error('Missing deployment bundle path.\n\n'
|
|
66
|
-
+ 'Usage: hypequery
|
|
103
|
+
+ 'Usage: hypequery deployment:submit analytics/hypequery-deployment '
|
|
67
104
|
+ '--release analytics/hypequery-deployment.release.json');
|
|
68
105
|
}
|
|
69
106
|
const releasePath = requiredConfiguration(options.release, 'Missing required --release <path>.');
|
|
70
|
-
const
|
|
71
|
-
const endpoint = requiredConfiguration(options.endpoint ?? env.HYPEQUERY_DEPLOYMENT_ENDPOINT, 'Missing deployment endpoint. Pass --endpoint or set HYPEQUERY_DEPLOYMENT_ENDPOINT.');
|
|
72
|
-
const token = requiredConfiguration(env.HYPEQUERY_API_TOKEN, 'Missing HYPEQUERY_API_TOKEN.');
|
|
107
|
+
const { endpoint: deploymentEndpoint, token } = await resolveDeploymentCredential(options.endpoint, dependencies);
|
|
73
108
|
let bundle;
|
|
74
109
|
try {
|
|
75
110
|
bundle = await verifyDeploymentBundle(bundlePath);
|
|
@@ -91,7 +126,10 @@ export async function deployCommand(bundlePath, options = {}, dependencies = {})
|
|
|
91
126
|
throw new DeploymentUploadError('HQ_UPLOAD_IDENTITY_MISMATCH', 'Release bundle identity does not match the verified deployment bundle.');
|
|
92
127
|
}
|
|
93
128
|
const createTransport = dependencies.createTransport ?? createHttpDeploymentUploadTransport;
|
|
94
|
-
const transportOptions = {
|
|
129
|
+
const transportOptions = {
|
|
130
|
+
endpoint: deploymentEndpoint,
|
|
131
|
+
token,
|
|
132
|
+
};
|
|
95
133
|
const result = await createTransport(transportOptions).submit(bundle, release);
|
|
96
134
|
logger.success(result.status === 'accepted'
|
|
97
135
|
? 'Deployment release accepted'
|
|
@@ -100,3 +138,87 @@ export async function deployCommand(bundlePath, options = {}, dependencies = {})
|
|
|
100
138
|
logger.info(`Bundle identity: ${result.bundleIdentity}`);
|
|
101
139
|
return result;
|
|
102
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* `deploy` takes an API module, but the deprecated form took a bundle
|
|
143
|
+
* directory. Dropping `--release` from an existing script would otherwise hand
|
|
144
|
+
* a bundle to the module loader and fail inside esbuild.
|
|
145
|
+
*/
|
|
146
|
+
async function rejectBundleDirectorySource(sourcePath) {
|
|
147
|
+
try {
|
|
148
|
+
await access(path.join(sourcePath, DEPLOYMENT_BUNDLE_MANIFEST));
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
throw new Error(`Cannot deploy a prebuilt bundle directory: ${sourcePath}\n\n`
|
|
154
|
+
+ '`hypequery deploy` takes the API module to build, for example '
|
|
155
|
+
+ '`hypequery deploy analytics/api.ts`.\n'
|
|
156
|
+
+ 'To upload this bundle, use `hypequery deployment:submit '
|
|
157
|
+
+ `${sourcePath} --release <file>\`.`);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* `deploy` exposes no runtime flags, so a runtime error from the shared build
|
|
161
|
+
* command would otherwise point at flags this command does not accept.
|
|
162
|
+
*/
|
|
163
|
+
function withRuntimeFlagHint(error) {
|
|
164
|
+
if (!(error instanceof Error) || !error.message.includes('--runtime'))
|
|
165
|
+
return error;
|
|
166
|
+
return new Error(`${error.message}\n\n`
|
|
167
|
+
+ 'Runtime overrides are not available on `hypequery deploy`. Build with '
|
|
168
|
+
+ '`hypequery deployment:build` (which accepts --runtime, --runtime-artifact, '
|
|
169
|
+
+ 'and --runtime-file), then upload with `hypequery deployment:submit`.');
|
|
170
|
+
}
|
|
171
|
+
function rejectLegacyOrchestrationOptions(options) {
|
|
172
|
+
if (options.project !== undefined
|
|
173
|
+
|| options.environment !== undefined
|
|
174
|
+
|| options.bundleOutput !== undefined
|
|
175
|
+
|| options.releaseOutput !== undefined) {
|
|
176
|
+
throw new Error('--release selects prebuilt submission mode and cannot be combined with '
|
|
177
|
+
+ '--project, --environment, --bundle-output, or --release-output. '
|
|
178
|
+
+ 'Use `hypequery deployment:submit` for explicit prebuilt uploads.');
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
export async function deployCommand(sourcePath, options = {}, dependencies = {}) {
|
|
182
|
+
if (!sourcePath) {
|
|
183
|
+
throw new Error('Missing API module path.\n\n'
|
|
184
|
+
+ 'Usage: hypequery deploy analytics/api.ts');
|
|
185
|
+
}
|
|
186
|
+
if (options.release !== undefined) {
|
|
187
|
+
rejectLegacyOrchestrationOptions(options);
|
|
188
|
+
logger.warn('`hypequery deploy <bundle> --release <file>` is deprecated. '
|
|
189
|
+
+ 'Use `hypequery deployment:submit <bundle> --release <file>` instead.');
|
|
190
|
+
return submitDeploymentCommand(sourcePath, {
|
|
191
|
+
release: options.release,
|
|
192
|
+
endpoint: options.endpoint,
|
|
193
|
+
}, dependencies);
|
|
194
|
+
}
|
|
195
|
+
await rejectBundleDirectorySource(sourcePath);
|
|
196
|
+
const bundlePath = options.bundleOutput ?? DEFAULT_BUNDLE_PATH;
|
|
197
|
+
const releasePath = options.releaseOutput
|
|
198
|
+
?? `${bundlePath.replace(/[\\/]+$/, '')}.release.json`;
|
|
199
|
+
const build = dependencies.buildDeployment ?? buildDeploymentCommand;
|
|
200
|
+
const prepareRelease = dependencies.prepareDeploymentRelease
|
|
201
|
+
?? prepareDeploymentReleaseCommand;
|
|
202
|
+
const submit = dependencies.submitDeployment ?? submitDeploymentCommand;
|
|
203
|
+
// Resolve credentials up front. Submission is the last step, so without this
|
|
204
|
+
// a missing or expired login only surfaces after a full bundle build.
|
|
205
|
+
await resolveDeploymentCredential(options.endpoint, dependencies);
|
|
206
|
+
try {
|
|
207
|
+
await build(sourcePath, { bundleOutput: bundlePath });
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
throw withRuntimeFlagHint(error);
|
|
211
|
+
}
|
|
212
|
+
await prepareRelease(bundlePath, {
|
|
213
|
+
project: options.project,
|
|
214
|
+
environment: options.environment,
|
|
215
|
+
output: releasePath,
|
|
216
|
+
}, {
|
|
217
|
+
loadCredential: dependencies.loadCredential,
|
|
218
|
+
outputFlagLabel: '--release-output',
|
|
219
|
+
});
|
|
220
|
+
return submit(bundlePath, {
|
|
221
|
+
release: releasePath,
|
|
222
|
+
endpoint: options.endpoint,
|
|
223
|
+
}, dependencies);
|
|
224
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ProtocolDeploymentContract, type ProtocolDeploymentReleaseEnvelope } from '@hypequery/protocol';
|
|
2
|
+
import { type StoredCloudCredential } from '../utils/cloud-credential-store.js';
|
|
2
3
|
export interface BuildDeploymentOptions {
|
|
3
4
|
bundleOutput?: string;
|
|
4
5
|
output?: string;
|
|
@@ -14,7 +15,15 @@ export interface PrepareDeploymentReleaseOptions {
|
|
|
14
15
|
environment?: string;
|
|
15
16
|
output?: string;
|
|
16
17
|
}
|
|
18
|
+
export interface PrepareDeploymentReleaseDependencies {
|
|
19
|
+
loadCredential?: () => Promise<StoredCloudCredential | null>;
|
|
20
|
+
/**
|
|
21
|
+
* Flag name to quote in output-path errors. `hypequery deploy` delegates
|
|
22
|
+
* here but spells the same option `--release-output`.
|
|
23
|
+
*/
|
|
24
|
+
outputFlagLabel?: string;
|
|
25
|
+
}
|
|
17
26
|
export declare function buildDeploymentCommand(apiPath: string | undefined, options?: BuildDeploymentOptions): Promise<ProtocolDeploymentContract>;
|
|
18
27
|
export declare function validateDeploymentCommand(artifactPath: string | undefined): Promise<ProtocolDeploymentContract>;
|
|
19
|
-
export declare function prepareDeploymentReleaseCommand(bundlePath: string | undefined, options?: PrepareDeploymentReleaseOptions): Promise<ProtocolDeploymentReleaseEnvelope>;
|
|
28
|
+
export declare function prepareDeploymentReleaseCommand(bundlePath: string | undefined, options?: PrepareDeploymentReleaseOptions, dependencies?: PrepareDeploymentReleaseDependencies): Promise<ProtocolDeploymentReleaseEnvelope>;
|
|
20
29
|
//# sourceMappingURL=deployment.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deployment.d.ts","sourceRoot":"","sources":["../../src/commands/deployment.ts"],"names":[],"mappings":"AAGA,OAAO,EAGL,KAAK,0BAA0B,EAC/B,KAAK,iCAAiC,EACvC,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"deployment.d.ts","sourceRoot":"","sources":["../../src/commands/deployment.ts"],"names":[],"mappings":"AAGA,OAAO,EAGL,KAAK,0BAA0B,EAC/B,KAAK,iCAAiC,EACvC,MAAM,qBAAqB,CAAC;AAc7B,OAAO,EAEL,KAAK,qBAAqB,EAC3B,MAAM,oCAAoC,CAAC;AAI5C,MAAM,WAAW,sBAAsB;IACrC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAC;IAC5B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,+BAA+B;IAC9C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,oCAAoC;IACnD,cAAc,CAAC,EAAE,MAAM,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAC,CAAC;IAC7D;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAoDD,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,0BAA0B,CAAC,CAwIrC;AAED,wBAAsB,yBAAyB,CAC7C,YAAY,EAAE,MAAM,GAAG,SAAS,GAC/B,OAAO,CAAC,0BAA0B,CAAC,CA2ErC;AAiED,wBAAsB,+BAA+B,CACnD,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,OAAO,GAAE,+BAAoC,EAC7C,YAAY,GAAE,oCAAyC,GACtD,OAAO,CAAC,iCAAiC,CAAC,CAkE5C"}
|
|
@@ -6,6 +6,7 @@ import { writeDeploymentBundle, verifyDeploymentBundle, readDeploymentRuntimeFil
|
|
|
6
6
|
import { buildNodeRuntimeArtifact, getDeploymentRuntimeEntrypoints, } from '../utils/deployment-runtime-artifact.js';
|
|
7
7
|
import { loadApiModule } from '../utils/load-api.js';
|
|
8
8
|
import { logger } from '../utils/logger.js';
|
|
9
|
+
import { loadCloudCredential, } from '../utils/cloud-credential-store.js';
|
|
9
10
|
const SHA256_PATTERN = /^[0-9a-f]{64}$/;
|
|
10
11
|
const DEFAULT_BUNDLE_OUTPUT = 'analytics/hypequery-deployment';
|
|
11
12
|
function runtimeName(options) {
|
|
@@ -217,7 +218,7 @@ export async function validateDeploymentCommand(artifactPath) {
|
|
|
217
218
|
logger.info(`Identity: ${digest}`);
|
|
218
219
|
return contract;
|
|
219
220
|
}
|
|
220
|
-
function assertOutputOutsideBundle(bundlePath, outputPath) {
|
|
221
|
+
function assertOutputOutsideBundle(bundlePath, outputPath, outputFlag) {
|
|
221
222
|
const bundle = path.resolve(bundlePath);
|
|
222
223
|
const output = path.resolve(outputPath);
|
|
223
224
|
const relative = path.relative(bundle, output);
|
|
@@ -225,10 +226,10 @@ function assertOutputOutsideBundle(bundlePath, outputPath) {
|
|
|
225
226
|
|| relative.startsWith(`..${path.sep}`)
|
|
226
227
|
|| path.isAbsolute(relative);
|
|
227
228
|
if (relative === '' || !outside) {
|
|
228
|
-
throw new Error(
|
|
229
|
+
throw new Error(`${outputFlag} must be outside the closed deployment bundle directory.`);
|
|
229
230
|
}
|
|
230
231
|
}
|
|
231
|
-
async function prospectiveRealPath(inputPath) {
|
|
232
|
+
async function prospectiveRealPath(inputPath, outputFlag) {
|
|
232
233
|
let current = path.resolve(inputPath);
|
|
233
234
|
const missingSegments = [];
|
|
234
235
|
for (;;) {
|
|
@@ -243,7 +244,7 @@ async function prospectiveRealPath(inputPath) {
|
|
|
243
244
|
try {
|
|
244
245
|
const stat = await lstat(current);
|
|
245
246
|
if (stat.isSymbolicLink()) {
|
|
246
|
-
throw new Error(
|
|
247
|
+
throw new Error(`${outputFlag} must not traverse a dangling symbolic link.`);
|
|
247
248
|
}
|
|
248
249
|
}
|
|
249
250
|
catch (lstatError) {
|
|
@@ -260,11 +261,11 @@ async function prospectiveRealPath(inputPath) {
|
|
|
260
261
|
}
|
|
261
262
|
}
|
|
262
263
|
}
|
|
263
|
-
async function assertOutputIsNotSymbolicLink(outputPath) {
|
|
264
|
+
async function assertOutputIsNotSymbolicLink(outputPath, outputFlag) {
|
|
264
265
|
try {
|
|
265
266
|
const outputStat = await lstat(outputPath);
|
|
266
267
|
if (outputStat.isSymbolicLink()) {
|
|
267
|
-
throw new Error(
|
|
268
|
+
throw new Error(`${outputFlag} must not be a symbolic link.`);
|
|
268
269
|
}
|
|
269
270
|
}
|
|
270
271
|
catch (error) {
|
|
@@ -274,20 +275,37 @@ async function assertOutputIsNotSymbolicLink(outputPath) {
|
|
|
274
275
|
}
|
|
275
276
|
}
|
|
276
277
|
}
|
|
277
|
-
export async function prepareDeploymentReleaseCommand(bundlePath, options = {}) {
|
|
278
|
+
export async function prepareDeploymentReleaseCommand(bundlePath, options = {}, dependencies = {}) {
|
|
278
279
|
if (!bundlePath) {
|
|
279
280
|
throw new Error('Missing deployment bundle path.\n\n'
|
|
280
|
-
+ 'Usage: hypequery deployment:release analytics/hypequery-deployment
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
281
|
+
+ 'Usage: hypequery deployment:release analytics/hypequery-deployment');
|
|
282
|
+
}
|
|
283
|
+
// Passing either flag opts out of the logged-in target entirely. Completing
|
|
284
|
+
// a half-specified override from the stored profile would silently retarget
|
|
285
|
+
// the release — `--project "$P" --environment "$E"` with an unset variable
|
|
286
|
+
// must fail loudly, not bind to whatever the last login used.
|
|
287
|
+
let project;
|
|
288
|
+
let environment;
|
|
289
|
+
if (options.project !== undefined || options.environment !== undefined) {
|
|
290
|
+
if (!options.project)
|
|
291
|
+
throw new Error('Missing required --project <project>.');
|
|
292
|
+
if (!options.environment)
|
|
293
|
+
throw new Error('Missing required --environment <environment>.');
|
|
294
|
+
project = options.project;
|
|
295
|
+
environment = options.environment;
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
const credential = await (dependencies.loadCredential ?? loadCloudCredential)();
|
|
299
|
+
project = credential?.target?.project;
|
|
300
|
+
environment = credential?.target?.environment;
|
|
301
|
+
if (!project || !environment) {
|
|
302
|
+
throw new Error('Missing deployment target. Run `hypequery login` or pass both '
|
|
303
|
+
+ '--project <project> and --environment <environment>.');
|
|
304
|
+
}
|
|
288
305
|
}
|
|
306
|
+
const outputFlag = dependencies.outputFlagLabel ?? '--output';
|
|
289
307
|
const outputPath = options.output ?? `${bundlePath.replace(/[\\/]+$/, '')}.release.json`;
|
|
290
|
-
assertOutputOutsideBundle(bundlePath, outputPath);
|
|
308
|
+
assertOutputOutsideBundle(bundlePath, outputPath, outputFlag);
|
|
291
309
|
let bundle;
|
|
292
310
|
try {
|
|
293
311
|
bundle = await verifyDeploymentBundle(bundlePath);
|
|
@@ -296,20 +314,21 @@ export async function prepareDeploymentReleaseCommand(bundlePath, options = {})
|
|
|
296
314
|
throw new Error(`Cannot prepare a release from an invalid deployment bundle: ${bundlePath}\n\n`
|
|
297
315
|
+ (error instanceof Error ? error.message : String(error)));
|
|
298
316
|
}
|
|
299
|
-
assertOutputOutsideBundle(bundle.directory, await prospectiveRealPath(outputPath));
|
|
317
|
+
assertOutputOutsideBundle(bundle.directory, await prospectiveRealPath(outputPath, outputFlag), outputFlag);
|
|
300
318
|
const prepared = prepareProtocolDeploymentReleaseEnvelope({
|
|
301
319
|
kind: 'hypequery-deployment-release',
|
|
302
320
|
version: 1,
|
|
303
321
|
bundleIdentity: bundle.identity,
|
|
304
322
|
target: {
|
|
305
|
-
project
|
|
306
|
-
environment
|
|
323
|
+
project,
|
|
324
|
+
environment,
|
|
307
325
|
},
|
|
308
326
|
});
|
|
309
327
|
await mkdir(path.dirname(outputPath), { recursive: true });
|
|
310
|
-
await assertOutputIsNotSymbolicLink(outputPath);
|
|
328
|
+
await assertOutputIsNotSymbolicLink(outputPath, outputFlag);
|
|
311
329
|
await writeFile(outputPath, `${prepared.canonical}\n`, 'utf8');
|
|
312
330
|
logger.success(`Deployment release written to ${outputPath}`);
|
|
331
|
+
logger.info(`Target: ${project}/${environment}`);
|
|
313
332
|
logger.info(`Release identity: ${prepared.identity}`);
|
|
314
333
|
logger.info(`Bundle identity: ${bundle.identity}`);
|
|
315
334
|
return prepared.release;
|