@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 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
- an explicit project/environment target. This command does not upload, authorize,
189
- or execute the release.
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>`: required target project identifier
204
- - `--environment <environment>`: required target environment identifier
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 deploy`
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
- HYPEQUERY_API_TOKEN=<token> \
215
- npx hypequery deploy analytics/hypequery-deployment \
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
- The token is accepted only through `HYPEQUERY_API_TOKEN`, keeping it out of
221
- shell history. The submission endpoint must use HTTPS, may also be supplied by
222
- `HYPEQUERY_DEPLOYMENT_ENDPOINT`, and must not contain credentials or a URL
223
- fragment. The release identity is sent as the idempotency key, so an unchanged
224
- release can be submitted safely again.
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; defaults to `HYPEQUERY_DEPLOYMENT_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;AAkBpC,QAAA,MAAM,OAAO,SAAgB,CAAC;AAa9B,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;EAKpE;AAgLD,OAAO,EAAE,OAAO,EAAE,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
- .requiredOption('--project <project>', 'Target project identifier')
137
- .requiredOption('--environment <environment>', 'Target environment identifier')
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('deploy <bundle>')
144
- .description('Submit a verified deployment bundle and release')
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 (or HYPEQUERY_DEPLOYMENT_ENDPOINT)')
160
+ .option('--endpoint <url>', 'HTTPS submission endpoint; requires HYPEQUERY_API_TOKEN')
147
161
  .action(runCommand(async (bundle, options) => {
148
- await deployCommand(bundle, options);
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 deploy analytics/hypequery-deployment --release analytics/hypequery-deployment.release.json');
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
- export interface DeployOptions {
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 DeployDependencies {
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 deployCommand(bundlePath: string | undefined, options?: DeployOptions, dependencies?: DeployDependencies): Promise<DeploymentSubmissionResponse>;
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":"AAKA,OAAO,EACL,mCAAmC,EAEnC,KAAK,4BAA4B,EAElC,MAAM,+BAA+B,CAAC;AAMvC,MAAM,WAAW,aAAa;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,kBAAkB;IACjC,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;CACvE;AA4DD,wBAAsB,aAAa,CACjC,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,OAAO,GAAE,aAAkB,EAC3B,YAAY,GAAE,kBAAuB,GACpC,OAAO,CAAC,4BAA4B,CAAC,CA0DvC"}
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"}
@@ -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
- export async function deployCommand(bundlePath, options = {}, dependencies = {}) {
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 deploy analytics/hypequery-deployment '
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 env = dependencies.env ?? process.env;
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 = { endpoint, token };
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;AAiB7B,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;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;AA0DD,wBAAsB,+BAA+B,CACnD,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,OAAO,GAAE,+BAAoC,GAC5C,OAAO,CAAC,iCAAiC,CAAC,CA8C5C"}
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('--output must be outside the closed deployment bundle directory.');
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('--output must not traverse a dangling symbolic link.');
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('--output must not be a symbolic link.');
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
- + '--project <project> --environment <environment>');
282
- }
283
- if (options.project === undefined) {
284
- throw new Error('Missing required --project <project>.');
285
- }
286
- if (options.environment === undefined) {
287
- throw new Error('Missing required --environment <environment>.');
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: options.project,
306
- environment: options.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;