@hypequery/cli 1.15.0 → 1.16.1

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
@@ -251,6 +251,34 @@ Options:
251
251
 
252
252
  ## Non-interactive Setup
253
253
 
254
+ ### Cloud deployment targets in CI
255
+
256
+ Login selects a stable Cloud deployment target. The deployment itself captures
257
+ the checked-out Git branch, commit, and dirty state as source provenance; none
258
+ of those values select or create an environment. Select the destination
259
+ explicitly when the same branch or commit deploys to more than one environment:
260
+
261
+ ```bash
262
+ npx hypequery login --environment development
263
+ ```
264
+
265
+ For CI, create a target-scoped API key in Cloud for each environment and store
266
+ it as a separate secret. The explicit release target and its matching key make
267
+ the destination independent of the source branch:
268
+
269
+ ```bash
270
+ HYPEQUERY_API_TOKEN="$HYPEQUERY_DEV_TOKEN" npx hypequery deploy analytics/api.ts \
271
+ --project acme:analytics \
272
+ --environment development
273
+
274
+ HYPEQUERY_API_TOKEN="$HYPEQUERY_PROD_TOKEN" npx hypequery deploy analytics/api.ts \
275
+ --project acme:analytics \
276
+ --environment production
277
+ ```
278
+
279
+ Set `HYPEQUERY_DEPLOYMENT_ENDPOINT` for both jobs. A key is scoped to one target,
280
+ so a production key cannot submit a development release or vice versa.
281
+
254
282
  For ClickHouse, `hypequery init --no-interactive` reads:
255
283
 
256
284
  - `CLICKHOUSE_URL` or deprecated `CLICKHOUSE_HOST`
package/dist/cli.js CHANGED
@@ -34,7 +34,7 @@ program
34
34
  .command('login')
35
35
  .description('Authorize this machine with Hypequery Cloud')
36
36
  .option('--cloud-url <url>', 'Cloud origin (or HYPEQUERY_CLOUD_URL)')
37
- .option('--no-branch', 'Do not send the current Git branch (or HYPEQUERY_CLI_SEND_BRANCH=0)')
37
+ .option('--environment <environment>', 'Stable deployment target')
38
38
  .action(runCommand(async (options) => {
39
39
  await loginCommand(options);
40
40
  }));
@@ -1,12 +1,11 @@
1
1
  import { deleteCloudCredential, loadCloudCredential, saveCloudCredential } from '../utils/cloud-credential-store.js';
2
2
  export interface LoginOptions {
3
3
  readonly cloudUrl?: string;
4
- /** Commander sets this to false for `--no-branch`. */
5
- readonly branch?: boolean;
4
+ /** Stable Cloud deployment target. */
5
+ readonly environment?: string;
6
6
  }
