@hypequery/cli 1.5.0 → 1.7.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
@@ -138,35 +138,48 @@ semantic keys such as `dataset:orders`.
138
138
 
139
139
  ### `hypequery deployment:build`
140
140
 
141
- Builds deployment metadata for an exported HypeQuery API and writes the
142
- artifact with its identity sidecar.
141
+ Builds a closed deployment bundle for an exported HypeQuery API. The bundle
142
+ contains canonical deployment metadata, every referenced runtime artifact, and
143
+ a manifest that binds their exact bytes and identities.
143
144
 
144
145
  ```bash
145
- npx hypequery deployment:build analytics/api.ts \
146
- --runtime-artifact 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
146
+ npx hypequery deployment:build analytics/api.ts
147
147
  ```
148
148
 
149
- The runtime artifact digest is required when named Serve handlers cannot be
150
- lowered to a portable implementation. Dataset-only APIs can omit it.
149
+ The default output is `analytics/hypequery-deployment/`. Named Serve handlers
150
+ are bundled into a Node runtime artifact automatically. Dataset-only APIs do
151
+ not produce a runtime artifact. For a separately built Node or Python runtime,
152
+ provide both its expected digest and file path:
153
+
154
+ ```bash
155
+ npx hypequery deployment:build analytics/api.ts \
156
+ --runtime python \
157
+ --runtime-artifact <sha256> \
158
+ --runtime-file dist/runtime.pyz
159
+ ```
151
160
 
152
161
  Options:
153
162
 
154
- - `--output <path>`: default `analytics/hypequery-deployment.json`
163
+ - `--bundle-output <directory>`: default `analytics/hypequery-deployment`
155
164
  - `--runtime <runtime>`: `node` (default) or `python`
156
- - `--runtime-artifact <sha256>`: lowercase SHA-256 of the built runtime artifact
165
+ - `--runtime-artifact <sha256>`: lowercase SHA-256 of a prebuilt runtime artifact
166
+ - `--runtime-file <path>`: bytes for the prebuilt runtime artifact
157
167
  - `--entrypoint-prefix <prefix>`: default `queries`
158
- - `--hash-output <path>`: default `<output>.sha256`
159
168
 
160
- The identity sidecar is not compatible with `sha256sum -c`. Use
161
- `deployment:validate` to validate the artifact and report its identity.
169
+ The compatibility options `--output`, `--runtime-output`, and `--hash-output`
170
+ still emit the earlier metadata files instead of a complete bundle. They cannot
171
+ be combined with `--bundle-output`.
162
172
 
163
173
  ### `hypequery deployment:validate`
164
174
 
165
- Validates an existing deployment artifact and reports its datasets, queries,
166
- runtime artifacts, and identity.
175
+ Verifies a complete deployment bundle before returning any contained metadata.
176
+ Verification rejects missing or undeclared files, symbolic links, path
177
+ traversal, byte-length or hash mismatches, deployment identity mismatches, and
178
+ runtime files not referenced by the deployment. Legacy deployment JSON files
179
+ are still accepted for metadata-only validation.
167
180
 
168
181
  ```bash
169
- npx hypequery deployment:validate analytics/hypequery-deployment.json
182
+ npx hypequery deployment:validate analytics/hypequery-deployment
170
183
  ```
171
184
 
172
185
  ## Non-interactive Setup
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;AAYpC,QAAA,MAAM,OAAO,SAAgB,CAAC;AAa9B,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;EAKpE;AAwJD,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;AAYpC,QAAA,MAAM,OAAO,SAAgB,CAAC;AAa9B,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;EAKpE;AA2JD,OAAO,EAAE,OAAO,EAAE,CAAC"}
package/dist/cli.js CHANGED
@@ -111,10 +111,13 @@ program
111
111
  }));
112
112
  program
113
113
  .command('deployment:build <api>')
114
- .description('Build a validated, canonical deployment contract and identity')
114
+ .description('Build a verified deployment bundle')
115
+ .option('--bundle-output <directory>', 'Bundle directory (default: analytics/hypequery-deployment)')
115
116
  .option('-o, --output <path>', 'Output JSON file (default: analytics/hypequery-deployment.json)')
116
117
  .option('--runtime <runtime>', 'Runtime for non-portable handlers: node or python (default: node)')
117
- .option('--runtime-artifact <sha256>', 'SHA-256 identity of the built runtime artifact')
118
+ .option('--runtime-artifact <sha256>', 'Use a prebuilt runtime artifact with this SHA-256 identity')
119
+ .option('--runtime-file <path>', 'Include the bytes for a prebuilt runtime artifact')
120
+ .option('--runtime-output <path>', 'Bundled Node runtime path (default: beside deployment JSON)')
118
121
  .option('--entrypoint-prefix <prefix>', 'Runtime entrypoint prefix (default: queries)')
119
122
  .option('--hash-output <path>', 'Deployment identity sidecar path (default: <output>.sha256)')
120
123
  .action(runCommand(async (api, options) => {
@@ -122,7 +125,7 @@ program
122
125
  }));
123
126
  program
124
127
  .command('deployment:validate <artifact>')
