@hypequery/cli 1.19.1 → 1.21.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
@@ -18,8 +18,29 @@ For a semantic layer:
18
18
 
19
19
  ```bash
20
20
  npx hypequery init --style datasets
21
+ npx hypequery login
22
+ npx hypequery deploy analytics/cloud.ts
21
23
  ```
22
24
 
25
+ The dataset scaffold writes `analytics/cloud.ts` alongside the local
26
+ `analytics/api.ts`. Its Cloud publication is explicit:
27
+
28
+ ```ts
29
+ import { publishToCloud } from '@hypequery/datasets';
30
+ import { datasets } from './datasets.js';
31
+
32
+ export const cloud = publishToCloud({
33
+ datasets,
34
+ access: { roles: [], scopes: [] },
35
+ });
36
+ ```
37
+
38
+ `api.ts` remains the local Serve entrypoint. Cloud deployment builds from
39
+ `cloud.ts` and does not load Serve configuration. This first Cloud publisher
40
+ accepts datasets only; it rejects named metrics rather than dropping them.
41
+ The required access policy makes the authenticated roles and scopes visible
42
+ before an existing project moves away from Serve-based publishing.
43
+
23
44
  For embedded analytics without a ClickHouse server:
24
45
 
25
46
  ```bash
@@ -38,7 +59,7 @@ npx hypequery init \
38
59
  | `hypequery generate:datasets` | Dataset definitions scaffolded from tables |
39
60
  | `hypequery generate:manifest` | A browser-safe route manifest for React hooks |
40
61
  | `hypequery login` | An authenticated Cloud target |
41
- | `hypequery deploy` | A verified deployment of the analytics API |
62
+ | `hypequery deploy analytics/cloud.ts` | A verified dataset-only Cloud deployment |
42
63
  | `hypequery pull` / `diff` | Live source inspection and comparison |
43
64
 
44
65
  Non-interactive ClickHouse commands read `CLICKHOUSE_URL`, `CLICKHOUSE_DATABASE`, `CLICKHOUSE_USERNAME`, and `CLICKHOUSE_PASSWORD`.
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;AAiCpC,QAAA,MAAM,OAAO,SAAgB,CAAC;AAa9B,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;EAKpE;AAqRD,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;AAiCpC,QAAA,MAAM,OAAO,SAAgB,CAAC;AAa9B,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;EAKpE;AAqQD,OAAO,EAAE,OAAO,EAAE,CAAC"}
package/dist/cli.js CHANGED
@@ -167,24 +167,16 @@ program
167
167
  await generateManifestCommand(api, options);
168
168
  }));
169
169
  program
170
- .command('deployment:build <api>')
171
- .description('Build a verified deployment bundle')
170
+ .command('deployment:build <cloud>')
171
+ .description('Build a verified dataset-only Cloud deployment bundle')
172
172
  .option('--bundle-output <directory>', 'Bundle directory (default: analytics/hypequery-deployment)')
173
- .option('-o, --output <path>', 'Output JSON file (default: analytics/hypequery-deployment.json)')
174
- .option('--runtime <runtime>', 'Runtime for non-portable handlers: node or python (default: node)')
175
- .option('--runtime-artifact <sha256>', 'Use a prebuilt runtime artifact with this SHA-256 identity')
176
- .option('--runtime-file <path>', 'Include the bytes for a prebuilt runtime artifact')
177
- .option('--runtime-output <path>', 'Bundled Node runtime path (default: beside deployment JSON)')
178
- .option('--entrypoint-prefix <prefix>', 'Runtime entrypoint prefix (default: queries)')
179
- .option('--hash-output <path>', 'Deployment identity sidecar path (default: <output>.sha256)')
180
173
  .option('--no-source', 'Exclude project source files from the deployment bundle')
181
- .option('--allow-unsupported-config', 'Deploy even though some Serve config will not be honoured in Cloud')
182
- .action(runCommand(async (api, options) => {
183
- await buildDeploymentCommand(api, options);
174
+ .action(runCommand(async (cloud, options) => {
175
+ await buildDeploymentCommand(cloud, options);
184
176
  }));
185
177
  program
186
178
  .command('deployment:validate <artifact>')
187
- .description('Verify a deployment bundle or validate a legacy deployment JSON file')
179
+ .description('Verify a deployment bundle or validate a deployment contract JSON file')
188
180
  .action(runCommand(async (artifact) => {
189
181
  await validateDeploymentCommand(artifact);
190
182
  }));
@@ -208,16 +200,14 @@ program
208
200
  }));
209
201
  program
210
202
  .command('deploy <source>')
211
- .description('Build, prepare, and deploy a Hypequery API module')
203
+ .description('Build, prepare, and deploy a dataset-only Cloud publication')
212
204
  .option('--bundle-output <directory>', 'Bundle directory (default: analytics/hypequery-deployment)')
213
205
  .option('--release-output <path>', 'Release JSON path (default: beside the bundle)')
214
206
  .option('--project <project>', 'Target project identifier (advanced override)')
215
207
  .option('--environment <environment>', 'Target environment identifier (advanced override)')
216
- .option('--release <path>', 'Submit a prebuilt bundle with this release (legacy)')
217
208
  .option('--no-source', 'Exclude project source files from the deployment bundle')
218
209
  .option('--endpoint <url>', 'HTTPS submission endpoint; requires HYPEQUERY_API_TOKEN')
219
210
  .option('--replace-restored', 'Intentionally replace a restored live release')
220
- .option('--allow-unsupported-config', 'Deploy even though some Serve config will not be honoured in Cloud')
221
211
  .action(runCommand(async (source, options) => {
222
212
  await deployCommand(source, options);
223
213
  }));
@@ -253,9 +243,9 @@ program.on('--help', () => {
253
243
  console.log(' hypequery generate:types --output analytics/schema.ts');
254
244
  console.log(' hypequery generate:datasets');
255
245
  console.log(' hypequery generate:manifest analytics/api.ts --output analytics/hypequery-manifest.json');
256
- console.log(' hypequery deploy analytics/api.ts');
257
- console.log(' hypequery deploy analytics/api.ts --project my-project --environment production');
258
- console.log(' hypequery deployment:build analytics/api.ts');
246
+ console.log(' hypequery deploy analytics/cloud.ts');
247
+ console.log(' hypequery deploy analytics/cloud.ts --project my-project --environment production');
248
+ console.log(' hypequery deployment:build analytics/cloud.ts');
259
249
  console.log(' hypequery deployment:validate analytics/hypequery-deployment');
260
250
  console.log(' hypequery deployment:release analytics/hypequery-deployment --project my-project --environment production');
261
251
  console.log(' hypequery deployment:submit analytics/hypequery-deployment --release analytics/hypequery-deployment.release.json');
@@ -7,13 +7,14 @@ export interface SubmitDeploymentOptions {
7
7
  endpoint?: string;
8
8
  replaceRestored?: boolean;
9
9
  }
10
- export interface DeployOptions extends SubmitDeploymentOptions {
10
+ export interface DeployOptions {
11
+ endpoint?: string;
12
+ replaceRestored?: boolean;
11
13
  project?: string;
12
14
  environment?: string;
13
15
  bundleOutput?: string;
14
16
  releaseOutput?: string;
15
17
  source?: boolean;
16
- allowUnsupportedConfig?: boolean;
17
18
  }
18
19
  export interface SubmitDeploymentDependencies {
19
20
  readonly env?: Readonly<Record<string, string | undefined>>;
@@ -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,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;IACjB,sBAAsB,CAAC,EAAE,OAAO,CAAC;CAClC;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,CAyDvC"}
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,aAAa;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,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;AAsBD,wBAAsB,aAAa,CACjC,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,OAAO,GAAE,aAAkB,EAC3B,YAAY,GAAE,kBAAuB,GACpC,OAAO,CAAC,4BAA4B,CAAC,CAuCvC"}
@@ -123,7 +123,7 @@ export async function submitDeploymentCommand(bundlePath, options = {}, dependen
123
123
  return result;
124
124
  }
125
125
  /**
126
- * `deploy` takes an API module, but the deprecated form took a bundle
126
+ * `deploy` takes a Cloud publication module, but the deprecated form took a bundle
127
127
  * directory. Dropping `--release` from an existing script would otherwise hand
128
128
  * a bundle to the module loader and fail inside esbuild.
129
129
  */
@@ -135,47 +135,15 @@ async function rejectBundleDirectorySource(sourcePath) {
135
135
  return;
136
136
  }
137
137
  throw new Error(`Cannot deploy a prebuilt bundle directory: ${sourcePath}\n\n`
138
- + '`hypequery deploy` takes the API module to build, for example '
139
- + '`hypequery deploy analytics/api.ts`.\n'
138
+ + '`hypequery deploy` takes a Cloud publication module, for example '
139
+ + '`hypequery deploy analytics/cloud.ts`.\n'
140
140
  + 'To upload this bundle, use `hypequery deployment:submit '
