@hypequery/cli 1.16.1 → 1.16.3

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
@@ -1,302 +1,60 @@
1
1
  # @hypequery/cli
2
2
 
3
- CLI for scaffolding and running the main hypequery path.
3
+ The fastest way to start a type-safe ClickHouse analytics backend in TypeScript.
4
4
 
5
- Use it to:
5
+ `@hypequery/cli` connects to ClickHouse or embedded chDB, generates schema types, scaffolds queries or semantic datasets, and runs a local API with interactive documentation. The same CLI can generate React route manifests and deploy a verified analytics bundle when you are ready.
6
6
 
7
- - generate schema types from ClickHouse or embedded chDB
8
- - scaffold `analytics/` files
9
- - run the local dev server with docs
10
- - build and validate portable deployment contracts
11
-
12
- ## Quick Start
13
-
14
- Run it directly:
15
-
16
- ```bash
17
- npx @hypequery/cli init
18
- npx @hypequery/cli dev
19
- npx @hypequery/cli generate
20
- ```
21
-
22
- Or install it once:
7
+ ## Start here
23
8
 
24
9
  ```bash
25
10
  npm install -D @hypequery/cli
26
- ```
27
-
28
- ## Commands
29
-
30
- ### `hypequery init`
31
-
32
- Scaffolds the standard hypequery setup.
33
-
34
- ```bash
35
11
  npx hypequery init
12
+ npx hypequery dev --open
36
13
  ```
37
14
 
38
- Interactive setup asks which database driver to use, whether to include
39
- request-context authentication scaffolding, where to write the generated
40
- files, and which API style to create. Selecting chDB replaces the remote
41
- credential questions with an embedded-storage choice.
15
+ `init` checks the database, writes an `analytics/` project, generates types, and installs the packages used by the scaffold.
42
16
 
43
- Run `init` from the project directory that contains `package.json`. If no
44
- package manifest is found, interactive setup asks for confirmation before
45
- writing files and dependency installation must be completed manually.
46
-
47
- It will:
48
-
49
- - connect to ClickHouse or start an embedded chDB session
50
- - generate schema types when the database is available
51
- - create client and query files
52
- - write `.env` values for ClickHouse connections
53
- - update `.gitignore`, including a project-local persistent chDB directory
54
- - install scaffold dependencies, including `zod` and the selected database adapter
55
-
56
- Options:
57
-
58
- - `--path <path>`: output directory, default `analytics/`
59
- - `--style <style>`: `queries` (default) or `datasets`
60
- - `--database <type>`: `clickhouse` (default) or `chdb`
61
- - `--chdb-path <path>`: persistent chDB data directory; omit for an in-memory session
62
- - `--auth <mode>`: `none` (default) or `context`
63
- - `--all-tables`: with `--style datasets`, scaffold every table
64
- - `--tables <names>`: with `--style datasets`, scaffold these comma-separated tables
65
- - `--exclude-tables <names>`: with `--style datasets`, exclude these comma-separated tables
66
- - `--no-example`: skip the example query
67
- - `--no-interactive`: skip prompts; ClickHouse connection details come from env vars
68
- - `--force`: overwrite existing scaffold files
69
- - `--skip-connection`: skip testing the selected database before scaffolding
70
-
71
- Set `HYPEQUERY_SKIP_INSTALL=1` to skip the automatic dependency install.
72
-
73
- To scaffold against persistent embedded chDB without server credentials:
17
+ For a semantic layer:
74
18
 
75
19
  ```bash
76
- npx hypequery init --database chdb --chdb-path ./analytics.chdb --no-interactive
20
+ npx hypequery init --style datasets
77
21
  ```
78
22
 
79
- ### `hypequery dev`
80
-
81
- Runs the local serve runtime with docs and hot reload.
23
+ For embedded analytics without a ClickHouse server:
82
24
 
