@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.
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +15 -0
- package/dist/commands/deploy.d.ts +1 -0
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +5 -1
- package/dist/commands/deployment.d.ts +1 -0
- package/dist/commands/deployment.d.ts.map +1 -1
- package/dist/commands/deployment.js +6 -1
- package/dist/commands/generate-datasets.d.ts +3 -0
- package/dist/commands/generate-datasets.d.ts.map +1 -1
- package/dist/commands/generate-datasets.js +60 -2
- package/dist/commands/mcp.d.ts +19 -0
- package/dist/commands/mcp.d.ts.map +1 -0
- package/dist/commands/mcp.js +94 -0
- package/dist/generators/dataset-generator.d.ts +3 -0
- package/dist/generators/dataset-generator.d.ts.map +1 -1
- package/dist/generators/dataset-generator.js +8 -5
- package/dist/utils/cloud-diagnostic.d.ts +10 -0
- package/dist/utils/cloud-diagnostic.d.ts.map +1 -0
- package/dist/utils/cloud-diagnostic.js +10 -0
- package/dist/utils/generated-file-diff.d.ts +7 -0
- package/dist/utils/generated-file-diff.d.ts.map +1 -0
- package/dist/utils/generated-file-diff.js +52 -0
- package/dist/utils/generated-file.d.ts +22 -0
- package/dist/utils/generated-file.d.ts.map +1 -0
- package/dist/utils/generated-file.js +100 -0
- package/dist/utils/mcp-console.d.ts +7 -0
- package/dist/utils/mcp-console.d.ts.map +1 -0
- package/dist/utils/mcp-console.js +20 -0
- package/dist/utils/mcp-lifecycle.d.ts +6 -0
- package/dist/utils/mcp-lifecycle.d.ts.map +1 -0
- package/dist/utils/mcp-lifecycle.js +22 -0
- package/dist/utils/mcp-source.d.ts +9 -0
- package/dist/utils/mcp-source.d.ts.map +1 -0
- package/dist/utils/mcp-source.js +28 -0
- 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;
|
|
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;
|
|
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"}
|
package/dist/commands/deploy.js
CHANGED
|
@@ -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, {
|
|
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);
|
|
@@ -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;
|
|
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(
|
|
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;
|
|
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
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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.
|
|
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.
|
|
34
|
-
"@hypequery/protocol": "0.
|
|
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/
|
|
54
|
+
"@hypequery/mcp": "0.6.0",
|
|
55
|
+
"@hypequery/serve": "0.17.0"
|
|
49
56
|
},
|
|
50
57
|
"repository": {
|
|
51
58
|
"type": "git",
|