@hypequery/cli 1.18.2 → 1.19.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.
Files changed (36) hide show
  1. package/dist/cli.d.ts.map +1 -1
  2. package/dist/cli.js +15 -0
  3. package/dist/commands/deploy.d.ts +1 -0
  4. package/dist/commands/deploy.d.ts.map +1 -1
  5. package/dist/commands/deploy.js +5 -1
  6. package/dist/commands/deployment.d.ts +1 -0
  7. package/dist/commands/deployment.d.ts.map +1 -1
  8. package/dist/commands/deployment.js +6 -1
  9. package/dist/commands/generate-datasets.d.ts +3 -0
  10. package/dist/commands/generate-datasets.d.ts.map +1 -1
  11. package/dist/commands/generate-datasets.js +60 -2
  12. package/dist/commands/mcp.d.ts +19 -0
  13. package/dist/commands/mcp.d.ts.map +1 -0
  14. package/dist/commands/mcp.js +94 -0
  15. package/dist/generators/dataset-generator.d.ts +3 -0
  16. package/dist/generators/dataset-generator.d.ts.map +1 -1
  17. package/dist/generators/dataset-generator.js +8 -5
  18. package/dist/utils/cloud-diagnostic.d.ts +10 -0
  19. package/dist/utils/cloud-diagnostic.d.ts.map +1 -0
  20. package/dist/utils/cloud-diagnostic.js +10 -0
  21. package/dist/utils/generated-file-diff.d.ts +7 -0
  22. package/dist/utils/generated-file-diff.d.ts.map +1 -0
  23. package/dist/utils/generated-file-diff.js +52 -0
  24. package/dist/utils/generated-file.d.ts +22 -0
  25. package/dist/utils/generated-file.d.ts.map +1 -0
  26. package/dist/utils/generated-file.js +100 -0
  27. package/dist/utils/mcp-console.d.ts +7 -0
  28. package/dist/utils/mcp-console.d.ts.map +1 -0
  29. package/dist/utils/mcp-console.js +20 -0
  30. package/dist/utils/mcp-lifecycle.d.ts +6 -0
  31. package/dist/utils/mcp-lifecycle.d.ts.map +1 -0
  32. package/dist/utils/mcp-lifecycle.js +22 -0
  33. package/dist/utils/mcp-source.d.ts +9 -0
  34. package/dist/utils/mcp-source.d.ts.map +1 -0
  35. package/dist/utils/mcp-source.js +28 -0
  36. package/package.json +11 -4
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;AAgCpC,QAAA,MAAM,OAAO,SAAgB,CAAC;AAa9B,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;EAKpE;AAgQD,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;AAqRD,OAAO,EAAE,OAAO,EAAE,CAAC"}
package/dist/cli.js CHANGED
@@ -2,6 +2,7 @@ import { readFileSync } from 'node:fs';
2
2
  import { fileURLToPath } from 'node:url';
3
3
  import { Command } from 'commander';
4
4
  import { initCommand } from './commands/init.js';
5
+ import { mcpCommand } from './commands/mcp.js';
5
6
  import { devCommand } from './commands/dev.js';
6
7
  import { generateCommand } from './commands/generate.js';
7
8
  import { generateDatasetsCommand } from './commands/generate-datasets.js';
@@ -126,6 +127,15 @@ program
126
127
  .action(runCommand(async (file, options) => {
127
128
  await devCommand(file, options);
128
129
  }));
130
+ program
131
+ .command('mcp [file]')
132
+ .description('Serve this project\'s datasets to an MCP client over stdio')
133
+ .option('--path <path>', 'Analytics directory (loads <path>/api.ts or <path>/queries.ts)')
134
+ .option('--tenant <id>', 'Trusted tenant applied to every tenant-scoped dataset')
135
+ .option('--self-test', 'Check the entrypoint and exit without speaking MCP')
136
+ .action(runCommand(async (file, options) => {
137
+ await mcpCommand(file, options);
138
+ }));
129
139
  // Generate command
130
140
  addTypeGenerationOptions(program.command('generate'))