141
141
  + `${sourcePath} --release <file>\`.`);
142
142
  }
143
- /**
144
- * `deploy` exposes no runtime flags, so a runtime error from the shared build
145
- * command would otherwise point at flags this command does not accept.
146
- */
147
- function withRuntimeFlagHint(error) {
148
- if (!(error instanceof Error) || !error.message.includes('--runtime'))
149
- return error;
150
- return new Error(`${error.message}\n\n`
151
- + 'Runtime overrides are not available on `hypequery deploy`. Build with '
152
- + '`hypequery deployment:build` (which accepts --runtime, --runtime-artifact, '
153
- + 'and --runtime-file), then upload with `hypequery deployment:submit`.');
154
- }
155
- function rejectLegacyOrchestrationOptions(options) {
156
- if (options.project !== undefined
157
- || options.environment !== undefined
158
- || options.bundleOutput !== undefined
159
- || options.releaseOutput !== undefined) {
160
- throw new Error('--release selects prebuilt submission mode and cannot be combined with '
161
- + '--project, --environment, --bundle-output, or --release-output. '
162
- + 'Use `hypequery deployment:submit` for explicit prebuilt uploads.');
163
- }
164
- }
165
143
  export async function deployCommand(sourcePath, options = {}, dependencies = {}) {
166
144
  if (!sourcePath) {
167
- throw new Error('Missing API module path.\n\n'
168
- + 'Usage: hypequery deploy analytics/api.ts');
169
- }
170
- if (options.release !== undefined) {
171
- rejectLegacyOrchestrationOptions(options);
172
- logger.warn('`hypequery deploy <bundle> --release <file>` is deprecated. '
173
- + 'Use `hypequery deployment:submit <bundle> --release <file>` instead.');
174
- return submitDeploymentCommand(sourcePath, {
175
- release: options.release,
176
- endpoint: options.endpoint,
177
- replaceRestored: options.replaceRestored,
178
- }, dependencies);
145
+ throw new Error('Missing Cloud publication module path.\n\n'
146
+ + 'Usage: hypequery deploy analytics/cloud.ts');
179
147
  }
180
148
  await rejectBundleDirectorySource(sourcePath);
181
149
  const bundlePath = options.bundleOutput ?? DEFAULT_BUNDLE_PATH;
@@ -188,16 +156,10 @@ export async function deployCommand(sourcePath, options = {}, dependencies = {})
188
156
  // Resolve credentials up front. Submission is the last step, so without this
189
157
  // a missing or expired login only surfaces after a full bundle build.
190
158
  await resolveDeploymentCredential(options.endpoint, dependencies);
191
- try {
192
- await build(sourcePath, {
193
- bundleOutput: bundlePath,
194
- source: options.source,
195
- allowUnsupportedConfig: options.allowUnsupportedConfig,
196
- });
197
- }
198
- catch (error) {
199
- throw withRuntimeFlagHint(error);
200
- }
159
+ await build(sourcePath, {
160
+ bundleOutput: bundlePath,
161
+ source: options.source,
162
+ });
201
163
  await prepareRelease(bundlePath, {
202
164
  project: options.project,
203
165
  environment: options.environment,
@@ -2,15 +2,7 @@ import { type ProtocolDeploymentContract, type ProtocolDeploymentReleaseEnvelope
2
2
  import { type StoredCloudCredential } from '../utils/cloud-credential-store.js';
3
3
  export interface BuildDeploymentOptions {
4
4
  bundleOutput?: string;
5
- output?: string;
6
- runtime?: 'node' | 'python';
7
- runtimeArtifact?: string;
8
- runtimeFile?: string;
9
- runtimeOutput?: string;
10
- entrypointPrefix?: string;
11
- hashOutput?: string;
12
5
  source?: boolean;
13
- allowUnsupportedConfig?: boolean;
14
6
  }
15
7
  export interface PrepareDeploymentReleaseOptions {
16
8
  project?: string;
@@ -25,7 +17,7 @@ export interface PrepareDeploymentReleaseDependencies {
25
17
  */
26
18
  outputFlagLabel?: string;
27
19
  }
28
- export declare function buildDeploymentCommand(apiPath: string | undefined, options?: BuildDeploymentOptions): Promise<ProtocolDeploymentContract>;
20
+ export declare function buildDeploymentCommand(sourcePath: string | undefined, options?: BuildDeploymentOptions): Promise<ProtocolDeploymentContract>;
29
21
  export declare function validateDeploymentCommand(artifactPath: string | undefined): Promise<ProtocolDeploymentContract>;
30
22
  export declare function prepareDeploymentReleaseCommand(bundlePath: string | undefined, options?: PrepareDeploymentReleaseOptions, dependencies?: PrepareDeploymentReleaseDependencies): Promise<ProtocolDeploymentReleaseEnvelope>;
31
23
  //# 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;AAgB7B,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;IACpB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,sBAAsB,CAAC,EAAE,OAAO,CAAC;CAClC;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;AAsDD,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,0BAA0B,CAAC,CA0JrC;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"}
1
+ {"version":3,"file":"deployment.d.ts","sourceRoot":"","sources":["../../src/commands/deployment.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,0BAA0B,EAC/B,KAAK,iCAAiC,EACvC,MAAM,qBAAqB,CAAC;AAQ7B,OAAO,EAEL,KAAK,qBAAqB,EAC3B,MAAM,oCAAoC,CAAC;AAE5C,MAAM,WAAW,sBAAsB;IACrC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;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;AAID,wBAAsB,sBAAsB,CAC1C,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,0BAA0B,CAAC,CAkCrC;AAUD,wBAAsB,yBAAyB,CAC7C,YAAY,EAAE,MAAM,GAAG,SAAS,GAC/B,OAAO,CAAC,0BAA0B,CAAC,CAqErC;AAiED,wBAAsB,+BAA+B,CACnD,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,OAAO,GAAE,+BAAoC,EAC7C,YAAY,GAAE,oCAAyC,GACtD,OAAO,CAAC,iCAAiC,CAAC,CAkE5C"}
@@ -1,177 +1,44 @@
1
- import { createHash } from 'node:crypto';
2
1
  import { lstat, mkdir, readFile, realpath, stat, writeFile } from 'node:fs/promises';
3
2
  import path from 'node:path';
4
3
  import { prepareProtocolDeploymentReleaseEnvelope, prepareProtocolDeploymentContract, } from '@hypequery/protocol';
5
- import { writeDeploymentBundle, verifyDeploymentBundle, readDeploymentRuntimeFile, } from '../utils/deployment-bundle.js';
6
- import { buildNodeRuntimeArtifact, getDeploymentRuntimeEntrypoints, } from '../utils/deployment-runtime-artifact.js';
4
+ import { writeDeploymentBundle, verifyDeploymentBundle, } from '../utils/deployment-bundle.js';
7
5
  import { captureDeploymentSourceSnapshot } from '../utils/deployment-source-snapshot.js';
8
- import { loadApiModule } from '../utils/load-api.js';
6
+ import { loadCloudPublication } from '../utils/load-cloud-publication.js';
9
7
  import { logger } from '../utils/logger.js';
10
- import { reportCloudDiagnostic } from '../utils/cloud-diagnostic.js';
11
8
  import { loadCloudCredential, } from '../utils/cloud-credential-store.js';
12
- const SHA256_PATTERN = /^[0-9a-f]{64}$/;
13
9
  const DEFAULT_BUNDLE_OUTPUT = 'analytics/hypequery-deployment';
14
- function runtimeName(options) {
15
- const runtime = options.runtime ?? 'node';
16
- if (runtime !== 'node' && runtime !== 'python') {
17
- throw new Error('--runtime must be either node or python.');
10
+ export async function buildDeploymentCommand(sourcePath, options = {}) {
11
+ if (!sourcePath) {
12
+ throw new Error('Missing Cloud publication module path.\n\n'
13
+ + 'Usage: hypequery deployment:build analytics/cloud.ts');
14
+ }
15
+ const bundleOutput = options.bundleOutput ?? DEFAULT_BUNDLE_OUTPUT;
16
+ const contract = await loadCloudPublication(sourcePath);
17
+ const candidate = contract;
18
+ if ((Array.isArray(candidate.queries) && candidate.queries.length > 0)
19
+ || (Array.isArray(candidate.artifacts) && candidate.artifacts.length > 0)) {
20
+ throw new Error('Cloud publication modules may publish datasets only.');
18
21
  }
19
- return runtime;
20
- }
21
- function configuredRuntimeArtifact(options, runtime) {
22
- if (options.runtimeArtifact === undefined)
23
- return undefined;
24
- if (!SHA256_PATTERN.test(options.runtimeArtifact)) {
25
- throw new Error('--runtime-artifact must be a lowercase 64-character SHA-256 digest.');
26
- }
27
- return {
28
- runtime,
29
- artifactSha256: options.runtimeArtifact,
30
- ...(options.entrypointPrefix !== undefined
31
- ? { entrypointPrefix: options.entrypointPrefix }
32
- : {}),
33
- };
34
- }
35
- function assertDistinctOutputPaths(paths) {
36
- const seen = new Map();
37
- for (const [label, value] of Object.entries(paths)) {
38
- if (value === undefined)
39
- continue;
40
- const resolved = path.resolve(value);
41
- const existing = seen.get(resolved);
42
- if (existing) {
43
- throw new Error(`${label} must use a different path from ${existing}.`);
44
- }
45
- seen.set(resolved, label);
46
- }
47
- }
48
- export async function buildDeploymentCommand(apiPath, options = {}) {
49
- if (!apiPath) {
50
- throw new Error('Missing API module path.\n\n'
51
- + 'Usage: hypequery deployment:build analytics/api.ts');
52
- }
53
- const runtime = runtimeName(options);
54
- const configuredArtifact = configuredRuntimeArtifact(options, runtime);
55
- const legacyOutputRequested = options.output !== undefined
56
- || options.hashOutput !== undefined
57
- || options.runtimeOutput !== undefined;
58
- if (options.bundleOutput !== undefined && legacyOutputRequested) {
59
- throw new Error('--bundle-output cannot be combined with --output, --hash-output, or --runtime-output.');
60
- }
61
- const bundleOutput = options.bundleOutput
62
- ?? (legacyOutputRequested ? undefined : DEFAULT_BUNDLE_OUTPUT);
63
- if (options.runtimeFile !== undefined && configuredArtifact === undefined) {
64
- throw new Error('--runtime-file requires --runtime-artifact.');
65
- }
66
- if (options.runtimeFile !== undefined && bundleOutput === undefined) {
67
- throw new Error('--runtime-file is only supported when building a deployment bundle.');
68
- }
69
- if (configuredArtifact && bundleOutput !== undefined && options.runtimeFile === undefined) {
70
- throw new Error('A complete deployment bundle requires --runtime-file with a prebuilt --runtime-artifact.');
71
- }
72
- if (configuredArtifact && options.runtimeOutput !== undefined) {
73
- throw new Error('--runtime-output cannot be used with a prebuilt --runtime-artifact.');
74
- }
75
- const outputPath = options.output ?? 'analytics/hypequery-deployment.json';
76
- const hashOutputPath = options.hashOutput ?? `${outputPath}.sha256`;
77
- if (options.runtimeOutput !== undefined) {
78
- assertDistinctOutputPaths({
79
- '--output': outputPath,
80
- '--hash-output': hashOutputPath,
81
- '--runtime-output': options.runtimeOutput,
82
- });
83
- }
84
- const api = await loadApiModule(apiPath);
85
- if (typeof api.deploymentContract !== 'function') {
86
- throw new Error(`Invalid API module: ${apiPath}\n\n`
87
- + 'The exported API must provide deploymentContract(). '
88
- + 'Upgrade @hypequery/serve and export the value returned by createAPI() or serve().');
89
- }
90
- let builtArtifact;
91
- let artifact = configuredArtifact;
92
- if (!artifact) {
93
- const runtimeEntrypoints = getDeploymentRuntimeEntrypoints(api);
94
- if (runtimeEntrypoints.length > 0) {
95
- if (runtime !== 'node') {
96
- throw new Error('Automatic runtime artifact builds currently support Node only. '
97
- + 'Provide --runtime-artifact for Python deployments.');
98
- }
99
- builtArtifact = await buildNodeRuntimeArtifact(apiPath, runtimeEntrypoints, options.entrypointPrefix);
100
- artifact = {
101
- runtime: 'node',
102
- artifactSha256: builtArtifact.artifactSha256,
103
- entrypointPrefix: builtArtifact.entrypointPrefix,
104
- };
105
- }
106
- }
107
- const contract = api.deploymentContract({
108
- ...(artifact ? { runtimeArtifact: artifact } : {}),
109
- ...(options.allowUnsupportedConfig ? { allowUnsupportedConfig: true } : {}),
110
- onCloudDiagnostic: diagnostic => reportCloudDiagnostic(diagnostic, options.allowUnsupportedConfig === true),
111
- });
112
22
  const prepared = prepareProtocolDeploymentContract(contract);
113
- const { canonical, contract: validated, identity: digest } = prepared;
114
- if (bundleOutput !== undefined) {
115
- const runtimeFiles = [];
116
- if (builtArtifact) {
117
- runtimeFiles.push({
118
- runtime: 'node',
119
- sha256: builtArtifact.artifactSha256,
120
- bytes: builtArtifact.bytes,
121
- });
122
- }
123
- else if (configuredArtifact && options.runtimeFile) {
124
- const bytes = await readDeploymentRuntimeFile(options.runtimeFile);
125
- const actualSha256 = createHash('sha256').update(bytes).digest('hex');
126
- if (actualSha256 !== configuredArtifact.artifactSha256) {
127
- throw new Error(`Prebuilt runtime artifact SHA-256 mismatch: ${options.runtimeFile}\n\n`
128
- + `Expected: ${configuredArtifact.artifactSha256}\nActual: ${actualSha256}`);
129
- }
130
- runtimeFiles.push({ runtime, sha256: actualSha256, bytes });
131
- }
132
- const sourceSnapshot = options.source === false
133
- ? undefined
134
- : await captureDeploymentSourceSnapshot(apiPath);
135
- const bundle = await writeDeploymentBundle(bundleOutput, prepared, runtimeFiles, sourceSnapshot);
136
- logger.success(`Deployment bundle written to ${bundle.directory}`);
137
- if (bundle.manifest.source) {
138
- logger.info(`Captured ${bundle.manifest.source.files.length} source `
139
- + `${bundle.manifest.source.files.length === 1 ? 'file' : 'files'}`);
140
- }
141
- logger.info(`Bundle identity: ${bundle.identity}`);
142
- logger.info(`Deployment identity: ${digest}`);
143
- return validated;
144
- }
145
- const runtimeOutputPath = builtArtifact
146
- ? options.runtimeOutput ?? path.join(path.dirname(outputPath), 'hypequery-runtime.mjs')
147
- : undefined;
148
- assertDistinctOutputPaths({
149
- '--output': outputPath,
150
- '--hash-output': hashOutputPath,
151
- '--runtime-output': runtimeOutputPath,
152
- });
153
- const identitySidecar = [
154
- '# Hypequery deployment identity v1; not a file checksum or sha256sum input.',
155
- '# SHA-256(UTF-8("hypequery:deployment:v1") || 0x00 || RFC 8785 canonical bytes); '
156
- + 'the output newline is excluded.',
157
- `${digest} ${path.basename(outputPath)}`,
158
- '',
159
- ].join('\n');
160
- await mkdir(path.dirname(outputPath), { recursive: true });
161
- await mkdir(path.dirname(hashOutputPath), { recursive: true });
162
- if (runtimeOutputPath && builtArtifact) {
163
- await mkdir(path.dirname(runtimeOutputPath), { recursive: true });
164
- await writeFile(runtimeOutputPath, builtArtifact.bytes);
165
- }
166
- await writeFile(outputPath, `${canonical}\n`, 'utf8');
167
- await writeFile(hashOutputPath, identitySidecar, 'utf8');
168
- if (runtimeOutputPath && builtArtifact) {
169
- logger.success(`Runtime artifact written to ${runtimeOutputPath}`);
170
- logger.info(`Runtime artifact SHA-256: ${builtArtifact.artifactSha256}`);
171
- }
172
- logger.success(`Deployment contract written to ${outputPath}`);
173
- logger.info(`Identity: ${digest}`);
174
- return validated;
23
+ const sourceSnapshot = options.source === false
24
+ ? undefined
25
+ : await captureDeploymentSourceSnapshot(sourcePath);
26
+ const bundle = await writeDeploymentBundle(bundleOutput, prepared, sourceSnapshot);
27
+ logger.success(`Deployment bundle written to ${bundle.directory}`);
28
+ if (bundle.manifest.source) {
29
+ logger.info(`Captured ${bundle.manifest.source.files.length} source `
30
+ + `${bundle.manifest.source.files.length === 1 ? 'file' : 'files'}`);
31
+ }
32
+ logger.info(`${prepared.contract.datasets.length} `
33
+ + `${prepared.contract.datasets.length === 1 ? 'dataset' : 'datasets'}, 0 runtime artifacts`);
34
+ logger.info(`Bundle identity: ${bundle.identity}`);
35
+ logger.info(`Deployment identity: ${prepared.identity}`);
36
+ return prepared.contract;
37
+ }
38
+ function describeContract(contract) {
39
+ const datasets = `${contract.datasets.length} `
40
+ + `${contract.datasets.length === 1 ? 'dataset' : 'datasets'}`;
41
+ return datasets;
175
42
  }
176
43
  export async function validateDeploymentCommand(artifactPath) {
177
44
  if (!artifactPath) {
@@ -194,8 +61,7 @@ export async function validateDeploymentCommand(artifactPath) {
194
61
  const bundle = await verifyDeploymentBundle(artifactPath);
195
62
  const contract = bundle.contract;
196
63
  logger.success(`Valid deployment bundle: ${artifactPath}`);
197
- logger.info(`${contract.datasets.length} datasets, ${contract.queries.length} queries, `
198
- + `${contract.artifacts.length} runtime artifacts`);
64
+ logger.info(describeContract(contract));
199
65
  logger.info(`Bundle identity: ${bundle.identity}`);
200
66
  logger.info(`Deployment identity: ${bundle.manifest.deployment.identity}`);
201
67
  return contract;
@@ -226,8 +92,7 @@ export async function validateDeploymentCommand(artifactPath) {
226
92
  }
227
93
  const { contract, identity: digest } = prepared;
228
94
  logger.success(`Valid deployment contract: ${artifactPath}`);
229
- logger.info(`${contract.datasets.length} datasets, ${contract.queries.length} queries, `
230
- + `${contract.artifacts.length} runtime artifacts`);
95
+ logger.info(describeContract(contract));
231
96
  logger.info(`Identity: ${digest}`);
232
97
  return contract;
233
98
  }
@@ -1 +1 @@
1
- {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAIA,OAAO,EAcL,KAAK,SAAS,EACf,MAAM,qBAAqB,CAAC;AAe7B,OAAO,EAA2B,KAAK,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAWzF,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,IAAI,CAAC,EAAE,gBAAgB,CAAC;CACzB;AAqKD,wBAAsB,WAAW,CAAC,OAAO,GAAE,WAAgB,iBAkb1D"}
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAIA,OAAO,EAcL,KAAK,SAAS,EACf,MAAM,qBAAqB,CAAC;AAe7B,OAAO,EAA2B,KAAK,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAYzF,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,IAAI,CAAC,EAAE,gBAAgB,CAAC;CACzB;AAqKD,wBAAsB,WAAW,CAAC,OAAO,GAAE,WAAgB,iBAsb1D"}
@@ -10,6 +10,7 @@ import { generateEnvTemplate, appendToEnv } from '../templates/env.js';
10
10
  import { generateClientTemplate } from '../templates/client.js';
11
11
  import { generateQueriesTemplate } from '../templates/queries.js';
12
12
  import { generateApiTemplate } from '../templates/api.js';
13
+ import { generateCloudTemplate } from '../templates/cloud.js';
13
14
  import { CONTEXT_AUTH_TENANT_COLUMN } from '../templates/auth-scaffold.js';
14
15
  import { generateDatasetsPlaceholderTemplate } from '../templates/datasets.js';
15
16
  import { appendToGitignore } from '../templates/gitignore.js';
@@ -234,6 +235,7 @@ export async function initCommand(options = {}) {
234
235
  ? [
235
236
  path.join(resolvedOutputDir, 'datasets.ts'),
236
237
  path.join(resolvedOutputDir, 'api.ts'),
238
+ path.join(resolvedOutputDir, 'cloud.ts'),
237
239
  ]
238
240
  : [
239
241
  path.join(resolvedOutputDir, 'queries.ts'),
@@ -421,6 +423,9 @@ export interface IntrospectedSchema {
421
423
  apiPath = path.join(resolvedOutputDir, 'api.ts');
422
424
  await writeFile(apiPath, generateApiTemplate({ auth }));
423
425
  logger.success(`Created API file (${path.relative(process.cwd(), apiPath)})`);
426
+ const cloudPath = path.join(resolvedOutputDir, 'cloud.ts');
427
+ await writeFile(cloudPath, generateCloudTemplate());
428
+ logger.success(`Created Cloud publication (${path.relative(process.cwd(), cloudPath)})`);
424
429
  }
425
430
  else {
426
431
  apiPath = path.join(resolvedOutputDir, 'queries.ts');
@@ -0,0 +1,3 @@
1
+ /** Generate the explicit Cloud publication entrypoint for dataset projects. */
2
+ export declare function generateCloudTemplate(): string;
3
+ //# sourceMappingURL=cloud.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cloud.d.ts","sourceRoot":"","sources":["../../src/templates/cloud.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,wBAAgB,qBAAqB,IAAI,MAAM,CAS9C"}
@@ -0,0 +1,11 @@
1
+ /** Generate the explicit Cloud publication entrypoint for dataset projects. */
2
+ export function generateCloudTemplate() {
3
+ return `import { publishToCloud } from '@hypequery/datasets';
4
+ import { datasets } from './datasets.js';
5
+
6
+ export const cloud = publishToCloud({
7
+ datasets,
8
+ access: { roles: [], scopes: [] },
9
+ });
10
+ `;
11
+ }
@@ -1,13 +1,8 @@
1
- import { type PreparedProtocolDeploymentContract, type ProtocolDeploymentBundleManifest, type ProtocolDeploymentContract } from '@hypequery/protocol';
1
+ import { type PreparedProtocolDeploymentContract, type ProtocolDeploymentContract, type ProtocolDeploymentBundleManifest } from '@hypequery/protocol';
2
2
  import { DEPLOYMENT_BUNDLE_CONTRACT, DEPLOYMENT_BUNDLE_MANIFEST, verifyDeploymentBundle } from '@hypequery/deployment';
3
3
  import type { VerifiedDeploymentBundle } from '@hypequery/deployment';
4
4
  export { DEPLOYMENT_BUNDLE_CONTRACT, DEPLOYMENT_BUNDLE_MANIFEST, verifyDeploymentBundle, };
5
5
  export type { VerifiedDeploymentBundle };
6
- export interface DeploymentBundleRuntimeFile {
7
- readonly runtime: 'node' | 'python';
8
- readonly sha256: string;
9
- readonly bytes: Uint8Array;
10
- }
11
6
  export interface DeploymentBundleSourceFile {
12
7
  readonly path: string;
13
8
  readonly bytes: Uint8Array;
@@ -28,6 +23,5 @@ export interface WrittenDeploymentBundle {
28
23
  readonly identity: string;
29
24
  readonly contract: ProtocolDeploymentContract;
30
25
  }
31
- export declare function writeDeploymentBundle(outputDirectory: string, prepared: PreparedProtocolDeploymentContract, runtimeFiles: readonly DeploymentBundleRuntimeFile[], sourceSnapshot?: DeploymentBundleSourceSnapshot): Promise<WrittenDeploymentBundle>;
32
- export declare function readDeploymentRuntimeFile(filePath: string): Promise<Uint8Array>;
26
+ export declare function writeDeploymentBundle(outputDirectory: string, prepared: PreparedProtocolDeploymentContract, sourceSnapshot?: DeploymentBundleSourceSnapshot): Promise<WrittenDeploymentBundle>;
33
27
  //# sourceMappingURL=deployment-bundle.d.ts.map
@@ -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;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
+ {"version":3,"file":"deployment-bundle.d.ts","sourceRoot":"","sources":["../../src/utils/deployment-bundle.ts"],"names":[],"mappings":"AAUA,OAAO,EAGL,KAAK,kCAAkC,EACvC,KAAK,0BAA0B,EAC/B,KAAK,gCAAgC,EACtC,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,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;AA4ED,wBAAsB,qBAAqB,CACzC,eAAe,EAAE,MAAM,EACvB,QAAQ,EAAE,kCAAkC,EAC5C,cAAc,CAAC,EAAE,8BAA8B,GAC9C,OAAO,CAAC,uBAAuB,CAAC,CAkFlC"}
@@ -1,5 +1,5 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
- import { constants, lstat, mkdir, mkdtemp, open, rename, rm, writeFile, } from 'node:fs/promises';
2
+ import { lstat, mkdir, mkdtemp, rename, rm, writeFile, } from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import { DEFAULT_PROTOCOL_DEPLOYMENT_BUNDLE_LIMITS, prepareProtocolDeploymentBundleManifest, } from '@hypequery/protocol';
5
5
  import { DEPLOYMENT_BUNDLE_CONTRACT, DEPLOYMENT_BUNDLE_MANIFEST, verifyDeploymentBundle, } from '@hypequery/deployment';
@@ -9,9 +9,6 @@ const utf8Encoder = new TextEncoder();
9
9
  function sha256(bytes) {
10
10
  return createHash('sha256').update(bytes).digest('hex');
11
11
  }
12
- function runtimeArtifactPath(runtime, digest) {
13
- return `artifacts/${digest}.${runtime === 'node' ? 'mjs' : 'pyz'}`;
14
- }
15
12
  const SOURCE_ROOT = 'source';
16
13
  function errorCode(error) {
17
14
  return typeof error === 'object' && error !== null && 'code' in error
@@ -37,53 +34,6 @@ async function existingVerifiedBundle(outputDirectory) {
37
34
  }
38
35
  return true;
39
36
  }
40
- function validateRuntimeFiles(prepared, files) {
41
- requireClosedContractArtifactSet(prepared.contract);
42
- const declared = new Map(prepared.contract.artifacts.map(artifact => [artifact.artifactSha256, artifact.runtime]));
43
- const supplied = new Map();
44
- for (const file of files) {
45
- if (sha256(file.bytes) !== file.sha256) {
46
- throw new Error(`Runtime artifact bytes do not match SHA-256 ${file.sha256}.`);
47
- }
48
- if (declared.get(file.sha256) !== file.runtime) {
49
- throw new Error(`Runtime artifact ${file.sha256} is not declared by the deployment contract as ${file.runtime}.`);
50
- }
51
- if (supplied.has(file.sha256)) {
52
- throw new Error(`Duplicate runtime artifact bytes supplied for ${file.sha256}.`);
53
- }
54
- if (file.bytes.byteLength < 1
55
- || file.bytes.byteLength > DEFAULT_PROTOCOL_DEPLOYMENT_BUNDLE_LIMITS.maxArtifactBytes) {
56
- throw new Error(`Runtime artifact ${file.sha256} exceeds the bundle byte limits.`);
57
- }
58
- supplied.set(file.sha256, file.runtime);
59
- }
60
- for (const [digest, runtime] of declared) {
61
- if (supplied.get(digest) !== runtime) {
62
- throw new Error(`Deployment bundle is missing ${runtime} runtime artifact ${digest}.`);
63
- }
64
- }
65
- return Object.freeze([...files].sort((left, right) => {
66
- const leftPath = runtimeArtifactPath(left.runtime, left.sha256);
67
- const rightPath = runtimeArtifactPath(right.runtime, right.sha256);
68
- return leftPath < rightPath ? -1 : leftPath > rightPath ? 1 : 0;
69
- }));
70
- }
71
- function requireClosedContractArtifactSet(contract) {
72
- const referenced = new Map();
73
- for (const query of contract.queries) {
74
- if (query.implementation.kind === 'runtime-reference') {
75
- referenced.set(query.implementation.artifactSha256, query.implementation.runtime);
76
- }
77
- }
78
- if (referenced.size !== contract.artifacts.length) {
79
- throw new Error('Deployment contract contains missing or unreferenced runtime artifacts.');
80
- }
81
- for (const artifact of contract.artifacts) {
82
- if (referenced.get(artifact.artifactSha256) !== artifact.runtime) {
83
- throw new Error(`Deployment runtime artifact ${artifact.artifactSha256} is not referenced by a named query.`);
84
- }
85
- }
86
- }
87
37
  function validateSourceSnapshot(source) {
88
38
  if (!source)
89
39
  return undefined;
@@ -122,12 +72,11 @@ function validateSourceSnapshot(source) {
122
72
  ...(source.revision ? { revision: Object.freeze({ ...source.revision }) } : {}),
123
73
  });
124
74
  }
125
- export async function writeDeploymentBundle(outputDirectory, prepared, runtimeFiles, sourceSnapshot) {
75
+ export async function writeDeploymentBundle(outputDirectory, prepared, sourceSnapshot) {
126
76
  const destination = path.resolve(outputDirectory);
127
77
  if (destination === path.parse(destination).root) {
128
78
  throw new Error('The deployment bundle output cannot be a filesystem root.');
129
79
  }
130
- const files = validateRuntimeFiles(prepared, runtimeFiles);
131
80
  const source = validateSourceSnapshot(sourceSnapshot);
132
81
  const deploymentBytes = utf8Encoder.encode(`${prepared.canonical}\n`);
133
82
  const manifestInput = {
@@ -139,12 +88,8 @@ export async function writeDeploymentBundle(outputDirectory, prepared, runtimeFi
139
88
  sha256: sha256(deploymentBytes),
140
89
  byteLength: deploymentBytes.byteLength,
141
90
  },
142
- artifacts: files.map(file => ({
143
- runtime: file.runtime,
144
- path: runtimeArtifactPath(file.runtime, file.sha256),
145
- sha256: file.sha256,
146
- byteLength: file.bytes.byteLength,
147
- })),
91
+ // A deployment carries datasets: there is nothing else to put here.
92
+ artifacts: [],
148
93
  ...(source ? {
149
94
  source: {
150
95
  root: SOURCE_ROOT,
@@ -164,12 +109,7 @@ export async function writeDeploymentBundle(outputDirectory, prepared, runtimeFi
164
109
  await mkdir(parent, { recursive: true });
165
110
  const staging = await mkdtemp(path.join(parent, `.${path.basename(destination)}.tmp-`));
166
111
  try {
167
- if (files.length > 0)
168
- await mkdir(path.join(staging, 'artifacts'));
169
112
  await writeFile(path.join(staging, DEPLOYMENT_BUNDLE_CONTRACT), deploymentBytes, { flag: 'wx' });
170
- for (const file of files) {
171
- await writeFile(path.join(staging, runtimeArtifactPath(file.runtime, file.sha256)), file.bytes, { flag: 'wx' });
172
- }
173
113
  for (const file of source?.files ?? []) {
174
114
  const outputPath = path.join(staging, SOURCE_ROOT, ...file.path.split('/'));
175
115
  await mkdir(path.dirname(outputPath), { recursive: true });
@@ -209,38 +149,3 @@ export async function writeDeploymentBundle(outputDirectory, prepared, runtimeFi
209
149
  contract: prepared.contract,
210
150
  });
211
151
  }
212
- async function readBoundedAbsoluteRegularFile(absolutePath, displayPath, maximum) {
213
- let handle;
214
- try {
215
- const initialStat = await lstat(absolutePath);
216
- if (initialStat.isSymbolicLink()) {
217
- throw new Error(`Bundle entry must not be a symbolic link: ${displayPath}`);
218
- }
219
- if (!initialStat.isFile()) {
220
- throw new Error(`Bundle entry is not a regular file: ${displayPath}`);
221
- }
222
- if (initialStat.size < 1 || initialStat.size > maximum) {
223
- throw new Error(`Bundle entry exceeds its byte limit: ${displayPath}`);
224
- }
225
- handle = await open(absolutePath, constants.O_RDONLY | constants.O_NOFOLLOW);
226
- const stat = await handle.stat();
227
- if (!stat.isFile())
228
- throw new Error(`Bundle entry is not a regular file: ${displayPath}`);
229
- if (stat.size < 1 || stat.size > maximum) {
230
- throw new Error(`Bundle entry exceeds its byte limit: ${displayPath}`);
231
- }
232
- return await handle.readFile();
233
- }
234
- catch (error) {
235
- if (errorCode(error) === 'ELOOP') {
236
- throw new Error(`Bundle entry must not be a symbolic link: ${displayPath}`);
237
- }
238
- throw error;
239
- }
240
- finally {
241
- await handle?.close();
242
- }
243
- }
244
- export async function readDeploymentRuntimeFile(filePath) {
245
- return readBoundedAbsoluteRegularFile(path.resolve(filePath), filePath, DEFAULT_PROTOCOL_DEPLOYMENT_BUNDLE_LIMITS.maxArtifactBytes);
246
- }
@@ -0,0 +1,4 @@
1
+ import type { ProtocolDeploymentContract } from '@hypequery/protocol';
2
+ /** Load the explicit dataset contract exported by a Cloud publication module. */
3
+ export declare function loadCloudPublication(sourcePath: string): Promise<ProtocolDeploymentContract>;
4
+ //# sourceMappingURL=load-cloud-publication.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"load-cloud-publication.d.ts","sourceRoot":"","sources":["../../src/utils/load-cloud-publication.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AAGtE,iFAAiF;AACjF,wBAAsB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,0BAA0B,CAAC,CAYlG"}
@@ -0,0 +1,13 @@
1
+ import { loadModule } from './load-api.js';
2
+ /** Load the explicit dataset contract exported by a Cloud publication module. */
3
+ export async function loadCloudPublication(sourcePath) {
4
+ const module = await loadModule(sourcePath);
5
+ const cloud = module.cloud ?? module.default;
6
+ if (typeof cloud !== 'object' || cloud === null
7
+ || !('kind' in cloud) || cloud.kind !== 'hypequery-deployment') {
8
+ throw new Error(`Invalid Cloud publication: ${sourcePath}\n\n`
9
+ + 'Export `cloud = publishToCloud({ datasets, access })` from @hypequery/datasets. '
10
+ + 'A Serve API is not a Cloud publication.');
11
+ }
12
+ return cloud;
13
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hypequery/cli",
3
- "version": "1.19.1",
3
+ "version": "1.21.0",
4
4
  "description": "CLI for scaffolding type-safe ClickHouse analytics, semantic datasets, React manifests, and deployments",
5
5
  "keywords": [
6
6
  "clickhouse",
@@ -30,8 +30,8 @@
30
30
  "open": "^10.0.0",
31
31
  "ora": "^8.0.1",
32
32
  "prompts": "^2.4.2",
33
- "@hypequery/deployment": "0.8.0",
34
- "@hypequery/protocol": "0.13.0"
33
+ "@hypequery/deployment": "0.9.1",
34
+ "@hypequery/protocol": "0.15.0"
35
35
  },
36
36
  "optionalDependencies": {
37
37
  "@napi-rs/keyring": "1.3.0"
@@ -51,8 +51,8 @@
51
51
  "@vitest/coverage-v8": "^3.2.6",
52
52
  "typescript": "^5.7.3",
53
53
  "vitest": "^3.2.6",
54
- "@hypequery/mcp": "0.7.0",
55
- "@hypequery/serve": "0.17.1"
54
+ "@hypequery/mcp": "0.8.2",
55
+ "@hypequery/serve": "0.19.0"
56
56
  },
57
57
  "repository": {
58
58
  "type": "git",
@@ -1,10 +0,0 @@
1
- export interface CloudCompatibilityDiagnosticLike {
2
- readonly severity: 'error' | 'warning';
3
- readonly code: string;
4
- readonly subject: string;
5
- readonly message: string;
6
- readonly remedy: string;
7
- }
8
- /** Report warnings and explicitly overridden errors before submitting a release. */
9
- export declare function reportCloudDiagnostic(diagnostic: CloudCompatibilityDiagnosticLike, allowUnsupportedConfig: boolean): void;
10
- //# sourceMappingURL=cloud-diagnostic.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"cloud-diagnostic.d.ts","sourceRoot":"","sources":["../../src/utils/cloud-diagnostic.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,gCAAgC;IAC/C,QAAQ,CAAC,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,oFAAoF;AACpF,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,gCAAgC,EAC5C,sBAAsB,EAAE,OAAO,GAC9B,IAAI,CAMN"}
@@ -1,10 +0,0 @@
1
- import { logger } from './logger.js';
2
- /** Report warnings and explicitly overridden errors before submitting a release. */
3
- export function reportCloudDiagnostic(diagnostic, allowUnsupportedConfig) {
4
- // Blocking errors are reported by the contract builder's exception.
5
- if (diagnostic.severity === 'error' && !allowUnsupportedConfig)
6
- return;
7
- logger.warn(`${diagnostic.code} (${diagnostic.subject})`);
8
- logger.indent(diagnostic.message);
9
- logger.indent(`→ ${diagnostic.remedy}`);
10
- }
@@ -1,9 +0,0 @@
1
- export interface NodeRuntimeArtifact {
2
- readonly bytes: Uint8Array;
3
- readonly artifactSha256: string;
4
- readonly entrypointPrefix: string;
5
- readonly runtimeEntrypoints: readonly string[];
6
- }
7
- export declare function getDeploymentRuntimeEntrypoints(input: unknown): readonly string[];
8
- export declare function buildNodeRuntimeArtifact(apiPath: string, runtimeEntrypoints: readonly string[], entrypointPrefix?: string): Promise<NodeRuntimeArtifact>;
9
- //# sourceMappingURL=deployment-runtime-artifact.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"deployment-runtime-artifact.d.ts","sourceRoot":"","sources":["../../src/utils/deployment-runtime-artifact.ts"],"names":[],"mappings":"AAyBA,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;IAC3B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,kBAAkB,EAAE,SAAS,MAAM,EAAE,CAAC;CAChD;AASD,wBAAgB,+BAA+B,CAAC,KAAK,EAAE,OAAO,GAAG,SAAS,MAAM,EAAE,CA6BjF;AAiED,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,MAAM,EACf,kBAAkB,EAAE,SAAS,MAAM,EAAE,EACrC,gBAAgB,SAAY,GAC3B,OAAO,CAAC,mBAAmB,CAAC,CAmD9B"}
@@ -1,162 +0,0 @@
1
- import { createHash } from 'node:crypto';
2
- import path from 'node:path';
3
- import { parseProtocolIdentifier, parseProtocolQualifiedIdentifier, } from '@hypequery/protocol';
4
- import { build } from 'esbuild';
5
- import { findNearestTsconfig } from './load-api.js';
6
- const deploymentBuildSourceSymbol = Symbol.for('hypequery.deployment-build-source.v1');
7
- function executableQuery(query) {
8
- return typeof query === 'function'
9
- || (typeof query === 'object'
10
- && query !== null
11
- && typeof query.run === 'function');
12
- }
13
- export function getDeploymentRuntimeEntrypoints(input) {
14
- if (typeof input !== 'object' || input === null) {
15
- throw new Error('Invalid API module: deployment build metadata is unavailable.');
16
- }
17
- const api = input;
18
- const source = api[deploymentBuildSourceSymbol];
19
- if (source?.version !== 1 || !Array.isArray(source.runtimeEntrypoints)) {
20
- throw new Error('The exported API does not expose deployment build metadata. '
21
- + 'Upgrade @hypequery/serve before building runtime artifacts.');
22
- }
23
- const names = source.runtimeEntrypoints.map((name) => {
24
- try {
25
- return parseProtocolIdentifier(name);
26
- }
27
- catch {
28
- throw new Error(`Invalid runtime entrypoint name: ${String(name)}`);
29
- }
30
- });
31
- if (new Set(names).size !== names.length) {
32
- throw new Error('Deployment build metadata contains duplicate runtime entrypoints.');
33
- }
34
- for (const name of names) {
35
- if (!executableQuery(api.queries?.[name]?.query)) {
36
- throw new Error(`Serve query "${name}" does not expose an executable runtime handler.`);
37
- }
38
- }
39
- return Object.freeze([...names].sort());
40
- }
41
- function projectRelativeImport(apiPath) {
42
- const resolved = path.resolve(process.cwd(), apiPath);
43
- const relative = path.relative(process.cwd(), resolved);
44
- if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
45
- throw new Error('The API module must be inside the current project for deterministic bundling.');
46
- }
47
- const portable = relative.split(path.sep).join('/');
48
- return {
49
- resolved,
50
- specifier: portable.startsWith('.') ? portable : `./${portable}`,
51
- };
52
- }
53
- function runtimeEntrySource(apiSpecifier, runtimeEntrypoints, entrypointPrefix) {
54
- const prefix = parseProtocolQualifiedIdentifier(entrypointPrefix).split('.');
55
- const names = JSON.stringify(runtimeEntrypoints);
56
- let registryExpression = 'runtimeEntries';
57
- for (let index = prefix.length - 1; index >= 1; index -= 1) {
58
- registryExpression = `Object.freeze({ ${prefix[index]}: ${registryExpression} })`;
59
- }
60
- return [
61
- `import * as apiModule from ${JSON.stringify(apiSpecifier)};`,
62
- 'const api = apiModule.api ?? Reflect.get(apiModule, "default");',
63
- 'if (!api || typeof api !== "object") throw new Error("Runtime API export is unavailable.");',
64
- 'const resolveQuery = (name) => {',
65
- ' if (typeof api.execute === "function") {',
66
- ' // Serve entrypoint names are validated against api.queries before bundling.',
67
- ' return (argument = {}) => api.execute(name, {',
68
- ' input: argument.input,',
69
- ' ...(argument.context === undefined ? {} : { context: argument.context }),',
70
- ' ...(argument.request === undefined ? {} : { request: argument.request }),',
71
- ' ...(argument.requestId === undefined ? {} : { requestId: argument.requestId }),',
72
- // A null principal means "no trusted principal"; forwarding it would skip the
73
- // API's own auth strategies instead of falling through to them.
74
- ' ...(argument.trustedAuth == null ? {} : { trustedAuth: argument.trustedAuth }),',
75
- ' });',
76
- ' }',
77
- ' const entry = api.queries?.[name];',
78
- ' const query = entry?.query;',
79
- ' let run;',
80
- ' if (typeof query === "function") run = query;',
81
- ' else if (query && typeof query === "object" && typeof query.run === "function") run = query.run.bind(query);',
82
- ' if (!run) throw new Error(`Runtime entrypoint ${name} is unavailable.`);',
83
- ' // Without a Serve execute() pipeline there is nothing to enforce roles, scopes,',
84
- ' // or tenancy, so refuse a trusted principal instead of silently ignoring it.',
85
- ' return (argument = {}) => {',
86
- ' if (argument?.trustedAuth != null) {',
87
- ' throw new Error(`Runtime entrypoint ${name} cannot enforce a trusted principal without a Serve execute() pipeline.`);',
88
- ' }',
89
- ' return run(argument);',
90
- ' };',
91
- '};',
92
- `const runtimeEntries = Object.freeze(Object.fromEntries(${names}.map(name => [name, resolveQuery(name)])));`,
93
- `export const ${prefix[0]} = ${registryExpression};`,
94
- '',
95
- ].join('\n');
96
- }
97
- export async function buildNodeRuntimeArtifact(apiPath, runtimeEntrypoints, entrypointPrefix = 'queries') {
98
- if (runtimeEntrypoints.length === 0) {
99
- throw new Error('Cannot build a runtime artifact without Serve query entrypoints.');
100
- }
101
- const names = getValidatedEntrypointNames(runtimeEntrypoints);
102
- const { resolved, specifier } = projectRelativeImport(apiPath);
103
- let source;
104
- try {
105
- source = runtimeEntrySource(specifier, names, entrypointPrefix);
106
- }
107
- catch {
108
- throw new Error(`Invalid runtime entrypoint prefix: ${entrypointPrefix}`);
109
- }
110
- try {
111
- const result = await build({
112
- stdin: {
113
- contents: source,
114
- loader: 'ts',
115
- resolveDir: process.cwd(),
116
- sourcefile: 'hypequery-runtime-entry.ts',
117
- },
118
- absWorkingDir: process.cwd(),
119
- bundle: true,
120
- charset: 'utf8',
121
- format: 'esm',
122
- legalComments: 'none',
123
- logLevel: 'warning',
124
- minify: true,
125
- platform: 'node',
126
- sourcemap: false,
127
- target: ['node18'],
128
- treeShaking: true,
129
- tsconfig: await findNearestTsconfig(resolved) ?? undefined,
130
- write: false,
131
- });
132
- const output = result.outputFiles?.find(file => file.path.endsWith('.js'))
133
- ?? result.outputFiles?.[0];
134
- if (!output)
135
- throw new Error('esbuild produced no output');
136
- const bytes = output.contents;
137
- return Object.freeze({
138
- bytes,
139
- artifactSha256: createHash('sha256').update(bytes).digest('hex'),
140
- entrypointPrefix,
141
- runtimeEntrypoints: names,
142
- });
143
- }
144
- catch (error) {
145
- throw new Error(`Failed to build the Node runtime artifact from ${apiPath}.\n`
146
- + `Original error: ${error instanceof Error ? error.message : String(error)}`);
147
- }
148
- }
149
- function getValidatedEntrypointNames(input) {
150
- const names = input.map((name) => {
151
- try {
152
- return parseProtocolIdentifier(name);
153
- }
154
- catch {
155
- throw new Error(`Invalid runtime entrypoint name: ${name}`);
156
- }
157
- });
158
- if (new Set(names).size !== names.length) {
159
- throw new Error('Runtime entrypoint names must be unique.');
160
- }
161
- return Object.freeze([...names].sort());
162
- }