7
7
  export interface LoginDependencies {
8
8
  readonly fetch?: typeof fetch;
9
- readonly getGitBranch?: () => Promise<string | null>;
10
9
  readonly openBrowser?: (url: string) => Promise<unknown>;
11
10
  readonly now?: () => number;
12
11
  readonly requestTimeoutMs?: number;
@@ -1 +1 @@
1
- {"version":3,"file":"login.d.ts","sourceRoot":"","sources":["../../src/commands/login.ts"],"names":[],"mappings":"AAMA,OAAO,EAEL,qBAAqB,EACrB,mBAAmB,EAGnB,mBAAmB,EAEpB,MAAM,oCAAoC,CAAC;AAiB5C,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,sDAAsD;IACtD,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IAC9B,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACrD,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACzD,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IAC5B,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,mBAAmB,CAAC;IACrD,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAaD,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IAC9B,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,mBAAmB,CAAC;IACrD,QAAQ,CAAC,gBAAgB,CAAC,EAAE,OAAO,qBAAqB,CAAC;IACzD,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;CACpC;AAsJD,wBAAsB,YAAY,CAChC,OAAO,GAAE,YAAiB,EAC1B,YAAY,GAAE,iBAAsB,iBAkErC;AAED,wBAAsB,aAAa,CAAC,YAAY,GAAE,kBAAuB,iBAoCxE"}
1
+ {"version":3,"file":"login.d.ts","sourceRoot":"","sources":["../../src/commands/login.ts"],"names":[],"mappings":"AAMA,OAAO,EAEL,qBAAqB,EACrB,mBAAmB,EAGnB,mBAAmB,EAEpB,MAAM,oCAAoC,CAAC;AAgB5C,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,sCAAsC;IACtC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IAC9B,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACzD,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IAC5B,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,mBAAmB,CAAC;IACrD,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IAC9B,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,mBAAmB,CAAC;IACrD,QAAQ,CAAC,gBAAgB,CAAC,EAAE,OAAO,qBAAqB,CAAC;IACzD,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;CACpC;AAsJD,wBAAsB,YAAY,CAChC,OAAO,GAAE,YAAiB,EAC1B,YAAY,GAAE,iBAAsB,iBAyErC;AAED,wBAAsB,aAAa,CAAC,YAAY,GAAE,kBAAuB,iBAoCxE"}
@@ -3,7 +3,6 @@ import { createServer } from 'node:http';
3
3
  import open from 'open';
4
4
  import { validateProtocolDeploymentReleaseTarget } from '@hypequery/protocol';
5
5
  import { CLOUD_DEPLOYMENT_SCOPE, deleteCloudCredential, loadCloudCredential, normalizeCloudDeploymentEndpoint, normalizeCloudOrigin, saveCloudCredential, } from '../utils/cloud-credential-store.js';
6
- import { currentGitBranch } from '../utils/git-branch.js';
7
6
  import { logger } from '../utils/logger.js';
8
7
  const DEFAULT_CLOUD_URL = 'https://cloud.hypequery.com';
9
8
  const LOGIN_TIMEOUT_MS = 5 * 60_000;
@@ -17,17 +16,6 @@ const MAX_TOKEN_LIFETIME_MS = 13 * 60 * 60_000;
17
16
  // CLI the day Cloud rotates its token format.
18
17
  const TOKEN_PATTERN = /^hqdp_[A-Za-z0-9_-]{16,512}$/;
19
18
  const utf8Decoder = new TextDecoder('utf-8', { fatal: true });
20
- /**
21
- * Branch names carry internal detail (customer names, embargoed identifiers),
22
- * so both an explicit `--no-branch` and the environment variable suppress the
23
- * parameter. The flag wins because it is the more specific signal.
24
- */
25
- function branchContextEnabled(options) {
26
- if (options.branch === false)
27
- return false;
28
- const configured = process.env.HYPEQUERY_CLI_SEND_BRANCH?.trim().toLowerCase();
29
- return configured !== '0' && configured !== 'false' && configured !== 'no';
30
- }
31
19
  function callbackHtml(success) {
32
20
  const title = success ? 'CLI authorized' : 'Authorization failed';
33
21
  const message = success
@@ -174,20 +162,26 @@ export async function loginCommand(options = {}, dependencies = {}) {
174
162
  const state = randomBytes(24).toString('base64url');
175
163
  const verifier = randomBytes(32).toString('base64url');
176
164
  const challenge = createHash('sha256').update(verifier).digest('base64url');
177
- // Resolve branch context before the loopback listener opens: shelling out to
178
- // git should not hold a server socket, and a caller-supplied resolver that
179
- // rejects must not leak one.
180
- const branch = branchContextEnabled(options)
181
- ? await (dependencies.getGitBranch ?? currentGitBranch)()
182
- : null;
165
+ let environment;
166
+ if (options.environment !== undefined) {
167
+ try {
168
+ environment = validateProtocolDeploymentReleaseTarget({
169
+ project: 'target',
170
+ environment: options.environment,
171
+ }).environment;
172
+ }
173
+ catch {
174
+ throw new Error('Invalid deployment environment. Use letters, numbers, dots, underscores, colons, or hyphens.');
175
+ }
176
+ }
183
177
  const callback = await callbackServer(state, dependencies.timeoutMs ?? LOGIN_TIMEOUT_MS);
184
178
  const authorizeUrl = new URL('/cli/authorize', origin);
185
179
  authorizeUrl.searchParams.set('redirect_uri', callback.redirectUri);
186
180
  authorizeUrl.searchParams.set('code_challenge', challenge);
187
181
  authorizeUrl.searchParams.set('code_challenge_method', 'S256');
188
182
  authorizeUrl.searchParams.set('state', state);
189
- if (branch)
190
- authorizeUrl.searchParams.set('branch', branch);
183
+ if (environment)
184
+ authorizeUrl.searchParams.set('environment', environment);
191
185
  try {
192
186
  logger.info('Opening your browser to authorize Hypequery CLI…');
193
187
  await (dependencies.openBrowser ?? open)(authorizeUrl.toString());
@@ -215,8 +209,8 @@ export async function loginCommand(options = {}, dependencies = {}) {
215
209
  const credential = tokenResponse(body, origin, (dependencies.now ?? Date.now)());
216
210
  await (dependencies.saveCredential ?? saveCloudCredential)(credential);
217
211
  logger.success('Logged in to Hypequery Cloud');
218
- // Cloud resolves the target, and branch context only expresses intent, so
219
- // print what was actually issued rather than what was requested.
212
+ // Cloud resolves the target, so print what was actually issued rather than
213
+ // what was requested.
220
214
  if (credential.target) {
221
215
  logger.info(`Deployments target ${credential.target.project} / ${credential.target.environment}`);
222
216
  }
@@ -19,6 +19,7 @@ export interface DeploymentBundleSourceSnapshot {
19
19
  readonly kind: 'git';
20
20
  readonly commit: string;
21
21
  readonly dirty: boolean;
22
+ readonly branch?: string;
22
23
  };
23
24
  }
24
25
  export interface WrittenDeploymentBundle {
@@ -1 +1 @@
1
- {"version":3,"file":"deployment-bundle.d.ts","sourceRoot":"","sources":["../../src/utils/deployment-bundle.ts"],"names":[],"mappings":"AAYA,OAAO,EAGL,KAAK,kCAAkC,EACvC,KAAK,gCAAgC,EACrC,KAAK,0BAA0B,EAChC,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,0BAA0B,EAC1B,0BAA0B,EAC1B,sBAAsB,EACvB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AAGtE,OAAO,EACL,0BAA0B,EAC1B,0BAA0B,EAC1B,sBAAsB,GACvB,CAAC;AACF,YAAY,EAAE,wBAAwB,EAAE,CAAC;AAEzC,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,QAAQ,CAAC;IACpC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;CAC5B;AAED,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;CAC5B;AAED,MAAM,WAAW,8BAA8B;IAC7C,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,SAAS,0BAA0B,EAAE,CAAC;IACtD,QAAQ,CAAC,QAAQ,CAAC,EAAE;QAClB,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;QACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;QACxB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;KACzB,CAAC;CACH;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,gCAAgC,CAAC;IACpD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,0BAA0B,CAAC;CAC/C;AA0ID,wBAAsB,qBAAqB,CACzC,eAAe,EAAE,MAAM,EACvB,QAAQ,EAAE,kCAAkC,EAC5C,YAAY,EAAE,SAAS,2BAA2B,EAAE,EACpD,cAAc,CAAC,EAAE,8BAA8B,GAC9C,OAAO,CAAC,uBAAuB,CAAC,CA+FlC;AAoCD,wBAAsB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAMrF"}
1
+ {"version":3,"file":"deployment-bundle.d.ts","sourceRoot":"","sources":["../../src/utils/deployment-bundle.ts"],"names":[],"mappings":"AAYA,OAAO,EAGL,KAAK,kCAAkC,EACvC,KAAK,gCAAgC,EACrC,KAAK,0BAA0B,EAChC,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,0BAA0B,EAC1B,0BAA0B,EAC1B,sBAAsB,EACvB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AAGtE,OAAO,EACL,0BAA0B,EAC1B,0BAA0B,EAC1B,sBAAsB,GACvB,CAAC;AACF,YAAY,EAAE,wBAAwB,EAAE,CAAC;AAEzC,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,QAAQ,CAAC;IACpC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;CAC5B;AAED,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;CAC5B;AAED,MAAM,WAAW,8BAA8B;IAC7C,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,SAAS,0BAA0B,EAAE,CAAC;IACtD,QAAQ,CAAC,QAAQ,CAAC,EAAE;QAClB,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;QACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;QACxB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;QACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;KAC1B,CAAC;CACH;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,gCAAgC,CAAC;IACpD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,0BAA0B,CAAC;CAC/C;AA0ID,wBAAsB,qBAAqB,CACzC,eAAe,EAAE,MAAM,EACvB,QAAQ,EAAE,kCAAkC,EAC5C,YAAY,EAAE,SAAS,2BAA2B,EAAE,EACpD,cAAc,CAAC,EAAE,8BAA8B,GAC9C,OAAO,CAAC,uBAAuB,CAAC,CA+FlC;AAoCD,wBAAsB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAMrF"}
@@ -1 +1 @@
1
- {"version":3,"file":"deployment-source-snapshot.d.ts","sourceRoot":"","sources":["../../src/utils/deployment-source-snapshot.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAEV,8BAA8B,EAC/B,MAAM,wBAAwB,CAAC;AAqEhC,wBAAsB,+BAA+B,CACnD,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,8BAA8B,CAAC,CAkDzC"}
1
+ {"version":3,"file":"deployment-source-snapshot.d.ts","sourceRoot":"","sources":["../../src/utils/deployment-source-snapshot.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAEV,8BAA8B,EAC/B,MAAM,wBAAwB,CAAC;AA0EhC,wBAAsB,+BAA+B,CACnD,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,8BAA8B,CAAC,CAkDzC"}
@@ -3,6 +3,7 @@ import { constants, lstat, open, realpath } from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import { DEFAULT_PROTOCOL_DEPLOYMENT_BUNDLE_LIMITS, } from '@hypequery/protocol';
5
5
  import { build } from 'esbuild';
6
+ import { currentGitBranch } from './git-branch.js';
6
7
  import { findNearestTsconfig } from './load-api.js';
7
8
  function portableProjectPath(projectRoot, absolutePath) {
8
9
  const relative = path.relative(projectRoot, absolutePath);
@@ -47,7 +48,10 @@ function gitOutput(projectRoot, arguments_) {
47
48
  });
48
49
  }
49
50
  async function sourceRevision(projectRoot) {
50
- const commit = await gitOutput(projectRoot, ['rev-parse', 'HEAD']);
51
+ const [commit, branch] = await Promise.all([
52
+ gitOutput(projectRoot, ['rev-parse', 'HEAD']),
53
+ currentGitBranch(projectRoot),
54
+ ]);
51
55
  if (!commit || !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(commit))
52
56
  return undefined;
53
57
  const status = await gitOutput(projectRoot, ['status', '--porcelain', '--untracked-files=all']);
@@ -57,6 +61,7 @@ async function sourceRevision(projectRoot) {
57
61
  kind: 'git',
58
62
  commit,
59
63
  dirty: status.length > 0,
64
+ ...(branch ? { branch } : {}),
60
65
  });
61
66
  }
62
67
  export async function captureDeploymentSourceSnapshot(apiPath) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hypequery/cli",
3
- "version": "1.15.0",
3
+ "version": "1.16.1",
4
4
  "description": "Command-line interface for hypequery",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -19,8 +19,8 @@
19
19
  "open": "^10.0.0",
20
20
  "ora": "^8.0.1",
21
21
  "prompts": "^2.4.2",
22
- "@hypequery/deployment": "0.7.0",
23
- "@hypequery/protocol": "0.10.0"
22
+ "@hypequery/deployment": "0.7.1",
23
+ "@hypequery/protocol": "0.10.1"
24
24
  },
25
25
  "optionalDependencies": {
26
26
  "@napi-rs/keyring": "1.3.0"