@hypequery/cli 1.21.2 → 1.23.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 (185) hide show
  1. package/README.md +1 -0
  2. package/dist/bin/cli.js +77 -12
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +33 -15
  5. package/dist/commands/deploy.d.ts +5 -0
  6. package/dist/commands/deploy.d.ts.map +1 -1
  7. package/dist/commands/deploy.js +10 -0
  8. package/dist/commands/deployment-status.d.ts +16 -0
  9. package/dist/commands/deployment-status.d.ts.map +1 -0
  10. package/dist/commands/deployment-status.js +36 -0
  11. package/dist/commands/dev.d.ts.map +1 -1
  12. package/dist/commands/dev.js +25 -5
  13. package/dist/commands/generate-datasets.d.ts +0 -6
  14. package/dist/commands/generate-datasets.d.ts.map +1 -1
  15. package/dist/commands/generate-datasets.js +17 -1
  16. package/dist/commands/generate-manifest.d.ts.map +1 -1
  17. package/dist/commands/generate-manifest.js +4 -0
  18. package/dist/commands/generate.d.ts.map +1 -1
  19. package/dist/commands/generate.js +14 -1
  20. package/dist/commands/init.d.ts.map +1 -1
  21. package/dist/commands/init.js +80 -55
  22. package/dist/commands/live-source.d.ts.map +1 -1
  23. package/dist/commands/live-source.js +3 -21
  24. package/dist/commands/mcp.d.ts +10 -0
  25. package/dist/commands/mcp.d.ts.map +1 -1
  26. package/dist/commands/mcp.js +93 -8
  27. package/dist/commands/telemetry.d.ts +3 -0
  28. package/dist/commands/telemetry.d.ts.map +1 -0
  29. package/dist/commands/telemetry.js +19 -0
  30. package/dist/generators/chdb.d.ts +3 -0
  31. package/dist/generators/chdb.d.ts.map +1 -1
  32. package/dist/generators/chdb.js +2 -0
  33. package/dist/generators/clickhouse.d.ts +3 -1
  34. package/dist/generators/clickhouse.d.ts.map +1 -1
  35. package/dist/generators/clickhouse.js +2 -0
  36. package/dist/test-utils.d.ts +1 -1
  37. package/dist/typegen/generate-types.d.ts +3 -0
  38. package/dist/typegen/generate-types.d.ts.map +1 -1
  39. package/dist/typegen/generate-types.js +5 -1
  40. package/dist/typegen/type-parsing.d.ts +1 -1
  41. package/dist/typegen/type-parsing.d.ts.map +1 -1
  42. package/dist/typegen/type-parsing.js +11 -7
  43. package/dist/utils/chdb-gitignore-entry.d.ts +2 -0
  44. package/dist/utils/chdb-gitignore-entry.d.ts.map +1 -0
  45. package/dist/utils/chdb-gitignore-entry.js +17 -0
  46. package/dist/utils/cli-version.d.ts +2 -0
  47. package/dist/utils/cli-version.d.ts.map +1 -0
  48. package/dist/utils/cli-version.js +11 -0
  49. package/dist/utils/cloud-credential-store.d.ts.map +1 -1
  50. package/dist/utils/cloud-credential-store.js +2 -13
  51. package/dist/utils/command-exit.d.ts +13 -0
  52. package/dist/utils/command-exit.d.ts.map +1 -0
  53. package/dist/utils/command-exit.js +31 -0
  54. package/dist/utils/config-directory.d.ts +3 -0
  55. package/dist/utils/config-directory.d.ts.map +1 -0
  56. package/dist/utils/config-directory.js +14 -0
  57. package/dist/utils/deployment-target.d.ts +7 -0
  58. package/dist/utils/deployment-target.d.ts.map +1 -0
  59. package/dist/utils/deployment-target.js +19 -0
  60. package/dist/utils/detect-database-environment.d.ts +3 -0
  61. package/dist/utils/detect-database-environment.d.ts.map +1 -0
  62. package/dist/utils/detect-database-environment.js +8 -0
  63. package/dist/utils/detect-database.d.ts.map +1 -1
  64. package/dist/utils/detect-database.js +4 -8
  65. package/dist/utils/hosted-endpoints-output.d.ts +4 -0
  66. package/dist/utils/hosted-endpoints-output.d.ts.map +1 -0
  67. package/dist/utils/hosted-endpoints-output.js +27 -0
  68. package/dist/utils/hosted-endpoints.d.ts +37 -0
  69. package/dist/utils/hosted-endpoints.d.ts.map +1 -0
  70. package/dist/utils/hosted-endpoints.js +81 -0
  71. package/dist/utils/init-options.d.ts +11 -0
  72. package/dist/utils/init-options.d.ts.map +1 -0
  73. package/dist/utils/init-options.js +28 -0
  74. package/dist/utils/live-deployment.d.ts +6 -0
  75. package/dist/utils/live-deployment.d.ts.map +1 -1
  76. package/dist/utils/live-deployment.js +6 -1
  77. package/dist/utils/load-api.d.ts +2 -0
  78. package/dist/utils/load-api.d.ts.map +1 -1
  79. package/dist/utils/load-api.js +50 -51
  80. package/dist/utils/load-env-files.d.ts +10 -0
  81. package/dist/utils/load-env-files.d.ts.map +1 -0
  82. package/dist/utils/load-env-files.js +27 -0
  83. package/dist/utils/mcp-remote-check.d.ts +19 -0
  84. package/dist/utils/mcp-remote-check.d.ts.map +1 -0
  85. package/dist/utils/mcp-remote-check.js +96 -0
  86. package/dist/utils/mcp-response.d.ts +3 -0
  87. package/dist/utils/mcp-response.d.ts.map +1 -0
  88. package/dist/utils/mcp-response.js +58 -0
  89. package/dist/utils/project-root.d.ts +14 -0
  90. package/dist/utils/project-root.d.ts.map +1 -0
  91. package/dist/utils/project-root.js +41 -0
  92. package/dist/utils/telemetry/buckets.d.ts +5 -0
  93. package/dist/utils/telemetry/buckets.d.ts.map +1 -0
  94. package/dist/utils/telemetry/buckets.js +23 -0
  95. package/dist/utils/telemetry/catalog-docs.d.ts +3 -0
  96. package/dist/utils/telemetry/catalog-docs.d.ts.map +1 -0
  97. package/dist/utils/telemetry/catalog-docs.js +52 -0
  98. package/dist/utils/telemetry/catalog.d.ts +1124 -0
  99. package/dist/utils/telemetry/catalog.d.ts.map +1 -0
  100. package/dist/utils/telemetry/catalog.js +154 -0
  101. package/dist/utils/telemetry/command-context.d.ts +17 -0
  102. package/dist/utils/telemetry/command-context.d.ts.map +1 -0
  103. package/dist/utils/telemetry/command-context.js +30 -0
  104. package/dist/utils/telemetry/config-schema.d.ts +12 -0
  105. package/dist/utils/telemetry/config-schema.d.ts.map +1 -0
  106. package/dist/utils/telemetry/config-schema.js +33 -0
  107. package/dist/utils/telemetry/config-store.d.ts +35 -0
  108. package/dist/utils/telemetry/config-store.d.ts.map +1 -0
  109. package/dist/utils/telemetry/config-store.js +147 -0
  110. package/dist/utils/telemetry/delivery.d.ts +13 -0
  111. package/dist/utils/telemetry/delivery.d.ts.map +1 -0
  112. package/dist/utils/telemetry/delivery.js +26 -0
  113. package/dist/utils/telemetry/detached-dispatch.d.ts +12 -0
  114. package/dist/utils/telemetry/detached-dispatch.d.ts.map +1 -0
  115. package/dist/utils/telemetry/detached-dispatch.js +33 -0
  116. package/dist/utils/telemetry/detached-sender.d.ts +2 -0
  117. package/dist/utils/telemetry/detached-sender.d.ts.map +1 -0
  118. package/dist/utils/telemetry/detached-sender.js +31 -0
  119. package/dist/utils/telemetry/dev-session.d.ts +15 -0
  120. package/dist/utils/telemetry/dev-session.d.ts.map +1 -0
  121. package/dist/utils/telemetry/dev-session.js +25 -0
  122. package/dist/utils/telemetry/domains.d.ts +69 -0
  123. package/dist/utils/telemetry/domains.d.ts.map +1 -0
  124. package/dist/utils/telemetry/domains.js +64 -0
  125. package/dist/utils/telemetry/endpoint.d.ts +3 -0
  126. package/dist/utils/telemetry/endpoint.d.ts.map +1 -0
  127. package/dist/utils/telemetry/endpoint.js +15 -0
  128. package/dist/utils/telemetry/environment.d.ts +27 -0
  129. package/dist/utils/telemetry/environment.d.ts.map +1 -0
  130. package/dist/utils/telemetry/environment.js +95 -0
  131. package/dist/utils/telemetry/error-code.d.ts +4 -0
  132. package/dist/utils/telemetry/error-code.d.ts.map +1 -0
  133. package/dist/utils/telemetry/error-code.js +57 -0
  134. package/dist/utils/telemetry/flags.d.ts +5 -0
  135. package/dist/utils/telemetry/flags.d.ts.map +1 -0
  136. package/dist/utils/telemetry/flags.js +41 -0
  137. package/dist/utils/telemetry/generation-metrics.d.ts +15 -0
  138. package/dist/utils/telemetry/generation-metrics.d.ts.map +1 -0
  139. package/dist/utils/telemetry/generation-metrics.js +35 -0
  140. package/dist/utils/telemetry/identity.d.ts +12 -0
  141. package/dist/utils/telemetry/identity.d.ts.map +1 -0
  142. package/dist/utils/telemetry/identity.js +42 -0
  143. package/dist/utils/telemetry/init-funnel.d.ts +15 -0
  144. package/dist/utils/telemetry/init-funnel.d.ts.map +1 -0
  145. package/dist/utils/telemetry/init-funnel.js +25 -0
  146. package/dist/utils/telemetry/invocation.d.ts +22 -0
  147. package/dist/utils/telemetry/invocation.d.ts.map +1 -0
  148. package/dist/utils/telemetry/invocation.js +46 -0
  149. package/dist/utils/telemetry/lifecycle.d.ts +32 -0
  150. package/dist/utils/telemetry/lifecycle.d.ts.map +1 -0
  151. package/dist/utils/telemetry/lifecycle.js +99 -0
  152. package/dist/utils/telemetry/mcp-session.d.ts +10 -0
  153. package/dist/utils/telemetry/mcp-session.d.ts.map +1 -0
  154. package/dist/utils/telemetry/mcp-session.js +36 -0
  155. package/dist/utils/telemetry/notice.d.ts +11 -0
  156. package/dist/utils/telemetry/notice.d.ts.map +1 -0
  157. package/dist/utils/telemetry/notice.js +24 -0
  158. package/dist/utils/telemetry/opt-out.d.ts +16 -0
  159. package/dist/utils/telemetry/opt-out.d.ts.map +1 -0
  160. package/dist/utils/telemetry/opt-out.js +32 -0
  161. package/dist/utils/telemetry/post-json.d.ts +13 -0
  162. package/dist/utils/telemetry/post-json.d.ts.map +1 -0
  163. package/dist/utils/telemetry/post-json.js +32 -0
  164. package/dist/utils/telemetry/project-context.d.ts +15 -0
  165. package/dist/utils/telemetry/project-context.d.ts.map +1 -0
  166. package/dist/utils/telemetry/project-context.js +86 -0
  167. package/dist/utils/telemetry/schema.d.ts +72 -0
  168. package/dist/utils/telemetry/schema.d.ts.map +1 -0
  169. package/dist/utils/telemetry/schema.js +17 -0
  170. package/dist/utils/telemetry/session-config.d.ts +6 -0
  171. package/dist/utils/telemetry/session-config.d.ts.map +1 -0
  172. package/dist/utils/telemetry/session-config.js +17 -0
  173. package/dist/utils/telemetry/settings-lock.d.ts +36 -0
  174. package/dist/utils/telemetry/settings-lock.d.ts.map +1 -0
  175. package/dist/utils/telemetry/settings-lock.js +102 -0
  176. package/dist/utils/telemetry/transport.d.ts +25 -0
  177. package/dist/utils/telemetry/transport.d.ts.map +1 -0
  178. package/dist/utils/telemetry/transport.js +76 -0
  179. package/dist/utils/telemetry/validation.d.ts +10 -0
  180. package/dist/utils/telemetry/validation.d.ts.map +1 -0
  181. package/dist/utils/telemetry/validation.js +77 -0
  182. package/dist/utils/telemetry/value-formats.d.ts +30 -0
  183. package/dist/utils/telemetry/value-formats.d.ts.map +1 -0
  184. package/dist/utils/telemetry/value-formats.js +13 -0
  185. package/package.json +16 -12