83
25
  ```bash
84
- npx hypequery dev
85
- ```
86
-
87
- Options:
88
-
89
- - `--port <port>`: default `4000`
90
- - `--hostname <host>`: default `localhost`
91
- - `--path <path>`: analytics directory to load (`<path>/api.ts` or `<path>/queries.ts`)
92
- - `--no-watch`: disable file watching
93
- - `--open`: open the browser automatically
94
- - `--quiet`: reduce startup output
95
-
96
- The CLI understands TypeScript entry files directly, so `analytics/queries.ts` works without an extra runner.
97
-
98
- ### `hypequery generate`
99
-
100
- Regenerates schema types from ClickHouse or embedded chDB.
101
-
102
- ```bash
103
- npx hypequery generate
104
- ```
105
-
106
- Options:
107
-
108
- - `--output <path>`: default `analytics/schema.ts`
109
- - `--path <path>`: analytics directory (derives `<path>/schema.ts`)
110
- - `--tables <names>`: comma-separated table list
111
- - `--database <type>`: `clickhouse` or `chdb`; chDB generation must be selected explicitly
112
- - `--chdb-path <path>`: persistent chDB data directory; omit for an in-memory session
113
-
114
- For a persistent chDB scaffold, pass the same path used by `init`:
115
-
116
- ```bash
117
- npx hypequery generate --database chdb --chdb-path ./analytics.chdb
118
- ```
119
-
120
- `hypequery generate:types` is an alias for `hypequery generate`.
121
-
122
- ### `hypequery generate:datasets`
123
-
124
- Generates dataset (semantic layer) definitions from ClickHouse.
125
-
126
- ```bash
127
- npx hypequery generate:datasets
128
- ```
129
-
130
- Options:
131
-
132
- - `--output <path>`: default `src/datasets/generated.ts`
133
- - `--path <path>`: analytics directory (derives `<path>/datasets.ts`)
134
- - `--tables <names>`: comma-separated table list
135
- - `--exclude-tables <names>`: comma-separated tables to exclude
136
-
137
- ### `hypequery generate:manifest`
138
-
139
- Generates a static React hook route manifest from an exported HypeQuery API.
140
-
141
- ```bash
142
- npx hypequery generate:manifest analytics/api.ts --output analytics/hypequery-manifest.json
143
- ```
144
-
145
- The output is the exact serializable JSON returned by `api.manifest()`, including
146
- semantic keys such as `dataset:orders`.
147
-
148
- ### `hypequery deployment:build`
149
-
150
- Builds a closed deployment bundle for an exported HypeQuery API. The bundle
151
- contains canonical deployment metadata, every referenced runtime artifact, and
152
- a manifest that binds their exact bytes and identities. It is the first step of
153
- a deployment pipeline, and produces the input `deployment:release` binds to a
154
- target.
155
-
156
- ```bash
157
- npx hypequery deployment:build analytics/api.ts
158
- ```
159
-
160
- The default output is `analytics/hypequery-deployment/`. Named Serve handlers
161
- are bundled into a Node runtime artifact automatically. Dataset-only APIs do
162
- not produce a runtime artifact. For a separately built Node or Python runtime,
163
- provide both its expected digest and file path:
164
-
165
- ```bash
166
- npx hypequery deployment:build analytics/api.ts \
167
- --runtime python \
168
- --runtime-artifact <sha256> \
169
- --runtime-file dist/runtime.pyz
170
- ```
171
-
172
- Options:
173
-
174
- - `--bundle-output <directory>`: default `analytics/hypequery-deployment`
175
- - `--runtime <runtime>`: `node` (default) or `python`
176
- - `--runtime-artifact <sha256>`: lowercase SHA-256 of a prebuilt runtime artifact
177
- - `--runtime-file <path>`: bytes for the prebuilt runtime artifact
178
- - `--entrypoint-prefix <prefix>`: default `queries`
179
-
180
- The compatibility options `--output`, `--runtime-output`, and `--hash-output`
181
- still emit the earlier metadata files instead of a complete bundle. They cannot
182
- be combined with `--bundle-output`.
183
-
184
- ### `hypequery deployment:validate`
185
-
186
- Verifies a complete deployment bundle before returning any contained metadata.
187
- Verification rejects missing or undeclared files, symbolic links, path
188
- traversal, byte-length or hash mismatches, deployment identity mismatches, and
189
- runtime files not referenced by the deployment. Legacy deployment JSON files
190
- are still accepted for metadata-only validation.
191
-
192
- ```bash
193
- npx hypequery deployment:validate analytics/hypequery-deployment
194
- ```
195
-
196
- ### `hypequery deployment:release`
197
-
198
- Prepares a deterministic release request from a verified deployment bundle and
199
- a project/environment target. This command does not upload, authorize, or
200
- execute the release.
201
-
202
- Pass the target as both flags. A half-specified target is rejected rather than
203
- completed from anywhere else, so an unset shell variable fails loudly instead of
204
- silently retargeting the release. The resolved target is printed alongside the
205
- release identity:
206
-
207
- ```bash
208
- npx hypequery deployment:release analytics/hypequery-deployment \
209
- --project my-project \
210
- --environment production
211
- ```
212
-
213
- The default output is `analytics/hypequery-deployment.release.json`. It is
214
- written beside the bundle because adding it inside the closed bundle would
215
- invalidate bundle verification.
216
-
217
- Options:
218
-
219
- - `--project <project>`: target project identifier; must be paired with
220
- `--environment`
221
- - `--environment <environment>`: target environment identifier; must be paired
222
- with `--project`
223
- - `--output <path>`: release JSON path, default beside the bundle
224
-
225
- ### `hypequery deployment:submit`
226
-
227
- Submits a verified deployment bundle with an already-prepared target-bound
228
- release. The command verifies both inputs again, requires their bundle
229
- identities to match, and streams only the files declared by the bundle.
230
-
231
- ```bash
232
- npx hypequery deployment:submit analytics/hypequery-deployment \
233
- --release analytics/hypequery-deployment.release.json
234
- ```
235
-
236
- Set `HYPEQUERY_API_TOKEN` together with either `--endpoint` or
237
- `HYPEQUERY_DEPLOYMENT_ENDPOINT`. Tokens are never accepted as command-line
238
- arguments, keeping them out of shell history. The submission endpoint must not
239
- contain credentials or a URL fragment, and must use HTTPS except for
240
- `127.0.0.1`/`localhost`, which is permitted for local development and warns that
241
- the token is sent in cleartext. The release identity is sent as the idempotency
242
- key, so an unchanged release can be submitted safely again.
243
-
244
- This command submits immutable deployment inputs. Activation, status changes,
245
- promotion, and rollback remain control-plane operations.
246
-
247
- Options:
248
-
249
- - `--release <path>`: required target-bound release JSON
250
- - `--endpoint <url>`: HTTPS submission endpoint; requires `HYPEQUERY_API_TOKEN`
251
-
252
- ## Non-interactive Setup
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
26
+ npx hypequery init \
27
+ --database chdb \
28
+ --chdb-path ./analytics.chdb
277
29
  ```
278
30
 
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.
31
+ ## The commands you will use
281
32
 
282
- For ClickHouse, `hypequery init --no-interactive` reads:
33
+ | Command | Outcome |
34
+ | --- | --- |
35
+ | `hypequery init` | A working typed analytics project |
36
+ | `hypequery dev` | Local API, docs, and hot reload |
37
+ | `hypequery generate` | Fresh TypeScript types from ClickHouse or chDB |
38
+ | `hypequery generate:datasets` | Dataset definitions scaffolded from tables |
39
+ | `hypequery generate:manifest` | A browser-safe route manifest for React hooks |
40
+ | `hypequery login` | An authenticated Cloud target |
41
+ | `hypequery deploy` | A verified deployment of the analytics API |
42
+ | `hypequery pull` / `diff` | Live source inspection and comparison |
283
43
 
284
- - `CLICKHOUSE_URL` or deprecated `CLICKHOUSE_HOST`
285
- - `CLICKHOUSE_DATABASE`
286
- - `CLICKHOUSE_USERNAME` or `CLICKHOUSE_USER`
287
- - `CLICKHOUSE_PASSWORD`
44
+ Non-interactive ClickHouse commands read `CLICKHOUSE_URL`, `CLICKHOUSE_DATABASE`, `CLICKHOUSE_USERNAME`, and `CLICKHOUSE_PASSWORD`.
288
45
 
289
- ## Notes
46
+ ## Why use the CLI
290
47
 
291
- - generated scaffold files use NodeNext-safe local `.js` imports
292
- - `CLICKHOUSE_URL` is now the preferred connection variable
293
- - the CLI bundles the ClickHouse driver for schema generation
294
- - chDB runs in memory unless `--chdb-path` is provided; persistent paths must be reused by later `generate` commands
48
+ - Get correct ClickHouse runtime types instead of guessing interfaces.
49
+ - Move from one local query to datasets, APIs, React, and MCP without changing tools.
50
+ - Keep generated types and route manifests reproducible in CI.
51
+ - Build closed, hash-verified deployment artifacts from the same source.
295
52
 
296
- ## Docs
53
+ ## Learn more
297
54
 
298
55
  - [Quick start](https://hypequery.com/docs/quick-start)
299
56
  - [CLI reference](https://hypequery.com/docs/reference/api/cli)
57
+ - [Current capabilities](https://hypequery.com/docs/capabilities)
300
58
 
301
59
  ## License
302
60
 
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;AA0BpC,QAAA,MAAM,OAAO,SAAgB,CAAC;AAa9B,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;EAKpE;AA+ND,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;AAgCpC,QAAA,MAAM,OAAO,SAAgB,CAAC;AAa9B,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;EAKpE;AA4PD,OAAO,EAAE,OAAO,EAAE,CAAC"}
package/dist/cli.js CHANGED
@@ -9,6 +9,7 @@ import { generateManifestCommand } from './commands/generate-manifest.js';
9
9
  import { buildDeploymentCommand, prepareDeploymentReleaseCommand, validateDeploymentCommand, } from './commands/deployment.js';
10
10
  import { deployCommand, submitDeploymentCommand, } from './commands/deploy.js';
11
11
  import { loginCommand, logoutCommand, } from './commands/login.js';
12
+ import { diffCommand, pullCommand, } from './commands/live-source.js';
12
13
  import { isPromptCancelled } from './utils/prompts.js';
13
14
  const program = new Command();
14
15
  function getCliVersion() {
@@ -44,6 +45,25 @@ program
44
45
  .action(runCommand(async () => {
45
46
  await logoutCommand();
46
47
  }));
48
+ program
49
+ .command('pull')
50
+ .description('Download the source snapshot from the live deployment')
51
+ .option('-o, --output <directory>', 'New destination directory')
52
+ .option('--project <project>', 'Target project identifier (advanced override)')
53
+ .option('--environment <environment>', 'Target environment identifier (advanced override)')
54
+ .option('--endpoint <url>', 'HTTPS submission endpoint; requires HYPEQUERY_API_TOKEN')
55
+ .action(runCommand(async (options) => {
56
+ await pullCommand(options);
57
+ }));
58
+ program
59
+ .command('diff [source]')
60
+ .description('Compare local source with the live deployment')
61
+ .option('--project <project>', 'Target project identifier (advanced override)')
62
+ .option('--environment <environment>', 'Target environment identifier (advanced override)')
63
+ .option('--endpoint <url>', 'HTTPS submission endpoint; requires HYPEQUERY_API_TOKEN')
64
+ .action(runCommand(async (source, options) => {
65
+ await diffCommand(source, options);
66
+ }));
47
67
  function runCommand(action) {
48
68
  return async (...args) => {
49
69
  try {
@@ -167,6 +187,7 @@ program
167
187
  .description('Submit a prebuilt deployment bundle and release')
168
188
  .requiredOption('--release <path>', 'Target-bound release JSON path')
169
189
  .option('--endpoint <url>', 'HTTPS submission endpoint; requires HYPEQUERY_API_TOKEN')
190
+ .option('--replace-restored', 'Intentionally replace a restored live release')
170
191
  .action(runCommand(async (bundle, options) => {
171
192
  await submitDeploymentCommand(bundle, options);
172
193
  }));
@@ -180,6 +201,7 @@ program
180
201
  .option('--release <path>', 'Submit a prebuilt bundle with this release (legacy)')
181
202
  .option('--no-source', 'Exclude project source files from the deployment bundle')
182
203
  .option('--endpoint <url>', 'HTTPS submission endpoint; requires HYPEQUERY_API_TOKEN')
204
+ .option('--replace-restored', 'Intentionally replace a restored live release')
183
205
  .action(runCommand(async (source, options) => {
184
206
  await deployCommand(source, options);
185
207
  }));
@@ -1,9 +1,11 @@
1
1
  import { createHttpDeploymentUploadTransport, type DeploymentSubmissionResponse } from '../utils/deployment-upload.js';
2
2
  import { type StoredCloudCredential } from '../utils/cloud-credential-store.js';
3
+ import { fetchLiveDeployment } from '../utils/live-deployment.js';
3
4
  import { buildDeploymentCommand, prepareDeploymentReleaseCommand } from './deployment.js';
4
5
  export interface SubmitDeploymentOptions {
5
6
  release?: string;
6
7
  endpoint?: string;
8
+ replaceRestored?: boolean;
7
9
  }
8
10
  export interface DeployOptions extends SubmitDeploymentOptions {
9
11
  project?: string;
@@ -16,6 +18,7 @@ export interface SubmitDeploymentDependencies {
16
18
  readonly env?: Readonly<Record<string, string | undefined>>;
17
19
  readonly createTransport?: typeof createHttpDeploymentUploadTransport;
18
20
  readonly loadCredential?: () => Promise<StoredCloudCredential | null>;
21
+ readonly fetchLive?: typeof fetchLiveDeployment;
19
22
  }
20
23
  export interface DeployDependencies extends SubmitDeploymentDependencies {
21
24
  readonly buildDeployment?: typeof buildDeploymentCommand;
@@ -1 +1 @@
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;IACvB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;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
+ {"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,EACL,KAAK,qBAAqB,EAC3B,MAAM,oCAAoC,CAAC;AAE5C,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,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;IAClB,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;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;IACvB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;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;IACtE,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,mBAAmB,CAAC;CACjD;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;AA4DD,wBAAsB,uBAAuB,CAC3C,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,OAAO,GAAE,uBAA4B,EACrC,YAAY,GAAE,4BAAiC,GAC9C,OAAO,CAAC,4BAA4B,CAAC,CA0EvC;AAmDD,wBAAsB,aAAa,CACjC,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,OAAO,GAAE,aAAkB,EAC3B,YAAY,GAAE,kBAAuB,GACpC,OAAO,CAAC,4BAA4B,CAAC,CAqDvC"}
@@ -4,7 +4,8 @@ import { prepareProtocolDeploymentReleaseEnvelope, } from '@hypequery/protocol';
4
4
  import { DEPLOYMENT_BUNDLE_MANIFEST, verifyDeploymentBundle, } from '../utils/deployment-bundle.js';
5
5
  import { createHttpDeploymentUploadTransport, DeploymentUploadError, } from '../utils/deployment-upload.js';
6
6
  import { logger } from '../utils/logger.js';
7
- import { loadCloudCredential, } from '../utils/cloud-credential-store.js';
7
+ import { resolveDeploymentCredential } from '../utils/cloud-deployment-access.js';
8
+ import { fetchLiveDeployment } from '../utils/live-deployment.js';
8
9
  import { buildDeploymentCommand, prepareDeploymentReleaseCommand, } from './deployment.js';
9
10
  const MAX_RELEASE_FILE_BYTES = 16 * 1024;
10
11
  const DEFAULT_BUNDLE_PATH = 'analytics/hypequery-deployment';
@@ -64,39 +65,6 @@ function requiredConfiguration(value, message) {
64
65
  throw new Error(message);
65
66
  return value;
66
67
  }
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
68
  export async function submitDeploymentCommand(bundlePath, options = {}, dependencies = {}) {
101
69
  if (!bundlePath) {
102
70
  throw new Error('Missing deployment bundle path.\n\n'
@@ -125,10 +93,26 @@ export async function submitDeploymentCommand(bundlePath, options = {}, dependen
125
93
  if (release.release.bundleIdentity !== bundle.identity) {
126
94
  throw new DeploymentUploadError('HQ_UPLOAD_IDENTITY_MISMATCH', 'Release bundle identity does not match the verified deployment bundle.');
127
95
  }
96
+ const live = await (dependencies.fetchLive ?? fetchLiveDeployment)({
97
+ endpoint: deploymentEndpoint,
98
+ token,
99
+ target: release.release.target,
100
+ resource: 'state',
101
+ });
102
+ if (live?.active?.restored
103
+ && live.active.releaseIdentity !== release.identity
104
+ && !options.replaceRestored) {
105
+ throw new Error('The live deployment is a restored release. '
106
+ + 'Run the command again with --replace-restored to replace it intentionally.');
107
+ }
128
108
  const createTransport = dependencies.createTransport ?? createHttpDeploymentUploadTransport;
129
109
  const transportOptions = {
130
110
  endpoint: deploymentEndpoint,
131
111
  token,
112
+ ...(live
113
+ ? { expectedActivationRevision: live.active?.revision ?? null }
114
+ : {}),
115
+ ...(options.replaceRestored ? { replaceRestored: true } : {}),
132
116
  };
133
117
  const result = await createTransport(transportOptions).submit(bundle, release);
134
118
  logger.success(result.status === 'accepted'
@@ -190,6 +174,7 @@ export async function deployCommand(sourcePath, options = {}, dependencies = {})
190
174
  return submitDeploymentCommand(sourcePath, {
191
175
  release: options.release,
192
176
  endpoint: options.endpoint,
177
+ replaceRestored: options.replaceRestored,
193
178
  }, dependencies);
194
179
  }
195
180
  await rejectBundleDirectorySource(sourcePath);
@@ -220,5 +205,6 @@ export async function deployCommand(sourcePath, options = {}, dependencies = {})
220
205
  return submit(bundlePath, {
221
206
  release: releasePath,
222
207
  endpoint: options.endpoint,
208
+ replaceRestored: options.replaceRestored,
223
209
  }, dependencies);
224
210
  }
@@ -0,0 +1,22 @@
1
+ import { type CloudDeploymentAccessDependencies } from '../utils/cloud-deployment-access.js';
2
+ import { captureDeploymentSourceSnapshot } from '../utils/deployment-source-snapshot.js';
3
+ import { fetchLiveDeployment } from '../utils/live-deployment.js';
4
+ export interface LiveSourceOptions {
5
+ readonly endpoint?: string;
6
+ readonly project?: string;
7
+ readonly environment?: string;
8
+ }
9
+ export interface PullOptions extends LiveSourceOptions {
10
+ readonly output?: string;
11
+ }
12
+ export interface LiveSourceDependencies extends CloudDeploymentAccessDependencies {
13
+ readonly fetchLive?: typeof fetchLiveDeployment;
14
+ readonly captureSource?: typeof captureDeploymentSourceSnapshot;
15
+ }
16
+ export type LiveSourceDifference = {
17
+ readonly status: 'A' | 'M' | 'D';
18
+ readonly path: string;
19
+ };
20
+ export declare function pullCommand(options?: PullOptions, dependencies?: LiveSourceDependencies): Promise<string>;
21
+ export declare function diffCommand(sourcePath: string | undefined, options?: LiveSourceOptions, dependencies?: LiveSourceDependencies): Promise<readonly LiveSourceDifference[]>;
22
+ //# sourceMappingURL=live-source.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"live-source.d.ts","sourceRoot":"","sources":["../../src/commands/live-source.ts"],"names":[],"mappings":"AAWA,OAAO,EAEL,KAAK,iCAAiC,EACvC,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EAAE,+BAA+B,EAAE,MAAM,wCAAwC,CAAC;AACzF,OAAO,EACL,mBAAmB,EAEpB,MAAM,6BAA6B,CAAC;AAGrC,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,WAAY,SAAQ,iBAAiB;IACpD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,sBAAuB,SAAQ,iCAAiC;IAC/E,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,mBAAmB,CAAC;IAChD,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,+BAA+B,CAAC;CACjE;AAED,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,CAAC,MAAM,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB,CAAC;AAkEF,wBAAsB,WAAW,CAC/B,OAAO,GAAE,WAAgB,EACzB,YAAY,GAAE,sBAA2B,mBAuC1C;AAED,wBAAsB,WAAW,CAC/B,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,OAAO,GAAE,iBAAsB,EAC/B,YAAY,GAAE,sBAA2B,GACxC,OAAO,CAAC,SAAS,oBAAoB,EAAE,CAAC,CA0B1C"}
@@ -0,0 +1,119 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { mkdir, rename, rm, stat, writeFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { validateProtocolDeploymentReleaseTarget, } from '@hypequery/protocol';
5
+ import { CLOUD_SOURCE_SCOPE, } from '../utils/cloud-credential-store.js';
6
+ import { resolveDeploymentCredential, } from '../utils/cloud-deployment-access.js';
7
+ import { captureDeploymentSourceSnapshot } from '../utils/deployment-source-snapshot.js';
8
+ import { fetchLiveDeployment, } from '../utils/live-deployment.js';
9
+ import { logger } from '../utils/logger.js';
10
+ function targetFromOptions(options, credential) {
11
+ if ((options.project === undefined) !== (options.environment === undefined)) {
12
+ throw new Error('Pass both --project and --environment, or omit both.');
13
+ }
14
+ const input = options.project === undefined
15
+ ? credential?.target
16
+ : { project: options.project, environment: options.environment };
17
+ if (!input) {
18
+ throw new Error('Missing deployment target. Run `hypequery login` or pass both '
19
+ + '--project and --environment.');
20
+ }
21
+ try {
22
+ return validateProtocolDeploymentReleaseTarget(input);
23
+ }
24
+ catch {
25
+ throw new Error('The deployment target is invalid.');
26
+ }
27
+ }
28
+ async function liveSource(options, dependencies) {
29
+ const access = await resolveDeploymentCredential(options.endpoint, dependencies);
30
+ if (access.storedCredential
31
+ && access.storedCredential.scope !== CLOUD_SOURCE_SCOPE) {
32
+ throw new Error('The stored CLI credential cannot read deployed source. Run `hypequery login` again.');
33
+ }
34
+ const target = targetFromOptions(options, access.storedCredential);
35
+ const live = await (dependencies.fetchLive ?? fetchLiveDeployment)({
36
+ endpoint: access.endpoint,
37
+ token: access.token,
38
+ target,
39
+ resource: 'source',
40
+ });
41
+ if (!live?.active?.source) {
42
+ throw new Error('The live deployment does not include a source snapshot.');
43
+ }
44
+ return live;
45
+ }
46
+ function digest(bytes) {
47
+ return createHash('sha256').update(bytes).digest('hex');
48
+ }
49
+ async function exists(input) {
50
+ try {
51
+ await stat(input);
52
+ return true;
53
+ }
54
+ catch (error) {
55
+ if (typeof error === 'object' && error !== null && 'code' in error
56
+ && error.code === 'ENOENT')
57
+ return false;
58
+ throw error;
59
+ }
60
+ }
61
+ export async function pullCommand(options = {}, dependencies = {}) {
62
+ const live = await liveSource(options, dependencies);
63
+ const active = live.active;
64
+ const source = active.source;
65
+ const destination = path.resolve(options.output
66
+ ?? path.join('.hypequery', 'live', live.target.environment, active.releaseIdentity.slice(0, 12)));
67
+ if (destination === path.parse(destination).root || await exists(destination)) {
68
+ throw new Error(`Refusing to overwrite an existing pull destination: ${destination}`);
69
+ }
70
+ const parent = path.dirname(destination);
71
+ await mkdir(parent, { recursive: true });
72
+ const staging = path.join(parent, `.${path.basename(destination)}.${randomUUID()}.tmp`);
73
+ await mkdir(staging, { mode: 0o700 });
74
+ try {
75
+ for (const file of source.files) {
76
+ const output = path.join(staging, ...file.path.split('/'));
77
+ await mkdir(path.dirname(output), { recursive: true });
78
+ await writeFile(output, file.bytes, { flag: 'wx' });
79
+ }
80
+ await rename(staging, destination);
81
+ }
82
+ catch (error) {
83
+ await rm(staging, { recursive: true, force: true }).catch(() => undefined);
84
+ throw error;
85
+ }
86
+ logger.success(`Live source pulled to ${destination}`);
87
+ logger.info(`Release identity: ${active.releaseIdentity}`);
88
+ logger.info(`Entrypoint: ${source.entrypoint}`);
89
+ return destination;
90
+ }
91
+ export async function diffCommand(sourcePath, options = {}, dependencies = {}) {
92
+ const live = await liveSource(options, dependencies);
93
+ const active = live.active;
94
+ const source = active.source;
95
+ const local = await (dependencies.captureSource ?? captureDeploymentSourceSnapshot)(sourcePath ?? source.entrypoint);
96
+ const liveFiles = new Map(source.files.map(file => [file.path, file.sha256]));
97
+ const localFiles = new Map(local.files.map(file => [file.path, digest(file.bytes)]));
98
+ const paths = [...new Set([...liveFiles.keys(), ...localFiles.keys()])].sort();
99
+ const differences = [];
100
+ for (const file of paths) {
101
+ const liveDigest = liveFiles.get(file);
102
+ const localDigest = localFiles.get(file);
103
+ if (liveDigest === undefined)
104
+ differences.push({ status: 'A', path: file });
105
+ else if (localDigest === undefined)
106
+ differences.push({ status: 'D', path: file });
107
+ else if (liveDigest !== localDigest)
108
+ differences.push({ status: 'M', path: file });
109
+ }
110
+ if (differences.length === 0) {
111
+ logger.success('Local source matches the live deployment');
112
+ }
113
+ else {
114
+ for (const difference of differences) {
115
+ logger.info(`${difference.status} ${difference.path}`);
116
+ }
117
+ }
118
+ return Object.freeze(differences);
119
+ }
@@ -2,7 +2,7 @@ import { createHash, randomBytes } from 'node:crypto';
2
2
  import { createServer } from 'node:http';
3
3
  import open from 'open';
4
4
  import { validateProtocolDeploymentReleaseTarget } from '@hypequery/protocol';
5
- import { CLOUD_DEPLOYMENT_SCOPE, deleteCloudCredential, loadCloudCredential, normalizeCloudDeploymentEndpoint, normalizeCloudOrigin, saveCloudCredential, } from '../utils/cloud-credential-store.js';
5
+ import { CLOUD_SOURCE_SCOPE, deleteCloudCredential, loadCloudCredential, normalizeCloudDeploymentEndpoint, normalizeCloudOrigin, saveCloudCredential, } from '../utils/cloud-credential-store.js';
6
6
  import { logger } from '../utils/logger.js';
7
7
  const DEFAULT_CLOUD_URL = 'https://cloud.hypequery.com';
8
8
  const LOGIN_TIMEOUT_MS = 5 * 60_000;
@@ -131,7 +131,7 @@ function tokenResponse(input, origin, now) {
131
131
  || value.token_type !== 'Bearer'
132
132
  || typeof value.expires_at !== 'string'
133
133
  || !Number.isFinite(Date.parse(value.expires_at))
134
- || value.scope !== CLOUD_DEPLOYMENT_SCOPE
134
+ || value.scope !== CLOUD_SOURCE_SCOPE
135
135
  || typeof value.deployment_endpoint !== 'string'
136
136
  || value.deployment_target === undefined) {
137
137
  throw new Error('Cloud returned an invalid CLI token response.');
@@ -1,5 +1,6 @@
1
1
  import { type ProtocolDeploymentReleaseTarget } from '@hypequery/protocol';
2
2
  export declare const CLOUD_DEPLOYMENT_SCOPE = "deploy:submit";
3
+ export declare const CLOUD_SOURCE_SCOPE = "deploy:submit deploy:read-source";
3
4
  export interface StoredCloudCredential {
4
5
  readonly cloudUrl: string;
5
6
  readonly deploymentEndpoint: string;
@@ -1 +1 @@
1
- {"version":3,"file":"cloud-credential-store.d.ts","sourceRoot":"","sources":["../../src/utils/cloud-credential-store.ts"],"names":[],"mappings":"AAIA,OAAO,EAEL,KAAK,+BAA+B,EACrC,MAAM,qBAAqB,CAAC;AAI7B,eAAO,MAAM,sBAAsB,kBAAkB,CAAC;AAGtD,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,CAAC,EAAE,+BAA+B,CAAC;IAClD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAYD,UAAU,YAAY;IACpB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,WAAW,IAAI,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACtD,cAAc,IAAI,OAAO,CAAC;CAC3B;AAQD,MAAM,WAAW,gCAAgC;IAC/C,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,CAC5B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,KACZ,OAAO,CAAC,YAAY,CAAC,GAAG,YAAY,CAAC;IAC1C,QAAQ,CAAC,GAAG,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;IAC5D,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;CACrC;AAyCD,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAc1D;AAED,wBAAgB,gCAAgC,CAC9C,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,GAClB,MAAM,CAmBR;AAmFD,wBAAsB,mBAAmB,CACvC,UAAU,EAAE,qBAAqB,EACjC,YAAY,GAAE,gCAAqC,iBAiEpD;AAED,wBAAsB,mBAAmB,CACvC,YAAY,GAAE,gCAAqC,GAClD,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAC,CAsBvC;AA0BD,wBAAsB,qBAAqB,CACzC,YAAY,GAAE,gCAAqC,iBA0BpD"}
1
+ {"version":3,"file":"cloud-credential-store.d.ts","sourceRoot":"","sources":["../../src/utils/cloud-credential-store.ts"],"names":[],"mappings":"AAIA,OAAO,EAEL,KAAK,+BAA+B,EACrC,MAAM,qBAAqB,CAAC;AAI7B,eAAO,MAAM,sBAAsB,kBAAkB,CAAC;AACtD,eAAO,MAAM,kBAAkB,qCAAqC,CAAC;AAGrE,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,CAAC,EAAE,+BAA+B,CAAC;IAClD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAYD,UAAU,YAAY;IACpB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,WAAW,IAAI,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACtD,cAAc,IAAI,OAAO,CAAC;CAC3B;AAQD,MAAM,WAAW,gCAAgC;IAC/C,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,CAC5B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,KACZ,OAAO,CAAC,YAAY,CAAC,GAAG,YAAY,CAAC;IAC1C,QAAQ,CAAC,GAAG,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;IAC5D,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;CACrC;AAyCD,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAc1D;AAED,wBAAgB,gCAAgC,CAC9C,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,GAClB,MAAM,CAmBR;AAmFD,wBAAsB,mBAAmB,CACvC,UAAU,EAAE,qBAAqB,EACjC,YAAY,GAAE,gCAAqC,iBAkEpD;AAED,wBAAsB,mBAAmB,CACvC,YAAY,GAAE,gCAAqC,GAClD,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAC,CAsBvC;AA0BD,wBAAsB,qBAAqB,CACzC,YAAY,GAAE,gCAAqC,iBA0BpD"}
@@ -6,6 +6,7 @@ import { validateProtocolDeploymentReleaseTarget, } from '@hypequery/protocol';
6
6
  const KEYCHAIN_SERVICE = 'dev.hypequery.cli';
7
7
  const PROFILE_FILE = 'cloud-profile.json';
8
8
  export const CLOUD_DEPLOYMENT_SCOPE = 'deploy:submit';
9
+ export const CLOUD_SOURCE_SCOPE = 'deploy:submit deploy:read-source';
9
10
  const MAX_KEYCHAIN_ACCOUNT_LENGTH = 2048;
10
11
  function defaultConfigDirectory(env, platform) {
11
12
  if (env.HYPEQUERY_CONFIG_DIR)
@@ -97,7 +98,7 @@ function parseProfile(input) {
97
98
  deploymentEndpoint = normalizeCloudDeploymentEndpoint(value.deploymentEndpoint, cloudUrl);
98
99
  if (value.keychainAccount !== cloudUrl
99
100
  || !Number.isFinite(Date.parse(value.expiresAt))
100
- || value.scope !== CLOUD_DEPLOYMENT_SCOPE) {
101
+ || (value.scope !== CLOUD_DEPLOYMENT_SCOPE && value.scope !== CLOUD_SOURCE_SCOPE)) {
101
102
  throw new Error('profile invariant mismatch');
102
103
  }
103
104
  target = value.target === undefined
@@ -164,7 +165,8 @@ export async function saveCloudCredential(credential, dependencies = {}) {
164
165
  const cloudUrl = normalizeCloudOrigin(credential.cloudUrl);
165
166
  const deploymentEndpoint = normalizeCloudDeploymentEndpoint(credential.deploymentEndpoint, cloudUrl);
166
167
  if (!Number.isFinite(Date.parse(credential.expiresAt))
167
- || credential.scope !== CLOUD_DEPLOYMENT_SCOPE) {
168
+ || (credential.scope !== CLOUD_DEPLOYMENT_SCOPE
169
+ && credential.scope !== CLOUD_SOURCE_SCOPE)) {
168
170
  throw new Error('Cannot store an invalid Hypequery Cloud credential.');
169
171
  }
170
172
  const keychainAccount = cloudUrl;
@@ -0,0 +1,12 @@
1
+ import { type StoredCloudCredential } from './cloud-credential-store.js';
2
+ export interface CloudDeploymentAccessDependencies {
3
+ readonly env?: Readonly<Record<string, string | undefined>>;
4
+ readonly loadCredential?: () => Promise<StoredCloudCredential | null>;
5
+ }
6
+ export interface ResolvedDeploymentCredential {
7
+ readonly endpoint: string;
8
+ readonly token: string;
9
+ readonly storedCredential?: StoredCloudCredential;
10
+ }
11
+ export declare function resolveDeploymentCredential(endpointOption: string | undefined, dependencies: CloudDeploymentAccessDependencies): Promise<ResolvedDeploymentCredential>;
12
+ //# sourceMappingURL=cloud-deployment-access.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cloud-deployment-access.d.ts","sourceRoot":"","sources":["../../src/utils/cloud-deployment-access.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,qBAAqB,EAC3B,MAAM,6BAA6B,CAAC;AAErC,MAAM,WAAW,iCAAiC;IAChD,QAAQ,CAAC,GAAG,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;IAC5D,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAC,CAAC;CACvE;AAED,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,gBAAgB,CAAC,EAAE,qBAAqB,CAAC;CACnD;AAUD,wBAAsB,2BAA2B,CAC/C,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,YAAY,EAAE,iCAAiC,GAC9C,OAAO,CAAC,4BAA4B,CAAC,CAsCvC"}
@@ -0,0 +1,37 @@
1
+ import { loadCloudCredential, } from './cloud-credential-store.js';
2
+ function requiredConfiguration(value, message) {
3
+ if (value === undefined || value.length === 0)
4
+ throw new Error(message);
5
+ return value;
6
+ }
7
+ export async function resolveDeploymentCredential(endpointOption, dependencies) {
8
+ const env = dependencies.env ?? process.env;
9
+ let deploymentEndpoint = endpointOption ?? env.HYPEQUERY_DEPLOYMENT_ENDPOINT;
10
+ let token = env.HYPEQUERY_API_TOKEN;
11
+ let storedCredential;
12
+ const hasExplicitEndpoint = Boolean(deploymentEndpoint);
13
+ const hasExplicitToken = Boolean(token);
14
+ if (hasExplicitEndpoint !== hasExplicitToken) {
15
+ throw new Error(hasExplicitEndpoint
16
+ ? 'An explicit deployment endpoint requires HYPEQUERY_API_TOKEN.'
17
+ : 'HYPEQUERY_API_TOKEN requires --endpoint or HYPEQUERY_DEPLOYMENT_ENDPOINT.\n\n'
18
+ + 'If you meant to use `hypequery login`, unset HYPEQUERY_API_TOKEN — '
19
+ + 'the CLI also reads it from a project .env file.');
20
+ }
21
+ if (!hasExplicitEndpoint) {
22
+ const credential = await (dependencies.loadCredential ?? loadCloudCredential)();
23
+ if (credential) {
24
+ if (Date.parse(credential.expiresAt) <= Date.now()) {
25
+ throw new Error('The stored Cloud credential has expired. Run `hypequery login` again.');
26
+ }
27
+ deploymentEndpoint = credential.deploymentEndpoint;
28
+ token = credential.token;
29
+ storedCredential = credential;
30
+ }
31
+ }
32
+ return {
33
+ endpoint: requiredConfiguration(deploymentEndpoint, 'Missing deployment endpoint. Run `hypequery login`, pass --endpoint, or set HYPEQUERY_DEPLOYMENT_ENDPOINT.'),
34
+ token: requiredConfiguration(token, 'Missing deployment credential. Run `hypequery login` or set HYPEQUERY_API_TOKEN.'),
35
+ ...(storedCredential ? { storedCredential } : {}),
36
+ };
37
+ }
@@ -27,6 +27,8 @@ export type DeploymentFetch = (input: string, init: DeploymentFetchInit) => Prom
27
27
  export interface HttpDeploymentUploadTransportOptions {
28
28
  readonly endpoint: string;
29
29
  readonly token: string;
30
+ readonly expectedActivationRevision?: string | null;
31
+ readonly replaceRestored?: boolean;
30
32
  readonly timeoutMs?: number;
31
33
  readonly fetch?: DeploymentFetch;
32
34
  }
@@ -1 +1 @@
1
- {"version":3,"file":"deployment-upload.d.ts","sourceRoot":"","sources":["../../src/utils/deployment-upload.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,EAIL,KAAK,yCAAyC,EAC/C,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAEL,KAAK,wBAAwB,EAC9B,MAAM,wBAAwB,CAAC;AAEhC,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,uBAAuB,CAAC;AAE1E,YAAY,EAAE,4BAA4B,EAAE,MAAM,uBAAuB,CAAC;AAQ1E,MAAM,MAAM,yBAAyB,GACjC,yBAAyB,GACzB,6BAA6B,GAC7B,0BAA0B,GAC1B,mBAAmB,GACnB,oBAAoB,GACpB,4BAA4B,CAAC;AAEjC,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,QAAQ,CAAC,IAAI,EAAE,yBAAyB,CAAC;IACzC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;gBAEb,IAAI,EAAE,yBAAyB,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM;CAM9E;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC,UAAU,CAAC,GAAG,IAAI,CAAC;CAClD;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACnD,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC,UAAU,CAAC,CAAC;IACzC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;CAC9B;AAED,MAAM,MAAM,eAAe,GAAG,CAC5B,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,mBAAmB,KACtB,OAAO,CAAC,sBAAsB,CAAC,CAAC;AAErC,MAAM,WAAW,oCAAoC;IACnD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,CAAC,EAAE,eAAe,CAAC;CAClC;AAED,MAAM,WAAW,yBAAyB;IACxC,MAAM,CACJ,MAAM,EAAE,wBAAwB,EAChC,OAAO,EAAE,yCAAyC,GACjD,OAAO,CAAC,4BAA4B,CAAC,CAAC;CAC1C;AA+WD,wBAAgB,mCAAmC,CACjD,OAAO,EAAE,oCAAoC,GAC5C,yBAAyB,CAsF3B"}
1
+ {"version":3,"file":"deployment-upload.d.ts","sourceRoot":"","sources":["../../src/utils/deployment-upload.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,EAIL,KAAK,yCAAyC,EAC/C,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAEL,KAAK,wBAAwB,EAC9B,MAAM,wBAAwB,CAAC;AAEhC,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,uBAAuB,CAAC;AAE1E,YAAY,EAAE,4BAA4B,EAAE,MAAM,uBAAuB,CAAC;AAQ1E,MAAM,MAAM,yBAAyB,GACjC,yBAAyB,GACzB,6BAA6B,GAC7B,0BAA0B,GAC1B,mBAAmB,GACnB,oBAAoB,GACpB,4BAA4B,CAAC;AAEjC,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,QAAQ,CAAC,IAAI,EAAE,yBAAyB,CAAC;IACzC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;gBAEb,IAAI,EAAE,yBAAyB,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM;CAM9E;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC,UAAU,CAAC,GAAG,IAAI,CAAC;CAClD;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACnD,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC,UAAU,CAAC,CAAC;IACzC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;CAC9B;AAED,MAAM,MAAM,eAAe,GAAG,CAC5B,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,mBAAmB,KACtB,OAAO,CAAC,sBAAsB,CAAC,CAAC;AAErC,MAAM,WAAW,oCAAoC;IACnD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,0BAA0B,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpD,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,CAAC;IACnC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,CAAC,EAAE,eAAe,CAAC;CAClC;AAED,MAAM,WAAW,yBAAyB;IACxC,MAAM,CACJ,MAAM,EAAE,wBAAwB,EAChC,OAAO,EAAE,yCAAyC,GACjD,OAAO,CAAC,4BAA4B,CAAC,CAAC;CAC1C;AA+WD,wBAAgB,mCAAmC,CACjD,OAAO,EAAE,oCAAoC,GAC5C,yBAAyB,CA+F3B"}
@@ -347,6 +347,14 @@ export function createHttpDeploymentUploadTransport(options) {
347
347
  'Idempotency-Key': preparedRelease.identity,
348
348
  'X-HypeQuery-Bundle-Identity': manifest.identity,
349
349
  'X-HypeQuery-Release-Identity': preparedRelease.identity,
350
+ ...(options.expectedActivationRevision !== undefined
351
+ ? {
352
+ 'X-HypeQuery-Expected-Activation-Revision': options.expectedActivationRevision ?? 'none',
353
+ }
354
+ : {}),
355
+ ...(options.replaceRestored
356
+ ? { 'X-HypeQuery-Replace-Restored': 'true' }
357
+ : {}),
350
358
  },
351
359
  body: multipartBody(boundary, parts),
352
360
  duplex: 'half',
@@ -0,0 +1,36 @@
1
+ import { type ProtocolDeploymentReleaseTarget } from '@hypequery/protocol';
2
+ export type LiveDeploymentSourceFile = {
3
+ readonly path: string;
4
+ readonly sha256: string;
5
+ readonly bytes: Uint8Array;
6
+ };
7
+ export type LiveDeploymentSource = {
8
+ readonly entrypoint: string;
9
+ readonly files: readonly LiveDeploymentSourceFile[];
10
+ readonly revision?: {
11
+ readonly kind: 'git';
12
+ readonly commit: string;
13
+ readonly branch?: string;
14
+ readonly dirty: boolean;
15
+ };
16
+ };
17
+ export type LiveDeployment = {
18
+ readonly target: ProtocolDeploymentReleaseTarget;
19
+ readonly active: null | {
20
+ readonly revision: string;
21
+ readonly releaseIdentity: string;
22
+ readonly activatedAt: string;
23
+ readonly restored: boolean;
24
+ readonly hasSource: boolean;
25
+ readonly source?: LiveDeploymentSource;
26
+ };
27
+ };
28
+ export type LiveDeploymentFetch = typeof fetch;
29
+ export declare function fetchLiveDeployment(input: {
30
+ readonly endpoint: string;
31
+ readonly token: string;
32
+ readonly target: ProtocolDeploymentReleaseTarget;
33
+ readonly resource: 'state' | 'source';
34
+ readonly fetch?: LiveDeploymentFetch;
35
+ }): Promise<LiveDeployment | undefined>;
36
+ //# sourceMappingURL=live-deployment.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"live-deployment.d.ts","sourceRoot":"","sources":["../../src/utils/live-deployment.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,+BAA+B,EACrC,MAAM,qBAAqB,CAAC;AAO7B,MAAM,MAAM,wBAAwB,GAAG;IACrC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,SAAS,wBAAwB,EAAE,CAAC;IACpD,QAAQ,CAAC,QAAQ,CAAC,EAAE;QAClB,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;QACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;QACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QACzB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;KACzB,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,QAAQ,CAAC,MAAM,EAAE,+BAA+B,CAAC;IACjD,QAAQ,CAAC,MAAM,EAAE,IAAI,GAAG;QACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;QAC1B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;QACjC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;QAC7B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;QAC3B,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;QAC5B,QAAQ,CAAC,MAAM,CAAC,EAAE,oBAAoB,CAAC;KACxC,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,OAAO,KAAK,CAAC;AA8K/C,wBAAsB,mBAAmB,CAAC,KAAK,EAAE;IAC/C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,+BAA+B,CAAC;IACjD,QAAQ,CAAC,QAAQ,EAAE,OAAO,GAAG,QAAQ,CAAC;IACtC,QAAQ,CAAC,KAAK,CAAC,EAAE,mBAAmB,CAAC;CACtC,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,CAuCtC"}
@@ -0,0 +1,205 @@
1
+ import { createHash } from 'node:crypto';
2
+ import path from 'node:path';
3
+ import { DEFAULT_PROTOCOL_DEPLOYMENT_BUNDLE_LIMITS, validateProtocolDeploymentReleaseTarget, } from '@hypequery/protocol';
4
+ const SHA256_PATTERN = /^[0-9a-f]{64}$/;
5
+ const MAX_RESPONSE_BYTES = Math.ceil(DEFAULT_PROTOCOL_DEPLOYMENT_BUNDLE_LIMITS.maxSourceBytes * 1.4) + 1_000_000;
6
+ function liveUrl(endpoint, target, resource) {
7
+ const url = new URL(endpoint);
8
+ if (!/\/v1\/deployments\/submissions\/?$/.test(url.pathname)) {
9
+ return undefined;
10
+ }
11
+ const loopbackHttp = url.protocol === 'http:'
12
+ && (url.hostname === '127.0.0.1' || url.hostname === 'localhost');
13
+ if ((url.protocol !== 'https:' && !loopbackHttp)
14
+ || url.username
15
+ || url.password
16
+ || url.hash) {
17
+ throw new Error('The deployment endpoint cannot be used to read live state safely.');
18
+ }
19
+ url.pathname = `/v1/deployments/targets/${encodeURIComponent(target.project)}`
20
+ + `/${encodeURIComponent(target.environment)}/${resource}`;
21
+ url.search = '';
22
+ url.hash = '';
23
+ return url.toString();
24
+ }
25
+ function safePath(input) {
26
+ return typeof input === 'string'
27
+ && input.length > 0
28
+ && input.length <= 1024
29
+ && input.split(path.sep).join('/') === input
30
+ && !path.isAbsolute(input)
31
+ && input.split('/').every(segment => segment && segment !== '.' && segment !== '..');
32
+ }
33
+ function decodeFile(input) {
34
+ if (typeof input !== 'object' || input === null || Array.isArray(input)) {
35
+ throw new Error('Cloud returned an invalid live source snapshot.');
36
+ }
37
+ const value = input;
38
+ if (!safePath(value.path)
39
+ || typeof value.sha256 !== 'string'
40
+ || !SHA256_PATTERN.test(value.sha256)
41
+ || !Number.isSafeInteger(value.byteLength)
42
+ || value.byteLength < 0
43
+ || value.byteLength
44
+ > DEFAULT_PROTOCOL_DEPLOYMENT_BUNDLE_LIMITS.maxSourceFileBytes
45
+ || typeof value.contentsBase64 !== 'string'
46
+ || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value.contentsBase64)) {
47
+ throw new Error('Cloud returned an invalid live source snapshot.');
48
+ }
49
+ const bytes = Buffer.from(value.contentsBase64, 'base64');
50
+ if (bytes.byteLength !== value.byteLength
51
+ || createHash('sha256').update(bytes).digest('hex') !== value.sha256) {
52
+ throw new Error('Cloud returned a corrupt live source snapshot.');
53
+ }
54
+ return Object.freeze({ path: value.path, sha256: value.sha256, bytes });
55
+ }
56
+ function parseSource(input) {
57
+ if (typeof input !== 'object' || input === null || Array.isArray(input)) {
58
+ throw new Error('Cloud returned an invalid live source snapshot.');
59
+ }
60
+ const value = input;
61
+ if (!safePath(value.entrypoint) || !Array.isArray(value.files)
62
+ || value.files.length < 1
63
+ || value.files.length > DEFAULT_PROTOCOL_DEPLOYMENT_BUNDLE_LIMITS.maxSourceFiles) {
64
+ throw new Error('Cloud returned an invalid live source snapshot.');
65
+ }
66
+ const files = value.files.map(decodeFile);
67
+ const paths = new Set();
68
+ let total = 0;
69
+ for (const file of files) {
70
+ const folded = file.path.toLowerCase();
71
+ if (paths.has(folded))
72
+ throw new Error('Cloud returned duplicate live source paths.');
73
+ paths.add(folded);
74
+ total += file.bytes.byteLength;
75
+ }
76
+ if (total > DEFAULT_PROTOCOL_DEPLOYMENT_BUNDLE_LIMITS.maxSourceBytes
77
+ || !files.some(file => file.path === value.entrypoint)) {
78
+ throw new Error('Cloud returned an invalid live source snapshot.');
79
+ }
80
+ let revision;
81
+ if (value.revision !== undefined) {
82
+ if (typeof value.revision !== 'object' || value.revision === null
83
+ || Array.isArray(value.revision)) {
84
+ throw new Error('Cloud returned an invalid live source revision.');
85
+ }
86
+ const candidate = value.revision;
87
+ if (candidate.kind !== 'git'
88
+ || typeof candidate.commit !== 'string'
89
+ || !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(candidate.commit)
90
+ || typeof candidate.dirty !== 'boolean'
91
+ || (candidate.branch !== undefined && typeof candidate.branch !== 'string')) {
92
+ throw new Error('Cloud returned an invalid live source revision.');
93
+ }
94
+ revision = {
95
+ kind: 'git',
96
+ commit: candidate.commit,
97
+ dirty: candidate.dirty,
98
+ ...(candidate.branch !== undefined ? { branch: candidate.branch } : {}),
99
+ };
100
+ }
101
+ return Object.freeze({
102
+ entrypoint: value.entrypoint,
103
+ files: Object.freeze(files),
104
+ ...(revision ? { revision: Object.freeze(revision) } : {}),
105
+ });
106
+ }
107
+ function parseResponse(input, expectedTarget) {
108
+ if (typeof input !== 'object' || input === null || Array.isArray(input)) {
109
+ throw new Error('Cloud returned an invalid live deployment response.');
110
+ }
111
+ const value = input;
112
+ let target;
113
+ try {
114
+ target = validateProtocolDeploymentReleaseTarget(value.target);
115
+ }
116
+ catch {
117
+ throw new Error('Cloud returned an invalid live deployment target.');
118
+ }
119
+ if (value.kind !== 'hypequery-live-deployment' || value.version !== 1
120
+ || target.project !== expectedTarget.project
121
+ || target.environment !== expectedTarget.environment) {
122
+ throw new Error('Cloud returned a mismatched live deployment response.');
123
+ }
124
+ if (value.active === null)
125
+ return Object.freeze({ target, active: null });
126
+ if (typeof value.active !== 'object' || Array.isArray(value.active)) {
127
+ throw new Error('Cloud returned an invalid live deployment response.');
128
+ }
129
+ const active = value.active;
130
+ if (typeof active.revision !== 'string' || !SHA256_PATTERN.test(active.revision)
131
+ || typeof active.releaseIdentity !== 'string'
132
+ || !SHA256_PATTERN.test(active.releaseIdentity)
133
+ || typeof active.activatedAt !== 'string'
134
+ || !Number.isFinite(Date.parse(active.activatedAt))
135
+ || typeof active.restored !== 'boolean'
136
+ || typeof active.hasSource !== 'boolean'
137
+ || (active.source !== undefined && !active.hasSource)) {
138
+ throw new Error('Cloud returned an invalid live deployment response.');
139
+ }
140
+ return Object.freeze({
141
+ target,
142
+ active: Object.freeze({
143
+ revision: active.revision,
144
+ releaseIdentity: active.releaseIdentity,
145
+ activatedAt: active.activatedAt,
146
+ restored: active.restored,
147
+ hasSource: active.hasSource,
148
+ ...(active.source !== undefined ? { source: parseSource(active.source) } : {}),
149
+ }),
150
+ });
151
+ }
152
+ async function errorMessage(response) {
153
+ try {
154
+ const input = await response.json();
155
+ const code = typeof input.error?.code === 'string' ? `${input.error.code}: ` : '';
156
+ const message = typeof input.error?.message === 'string'
157
+ ? input.error.message
158
+ : response.statusText;
159
+ return `${code}${message}`;
160
+ }
161
+ catch {
162
+ return response.statusText;
163
+ }
164
+ }
165
+ export async function fetchLiveDeployment(input) {
166
+ if (input.token.length < 1 || input.token.length > 4096
167
+ || input.token.trim() !== input.token
168
+ || [...input.token].some(character => {
169
+ const code = character.charCodeAt(0);
170
+ return code < 0x21 || code > 0x7e;
171
+ })) {
172
+ throw new Error('The deployment credential contains invalid characters or length.');
173
+ }
174
+ const request = input.fetch ?? fetch;
175
+ const url = liveUrl(input.endpoint, input.target, input.resource);
176
+ if (!url)
177
+ return undefined;
178
+ const response = await request(url, {
179
+ method: 'GET',
180
+ headers: { Accept: 'application/json', Authorization: `Bearer ${input.token}` },
181
+ redirect: 'error',
182
+ signal: AbortSignal.timeout(30_000),
183
+ });
184
+ if (input.resource === 'state' && response.status === 404)
185
+ return undefined;
186
+ if (!response.ok) {
187
+ throw new Error(`Could not read the live deployment (${response.status}): ${await errorMessage(response)}`);
188
+ }
189
+ const contentLength = Number(response.headers.get('content-length'));
190
+ if (Number.isFinite(contentLength) && contentLength > MAX_RESPONSE_BYTES) {
191
+ throw new Error('Cloud returned an oversized live deployment response.');
192
+ }
193
+ const bytes = new Uint8Array(await response.arrayBuffer());
194
+ if (bytes.byteLength > MAX_RESPONSE_BYTES) {
195
+ throw new Error('Cloud returned an oversized live deployment response.');
196
+ }
197
+ let parsed;
198
+ try {
199
+ parsed = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
200
+ }
201
+ catch {
202
+ throw new Error('Cloud returned an invalid live deployment response.');
203
+ }
204
+ return parseResponse(parsed, input.target);
205
+ }
package/package.json CHANGED
@@ -1,7 +1,18 @@
1
1
  {
2
2
  "name": "@hypequery/cli",
3
- "version": "1.16.1",
4
- "description": "Command-line interface for hypequery",
3
+ "version": "1.16.3",
4
+ "description": "CLI for scaffolding type-safe ClickHouse analytics, semantic datasets, React manifests, and deployments",
5
+ "keywords": [
6
+ "clickhouse",
7
+ "typescript",
8
+ "analytics",
9
+ "semantic-layer",
10
+ "query-builder",
11
+ "orm",
12
+ "code-generation",
13
+ "chdb",
14
+ "cli"
15
+ ],
5
16
  "license": "Apache-2.0",
6
17
  "type": "module",
7
18
  "bin": {
@@ -19,8 +30,8 @@
19
30
  "open": "^10.0.0",
20
31
  "ora": "^8.0.1",
21
32
  "prompts": "^2.4.2",
22
- "@hypequery/deployment": "0.7.1",
23
- "@hypequery/protocol": "0.10.1"
33
+ "@hypequery/deployment": "0.7.2",
34
+ "@hypequery/protocol": "0.10.2"
24
35
  },
25
36
  "optionalDependencies": {
26
37
  "@napi-rs/keyring": "1.3.0"
@@ -34,13 +45,14 @@
34
45
  "@vitest/coverage-v8": "^3.2.6",
35
46
  "typescript": "^5.7.3",
36
47
  "vitest": "^3.2.6",
37
- "@hypequery/serve": "0.15.0"
48
+ "@hypequery/serve": "0.15.2"
38
49
  },
39
50
  "repository": {
40
51
  "type": "git",
41
- "url": "https://github.com/hypequery/hypequery.git"
52
+ "url": "git+https://github.com/hypequery/hypequery.git",
53
+ "directory": "packages/cli"
42
54
  },
43
- "homepage": "https://www.hypequery.com",
55
+ "homepage": "https://hypequery.com",
44
56
  "bugs": {
45
57
  "url": "https://github.com/hypequery/hypequery/issues"
46
58
  },