125
- .description('Validate a deployment contract and report its identity')
128
+ .description('Verify a deployment bundle or validate a legacy deployment JSON file')
126
129
  .action(runCommand(async (artifact) => {
127
130
  await validateDeploymentCommand(artifact);
128
131
  }));
@@ -156,8 +159,8 @@ program.on('--help', () => {
156
159
  console.log(' hypequery generate:types --output analytics/schema.ts');
157
160
  console.log(' hypequery generate:datasets');
158
161
  console.log(' hypequery generate:manifest analytics/api.ts --output analytics/hypequery-manifest.json');
159
- console.log(' hypequery deployment:build analytics/api.ts --runtime-artifact <sha256>');
160
- console.log(' hypequery deployment:validate analytics/hypequery-deployment.json');
162
+ console.log(' hypequery deployment:build analytics/api.ts');
163
+ console.log(' hypequery deployment:validate analytics/hypequery-deployment');
161
164
  console.log('');
162
165
  console.log('Docs: https://hypequery.com/docs');
163
166
  });
@@ -1,8 +1,11 @@
1
1
  import { type ProtocolDeploymentContract } from '@hypequery/protocol';
2
2
  export interface BuildDeploymentOptions {
3
+ bundleOutput?: string;
3
4
  output?: string;
4
5
  runtime?: 'node' | 'python';
5
6
  runtimeArtifact?: string;
7
+ runtimeFile?: string;
8
+ runtimeOutput?: string;
6
9
  entrypointPrefix?: string;
7
10
  hashOutput?: string;
8
11
  }