@@ -3,7 +3,12 @@ import path from 'node:path';
3
3
  import { DEFAULT_PROTOCOL_DEPLOYMENT_BUNDLE_LIMITS, validateProtocolDeploymentReleaseTarget, } from '@hypequery/protocol';
4
4
  const SHA256_PATTERN = /^[0-9a-f]{64}$/;
5
5
  const MAX_RESPONSE_BYTES = Math.ceil(DEFAULT_PROTOCOL_DEPLOYMENT_BUNDLE_LIMITS.maxSourceBytes * 1.4) + 1_000_000;
6
- function liveUrl(endpoint, target, resource) {
6
+ /**
7
+ * The Cloud target resource beside a submissions endpoint, or undefined when
8
+ * the endpoint is not a Cloud submissions URL. Refuses anything but https (or
9
+ * loopback http) so a credential is never sent in the clear.
10
+ */
11
+ export function liveUrl(endpoint, target, resource) {
7
12
  const url = new URL(endpoint);
8
13
  if (!/\/v1\/deployments\/submissions\/?$/.test(url.pathname)) {
9
14
  return undefined;
@@ -1,4 +1,6 @@
1
1
  export declare function loadApiModule(modulePath: string): Promise<any>;
2
2
  export declare function loadModule(modulePath: string): Promise<any>;
3
+ /** Remove this process's bundles; the CLI awaits this before it exits. */
4
+ export declare function cleanupLoadedApiArtifacts(): Promise<void>;
3
5
  export declare function findNearestTsconfig(filePath: string): Promise<string | null>;
4
6
  //# 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;AA0JD,wBAAsB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,0BA8BzD"}
1
+ {"version":3,"file":"load-api.d.ts","sourceRoot":"","sources":["../../src/utils/load-api.ts"],"names":[],"mappings":"AAgBA,wBAAsB,aAAa,CAAC,UAAU,EAAE,MAAM,gBA2DrD;AAED,wBAAsB,UAAU,CAAC,UAAU,EAAE,MAAM,gBAwClD;AAuED,0EAA0E;AAC1E,wBAAsB,yBAAyB,kBAE9C;AAqED,wBAAsB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,0BA8BzD"}
@@ -1,8 +1,11 @@
1
1
  import { pathToFileURL } from 'node:url';
2
- import { access, mkdtemp, rm, mkdir, writeFile } from 'node:fs/promises';
2
+ import { rmdirSync, rmSync } from 'node:fs';
3
+ import { access, mkdtemp, mkdir, writeFile } from 'node:fs/promises';
3
4
  import os from 'node:os';
4
5
  import path from 'node:path';
5
6
  import { build } from 'esbuild';
7
+ import { loadEnvFiles } from './load-env-files.js';
8
+ import { findProjectRoot } from './project-root.js';
6
9
  if (typeof process.setMaxListeners === 'function') {
7
10
  process.setMaxListeners(0);
8
11
  }
@@ -68,6 +71,11 @@ export async function loadModule(modulePath) {
68
71
  const relativePath = path.relative(process.cwd(), resolved);
69
72
  throw new Error(`File not found: ${relativePath}`);
70
73
  }
74
+ // The CLI loaded the launch directory's env files at startup. An entry in
75
+ // another project (`hypequery mcp /abs/path/api.ts`) reads its connection
76
+ // settings from that project's env files, so load those too before its
77
+ // client module runs. Values already set keep precedence.
78
+ await loadEnvFiles(await findProjectRoot(resolved));
71
79
  const extension = path.extname(resolved).toLowerCase();
72
80
  const isTypeScript = TYPESCRIPT_EXTENSIONS.has(extension);
73
81
  const moduleUrl = isTypeScript
@@ -92,23 +100,26 @@ export async function loadModule(modulePath) {
92
100
  }
93
101
  }
94
102
  const globalState = globalThis;
95
- let tempDirPromise = globalState.__hypequeryCliTempDirPromise ?? null;
96
- const tempFiles = globalState.__hypequeryCliTempFiles ?? new Set();
97
- const tempDirs = globalState.__hypequeryCliTempDirs ?? new Set();
103
+ // One bundle directory per project root, keyed by that root.
104
+ const tempDirPromises = globalState.__hypequeryCliTempDirPromises ??= new Map();
105
+ const tempFiles = globalState.__hypequeryCliTempFiles ??= new Set();
106
+ const tempDirs = globalState.__hypequeryCliTempDirs ??= new Set();
107
+ const tempRoots = globalState.__hypequeryCliTempRoots ??= new Set();
98
108
  let cleanupHooksInstalled = globalState.__hypequeryCliCleanupInstalled ?? false;
99
- if (!globalState.__hypequeryCliTempFiles) {
100
- globalState.__hypequeryCliTempFiles = tempFiles;
101
- }
102
- if (!globalState.__hypequeryCliTempDirs) {
103
- globalState.__hypequeryCliTempDirs = tempDirs;
104
- }
105
- function ensureTempDir() {
109
+ /**
110
+ * The bundle has to live inside the entry's project: its bare imports
111
+ * (`@hypequery/serve`, ...) resolve from the bundle's location, and the
112
+ * process's working directory may be anywhere (MCP clients launch from `/`).
113
+ */
114
+ function ensureTempDir(projectRoot) {
106
115
  installCleanupHooks();
107
- if (!tempDirPromise) {
108
- tempDirPromise = (async () => {
109
- const projectTempRoot = path.join(process.cwd(), '.hypequery', 'tmp');
116
+ let promise = tempDirPromises.get(projectRoot);
117
+ if (!promise) {
118
+ promise = (async () => {
119
+ const projectTempRoot = path.join(projectRoot, '.hypequery', 'tmp');
110
120
  try {
111
121
  await mkdir(projectTempRoot, { recursive: true });
122
+ tempRoots.add(projectTempRoot);
112
123
  const dir = await mkdtemp(path.join(projectTempRoot, 'bundle-'));
113
124
  tempDirs.add(dir);
114
125
  return dir;
@@ -119,65 +130,53 @@ function ensureTempDir() {
119
130
  return fallbackDir;
120
131
  }
121
132
  })();
122
- globalState.__hypequeryCliTempDirPromise = tempDirPromise;
133
+ tempDirPromises.set(projectRoot, promise);
123
134
  }
124
- return tempDirPromise;
135
+ return promise;
125
136
  }
126
- async function cleanupTempFiles() {
127
- if (tempFiles.size === 0)
128
- return;
129
- await Promise.all(Array.from(tempFiles).map(async (file) => {
137
+ // Synchronous so it completes inside an 'exit' handler.
138
+ function cleanupTempArtifacts() {
139
+ for (const target of [...tempFiles, ...tempDirs]) {
130
140
  try {
131
- await rm(file, { force: true });
141
+ rmSync(target, { recursive: true, force: true });
132
142
  }
133
143
  catch {
134
144
  // ignore cleanup failures
135
145
  }
136
- }));
146
+ }
137
147
  tempFiles.clear();
138
- }
139
- async function cleanupTempDirs() {
140
- if (tempDirs.size === 0)
141
- return;
142
- await Promise.all(Array.from(tempDirs).map(async (dir) => {
148
+ tempDirs.clear();
149
+ tempDirPromises.clear();
150
+ // Only remove the shared tmp root once empty: another hypequery process in the
151
+ // same project (e.g. `hypequery dev` beside an MCP client) may still use it.
152
+ for (const root of tempRoots) {
143
153
  try {
144
- await rm(dir, { recursive: true, force: true });
154
+ rmdirSync(root);
145
155
  }
146
156
  catch {
147
- // ignore cleanup failures
157
+ // not empty or already gone
148
158
  }
149
- }));
150
- tempDirs.clear();
151
- const projectTempRoot = path.join(process.cwd(), '.hypequery', 'tmp');
152
- try {
153
- await rm(projectTempRoot, { recursive: true, force: true });
154
- }
155
- catch {
156
- // ignore cleanup failures
157
159
  }
160
+ tempRoots.clear();
158
161
  }
159
- async function cleanupTempArtifacts() {
160
- await cleanupTempFiles();
161
- await cleanupTempDirs();
162
+ /** Remove this process's bundles; the CLI awaits this before it exits. */
163
+ export async function cleanupLoadedApiArtifacts() {
164
+ cleanupTempArtifacts();
162
165
  }
163
166
  function installCleanupHooks() {
164
167
  if (cleanupHooksInstalled)
165
168
  return;
166
169
  cleanupHooksInstalled = true;
167
170
  globalState.__hypequeryCliCleanupInstalled = true;
168
- process.once('exit', () => {
169
- cleanupTempArtifacts().catch(() => undefined);
170
- });
171
- ['SIGINT', 'SIGTERM'].forEach(signal => {
172
- process.once(signal, () => {
173
- cleanupTempArtifacts().catch(() => undefined);
174
- process.exit();
175
- });
176
- });
171
+ // Synchronous, so it still completes when this is the last thing to run.
172
+ process.once('exit', cleanupTempArtifacts);
173
+ // Signal shutdown belongs to the CLI/owning server. Exiting from a module
174
+ // cleanup listener bypassed server teardown and the bounded telemetry flush.
177
175
  }
178
176
  async function bundleTypeScriptModule(entryPath) {
179
177
  const relativePath = path.relative(process.cwd(), entryPath);
180
178
  const tsconfigPath = await findNearestTsconfig(entryPath);
179
+ const projectRoot = await findProjectRoot(entryPath);
181
180
  try {
182
181
  const result = await build({
183
182
  entryPoints: [entryPath],
@@ -188,7 +187,7 @@ async function bundleTypeScriptModule(entryPath) {
188
187
  sourcemap: 'inline',
189
188
  write: false,
190
189
  logLevel: 'silent',
191
- absWorkingDir: process.cwd(),
190
+ absWorkingDir: projectRoot,
192
191
  packages: 'external',
193
192
  tsconfig: tsconfigPath ?? undefined,
194
193
  loader: {
@@ -202,7 +201,7 @@ async function bundleTypeScriptModule(entryPath) {
202
201
  if (!output) {
203
202
  throw new Error('esbuild produced no output');
204
203
  }
205
- const tempDir = await ensureTempDir();
204
+ const tempDir = await ensureTempDir(projectRoot);
206
205
  const timestamp = Date.now();
207
206
  const tempFile = path.join(tempDir, `${path.basename(entryPath, path.extname(entryPath))}-${timestamp}.mjs`);
208
207
  const contents = `${output.text}\n` +
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Loads the `.env*` cascade from `directory` into `process.env`.
3
+ *
4
+ * `dotenv` never overwrites a variable that is already set, so a real
5
+ * environment variable beats every file, and a directory loaded earlier (the
6
+ * launch directory, at startup) beats one loaded later. Each directory is read
7
+ * once per process; `hypequery dev` reloads the API on every change.
8
+ */
9
+ export declare function loadEnvFiles(directory: string): Promise<void>;
10
+ //# sourceMappingURL=load-env-files.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"load-env-files.d.ts","sourceRoot":"","sources":["../../src/utils/load-env-files.ts"],"names":[],"mappings":"AAKA;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAcnE"}
@@ -0,0 +1,27 @@
1
+ import path from 'node:path';
2
+ import { envFiles } from './env-files.js';
3
+ const loadedDirectories = new Set();
4
+ /**
5
+ * Loads the `.env*` cascade from `directory` into `process.env`.
6
+ *
7
+ * `dotenv` never overwrites a variable that is already set, so a real
8
+ * environment variable beats every file, and a directory loaded earlier (the
9
+ * launch directory, at startup) beats one loaded later. Each directory is read
10
+ * once per process; `hypequery dev` reloads the API on every change.
11
+ */
12
+ export async function loadEnvFiles(directory) {
13
+ const resolved = path.resolve(directory);
14
+ if (loadedDirectories.has(resolved))
15
+ return;
16
+ loadedDirectories.add(resolved);
17
+ try {
18
+ const { config } = await import('dotenv');
19
+ for (const file of envFiles(process.env.NODE_ENV)) {
20
+ // Missing files are a no-op.
21
+ config({ path: path.join(resolved, file) });
22
+ }
23
+ }
24
+ catch {
25
+ // dotenv is optional; continue if not available
26
+ }
27
+ }
@@ -0,0 +1,19 @@
1
+ export type RemoteMcpCheck = {
2
+ readonly ok: true;
3
+ readonly tools: number;
4
+ readonly server?: string;
5
+ } | {
6
+ readonly ok: false;
7
+ readonly reason: string;
8
+ };
9
+ /**
10
+ * A non-destructive check of a hosted MCP endpoint: initialize, then list
11
+ * tools. It never calls a tool, so it runs no query and changes nothing.
12
+ * The key is sent only as a bearer header and never included in the result.
13
+ */
14
+ export declare function checkRemoteMcp(input: {
15
+ readonly url: string;
16
+ readonly key: string;
17
+ readonly fetch?: typeof fetch;
18
+ }): Promise<RemoteMcpCheck>;
19
+ //# sourceMappingURL=mcp-remote-check.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp-remote-check.d.ts","sourceRoot":"","sources":["../../src/utils/mcp-remote-check.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,cAAc,GACtB;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GACvE;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAkCpD;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,KAAK,EAAE;IAC1C,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;CAC/B,GAAG,OAAO,CAAC,cAAc,CAAC,CAsD1B"}
@@ -0,0 +1,96 @@
1
+ import { readMcpResponse } from './mcp-response.js';
2
+ const PROTOCOL_VERSION = '2025-06-18';
3
+ const TIMEOUT_MS = 15_000;
4
+ const SUPPORTED_PROTOCOL_VERSIONS = new Set(['2025-03-26', PROTOCOL_VERSION, '2025-11-25']);
5
+ async function rpc(fetchFn, url, key, message, session, protocolVersion = PROTOCOL_VERSION) {
6
+ const response = await fetchFn(url, {
7
+ method: 'POST',
8
+ headers: {
9
+ authorization: `Bearer ${key}`,
10
+ 'content-type': 'application/json',
11
+ accept: 'application/json, text/event-stream',
12
+ 'mcp-protocol-version': protocolVersion,
13
+ ...(session ? { 'mcp-session-id': session } : {}),
14
+ },
15
+ body: JSON.stringify(message),
16
+ redirect: 'error',
17
+ signal: AbortSignal.timeout(TIMEOUT_MS),
18
+ });
19
+ const body = await readMcpResponse(response, message.id);
20
+ return { response, body };
21
+ }
22
+ function refusal(status) {
23
+ if (status === 401)
24
+ return 'the API key was refused (401). Check HYPEQUERY_API_KEY.';
25
+ if (status === 403)
26
+ return 'the API key may not use MCP here (403). Check its scopes.';
27
+ if (status === 404)
28
+ return 'no MCP endpoint is published at this URL (404).';
29
+ return `the endpoint answered HTTP ${status}.`;
30
+ }
31
+ /**
32
+ * A non-destructive check of a hosted MCP endpoint: initialize, then list
33
+ * tools. It never calls a tool, so it runs no query and changes nothing.
34
+ * The key is sent only as a bearer header and never included in the result.
35
+ */
36
+ export async function checkRemoteMcp(input) {
37
+ const fetchFn = input.fetch ?? fetch;
38
+ let url;
39
+ try {
40
+ url = new URL(input.url);
41
+ }
42
+ catch {
43
+ return { ok: false, reason: 'the URL is not valid.' };
44
+ }
45
+ const loopback = url.hostname === 'localhost' || url.hostname === '127.0.0.1';
46
+ if (url.protocol !== 'https:' && !(url.protocol === 'http:' && loopback)) {
47
+ return { ok: false, reason: 'the URL must use https, so the API key is not sent in the clear.' };
48
+ }
49
+ try {
50
+ const init = await rpc(fetchFn, url.href, input.key, {
51
+ jsonrpc: '2.0',
52
+ id: 1,
53
+ method: 'initialize',
54
+ params: {
55
+ protocolVersion: PROTOCOL_VERSION,
56
+ capabilities: {},
57
+ clientInfo: { name: 'hypequery-cli-self-test', version: '1.0.0' },
58
+ },
59
+ });
60
+ if (!init.response.ok)
61
+ return { ok: false, reason: refusal(init.response.status) };
62
+ if (!init.body?.result)
63
+ return { ok: false, reason: 'initialize returned no result.' };
64
+ const protocolVersion = init.body.result.protocolVersion;
65
+ if (typeof protocolVersion !== 'string' || !SUPPORTED_PROTOCOL_VERSIONS.has(protocolVersion)) {
66
+ return { ok: false, reason: 'initialize returned an unsupported protocol version.' };
67
+ }
68
+ const session = init.response.headers.get('mcp-session-id') ?? undefined;
69
+ const initialized = await rpc(fetchFn, url.href, input.key, {
70
+ jsonrpc: '2.0',
71
+ method: 'notifications/initialized',
72
+ }, session, protocolVersion);
73
+ if (!initialized.response.ok)
74
+ return { ok: false, reason: refusal(initialized.response.status) };
75
+ const listed = await rpc(fetchFn, url.href, input.key, {
76
+ jsonrpc: '2.0',
77
+ id: 2,
78
+ method: 'tools/list',
79
+ }, session, protocolVersion);
80
+ if (!listed.response.ok)
81
+ return { ok: false, reason: refusal(listed.response.status) };
82
+ const tools = listed.body?.result?.tools;
83
+ if (!Array.isArray(tools))
84
+ return { ok: false, reason: 'tools/list returned no tools.' };
85
+ const server = init.body.result.serverInfo?.name;
86
+ return { ok: true, tools: tools.length, ...(typeof server === 'string' ? { server } : {}) };
87
+ }
88
+ catch (error) {
89
+ return {
90
+ ok: false,
91
+ reason: error?.name === 'TimeoutError'
92
+ ? 'the endpoint did not answer in time.'
93
+ : 'the endpoint could not be reached.',
94
+ };
95
+ }
96
+ }
@@ -0,0 +1,3 @@
1
+ /** Read a JSON-RPC response, including responses delivered over an SSE stream. */
2
+ export declare function readMcpResponse(response: Response, requestId: unknown): Promise<Record<string, any> | null>;
3
+ //# sourceMappingURL=mcp-response.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp-response.d.ts","sourceRoot":"","sources":["../../src/utils/mcp-response.ts"],"names":[],"mappings":"AAAA,kFAAkF;AAClF,wBAAsB,eAAe,CACnC,QAAQ,EAAE,QAAQ,EAClB,SAAS,EAAE,OAAO,GACjB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,CAAC,CAiDrC"}
@@ -0,0 +1,58 @@
1
+ /** Read a JSON-RPC response, including responses delivered over an SSE stream. */
2
+ export async function readMcpResponse(response, requestId) {
3
+ if (!response.ok || requestId === undefined) {
4
+ await response.body?.cancel();
5
+ return null;
6
+ }
7
+ const contentType = response.headers.get('content-type')?.split(';')[0]?.trim().toLowerCase();
8
+ if (contentType !== 'text/event-stream') {
9
+ try {
10
+ const body = await response.json();
11
+ return body?.jsonrpc === '2.0' && body.id === requestId ? body : null;
12
+ }
13
+ catch (error) {
14
+ if (error?.name === 'TimeoutError' || error?.name === 'AbortError') {
15
+ throw error;
16
+ }
17
+ return null;
18
+ }
19
+ }
20
+ if (!response.body)
21
+ return null;
22
+ const reader = response.body.getReader();
23
+ const decoder = new TextDecoder();
24
+ let pending = '';
25
+ let data = [];
26
+ try {
27
+ for (;;) {
28
+ const chunk = await reader.read();
29
+ pending += decoder.decode(chunk.value, { stream: !chunk.done });
30
+ let newline;
31
+ while ((newline = /\r\n|\r|\n/.exec(pending))) {
32
+ // A CR at the chunk boundary may be the first half of CRLF.
33
+ if (!chunk.done && newline[0] === '\r' && newline.index === pending.length - 1)
34
+ break;
35
+ const line = pending.slice(0, newline.index);
36
+ pending = pending.slice(newline.index + newline[0].length);
37
+ if (line === '') {
38
+ if (data.length > 0) {
39
+ const body = JSON.parse(data.join('\n'));
40
+ data = [];
41
+ if (body?.jsonrpc === '2.0' && body.id === requestId
42
+ && ('result' in body || 'error' in body))
43
+ return body;
44
+ }
45
+ }
46
+ else if (line === 'data' || line.startsWith('data:')) {
47
+ data.push(line === 'data' ? '' : line.slice(5).replace(/^ /, ''));
48
+ }
49
+ }
50
+ if (chunk.done)
51
+ return null;
52
+ }
53
+ }
54
+ finally {
55
+ await reader.cancel();
56
+ reader.releaseLock();
57
+ }
58
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Finds the directory that owns `filePath`: the nearest ancestor with a
3
+ * `package.json`, or the file's own directory when there is none. Bare imports
4
+ * in the file resolve from here, whatever the process's working directory is.
5
+ */
6
+ export declare function findProjectRoot(filePath: string): Promise<string>;
7
+ /**
8
+ * Imports `specifier` as `fromFile` would see it, so a package installed in the
9
+ * user's project is found even when the CLI runs from elsewhere (for example
10
+ * through `npx -y @hypequery/cli`). Falls back to `fallback` — the CLI's own
11
+ * resolution — when the project does not have the package.
12
+ */
13
+ export declare function importFromProject<T>(specifier: string, fromFile: string, fallback: () => Promise<T>): Promise<T>;
14
+ //# sourceMappingURL=project-root.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project-root.d.ts","sourceRoot":"","sources":["../../src/utils/project-root.ts"],"names":[],"mappings":"AAKA;;;;GAIG;AACH,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAcvE;AAED;;;;;GAKG;AACH,wBAAsB,iBAAiB,CAAC,CAAC,EACvC,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACzB,OAAO,CAAC,CAAC,CAAC,CAQZ"}
@@ -0,0 +1,41 @@
1
+ import { access } from 'node:fs/promises';
2
+ import { createRequire } from 'node:module';
3
+ import path from 'node:path';
4
+ import { pathToFileURL } from 'node:url';
5
+ /**
6
+ * Finds the directory that owns `filePath`: the nearest ancestor with a
7
+ * `package.json`, or the file's own directory when there is none. Bare imports
8
+ * in the file resolve from here, whatever the process's working directory is.
9
+ */
10
+ export async function findProjectRoot(filePath) {
11
+ const start = path.dirname(path.resolve(filePath));
12
+ let dir = start;
13
+ while (true) {
14
+ try {
15
+ await access(path.join(dir, 'package.json'));
16
+ return dir;
17
+ }
18
+ catch {
19
+ const parent = path.dirname(dir);
20
+ if (parent === dir)
21
+ return start;
22
+ dir = parent;
23
+ }
24
+ }
25
+ }
26
+ /**
27
+ * Imports `specifier` as `fromFile` would see it, so a package installed in the
28
+ * user's project is found even when the CLI runs from elsewhere (for example
29
+ * through `npx -y @hypequery/cli`). Falls back to `fallback` — the CLI's own
30
+ * resolution — when the project does not have the package.
31
+ */
32
+ export async function importFromProject(specifier, fromFile, fallback) {
33
+ let resolved;
34
+ try {
35
+ resolved = createRequire(path.resolve(fromFile)).resolve(specifier);
36
+ }
37
+ catch {
38
+ return fallback();
39
+ }
40
+ return import(/* @vite-ignore */ pathToFileURL(resolved).href);
41
+ }
@@ -0,0 +1,5 @@
1
+ import { COUNT_BUCKETS, DURATION_BUCKETS } from './domains.js';
2
+ /** Invalid measurements are omitted rather than coerced into a misleading bucket. */
3
+ export declare function countBucket(count: number): typeof COUNT_BUCKETS[number] | undefined;
4
+ export declare function durationBucket(milliseconds: number): typeof DURATION_BUCKETS[number] | undefined;
5
+ //# sourceMappingURL=buckets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"buckets.d.ts","sourceRoot":"","sources":["../../../src/utils/telemetry/buckets.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAE/D,qFAAqF;AACrF,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,aAAa,CAAC,MAAM,CAAC,GAAG,SAAS,CAQnF;AAED,wBAAgB,cAAc,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,gBAAgB,CAAC,MAAM,CAAC,GAAG,SAAS,CAIhG"}
@@ -0,0 +1,23 @@
1
+ import { DURATION_BUCKETS } from './domains.js';
2
+ /** Invalid measurements are omitted rather than coerced into a misleading bucket. */
3
+ export function countBucket(count) {
4
+ if (!Number.isSafeInteger(count) || count < 0)
5
+ return undefined;
6
+ if (count === 0)
7
+ return '0';
8
+ if (count === 1)
9
+ return '1';
10
+ if (count <= 5)
11
+ return '2-5';
12
+ if (count <= 20)
13
+ return '6-20';
14
+ if (count <= 100)
15
+ return '21-100';
16
+ return '101+';
17
+ }
18
+ export function durationBucket(milliseconds) {
19
+ if (!Number.isFinite(milliseconds) || milliseconds < 0)
20
+ return undefined;
21
+ const boundaries = [100, 1_000, 10_000, 60_000, 600_000, 3_600_000];
22
+ return DURATION_BUCKETS[boundaries.findIndex(boundary => milliseconds < boundary)] ?? '1h+';
23
+ }
@@ -0,0 +1,3 @@
1
+ /** Generated section only; the public page retains its human-written policy and controls. */
2
+ export declare function renderTelemetryCatalog(): string;
3
+ //# sourceMappingURL=catalog-docs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog-docs.d.ts","sourceRoot":"","sources":["../../../src/utils/telemetry/catalog-docs.ts"],"names":[],"mappings":"AA6BA,6FAA6F;AAC7F,wBAAgB,sBAAsB,IAAI,MAAM,CA4B/C"}
@@ -0,0 +1,52 @@
1
+ import { COMMON_PROPERTIES, EVENT_CATALOG, EVENT_SCHEMA_VERSION } from './catalog.js';
2
+ import { VALUE_FORMATS } from './value-formats.js';
3
+ import { COMMAND_FLAGS, GLOBAL_FLAGS } from './domains.js';
4
+ function domain(field) {
5
+ switch (field.kind) {
6
+ case 'boolean': return '`true`, `false`';
7
+ case 'enum': return field.values.map(value => `\`${value}\``).join(', ');
8
+ case 'enum_list': return `Unique list of: ${field.values.map(value => `\`${value}\``).join(', ')}`;
9
+ case 'record': return 'Object with only the documented keys below';
10
+ case 'format': return VALUE_FORMATS[field.format].description;
11
+ }
12
+ }
13
+ function rows(fields, prefix = '') {
14
+ return Object.entries(fields).flatMap(([name, field]) => {
15
+ const key = `${prefix}${name}`;
16
+ return [
17
+ `| \`${key}\` | ${field.optional ? 'Optional' : 'Required'} | ${domain(field)} | ${field.description} |`,
18
+ ...(field.kind === 'record' ? rows(field.fields, `${key}.`) : []),
19
+ ];
20
+ });
21
+ }
22
+ function table(fields) {
23
+ return ['| Property | Presence | Allowed values | Question answered |', '| --- | --- | --- | --- |', ...rows(fields)].join('\n');
24
+ }
25
+ /** Generated section only; the public page retains its human-written policy and controls. */
26
+ export function renderTelemetryCatalog() {
27
+ const parts = [
28
+ '{/* Generated by pnpm --filter @hypequery/cli telemetry:docs. Do not edit this section by hand. */}',
29
+ '## Event reference',
30
+ `Schema version: **${EVENT_SCHEMA_VERSION}**. Default collection is inactive. Debug or explicitly configured ingest mode emits command/crash lifecycle events, init and generation metrics, and dev/MCP session events. Cloud command and project-feature instrumentation follows separately.`,
31
+ 'Unknown events, properties, record keys, flag names, enum values, and malformed identities or versions are rejected. No argument values, paths, names, SQL, messages, stacks, hosts, tenants, or credentials are permitted.',
32
+ 'Counts use inclusive integer ranges: 0, 1, 2–5, 6–20, 21–100, and 101 or more. Duration and nonzero size buckets include their lower bound and exclude their upper bound. Raw counts, durations, ports and byte sizes are never sent.',
33
+ '### Common properties',
34
+ 'Every event includes these fields. Unknown environment details use the documented unknown value; unavailable optional fields are omitted.',
35
+ table(COMMON_PROPERTIES),
36
+ ];
37
+ for (const [name, definition] of Object.entries(EVENT_CATALOG)) {
38
+ parts.push(`### \`${name}\``, definition.purpose, table(definition.properties));
39
+ if ('variants' in definition) {
40
+ for (const [command, fields] of Object.entries(definition.variants)) {
41
+ parts.push(`#### \`${name}\`: \`${command}\``);
42
+ if (name === 'cli_command_completed') {
43
+ const flags = [...COMMAND_FLAGS[command], ...GLOBAL_FLAGS];
44
+ parts.push(`Allowed \`flags_used\`: ${flags.map(flag => `\`${flag}\``).join(', ')}. The two type-generation aliases remain distinct command values.`);
45
+ }
46
+ parts.push(Object.keys(fields).length ? table(fields) : 'No additional properties.');
47
+ }
48
+ }
49
+ }
50
+ parts.push('MCP client identity/version is excluded from v1 pending the privacy decision. Auth strategies are recorded only as none/configured/unknown, since user-supplied functions have no safe built-in type identifier.', 'Cache scopes are classified only as unscoped/tenant/explicit/unknown from configuration presence. Explicit scopes are user content and are never read into a payload.', 'Project IDs follow HQ-233: the CLI uses the shared global salt, then the ingest proxy re-keys the digest with a server-side HMAC secret before storage. A hash is a pseudonymous project signal, not proof of anonymity. Cloud accounts are never linked to install IDs.');
51
+ return `${parts.join('\n\n')}\n`;
52
+ }