131
141
  .action(runCommand(async (options) => {
@@ -143,6 +153,9 @@ program
143
153
  .option('--tables <names>', 'Only generate for specific tables (comma-separated)')
144
154
  .option('--exclude-tables <names>', 'Exclude specific tables (comma-separated)')
145
155
  .option('--tenant-column <column>', 'Set tenantKey to this column on tables that have it (requires a trusted runtime tenant scope)')
156
+ .option('--force', 'Replace an existing output file when generated contents differ')
157
+ .option('--check', 'Exit with an error when the output file is missing or out of date')
158
+ .option('--diff', 'Print generated changes without writing them')
146
159
  .action(runCommand(async (options) => {
147
160
  await generateDatasetsCommand(options);
148
161
  }));
@@ -165,6 +178,7 @@ program
165
178
  .option('--entrypoint-prefix <prefix>', 'Runtime entrypoint prefix (default: queries)')
166
179
  .option('--hash-output <path>', 'Deployment identity sidecar path (default: <output>.sha256)')
167
180
  .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')
168
182
  .action(runCommand(async (api, options) => {
169
183
  await buildDeploymentCommand(api, options);
170
184
  }));
@@ -203,6 +217,7 @@ program
203
217
  .option('--no-source', 'Exclude project source files from the deployment bundle')
204
218
  .option('--endpoint <url>', 'HTTPS submission endpoint; requires HYPEQUERY_API_TOKEN')
205
219
  .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')
206
221
  .action(runCommand(async (source, options) => {
207
222
  await deployCommand(source, options);
208
223
  }));
@@ -13,6 +13,7 @@ export interface DeployOptions extends SubmitDeploymentOptions {
13
13
  bundleOutput?: string;
14
14
  releaseOutput?: string;
15
15
  source?: boolean;
16
+ allowUnsupportedConfig?: boolean;
16
17
  }
17
18
  export interface SubmitDeploymentDependencies {
18
19
  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;CAClB;AAED,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,CAAC,GAAG,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;IAC5D,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,mCAAmC,CAAC;IACtE,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAC,CAAC;IACtE,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,mBAAmB,CAAC;CACjD;AAED,MAAM,WAAW,kBAAmB,SAAQ,4BAA4B;IACtE,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,sBAAsB,CAAC;IACzD,QAAQ,CAAC,wBAAwB,CAAC,EAAE,OAAO,+BAA+B,CAAC;IAC3E,QAAQ,CAAC,gBAAgB,CAAC,EAAE,OAAO,uBAAuB,CAAC;CAC5D;AA4DD,wBAAsB,uBAAuB,CAC3C,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,OAAO,GAAE,uBAA4B,EACrC,YAAY,GAAE,4BAAiC,GAC9C,OAAO,CAAC,4BAA4B,CAAC,CA0EvC;AAmDD,wBAAsB,aAAa,CACjC,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,OAAO,GAAE,aAAkB,EAC3B,YAAY,GAAE,kBAAuB,GACpC,OAAO,CAAC,4BAA4B,CAAC,CAqDvC"}
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"}
@@ -189,7 +189,11 @@ export async function deployCommand(sourcePath, options = {}, dependencies = {})
189
189
  // a missing or expired login only surfaces after a full bundle build.
190
190
  await resolveDeploymentCredential(options.endpoint, dependencies);
191
191
  try {
192
- await build(sourcePath, { bundleOutput: bundlePath, source: options.source });
192
+ await build(sourcePath, {
193
+ bundleOutput: bundlePath,
194
+ source: options.source,
195
+ allowUnsupportedConfig: options.allowUnsupportedConfig,
196
+ });
193
197
  }
194
198
  catch (error) {
195
199
  throw withRuntimeFlagHint(error);
@@ -10,6 +10,7 @@ export interface BuildDeploymentOptions {
10
10
  entrypointPrefix?: string;
11
11
  hashOutput?: string;
12
12
  source?: boolean;
13
+ allowUnsupportedConfig?: boolean;
13
14
  }
14
15
  export interface PrepareDeploymentReleaseOptions {
15
16
  project?: string;
@@ -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;AAe7B,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;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;AAoDD,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,0BAA0B,CAAC,CAsJrC;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":"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"}
@@ -7,6 +7,7 @@ import { buildNodeRuntimeArtifact, getDeploymentRuntimeEntrypoints, } from '../u
7
7
  import { captureDeploymentSourceSnapshot } from '../utils/deployment-source-snapshot.js';
8
8
  import { loadApiModule } from '../utils/load-api.js';
9
9
  import { logger } from '../utils/logger.js';
10
+ import { reportCloudDiagnostic } from '../utils/cloud-diagnostic.js';
10
11
  import { loadCloudCredential, } from '../utils/cloud-credential-store.js';
11
12
  const SHA256_PATTERN = /^[0-9a-f]{64}$/;
12
13
  const DEFAULT_BUNDLE_OUTPUT = 'analytics/hypequery-deployment';
@@ -103,7 +104,11 @@ export async function buildDeploymentCommand(apiPath, options = {}) {
103
104
  };
104
105
  }
105
106
  }
106
- const contract = api.deploymentContract(artifact ? { runtimeArtifact: artifact } : {});
107
+ const contract = api.deploymentContract({
108
+ ...(artifact ? { runtimeArtifact: artifact } : {}),
109
+ ...(options.allowUnsupportedConfig ? { allowUnsupportedConfig: true } : {}),
110
+ onCloudDiagnostic: diagnostic => reportCloudDiagnostic(diagnostic, options.allowUnsupportedConfig === true),
111
+ });
107
112
  const prepared = prepareProtocolDeploymentContract(contract);
108
113
  const { canonical, contract: validated, identity: digest } = prepared;
109
114
  if (bundleOutput !== undefined) {
@@ -10,6 +10,9 @@ export interface GenerateDatasetsOptions {
10
10
  tables?: string;
11
11
  excludeTables?: string;
12
12
  tenantColumn?: string;
13
+ force?: boolean;
14
+ check?: boolean;
15
+ diff?: boolean;
13
16
  }
14
17
  export declare function generateDatasetsCommand(options?: GenerateDatasetsOptions): Promise<void>;
15
18
  //# sourceMappingURL=generate-datasets.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"generate-datasets.d.ts","sourceRoot":"","sources":["../../src/commands/generate-datasets.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAUH,MAAM,WAAW,uBAAuB;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAqBD,wBAAsB,uBAAuB,CAAC,OAAO,GAAE,uBAA4B,iBA8GlF"}
1
+ {"version":3,"file":"generate-datasets.d.ts","sourceRoot":"","sources":["../../src/commands/generate-datasets.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAYH,MAAM,WAAW,uBAAuB;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AA2BD,wBAAsB,uBAAuB,CAAC,OAAO,GAAE,uBAA4B,iBAqKlF"}
@@ -11,6 +11,8 @@ import { getTableCount } from '../utils/detect-database.js';
11
11
  import { generateDatasets } from '../generators/dataset-generator.js';
12
12
  import { redactConnectionUrl } from '../utils/redact-connection-url.js';
13
13
  import { logDatasetGenerationWarnings } from '../utils/dataset-generation-warnings.js';
14
+ import { formatGeneratedFileDiff } from '../utils/generated-file-diff.js';
15
+ import { readGeneratedFile, writeGeneratedFileAtomically } from '../utils/generated-file.js';
14
16
  /**
15
17
  * Turns the written path into something pasteable into an import statement:
16
18
  * strips the `.ts` extension and makes it explicitly relative.
@@ -20,6 +22,11 @@ function toImportSpecifier(relativePath) {
20
22
  const normalized = withoutExtension.split(path.sep).join('/');
21
23
  return normalized.startsWith('.') ? normalized : `./${normalized}`;
22
24
  }
25
+ function refuseOverwrite(relativeOutput) {
26
+ logger.warn(`Refusing to overwrite existing dataset definitions: ${relativeOutput}`);
27
+ logger.info('Run again with --diff to inspect changes or --force to replace the file.');
28
+ process.exitCode = 1;
29
+ }
23
30
  function parseTableList(value) {
24
31
  const parsed = value
25
32
  ?.split(',')
@@ -43,6 +50,11 @@ export async function generateDatasetsCommand(options = {}) {
43
50
  }
44
51
  const parsedTables = parseTableList(options.tables);
45
52
  const excludedTables = parseTableList(options.excludeTables);
53
+ if (options.force && (options.check || options.diff)) {
54
+ logger.error('--force cannot be combined with --check or --diff.');
55
+ process.exitCode = 1;
56
+ return;
57
+ }
46
58
  logger.newline();
47
59
  logger.header('hypequery generate datasets');
48
60
  const spinner = ora('Connecting to ClickHouse...').start();
@@ -62,13 +74,59 @@ export async function generateDatasetsCommand(options = {}) {
62
74
  // Regeneration replaces the whole file, so a tenant boundary configured at
63
75
  // init has to be restated here or it is dropped on every refresh.
64
76
  tenantColumn: options.tenantColumn,
77
+ writeOutput: false,
65
78
  });
66
79
  const generatedTables = generated?.tables ?? parsedTables ?? [];
67
80
  const generatedCount = generatedTables.length || tableCount;
68
81
  datasetSpinner.succeed(`Generated dataset definitions for ${generatedCount} ${generatedCount === 1 ? 'table' : 'tables'}`);
69
- const relativeOutput = path.relative(process.cwd(), outputPath);
70
- logger.success(`Created ${relativeOutput}`);
71
82
  logDatasetGenerationWarnings(generated?.warnings);
83
+ const relativeOutput = path.relative(process.cwd(), outputPath);
84
+ const currentContents = await readGeneratedFile(outputPath);
85
+ const contentsMatch = currentContents === generated.contents;
86
+ if (contentsMatch) {
87
+ logger.success(options.check || options.diff
88
+ ? `Dataset definitions are up to date: ${relativeOutput}`
89
+ : `Unchanged ${relativeOutput}`);
90
+ if (options.check || options.diff) {
91
+ return;
92
+ }
93
+ }
94
+ else if (options.diff) {
95
+ logger.raw(formatGeneratedFileDiff(currentContents ?? '', generated.contents, relativeOutput));
96
+ logger.error(`Generated dataset definitions differ: ${relativeOutput}`);
97
+ process.exitCode = 1;
98
+ return;
99
+ }
100
+ else if (options.check) {
101
+ logger.error(currentContents === undefined
102
+ ? `Dataset definitions are missing: ${relativeOutput}`
103
+ : `Generated dataset definitions are out of date: ${relativeOutput}`);
104
+ process.exitCode = 1;
105
+ return;
106
+ }
107
+ else if (currentContents !== undefined && !options.force) {
108
+ refuseOverwrite(relativeOutput);
109
+ return;
110
+ }
111
+ else {
112
+ try {
113
+ // Without --force this is an exclusive create, so a file written
114
+ // between the read above and here is refused rather than clobbered.
115
+ await writeGeneratedFileAtomically(outputPath, generated.contents, {
116
+ overwrite: options.force === true,
117
+ });
118
+ }
119
+ catch (error) {
120
+ if (error.code === 'EEXIST') {
121
+ refuseOverwrite(relativeOutput);
122
+ return;
123
+ }
124
+ throw error;
125
+ }
126
+ logger.success(currentContents === undefined
127
+ ? `Created ${relativeOutput}`
128
+ : `Updated ${relativeOutput}`);
129
+ }
72
130
  logger.newline();
73
131
  logger.header('Next steps:');
74
132
  logger.indent('1. Review and customize the generated datasets');
@@ -0,0 +1,19 @@
1
+ import { type CloseableMcpServer } from '../utils/mcp-lifecycle.js';
2
+ export interface McpOptions {
3
+ /** Analytics directory, matching `hypequery dev --path`. */
4
+ path?: string;
5
+ /** Trusted tenant applied to every tenant-scoped dataset. */
6
+ tenant?: string;
7
+ /** Check the entrypoint and exit instead of speaking MCP over stdio. */
8
+ selfTest?: boolean;
9
+ }
10
+ export interface McpDependencies {
11
+ loadApi?: (file: string) => Promise<unknown>;
12
+ start?: (config: {
13
+ datasets: Record<string, unknown>;
14
+ analytics: unknown;
15
+ tenantId?: string;
16
+ }) => Promise<CloseableMcpServer>;
17
+ }
18
+ export declare function mcpCommand(file?: string, options?: McpOptions, dependencies?: McpDependencies): Promise<void>;
19
+ //# sourceMappingURL=mcp.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../../src/commands/mcp.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,KAAK,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAQvF,MAAM,WAAW,UAAU;IACzB,4DAA4D;IAC5D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,6DAA6D;IAC7D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wEAAwE;IACxE,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC7C,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE;QACf,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAClC,SAAS,EAAE,OAAO,CAAC;QACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,KAAK,OAAO,CAAC,kBAAkB,CAAC,CAAC;CACnC;AAyCD,wBAAsB,UAAU,CAC9B,IAAI,CAAC,EAAE,MAAM,EACb,OAAO,GAAE,UAAe,EACxB,YAAY,GAAE,eAAoB,GACjC,OAAO,CAAC,IAAI,CAAC,CAgDf"}
@@ -0,0 +1,94 @@
1
+ import { runMcpUntilSignal } from '../utils/mcp-lifecycle.js';
2
+ import { routeConsoleOutputToStderr } from '../utils/mcp-console.js';
3
+ import { readServeMcpSource, tenantScopedDatasets } from '../utils/mcp-source.js';
4
+ import path from 'node:path';
5
+ import { findApiFileForPath, findQueriesFile } from '../utils/find-files.js';
6
+ import { loadApiModule } from '../utils/load-api.js';
7
+ import { logger } from '../utils/logger.js';
8
+ function entrypointNotFound() {
9
+ logger.error('Could not find a hypequery API file to serve over MCP');
10
+ logger.newline();
11
+ logger.info('Expected one of:');
12
+ for (const candidate of [
13
+ 'hypequery.ts', 'analytics/api.ts', 'src/analytics/api.ts',
14
+ 'api.ts', 'src/api.ts', 'analytics/queries.ts', 'src/analytics/queries.ts',
15
+ ]) {
16
+ logger.indent(`• ${candidate}`);
17
+ }
18
+ logger.newline();
19
+ logger.info('Or specify the file explicitly:');
20
+ logger.indent('hypequery mcp ./path/to/api.ts');
21
+ logger.newline();
22
+ process.exit(1);
23
+ }
24
+ function noDatasets(file) {
25
+ logger.error(`${path.relative(process.cwd(), file)} does not register any datasets`);
26
+ logger.newline();
27
+ logger.info('MCP exposes datasets and named metrics, so add them to defineServe:');
28
+ logger.indent('defineServe({ queryBuilder: db, datasets: { orders: Orders } })');
29
+ logger.newline();
30
+ process.exit(1);
31
+ }
32
+ async function resolveEntrypoint(file, options) {
33
+ const resolved = file
34
+ ? await findQueriesFile(file)
35
+ : options.path
36
+ ? await findApiFileForPath(options.path)
37
+ : await findQueriesFile();
38
+ return resolved ?? entrypointNotFound();
39
+ }
40
+ function resolveSource(api, file) {
41
+ return readServeMcpSource(api) ?? noDatasets(file);
42
+ }
43
+ export async function mcpCommand(file, options = {}, dependencies = {}) {
44
+ const restoreConsole = routeConsoleOutputToStderr();
45
+ try {
46
+ const entrypoint = await resolveEntrypoint(file, options);
47
+ const loadApi = dependencies.loadApi ?? loadApiModule;
48
+ const source = resolveSource(await loadApi(entrypoint), entrypoint);
49
+ const datasets = source.datasets;
50
+ const names = Object.keys(datasets).sort();
51
+ if (names.length === 0)
52
+ noDatasets(entrypoint);
53
+ const scoped = tenantScopedDatasets(datasets);
54
+ if (scoped.length > 0 && !options.tenant) {
55
+ // Fail closed rather than serving a tenant-scoped dataset unscoped.
56
+ logger.error(`--tenant is required for tenant-scoped datasets: ${scoped.join(', ')}`);
57
+ logger.newline();
58
+ logger.info('MCP has no request to resolve a tenant from, so it must be given one:');
59
+ logger.indent('hypequery mcp --tenant acme');
60
+ logger.newline();
61
+ process.exit(1);
62
+ }
63
+ const analytics = source.resolveAnalytics();
64
+ if (options.selfTest) {
65
+ logger.success(`Loaded ${path.relative(process.cwd(), entrypoint)}`);
66
+ logger.info(`Datasets: ${names.join(', ')}`);
67
+ logger.info(options.tenant
68
+ ? `Trusted tenant: ${options.tenant}`
69
+ : 'Trusted tenant: none (no dataset is tenant-scoped)');
70
+ logger.newline();
71
+ logger.info('The entrypoint is ready to serve over MCP.');
72
+ return;
73
+ }
74
+ const start = dependencies.start ?? defaultStart;
75
+ await runMcpUntilSignal(async () => {
76
+ const server = await start({
77
+ datasets,
78
+ analytics,
79
+ ...(options.tenant ? { tenantId: options.tenant } : {}),
80
+ });
81
+ process.stderr.write(`hypequery MCP serving ${names.length} dataset(s): ${names.join(', ')}\n`);
82
+ return server;
83
+ });
84
+ }
85
+ finally {
86
+ restoreConsole();
87
+ }
88
+ }
89
+ async function defaultStart(config) {
90
+ // Imported lazily so `--self-test` and the argument errors above do not pay
91
+ // for the MCP SDK, and so the CLI still loads when it is not installed.
92
+ const { startStdioMCPServer } = await import('@hypequery/mcp');
93
+ return startStdioMCPServer(config);
94
+ }
@@ -19,6 +19,8 @@ export interface DatasetGeneratorOptions {
19
19
  * being silently left unscoped.
20
20
  */
21
21
  tenantColumn?: string;
22
+ /** Whether to write the output file. Defaults to true; false renders only. */
23
+ writeOutput?: boolean;
22
24
  }
23
25
  export type DatasetGenerationWarning = {
24
26
  kind: 'tenant-key-candidate';
@@ -41,5 +43,6 @@ export declare function generateDatasets(options: DatasetGeneratorOptions): Prom
41
43
  exportName: string;
42
44
  }[];
43
45
  warnings: DatasetGenerationWarning[];
46
+ contents: string;
44
47
  }>;
45
48
  //# sourceMappingURL=dataset-generator.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dataset-generator.d.ts","sourceRoot":"","sources":["../../src/generators/dataset-generator.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EAAE,8BAA8B,EAAE,MAAM,qBAAqB,CAAC;AAG1E,MAAM,WAAW,uBAAuB;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,MAAM,CAAC,EAAE,8BAA8B,CAAC;IACxC;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,MAAM,wBAAwB,GAChC;IACE,IAAI,EAAE,sBAAsB,CAAC;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CACjB,GACD;IACE,IAAI,EAAE,uBAAuB,CAAC;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAkSN;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,uBAAuB;;;;;;;GAsEtE"}
1
+ {"version":3,"file":"dataset-generator.d.ts","sourceRoot":"","sources":["../../src/generators/dataset-generator.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EAAE,8BAA8B,EAAE,MAAM,qBAAqB,CAAC;AAG1E,MAAM,WAAW,uBAAuB;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,MAAM,CAAC,EAAE,8BAA8B,CAAC;IACxC;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,8EAA8E;IAC9E,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,MAAM,wBAAwB,GAChC;IACE,IAAI,EAAE,sBAAsB,CAAC;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CACjB,GACD;IACE,IAAI,EAAE,uBAAuB,CAAC;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAkSN;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,uBAAuB;;;;;;;;GAyEtE"}
@@ -277,11 +277,13 @@ ${tables.map((table) => ` ${table.name}: ${tableToPascalCase(table.name)}Datase
277
277
  };
278
278
  `;
279
279
  const output = header + datasetDefinitions.join('\n') + footer;
280
- // Ensure output directory exists
281
- const outputDir = path.dirname(path.resolve(options.outputPath));
282
- await fs.mkdir(outputDir, { recursive: true });
283
- // Write the file
284
- await fs.writeFile(path.resolve(options.outputPath), output);
280
+ if (options.writeOutput !== false) {
281
+ // Ensure output directory exists
282
+ const outputDir = path.dirname(path.resolve(options.outputPath));
283
+ await fs.mkdir(outputDir, { recursive: true });
284
+ // Write the file
285
+ await fs.writeFile(path.resolve(options.outputPath), output);
286
+ }
285
287
  // Returned so the CLI can report what it actually generated, and print an
286
288
  // example that matches the file rather than a hardcoded placeholder.
287
289
  return {
@@ -291,5 +293,6 @@ ${tables.map((table) => ` ${table.name}: ${tableToPascalCase(table.name)}Datase
291
293
  exportName: `${tableToPascalCase(table.name)}Dataset`,
292
294
  })),
293
295
  warnings,
296
+ contents: output,
294
297
  };
295
298
  }
@@ -0,0 +1,10 @@
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
@@ -0,0 +1 @@
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"}
@@ -0,0 +1,10 @@
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
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Formats one unified hunk spanning the changed region, with nearby context.
3
+ * Generated files are replaced as a unit, so a compact single hunk is easier
4
+ * to review than treating every regenerated line as an unrelated edit.
5
+ */
6
+ export declare function formatGeneratedFileDiff(currentContents: string, generatedContents: string, displayPath: string): string;
7
+ //# sourceMappingURL=generated-file-diff.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generated-file-diff.d.ts","sourceRoot":"","sources":["../../src/utils/generated-file-diff.ts"],"names":[],"mappings":"AAQA;;;;GAIG;AACH,wBAAgB,uBAAuB,CACrC,eAAe,EAAE,MAAM,EACvB,iBAAiB,EAAE,MAAM,EACzB,WAAW,EAAE,MAAM,GAClB,MAAM,CA+CR"}
@@ -0,0 +1,52 @@
1
+ function splitLines(contents) {
2
+ if (contents.length === 0) {
3
+ return [];
4
+ }
5
+ // Keep a trailing empty line so newline-only changes remain visible.
6
+ return contents.split('\n');
7
+ }
8
+ /**
9
+ * Formats one unified hunk spanning the changed region, with nearby context.
10
+ * Generated files are replaced as a unit, so a compact single hunk is easier
11
+ * to review than treating every regenerated line as an unrelated edit.
12
+ */
13
+ export function formatGeneratedFileDiff(currentContents, generatedContents, displayPath) {
14
+ if (currentContents === generatedContents) {
15
+ return '';
16
+ }
17
+ const currentLines = splitLines(currentContents);
18
+ const generatedLines = splitLines(generatedContents);
19
+ let sharedPrefix = 0;
20
+ while (sharedPrefix < currentLines.length
21
+ && sharedPrefix < generatedLines.length
22
+ && currentLines[sharedPrefix] === generatedLines[sharedPrefix]) {
23
+ sharedPrefix += 1;
24
+ }
25
+ let sharedSuffix = 0;
26
+ while (sharedSuffix < currentLines.length - sharedPrefix
27
+ && sharedSuffix < generatedLines.length - sharedPrefix
28
+ && currentLines[currentLines.length - sharedSuffix - 1]
29
+ === generatedLines[generatedLines.length - sharedSuffix - 1]) {
30
+ sharedSuffix += 1;
31
+ }
32
+ const contextLines = 3;
33
+ const currentStart = Math.max(0, sharedPrefix - contextLines);
34
+ const generatedStart = Math.max(0, sharedPrefix - contextLines);
35
+ const currentChangedEnd = currentLines.length - sharedSuffix;
36
+ const generatedChangedEnd = generatedLines.length - sharedSuffix;
37
+ const currentEnd = Math.min(currentLines.length, currentChangedEnd + contextLines);
38
+ const generatedEnd = Math.min(generatedLines.length, generatedChangedEnd + contextLines);
39
+ const prefixContext = currentLines.slice(currentStart, sharedPrefix);
40
+ const suffixContext = currentLines.slice(currentChangedEnd, currentEnd);
41
+ const removedLines = currentLines.slice(sharedPrefix, currentChangedEnd);
42
+ const addedLines = generatedLines.slice(sharedPrefix, generatedChangedEnd);
43
+ return [
44
+ `--- ${displayPath}`,
45
+ `+++ ${displayPath} (generated)`,
46
+ `@@ -${currentStart + 1},${currentEnd - currentStart} +${generatedStart + 1},${generatedEnd - generatedStart} @@`,
47
+ ...prefixContext.map((line) => ` ${line}`),
48
+ ...removedLines.map((line) => `-${line}`),
49
+ ...addedLines.map((line) => `+${line}`),
50
+ ...suffixContext.map((line) => ` ${line}`),
51
+ ].join('\n');
52
+ }
@@ -0,0 +1,22 @@
1
+ export interface WriteGeneratedFileOptions {
2
+ /**
3
+ * Whether an existing destination may be replaced.
4
+ *
5
+ * Defaults to `false`, which makes the write an exclusive create: the
6
+ * existence check and the write are a single filesystem operation, so a file
7
+ * that appeared since the caller last looked raises
8
+ * {@link GeneratedFileExistsError} instead of being silently clobbered.
9
+ */
10
+ overwrite?: boolean;
11
+ }
12
+ /**
13
+ * Thrown when a non-overwriting write finds the destination already present.
14
+ */
15
+ export declare class GeneratedFileExistsError extends Error {
16
+ readonly filePath: string;
17
+ readonly code = "EEXIST";
18
+ constructor(filePath: string);
19
+ }
20
+ export declare function readGeneratedFile(filePath: string): Promise<string | undefined>;
21
+ export declare function writeGeneratedFileAtomically(filePath: string, contents: string, options?: WriteGeneratedFileOptions): Promise<void>;
22
+ //# sourceMappingURL=generated-file.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generated-file.d.ts","sourceRoot":"","sources":["../../src/utils/generated-file.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,yBAAyB;IACxC;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED;;GAEG;AACH,qBAAa,wBAAyB,SAAQ,KAAK;IAGrC,QAAQ,CAAC,QAAQ,EAAE,MAAM;IAFrC,QAAQ,CAAC,IAAI,YAAY;gBAEJ,QAAQ,EAAE,MAAM;CAItC;AAED,wBAAsB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CASrF;AAgCD,wBAAsB,4BAA4B,CAChD,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,yBAA8B,GACtC,OAAO,CAAC,IAAI,CAAC,CA4Df"}
@@ -0,0 +1,100 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { chmod, mkdir, readFile, rename, stat, unlink, writeFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ /**
5
+ * Thrown when a non-overwriting write finds the destination already present.
6
+ */
7
+ export class GeneratedFileExistsError extends Error {
8
+ filePath;
9
+ code = 'EEXIST';
10
+ constructor(filePath) {
11
+ super(`Refusing to overwrite existing file: ${filePath}`);
12
+ this.filePath = filePath;
13
+ this.name = 'GeneratedFileExistsError';
14
+ }
15
+ }
16
+ export async function readGeneratedFile(filePath) {
17
+ try {
18
+ return await readFile(filePath, 'utf8');
19
+ }
20
+ catch (error) {
21
+ if (error.code === 'ENOENT') {
22
+ return undefined;
23
+ }
24
+ throw error;
25
+ }
26
+ }
27
+ /**
28
+ * Tightest of the permission bits observed on a destination, ignoring the
29
+ * observations where it did not exist.
30
+ *
31
+ * A `chmod` that lands between two observations must not be undone by the
32
+ * earlier snapshot, and the safe direction is always the more restrictive one:
33
+ * the replacement is never more permissive than anything the destination was
34
+ * seen to be.
35
+ */
36
+ function mostRestrictiveMode(...modes) {
37
+ const observed = modes.filter((mode) => mode !== undefined);
38
+ return observed.length > 0 ? observed.reduce((tightest, mode) => tightest & mode) : undefined;
39
+ }
40
+ /**
41
+ * Permission bits of an existing file, or undefined when it does not exist.
42
+ */
43
+ async function readExistingMode(filePath) {
44
+ try {
45
+ const stats = await stat(filePath);
46
+ return stats.mode & 0o777;
47
+ }
48
+ catch (error) {
49
+ if (error.code === 'ENOENT') {
50
+ return undefined;
51
+ }
52
+ throw error;
53
+ }
54
+ }
55
+ export async function writeGeneratedFileAtomically(filePath, contents, options = {}) {
56
+ const resolvedPath = path.resolve(filePath);
57
+ const outputDirectory = path.dirname(resolvedPath);
58
+ await mkdir(outputDirectory, { recursive: true });
59
+ if (!options.overwrite) {
60
+ try {
61
+ // O_EXCL: the destination is created only if it is still absent, so a
62
+ // concurrent create between the caller's check and this write is never
63
+ // lost.
64
+ await writeFile(resolvedPath, contents, { flag: 'wx' });
65
+ return;
66
+ }
67
+ catch (error) {
68
+ if (error.code === 'EEXIST') {
69
+ throw new GeneratedFileExistsError(resolvedPath);
70
+ }
71
+ throw error;
72
+ }
73
+ }
74
+ const temporaryPath = path.join(outputDirectory, `.${path.basename(resolvedPath)}.${process.pid}.${randomUUID()}.tmp`);
75
+ // Read the destination's mode first: the temporary file must never be created
76
+ // more permissively than the file it replaces, or its contents are briefly
77
+ // exposed to other local users in a shared directory.
78
+ const existingMode = await readExistingMode(resolvedPath);
79
+ try {
80
+ await writeFile(temporaryPath, contents, existingMode !== undefined ? { mode: existingMode } : {});
81
+ // The destination can be chmod'ed — or created — while the contents above
82
+ // are being written. Renaming with only the pre-write snapshot would undo
83
+ // that, replacing a file just tightened to 0600 with a 0644 one, so the
84
+ // tighter of the two observations wins.
85
+ const destinationMode = mostRestrictiveMode(existingMode, await readExistingMode(resolvedPath));
86
+ // open() also applies the umask, and rename() swaps in a new inode, so the
87
+ // destination's exact mode has to be restored explicitly or a customized
88
+ // mode is silently reset. A chmod landing after this read still races the
89
+ // rename — rename() cannot be made conditional on the destination's mode —
90
+ // but the window is now two syscalls rather than the whole write.
91
+ if (destinationMode !== undefined) {
92
+ await chmod(temporaryPath, destinationMode);
93
+ }
94
+ await rename(temporaryPath, resolvedPath);
95
+ }
96
+ catch (error) {
97
+ await unlink(temporaryPath).catch(() => undefined);
98
+ throw error;
99
+ }
100
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * MCP speaks its protocol over stdout, so anything an application logs while
3
+ * loading would corrupt the stream. Route it to stderr before importing the
4
+ * entrypoint, not after.
5
+ */
6
+ export declare function routeConsoleOutputToStderr(): () => void;
7
+ //# sourceMappingURL=mcp-console.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp-console.d.ts","sourceRoot":"","sources":["../../src/utils/mcp-console.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,wBAAgB,0BAA0B,IAAI,MAAM,IAAI,CAavD"}
@@ -0,0 +1,20 @@
1
+ import { format } from 'node:util';
2
+ /**
3
+ * MCP speaks its protocol over stdout, so anything an application logs while
4
+ * loading would corrupt the stream. Route it to stderr before importing the
5
+ * entrypoint, not after.
6
+ */
7
+ export function routeConsoleOutputToStderr() {
8
+ const original = {
9
+ log: console.log,
10
+ info: console.info,
11
+ debug: console.debug,
12
+ };
13
+ const write = (...args) => {
14
+ process.stderr.write(`${format(...args)}\n`);
15
+ };
16
+ console.log = write;
17
+ console.info = write;
18
+ console.debug = write;
19
+ return () => Object.assign(console, original);
20
+ }
@@ -0,0 +1,6 @@
1
+ export interface CloseableMcpServer {
2
+ close(): Promise<void>;
3
+ }
4
+ /** Own signal listeners for exactly the lifetime of the stdio server. */
5
+ export declare function runMcpUntilSignal(start: () => Promise<CloseableMcpServer>): Promise<void>;
6
+ //# sourceMappingURL=mcp-lifecycle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp-lifecycle.d.ts","sourceRoot":"","sources":["../../src/utils/mcp-lifecycle.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,kBAAkB;IACjC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,yEAAyE;AACzE,wBAAsB,iBAAiB,CAAC,KAAK,EAAE,MAAM,OAAO,CAAC,kBAAkB,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAkB/F"}
@@ -0,0 +1,22 @@
1
+ /** Own signal listeners for exactly the lifetime of the stdio server. */
2
+ export async function runMcpUntilSignal(start) {
3
+ let stop;
4
+ const stopped = new Promise(resolve => { stop = resolve; });
5
+ // Install before awaiting startup so a signal during connection is retained.
6
+ process.on('SIGINT', stop);
7
+ process.on('SIGTERM', stop);
8
+ let server;
9
+ try {
10
+ server = await start();
11
+ await stopped;
12
+ }
13
+ finally {
14
+ try {
15
+ await server?.close();
16
+ }
17
+ finally {
18
+ process.removeListener('SIGINT', stop);
19
+ process.removeListener('SIGTERM', stop);
20
+ }
21
+ }
22
+ }
@@ -0,0 +1,9 @@
1
+ export interface ServeMcpSource {
2
+ readonly version: 1;
3
+ readonly datasets: Readonly<Record<string, unknown>>;
4
+ readonly resolveAnalytics: () => unknown;
5
+ }
6
+ export declare function readServeMcpSource(value: unknown): ServeMcpSource | undefined;
7
+ /** Datasets a caller cannot query unless a trusted tenant is configured. */
8
+ export declare function tenantScopedDatasets(datasets: Record<string, unknown>): string[];
9
+ //# sourceMappingURL=mcp-source.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp-source.d.ts","sourceRoot":"","sources":["../../src/utils/mcp-source.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC;IACpB,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACrD,QAAQ,CAAC,gBAAgB,EAAE,MAAM,OAAO,CAAC;CAC1C;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,cAAc,GAAG,SAAS,CAU7E;AAED,4EAA4E;AAC5E,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,EAAE,CAShF"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Mirrors `@hypequery/serve`'s `ServeMcpSource`. Read through the global symbol
3
+ * registry rather than by importing serve: serve is a peer dependency that
4
+ * lives in the user's project, and a static import here would be resolved by
5
+ * Node before any CLI code runs, breaking even `hypequery --help` on a clean
6
+ * install. Keep the key in step with `server/mcp-source.ts`.
7
+ */
8
+ const MCP_SOURCE_SYMBOL = Symbol.for('hypequery.mcp-source.v1');
9
+ export function readServeMcpSource(value) {
10
+ if (value === null || (typeof value !== 'object' && typeof value !== 'function')) {
11
+ return undefined;
12
+ }
13
+ const source = value[MCP_SOURCE_SYMBOL];
14
+ return source?.version === 1 && typeof source.resolveAnalytics === 'function'
15
+ ? source
16
+ : undefined;
17
+ }
18
+ /** Datasets a caller cannot query unless a trusted tenant is configured. */
19
+ export function tenantScopedDatasets(datasets) {
20
+ return Object.entries(datasets)
21
+ .filter(([, dataset]) => {
22
+ const value = dataset;
23
+ const tenantKey = value?.tenantKey ?? value?.config?.tenantKey;
24
+ return typeof tenantKey === 'string' && tenantKey.length > 0;
25
+ })
26
+ .map(([name]) => name)
27
+ .sort();
28
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hypequery/cli",
3
- "version": "1.18.2",
3
+ "version": "1.19.0",
4
4
  "description": "CLI for scaffolding type-safe ClickHouse analytics, semantic datasets, React manifests, and deployments",
5
5
  "keywords": [
6
6
  "clickhouse",
@@ -30,22 +30,29 @@
30
30
  "open": "^10.0.0",
31
31
  "ora": "^8.0.1",
32
32
  "prompts": "^2.4.2",
33
- "@hypequery/deployment": "0.7.4",
34
- "@hypequery/protocol": "0.11.1"
33
+ "@hypequery/deployment": "0.7.5",
34
+ "@hypequery/protocol": "0.12.0"
35
35
  },
36
36
  "optionalDependencies": {
37
37
  "@napi-rs/keyring": "1.3.0"
38
38
  },
39
39
  "peerDependencies": {
40
+ "@hypequery/mcp": "*",
40
41
  "@hypequery/serve": "*"
41
42
  },
43
+ "peerDependenciesMeta": {
44
+ "@hypequery/mcp": {
45
+ "optional": true
46
+ }
47
+ },
42
48
  "devDependencies": {
43
49
  "@types/node": "^20.11.30",
44
50
  "@types/prompts": "^2.4.9",
45
51
  "@vitest/coverage-v8": "^3.2.6",
46
52
  "typescript": "^5.7.3",
47
53
  "vitest": "^3.2.6",
48
- "@hypequery/serve": "0.16.2"
54
+ "@hypequery/mcp": "0.6.0",
55
+ "@hypequery/serve": "0.17.0"
49
56
  },
50
57
  "repository": {
51
58
  "type": "git",