@@ -1 +1 @@
1
- {"version":3,"file":"deployment.d.ts","sourceRoot":"","sources":["../../src/commands/deployment.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,0BAA0B,EAChC,MAAM,qBAAqB,CAAC;AAM7B,MAAM,WAAW,sBAAsB;IACrC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAC;IAC5B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AA8BD,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,0BAA0B,CAAC,CAuCrC;AAED,wBAAsB,yBAAyB,CAC7C,YAAY,EAAE,MAAM,GAAG,SAAS,GAC/B,OAAO,CAAC,0BAA0B,CAAC,CAoCrC"}
1
+ {"version":3,"file":"deployment.d.ts","sourceRoot":"","sources":["../../src/commands/deployment.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,0BAA0B,EAChC,MAAM,qBAAqB,CAAC;AAiB7B,MAAM,WAAW,sBAAsB;IACrC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAC;IAC5B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAoDD,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,0BAA0B,CAAC,CAwIrC;AAED,wBAAsB,yBAAyB,CAC7C,YAAY,EAAE,MAAM,GAAG,SAAS,GAC/B,OAAO,CAAC,0BAA0B,CAAC,CA2ErC"}
@@ -1,19 +1,26 @@
1
- import { mkdir, readFile, writeFile } from 'node:fs/promises';
1
+ import { createHash } from 'node:crypto';
2
+ import { mkdir, readFile, stat, writeFile } from 'node:fs/promises';
2
3
  import path from 'node:path';
3
4
  import { 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
7
  import { loadApiModule } from '../utils/load-api.js';
5
8
  import { logger } from '../utils/logger.js';
6
9
  const SHA256_PATTERN = /^[0-9a-f]{64}$/;
7
- function runtimeArtifact(options) {
10
+ const DEFAULT_BUNDLE_OUTPUT = 'analytics/hypequery-deployment';
11
+ function runtimeName(options) {
12
+ const runtime = options.runtime ?? 'node';
13
+ if (runtime !== 'node' && runtime !== 'python') {
14
+ throw new Error('--runtime must be either node or python.');
15
+ }
16
+ return runtime;
17
+ }
18
+ function configuredRuntimeArtifact(options, runtime) {
8
19
  if (options.runtimeArtifact === undefined)
9
20
  return undefined;
10
21
  if (!SHA256_PATTERN.test(options.runtimeArtifact)) {
11
22
  throw new Error('--runtime-artifact must be a lowercase 64-character SHA-256 digest.');
12
23
  }
13
- const runtime = options.runtime ?? 'node';
14
- if (runtime !== 'node' && runtime !== 'python') {
15
- throw new Error('--runtime must be either node or python.');
16
- }
17
24
  return {
18
25
  runtime,
19
26
  artifactSha256: options.runtimeArtifact,
@@ -22,23 +29,113 @@ function runtimeArtifact(options) {
22
29
  : {}),
23
30
  };
24
31
  }
32
+ function assertDistinctOutputPaths(paths) {
33
+ const seen = new Map();
34
+ for (const [label, value] of Object.entries(paths)) {
35
+ if (value === undefined)
36
+ continue;
37
+ const resolved = path.resolve(value);
38
+ const existing = seen.get(resolved);
39
+ if (existing) {
40
+ throw new Error(`${label} must use a different path from ${existing}.`);
41
+ }
42
+ seen.set(resolved, label);
43
+ }
44
+ }
25
45
  export async function buildDeploymentCommand(apiPath, options = {}) {
26
46
  if (!apiPath) {
27
47
  throw new Error('Missing API module path.\n\n'
28
48
  + 'Usage: hypequery deployment:build analytics/api.ts');
29
49
  }
30
- const artifact = runtimeArtifact(options);
50
+ const runtime = runtimeName(options);
51
+ const configuredArtifact = configuredRuntimeArtifact(options, runtime);
52
+ const legacyOutputRequested = options.output !== undefined
53
+ || options.hashOutput !== undefined
54
+ || options.runtimeOutput !== undefined;
55
+ if (options.bundleOutput !== undefined && legacyOutputRequested) {
56
+ throw new Error('--bundle-output cannot be combined with --output, --hash-output, or --runtime-output.');
57
+ }
58
+ const bundleOutput = options.bundleOutput
59
+ ?? (legacyOutputRequested ? undefined : DEFAULT_BUNDLE_OUTPUT);
60
+ if (options.runtimeFile !== undefined && configuredArtifact === undefined) {
61
+ throw new Error('--runtime-file requires --runtime-artifact.');
62
+ }
63
+ if (options.runtimeFile !== undefined && bundleOutput === undefined) {
64
+ throw new Error('--runtime-file is only supported when building a deployment bundle.');
65
+ }
66
+ if (configuredArtifact && bundleOutput !== undefined && options.runtimeFile === undefined) {
67
+ throw new Error('A complete deployment bundle requires --runtime-file with a prebuilt --runtime-artifact.');
68
+ }
69
+ if (configuredArtifact && options.runtimeOutput !== undefined) {
70
+ throw new Error('--runtime-output cannot be used with a prebuilt --runtime-artifact.');
71
+ }
72
+ const outputPath = options.output ?? 'analytics/hypequery-deployment.json';
73
+ const hashOutputPath = options.hashOutput ?? `${outputPath}.sha256`;
74
+ if (options.runtimeOutput !== undefined) {
75
+ assertDistinctOutputPaths({
76
+ '--output': outputPath,
77
+ '--hash-output': hashOutputPath,
78
+ '--runtime-output': options.runtimeOutput,
79
+ });
80
+ }
31
81
  const api = await loadApiModule(apiPath);
32
82
  if (typeof api.deploymentContract !== 'function') {
33
83
  throw new Error(`Invalid API module: ${apiPath}\n\n`
34
84
  + 'The exported API must provide deploymentContract(). '
35
85
  + 'Upgrade @hypequery/serve and export the value returned by createAPI() or serve().');
36
86
  }
87
+ let builtArtifact;
88
+ let artifact = configuredArtifact;
89
+ if (!artifact) {
90
+ const runtimeEntrypoints = getDeploymentRuntimeEntrypoints(api);
91
+ if (runtimeEntrypoints.length > 0) {
92
+ if (runtime !== 'node') {
93
+ throw new Error('Automatic runtime artifact builds currently support Node only. '
94
+ + 'Provide --runtime-artifact for Python deployments.');
95
+ }
96
+ builtArtifact = await buildNodeRuntimeArtifact(apiPath, runtimeEntrypoints, options.entrypointPrefix);
97
+ artifact = {
98
+ runtime: 'node',
99
+ artifactSha256: builtArtifact.artifactSha256,
100
+ entrypointPrefix: builtArtifact.entrypointPrefix,
101
+ };
102
+ }
103
+ }
37
104
  const contract = api.deploymentContract(artifact ? { runtimeArtifact: artifact } : {});
38
105
  const prepared = prepareProtocolDeploymentContract(contract);
39
106
  const { canonical, contract: validated, identity: digest } = prepared;
40
- const outputPath = options.output ?? 'analytics/hypequery-deployment.json';
41
- const hashOutputPath = options.hashOutput ?? `${outputPath}.sha256`;
107
+ if (bundleOutput !== undefined) {
108
+ const runtimeFiles = [];
109
+ if (builtArtifact) {
110
+ runtimeFiles.push({
111
+ runtime: 'node',
112
+ sha256: builtArtifact.artifactSha256,
113
+ bytes: builtArtifact.bytes,
114
+ });
115
+ }
116
+ else if (configuredArtifact && options.runtimeFile) {
117
+ const bytes = await readDeploymentRuntimeFile(options.runtimeFile);
118
+ const actualSha256 = createHash('sha256').update(bytes).digest('hex');
119
+ if (actualSha256 !== configuredArtifact.artifactSha256) {
120
+ throw new Error(`Prebuilt runtime artifact SHA-256 mismatch: ${options.runtimeFile}\n\n`
121
+ + `Expected: ${configuredArtifact.artifactSha256}\nActual: ${actualSha256}`);
122
+ }
123
+ runtimeFiles.push({ runtime, sha256: actualSha256, bytes });
124
+ }
125
+ const bundle = await writeDeploymentBundle(bundleOutput, prepared, runtimeFiles);
126
+ logger.success(`Deployment bundle written to ${bundle.directory}`);
127
+ logger.info(`Bundle identity: ${bundle.identity}`);
128
+ logger.info(`Deployment identity: ${digest}`);
129
+ return validated;
130
+ }
131
+ const runtimeOutputPath = builtArtifact
132
+ ? options.runtimeOutput ?? path.join(path.dirname(outputPath), 'hypequery-runtime.mjs')
133
+ : undefined;
134
+ assertDistinctOutputPaths({
135
+ '--output': outputPath,
136
+ '--hash-output': hashOutputPath,
137
+ '--runtime-output': runtimeOutputPath,
138
+ });
42
139
  const identitySidecar = [
43
140
  '# Hypequery deployment identity v1; not a file checksum or sha256sum input.',
44
141
  '# SHA-256(UTF-8("hypequery:deployment:v1") || 0x00 || RFC 8785 canonical bytes); '
@@ -48,8 +145,16 @@ export async function buildDeploymentCommand(apiPath, options = {}) {
48
145
  ].join('\n');
49
146
  await mkdir(path.dirname(outputPath), { recursive: true });
50
147
  await mkdir(path.dirname(hashOutputPath), { recursive: true });
148
+ if (runtimeOutputPath && builtArtifact) {
149
+ await mkdir(path.dirname(runtimeOutputPath), { recursive: true });
150
+ await writeFile(runtimeOutputPath, builtArtifact.bytes);
151
+ }
51
152
  await writeFile(outputPath, `${canonical}\n`, 'utf8');
52
153
  await writeFile(hashOutputPath, identitySidecar, 'utf8');
154
+ if (runtimeOutputPath && builtArtifact) {
155
+ logger.success(`Runtime artifact written to ${runtimeOutputPath}`);
156
+ logger.info(`Runtime artifact SHA-256: ${builtArtifact.artifactSha256}`);
157
+ }
53
158
  logger.success(`Deployment contract written to ${outputPath}`);
54
159
  logger.info(`Identity: ${digest}`);
55
160
  return validated;
@@ -57,7 +162,37 @@ export async function buildDeploymentCommand(apiPath, options = {}) {
57
162
  export async function validateDeploymentCommand(artifactPath) {
58
163
  if (!artifactPath) {
59
164
  throw new Error('Missing deployment artifact path.\n\n'
60
- + 'Usage: hypequery deployment:validate analytics/hypequery-deployment.json');
165
+ + 'Usage: hypequery deployment:validate analytics/hypequery-deployment');
166
+ }
167
+ let artifactStat;
168
+ try {
169
+ artifactStat = await stat(artifactPath);
170
+ }
171
+ catch (error) {
172
+ if (!(typeof error === 'object' && error !== null && 'code' in error
173
+ && error.code === 'ENOENT')) {
174
+ throw new Error(`Cannot inspect deployment input: ${artifactPath}\n\n`
175
+ + (error instanceof Error ? error.message : String(error)));
176
+ }
177
+ }
178
+ if (artifactStat?.isDirectory()) {
179
+ try {
180
+ const bundle = await verifyDeploymentBundle(artifactPath);
181
+ const contract = bundle.contract;
182
+ logger.success(`Valid deployment bundle: ${artifactPath}`);
183
+ logger.info(`${contract.datasets.length} datasets, ${contract.queries.length} queries, `
184
+ + `${contract.artifacts.length} runtime artifacts`);
185
+ logger.info(`Bundle identity: ${bundle.identity}`);
186
+ logger.info(`Deployment identity: ${bundle.manifest.deployment.identity}`);
187
+ return contract;
188
+ }
189
+ catch (error) {
190
+ throw new Error(`Invalid deployment bundle: ${artifactPath}\n\n`
191
+ + (error instanceof Error ? error.message : String(error)));
192
+ }
193
+ }
194
+ if (artifactStat && !artifactStat.isFile()) {
195
+ throw new Error(`Deployment input must be a regular JSON file or bundle directory: ${artifactPath}`);
61
196
  }
62
197
  let input;
63
198
  try {
@@ -0,0 +1,19 @@
1
+ import { type PreparedProtocolDeploymentContract, type ProtocolDeploymentBundleManifest, type ProtocolDeploymentContract } from '@hypequery/protocol';
2
+ export declare const DEPLOYMENT_BUNDLE_MANIFEST = "bundle.json";
3
+ export declare const DEPLOYMENT_BUNDLE_CONTRACT = "deployment.json";
4
+ export interface DeploymentBundleRuntimeFile {
5
+ readonly runtime: 'node' | 'python';
6
+ readonly sha256: string;
7
+ readonly bytes: Uint8Array;
8
+ }
9
+ export interface WrittenDeploymentBundle {
10
+ readonly directory: string;
11
+ readonly manifest: ProtocolDeploymentBundleManifest;
12
+ readonly identity: string;
13
+ readonly contract: ProtocolDeploymentContract;
14
+ }
15
+ export type VerifiedDeploymentBundle = WrittenDeploymentBundle;
16
+ export declare function writeDeploymentBundle(outputDirectory: string, prepared: PreparedProtocolDeploymentContract, runtimeFiles: readonly DeploymentBundleRuntimeFile[]): Promise<WrittenDeploymentBundle>;
17
+ export declare function readDeploymentRuntimeFile(filePath: string): Promise<Uint8Array>;
18
+ export declare function verifyDeploymentBundle(bundleDirectory: string): Promise<VerifiedDeploymentBundle>;
19
+ //# sourceMappingURL=deployment-bundle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deployment-bundle.d.ts","sourceRoot":"","sources":["../../src/utils/deployment-bundle.ts"],"names":[],"mappings":"AAaA,OAAO,EAIL,KAAK,kCAAkC,EAEvC,KAAK,gCAAgC,EACrC,KAAK,0BAA0B,EAChC,MAAM,qBAAqB,CAAC;AAG7B,eAAO,MAAM,0BAA0B,gBAAgB,CAAC;AACxD,eAAO,MAAM,0BAA0B,oBAAoB,CAAC;AAE5D,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,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;AAED,MAAM,MAAM,wBAAwB,GAAG,uBAAuB,CAAC;AA+F/D,wBAAsB,qBAAqB,CACzC,eAAe,EAAE,MAAM,EACvB,QAAQ,EAAE,kCAAkC,EAC5C,YAAY,EAAE,SAAS,2BAA2B,EAAE,GACnD,OAAO,CAAC,uBAAuB,CAAC,CA6ElC;AAgDD,wBAAsB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAMrF;AA+FD,wBAAsB,sBAAsB,CAC1C,eAAe,EAAE,MAAM,GACtB,OAAO,CAAC,wBAAwB,CAAC,CA+DnC"}
@@ -0,0 +1,322 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { constants, lstat, mkdir, mkdtemp, open, readdir, rename, rm, writeFile, } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { DEFAULT_PROTOCOL_DEPLOYMENT_BUNDLE_LIMITS, prepareProtocolDeploymentBundleManifest, prepareProtocolDeploymentContract, } from '@hypequery/protocol';
5
+ import { logger } from './logger.js';
6
+ export const DEPLOYMENT_BUNDLE_MANIFEST = 'bundle.json';
7
+ export const DEPLOYMENT_BUNDLE_CONTRACT = 'deployment.json';
8
+ const utf8Decoder = new TextDecoder('utf-8', { fatal: true });
9
+ const utf8Encoder = new TextEncoder();
10
+ function sha256(bytes) {
11
+ return createHash('sha256').update(bytes).digest('hex');
12
+ }
13
+ function runtimeArtifactPath(runtime, digest) {
14
+ return `artifacts/${digest}.${runtime === 'node' ? 'mjs' : 'pyz'}`;
15
+ }
16
+ function errorCode(error) {
17
+ return typeof error === 'object' && error !== null && 'code' in error
18
+ ? String(error.code)
19
+ : undefined;
20
+ }
21
+ async function existingVerifiedBundle(outputDirectory) {
22
+ try {
23
+ await lstat(outputDirectory);
24
+ }
25
+ catch (error) {
26
+ if (errorCode(error) === 'ENOENT')
27
+ return false;
28
+ throw error;
29
+ }
30
+ try {
31
+ await verifyDeploymentBundle(outputDirectory);
32
+ }
33
+ catch (error) {
34
+ throw new Error('Refusing to replace an existing path that is not a valid deployment bundle: '
35
+ + `${outputDirectory}\n\n`
36
+ + (error instanceof Error ? error.message : String(error)));
37
+ }
38
+ return true;
39
+ }
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
+ export async function writeDeploymentBundle(outputDirectory, prepared, runtimeFiles) {
88
+ const destination = path.resolve(outputDirectory);
89
+ if (destination === path.parse(destination).root) {
90
+ throw new Error('The deployment bundle output cannot be a filesystem root.');
91
+ }
92
+ const files = validateRuntimeFiles(prepared, runtimeFiles);
93
+ const deploymentBytes = utf8Encoder.encode(`${prepared.canonical}\n`);
94
+ const manifestInput = {
95
+ kind: 'hypequery-deployment-bundle',
96
+ version: 1,
97
+ deployment: {
98
+ path: DEPLOYMENT_BUNDLE_CONTRACT,
99
+ identity: prepared.identity,
100
+ sha256: sha256(deploymentBytes),
101
+ byteLength: deploymentBytes.byteLength,
102
+ },
103
+ artifacts: files.map(file => ({
104
+ runtime: file.runtime,
105
+ path: runtimeArtifactPath(file.runtime, file.sha256),
106
+ sha256: file.sha256,
107
+ byteLength: file.bytes.byteLength,
108
+ })),
109
+ };
110
+ const preparedManifest = prepareProtocolDeploymentBundleManifest(manifestInput);
111
+ const replaceExisting = await existingVerifiedBundle(destination);
112
+ const parent = path.dirname(destination);
113
+ await mkdir(parent, { recursive: true });
114
+ const staging = await mkdtemp(path.join(parent, `.${path.basename(destination)}.tmp-`));
115
+ try {
116
+ if (files.length > 0)
117
+ await mkdir(path.join(staging, 'artifacts'));
118
+ await writeFile(path.join(staging, DEPLOYMENT_BUNDLE_CONTRACT), deploymentBytes, { flag: 'wx' });
119
+ for (const file of files) {
120
+ await writeFile(path.join(staging, runtimeArtifactPath(file.runtime, file.sha256)), file.bytes, { flag: 'wx' });
121
+ }
122
+ await writeFile(path.join(staging, DEPLOYMENT_BUNDLE_MANIFEST), `${preparedManifest.canonical}\n`, { encoding: 'utf8', flag: 'wx' });
123
+ if (!replaceExisting) {
124
+ await rename(staging, destination);
125
+ }
126
+ else {
127
+ const backup = path.join(parent, `.${path.basename(destination)}.previous-${randomUUID()}`);
128
+ await rename(destination, backup);
129
+ try {
130
+ await rename(staging, destination);
131
+ }
132
+ catch (error) {
133
+ await rename(backup, destination);
134
+ throw error;
135
+ }
136
+ try {
137
+ await rm(backup, { recursive: true });
138
+ }
139
+ catch (error) {
140
+ logger.warn(`Deployment bundle was replaced, but its previous backup could not be removed: ${backup}`
141
+ + ` (${error instanceof Error ? error.message : String(error)})`);
142
+ }
143
+ }
144
+ }
145
+ catch (error) {
146
+ await rm(staging, { force: true, recursive: true });
147
+ throw error;
148
+ }
149
+ return Object.freeze({
150
+ directory: destination,
151
+ manifest: preparedManifest.manifest,
152
+ identity: preparedManifest.identity,
153
+ contract: prepared.contract,
154
+ });
155
+ }
156
+ async function readBoundedAbsoluteRegularFile(absolutePath, displayPath, maximum) {
157
+ let handle;
158
+ try {
159
+ const initialStat = await lstat(absolutePath);
160
+ if (initialStat.isSymbolicLink()) {
161
+ throw new Error(`Bundle entry must not be a symbolic link: ${displayPath}`);
162
+ }
163
+ if (!initialStat.isFile()) {
164
+ throw new Error(`Bundle entry is not a regular file: ${displayPath}`);
165
+ }
166
+ if (initialStat.size < 1 || initialStat.size > maximum) {
167
+ throw new Error(`Bundle entry exceeds its byte limit: ${displayPath}`);
168
+ }
169
+ handle = await open(absolutePath, constants.O_RDONLY | constants.O_NOFOLLOW);
170
+ const stat = await handle.stat();
171
+ if (!stat.isFile())
172
+ throw new Error(`Bundle entry is not a regular file: ${displayPath}`);
173
+ if (stat.size < 1 || stat.size > maximum) {
174
+ throw new Error(`Bundle entry exceeds its byte limit: ${displayPath}`);
175
+ }
176
+ return await handle.readFile();
177
+ }
178
+ catch (error) {
179
+ if (errorCode(error) === 'ELOOP') {
180
+ throw new Error(`Bundle entry must not be a symbolic link: ${displayPath}`);
181
+ }
182
+ throw error;
183
+ }
184
+ finally {
185
+ await handle?.close();
186
+ }
187
+ }
188
+ async function readBoundedRegularFile(root, relativePath, maximum) {
189
+ return readBoundedAbsoluteRegularFile(path.join(root, ...relativePath.split('/')), relativePath, maximum);
190
+ }
191
+ export async function readDeploymentRuntimeFile(filePath) {
192
+ return readBoundedAbsoluteRegularFile(path.resolve(filePath), filePath, DEFAULT_PROTOCOL_DEPLOYMENT_BUNDLE_LIMITS.maxArtifactBytes);
193
+ }
194
+ function expectedDirectories(files) {
195
+ const result = new Set();
196
+ for (const file of files) {
197
+ const segments = file.split('/');
198
+ for (let index = 1; index < segments.length; index += 1) {
199
+ result.add(segments.slice(0, index).join('/'));
200
+ }
201
+ }
202
+ return result;
203
+ }
204
+ async function verifyExactEntries(root, manifest) {
205
+ const expectedFiles = new Set([
206
+ DEPLOYMENT_BUNDLE_MANIFEST,
207
+ manifest.deployment.path,
208
+ ...manifest.artifacts.map(artifact => artifact.path),
209
+ ]);
210
+ const expectedDirectoryPaths = expectedDirectories([...expectedFiles]);
211
+ const seenFiles = new Set();
212
+ const seenDirectories = new Set();
213
+ async function visit(relativeDirectory) {
214
+ const absoluteDirectory = relativeDirectory
215
+ ? path.join(root, ...relativeDirectory.split('/'))
216
+ : root;
217
+ const names = await readdir(absoluteDirectory);
218
+ names.sort();
219
+ for (const name of names) {
220
+ const relativePath = relativeDirectory ? `${relativeDirectory}/${name}` : name;
221
+ const stat = await lstat(path.join(absoluteDirectory, name));
222
+ if (stat.isSymbolicLink()) {
223
+ throw new Error(`Bundle entries must not be symbolic links: ${relativePath}`);
224
+ }
225
+ if (stat.isDirectory()) {
226
+ if (!expectedDirectoryPaths.has(relativePath)) {
227
+ throw new Error(`Deployment bundle contains an undeclared directory: ${relativePath}`);
228
+ }
229
+ seenDirectories.add(relativePath);
230
+ await visit(relativePath);
231
+ }
232
+ else if (stat.isFile()) {
233
+ if (!expectedFiles.has(relativePath)) {
234
+ throw new Error(`Deployment bundle contains an undeclared file: ${relativePath}`);
235
+ }
236
+ seenFiles.add(relativePath);
237
+ }
238
+ else {
239
+ throw new Error(`Bundle entry is not a regular file or directory: ${relativePath}`);
240
+ }
241
+ }
242
+ }
243
+ await visit('');
244
+ const missingFiles = [...expectedFiles].filter(file => !seenFiles.has(file)).sort();
245
+ if (missingFiles.length > 0) {
246
+ throw new Error(`Deployment bundle is missing files: ${missingFiles.join(', ')}`);
247
+ }
248
+ const missingDirectories = [...expectedDirectoryPaths]
249
+ .filter(directory => !seenDirectories.has(directory))
250
+ .sort();
251
+ if (missingDirectories.length > 0) {
252
+ throw new Error(`Deployment bundle is missing directories: ${missingDirectories.join(', ')}`);
253
+ }
254
+ }
255
+ function verifyFile(bytes, file) {
256
+ if (bytes.byteLength !== file.byteLength) {
257
+ throw new Error(`Bundle entry byte length does not match the manifest: ${file.path}`);
258
+ }
259
+ if (sha256(bytes) !== file.sha256) {
260
+ throw new Error(`Bundle entry SHA-256 does not match the manifest: ${file.path}`);
261
+ }
262
+ }
263
+ function verifyRuntimeReferences(contract, artifacts) {
264
+ requireClosedContractArtifactSet(contract);
265
+ const declared = new Map(contract.artifacts.map(artifact => [artifact.artifactSha256, artifact.runtime]));
266
+ if (declared.size !== artifacts.length) {
267
+ throw new Error('Bundle runtime artifacts do not match the deployment contract.');
268
+ }
269
+ for (const artifact of artifacts) {
270
+ if (declared.get(artifact.sha256) !== artifact.runtime) {
271
+ throw new Error(`Bundle artifact ${artifact.sha256} does not match a deployment runtime reference.`);
272
+ }
273
+ }
274
+ }
275
+ export async function verifyDeploymentBundle(bundleDirectory) {
276
+ const root = path.resolve(bundleDirectory);
277
+ const rootStat = await lstat(root);
278
+ if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
279
+ throw new Error(`Deployment bundle path must be a regular directory: ${bundleDirectory}`);
280
+ }
281
+ const manifestBytes = await readBoundedRegularFile(root, DEPLOYMENT_BUNDLE_MANIFEST, 1024 * 1024);
282
+ let input;
283
+ try {
284
+ input = JSON.parse(utf8Decoder.decode(manifestBytes));
285
+ }
286
+ catch (error) {
287
+ throw new Error(`Invalid deployment bundle manifest: ${path.join(root, DEPLOYMENT_BUNDLE_MANIFEST)}\n\n`
288
+ + (error instanceof Error ? error.message : String(error)));
289
+ }
290
+ const preparedManifest = prepareProtocolDeploymentBundleManifest(input);
291
+ const expectedManifestBytes = utf8Encoder.encode(`${preparedManifest.canonical}\n`);
292
+ if (!Buffer.from(manifestBytes).equals(Buffer.from(expectedManifestBytes))) {
293
+ throw new Error('Deployment bundle manifest must contain canonical JSON followed by one newline.');
294
+ }
295
+ await verifyExactEntries(root, preparedManifest.manifest);
296
+ const deployment = preparedManifest.manifest.deployment;
297
+ const deploymentBytes = await readBoundedRegularFile(root, deployment.path, DEFAULT_PROTOCOL_DEPLOYMENT_BUNDLE_LIMITS.maxDeploymentBytes);
298
+ verifyFile(deploymentBytes, deployment);
299
+ let contractInput;
300
+ try {
301
+ contractInput = JSON.parse(utf8Decoder.decode(deploymentBytes));
302
+ }
303
+ catch (error) {
304
+ throw new Error(`Invalid deployment JSON in bundle: ${deployment.path}\n\n`
305
+ + (error instanceof Error ? error.message : String(error)));
306
+ }
307
+ const preparedContract = prepareProtocolDeploymentContract(contractInput);
308
+ if (preparedContract.identity !== deployment.identity) {
309
+ throw new Error('Deployment identity does not match the bundle manifest.');
310
+ }
311
+ for (const artifact of preparedManifest.manifest.artifacts) {
312
+ const bytes = await readBoundedRegularFile(root, artifact.path, DEFAULT_PROTOCOL_DEPLOYMENT_BUNDLE_LIMITS.maxArtifactBytes);
313
+ verifyFile(bytes, artifact);
314
+ }
315
+ verifyRuntimeReferences(preparedContract.contract, preparedManifest.manifest.artifacts);
316
+ return Object.freeze({
317
+ directory: root,
318
+ manifest: preparedManifest.manifest,
319
+ identity: preparedManifest.identity,
320
+ contract: preparedContract.contract,
321
+ });
322
+ }
@@ -0,0 +1,9 @@
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
@@ -0,0 +1 @@
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;AA6CD,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,MAAM,EACf,kBAAkB,EAAE,SAAS,MAAM,EAAE,EACrC,gBAAgB,SAAY,GAC3B,OAAO,CAAC,mBAAmB,CAAC,CAmD9B"}
@@ -0,0 +1,142 @@
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
+ ' const query = api.queries?.[name]?.query;',
66
+ ' if (typeof query === "function") return query;',
67
+ ' if (query && typeof query === "object" && typeof query.run === "function") {',
68
+ ' return query.run.bind(query);',
69
+ ' }',
70
+ ' throw new Error(`Runtime entrypoint ${name} is unavailable.`);',
71
+ '};',
72
+ `const runtimeEntries = Object.freeze(Object.fromEntries(${names}.map(name => [name, resolveQuery(name)])));`,
73
+ `export const ${prefix[0]} = ${registryExpression};`,
74
+ '',
75
+ ].join('\n');
76
+ }
77
+ export async function buildNodeRuntimeArtifact(apiPath, runtimeEntrypoints, entrypointPrefix = 'queries') {
78
+ if (runtimeEntrypoints.length === 0) {
79
+ throw new Error('Cannot build a runtime artifact without Serve query entrypoints.');
80
+ }
81
+ const names = getValidatedEntrypointNames(runtimeEntrypoints);
82
+ const { resolved, specifier } = projectRelativeImport(apiPath);
83
+ let source;
84
+ try {
85
+ source = runtimeEntrySource(specifier, names, entrypointPrefix);
86
+ }
87
+ catch {
88
+ throw new Error(`Invalid runtime entrypoint prefix: ${entrypointPrefix}`);
89
+ }
90
+ try {
91
+ const result = await build({
92
+ stdin: {
93
+ contents: source,
94
+ loader: 'ts',
95
+ resolveDir: process.cwd(),
96
+ sourcefile: 'hypequery-runtime-entry.ts',
97
+ },
98
+ absWorkingDir: process.cwd(),
99
+ bundle: true,
100
+ charset: 'utf8',
101
+ format: 'esm',
102
+ legalComments: 'none',
103
+ logLevel: 'warning',
104
+ minify: true,
105
+ platform: 'node',
106
+ sourcemap: false,
107
+ target: ['node18'],
108
+ treeShaking: true,
109
+ tsconfig: await findNearestTsconfig(resolved) ?? undefined,
110
+ write: false,
111
+ });
112
+ const output = result.outputFiles?.find(file => file.path.endsWith('.js'))
113
+ ?? result.outputFiles?.[0];
114
+ if (!output)
115
+ throw new Error('esbuild produced no output');
116
+ const bytes = output.contents;
117
+ return Object.freeze({
118
+ bytes,
119
+ artifactSha256: createHash('sha256').update(bytes).digest('hex'),
120
+ entrypointPrefix,
121
+ runtimeEntrypoints: names,
122
+ });
123
+ }
124
+ catch (error) {
125
+ throw new Error(`Failed to build the Node runtime artifact from ${apiPath}.\n`
126
+ + `Original error: ${error instanceof Error ? error.message : String(error)}`);
127
+ }
128
+ }
129
+ function getValidatedEntrypointNames(input) {
130
+ const names = input.map((name) => {
131
+ try {
132
+ return parseProtocolIdentifier(name);
133
+ }
134
+ catch {
135
+ throw new Error(`Invalid runtime entrypoint name: ${name}`);
136
+ }
137
+ });
138
+ if (new Set(names).size !== names.length) {
139
+ throw new Error('Runtime entrypoint names must be unique.');
140
+ }
141
+ return Object.freeze([...names].sort());
142
+ }
@@ -1,3 +1,4 @@
1
1
  export declare function loadApiModule(modulePath: string): Promise<any>;
2
2
  export declare function loadModule(modulePath: string): Promise<any>;
3
+ export declare function findNearestTsconfig(filePath: string): Promise<string | null>;
3
4
  //# sourceMappingURL=load-api.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"load-api.d.ts","sourceRoot":"","sources":["../../src/utils/load-api.ts"],"names":[],"mappings":"AAaA,wBAAsB,aAAa,CAAC,UAAU,EAAE,MAAM,gBA2DrD;AAED,wBAAsB,UAAU,CAAC,UAAU,EAAE,MAAM,gBAkClD"}
1
+ {"version":3,"file":"load-api.d.ts","sourceRoot":"","sources":["../../src/utils/load-api.ts"],"names":[],"mappings":"AAaA,wBAAsB,aAAa,CAAC,UAAU,EAAE,MAAM,gBA2DrD;AAED,wBAAsB,UAAU,CAAC,UAAU,EAAE,MAAM,gBAkClD;AA0JD,wBAAsB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,0BA8BzD"}
@@ -217,7 +217,7 @@ async function bundleTypeScriptModule(entryPath) {
217
217
  `Original error: ${error?.message ?? error}`);
218
218
  }
219
219
  }
220
- async function findNearestTsconfig(filePath) {
220
+ export async function findNearestTsconfig(filePath) {
221
221
  let dir = path.dirname(filePath);
222
222
  const visited = [];
223
223
  while (dir) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hypequery/cli",
3
- "version": "1.5.0",
3
+ "version": "1.7.0",
4
4
  "description": "Command-line interface for hypequery",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -19,7 +19,7 @@
19
19
  "open": "^10.0.0",
20
20
  "ora": "^8.0.1",
21
21
  "prompts": "^2.4.2",
22
- "@hypequery/protocol": "0.3.0"
22
+ "@hypequery/protocol": "0.4.0"
23
23
  },
24
24
  "peerDependencies": {
25
25
  "@hypequery/clickhouse": "*",
@@ -37,7 +37,7 @@
37
37
  "typescript": "^5.7.3",
38
38
  "vitest": "^3.2.6",
39
39
  "@hypequery/clickhouse": "2.4.0",
40
- "@hypequery/serve": "0.12.0"
40
+ "@hypequery/serve": "0.13.1"
41
41
  },
42
42
  "repository": {
43
43
  "type": "git",