@hypequery/cli 1.22.0 → 1.24.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/bin/cli.js +77 -12
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +20 -15
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +25 -5
- package/dist/commands/generate-datasets.d.ts +0 -6
- package/dist/commands/generate-datasets.d.ts.map +1 -1
- package/dist/commands/generate-datasets.js +17 -1
- package/dist/commands/generate-manifest.d.ts.map +1 -1
- package/dist/commands/generate-manifest.js +4 -0
- package/dist/commands/generate.d.ts.map +1 -1
- package/dist/commands/generate.js +14 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +80 -55
- package/dist/commands/mcp.d.ts.map +1 -1
- package/dist/commands/mcp.js +47 -10
- package/dist/commands/telemetry.d.ts +3 -0
- package/dist/commands/telemetry.d.ts.map +1 -0
- package/dist/commands/telemetry.js +19 -0
- package/dist/generators/chdb.d.ts +3 -0
- package/dist/generators/chdb.d.ts.map +1 -1
- package/dist/generators/chdb.js +2 -0
- package/dist/generators/clickhouse.d.ts +3 -1
- package/dist/generators/clickhouse.d.ts.map +1 -1
- package/dist/generators/clickhouse.js +2 -0
- package/dist/typegen/generate-types.d.ts +3 -0
- package/dist/typegen/generate-types.d.ts.map +1 -1
- package/dist/typegen/generate-types.js +5 -1
- package/dist/typegen/type-parsing.d.ts +1 -1
- package/dist/typegen/type-parsing.d.ts.map +1 -1
- package/dist/typegen/type-parsing.js +11 -7
- package/dist/utils/chdb-gitignore-entry.d.ts +2 -0
- package/dist/utils/chdb-gitignore-entry.d.ts.map +1 -0
- package/dist/utils/chdb-gitignore-entry.js +17 -0
- package/dist/utils/cli-version.d.ts +2 -0
- package/dist/utils/cli-version.d.ts.map +1 -0
- package/dist/utils/cli-version.js +11 -0
- package/dist/utils/cloud-credential-store.d.ts.map +1 -1
- package/dist/utils/cloud-credential-store.js +2 -13
- package/dist/utils/command-exit.d.ts +13 -0
- package/dist/utils/command-exit.d.ts.map +1 -0
- package/dist/utils/command-exit.js +31 -0
- package/dist/utils/config-directory.d.ts +3 -0
- package/dist/utils/config-directory.d.ts.map +1 -0
- package/dist/utils/config-directory.js +14 -0
- package/dist/utils/detect-database-environment.d.ts +3 -0
- package/dist/utils/detect-database-environment.d.ts.map +1 -0
- package/dist/utils/detect-database-environment.js +8 -0
- package/dist/utils/detect-database.d.ts.map +1 -1
- package/dist/utils/detect-database.js +4 -8
- package/dist/utils/init-options.d.ts +11 -0
- package/dist/utils/init-options.d.ts.map +1 -0
- package/dist/utils/init-options.js +28 -0
- package/dist/utils/load-api.d.ts +2 -0
- package/dist/utils/load-api.d.ts.map +1 -1
- package/dist/utils/load-api.js +50 -51
- package/dist/utils/load-env-files.d.ts +10 -0
- package/dist/utils/load-env-files.d.ts.map +1 -0
- package/dist/utils/load-env-files.js +27 -0
- package/dist/utils/project-root.d.ts +14 -0
- package/dist/utils/project-root.d.ts.map +1 -0
- package/dist/utils/project-root.js +41 -0
- package/dist/utils/telemetry/buckets.d.ts +5 -0
- package/dist/utils/telemetry/buckets.d.ts.map +1 -0
- package/dist/utils/telemetry/buckets.js +23 -0
- package/dist/utils/telemetry/catalog-docs.d.ts +3 -0
- package/dist/utils/telemetry/catalog-docs.d.ts.map +1 -0
- package/dist/utils/telemetry/catalog-docs.js +52 -0
- package/dist/utils/telemetry/catalog.d.ts +1124 -0
- package/dist/utils/telemetry/catalog.d.ts.map +1 -0
- package/dist/utils/telemetry/catalog.js +154 -0
- package/dist/utils/telemetry/command-context.d.ts +17 -0
- package/dist/utils/telemetry/command-context.d.ts.map +1 -0
- package/dist/utils/telemetry/command-context.js +30 -0
- package/dist/utils/telemetry/config-schema.d.ts +14 -0
- package/dist/utils/telemetry/config-schema.d.ts.map +1 -0
- package/dist/utils/telemetry/config-schema.js +35 -0
- package/dist/utils/telemetry/config-store.d.ts +35 -0
- package/dist/utils/telemetry/config-store.d.ts.map +1 -0
- package/dist/utils/telemetry/config-store.js +147 -0
- package/dist/utils/telemetry/delivery.d.ts +13 -0
- package/dist/utils/telemetry/delivery.d.ts.map +1 -0
- package/dist/utils/telemetry/delivery.js +26 -0
- package/dist/utils/telemetry/detached-dispatch.d.ts +12 -0
- package/dist/utils/telemetry/detached-dispatch.d.ts.map +1 -0
- package/dist/utils/telemetry/detached-dispatch.js +33 -0
- package/dist/utils/telemetry/detached-sender.d.ts +2 -0
- package/dist/utils/telemetry/detached-sender.d.ts.map +1 -0
- package/dist/utils/telemetry/detached-sender.js +31 -0
- package/dist/utils/telemetry/dev-session.d.ts +15 -0
- package/dist/utils/telemetry/dev-session.d.ts.map +1 -0
- package/dist/utils/telemetry/dev-session.js +25 -0
- package/dist/utils/telemetry/domains.d.ts +69 -0
- package/dist/utils/telemetry/domains.d.ts.map +1 -0
- package/dist/utils/telemetry/domains.js +64 -0
- package/dist/utils/telemetry/endpoint.d.ts +3 -0
- package/dist/utils/telemetry/endpoint.d.ts.map +1 -0
- package/dist/utils/telemetry/endpoint.js +15 -0
- package/dist/utils/telemetry/environment.d.ts +27 -0
- package/dist/utils/telemetry/environment.d.ts.map +1 -0
- package/dist/utils/telemetry/environment.js +95 -0
- package/dist/utils/telemetry/error-code.d.ts +4 -0
- package/dist/utils/telemetry/error-code.d.ts.map +1 -0
- package/dist/utils/telemetry/error-code.js +57 -0
- package/dist/utils/telemetry/flags.d.ts +5 -0
- package/dist/utils/telemetry/flags.d.ts.map +1 -0
- package/dist/utils/telemetry/flags.js +41 -0
- package/dist/utils/telemetry/generation-metrics.d.ts +15 -0
- package/dist/utils/telemetry/generation-metrics.d.ts.map +1 -0
- package/dist/utils/telemetry/generation-metrics.js +35 -0
- package/dist/utils/telemetry/identity.d.ts +12 -0
- package/dist/utils/telemetry/identity.d.ts.map +1 -0
- package/dist/utils/telemetry/identity.js +42 -0
- package/dist/utils/telemetry/init-funnel.d.ts +15 -0
- package/dist/utils/telemetry/init-funnel.d.ts.map +1 -0
- package/dist/utils/telemetry/init-funnel.js +25 -0
- package/dist/utils/telemetry/invocation.d.ts +22 -0
- package/dist/utils/telemetry/invocation.d.ts.map +1 -0
- package/dist/utils/telemetry/invocation.js +44 -0
- package/dist/utils/telemetry/lifecycle.d.ts +32 -0
- package/dist/utils/telemetry/lifecycle.d.ts.map +1 -0
- package/dist/utils/telemetry/lifecycle.js +99 -0
- package/dist/utils/telemetry/mcp-session.d.ts +10 -0
- package/dist/utils/telemetry/mcp-session.d.ts.map +1 -0
- package/dist/utils/telemetry/mcp-session.js +36 -0
- package/dist/utils/telemetry/notice.d.ts +11 -0
- package/dist/utils/telemetry/notice.d.ts.map +1 -0
- package/dist/utils/telemetry/notice.js +24 -0
- package/dist/utils/telemetry/opt-out.d.ts +16 -0
- package/dist/utils/telemetry/opt-out.d.ts.map +1 -0
- package/dist/utils/telemetry/opt-out.js +32 -0
- package/dist/utils/telemetry/post-json.d.ts +13 -0
- package/dist/utils/telemetry/post-json.d.ts.map +1 -0
- package/dist/utils/telemetry/post-json.js +32 -0
- package/dist/utils/telemetry/project-context.d.ts +15 -0
- package/dist/utils/telemetry/project-context.d.ts.map +1 -0
- package/dist/utils/telemetry/project-context.js +86 -0
- package/dist/utils/telemetry/schema.d.ts +72 -0
- package/dist/utils/telemetry/schema.d.ts.map +1 -0
- package/dist/utils/telemetry/schema.js +17 -0
- package/dist/utils/telemetry/session-config.d.ts +6 -0
- package/dist/utils/telemetry/session-config.d.ts.map +1 -0
- package/dist/utils/telemetry/session-config.js +17 -0
- package/dist/utils/telemetry/settings-lock.d.ts +36 -0
- package/dist/utils/telemetry/settings-lock.d.ts.map +1 -0
- package/dist/utils/telemetry/settings-lock.js +102 -0
- package/dist/utils/telemetry/transport.d.ts +25 -0
- package/dist/utils/telemetry/transport.d.ts.map +1 -0
- package/dist/utils/telemetry/transport.js +76 -0
- package/dist/utils/telemetry/validation.d.ts +10 -0
- package/dist/utils/telemetry/validation.d.ts.map +1 -0
- package/dist/utils/telemetry/validation.js +77 -0
- package/dist/utils/telemetry/value-formats.d.ts +30 -0
- package/dist/utils/telemetry/value-formats.d.ts.map +1 -0
- package/dist/utils/telemetry/value-formats.js +13 -0
- package/package.json +12 -8
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
/** Explicit file arguments are classified without examining or retaining their path. */
|
|
3
|
+
export function entryType(resolved, explicit) {
|
|
4
|
+
if (explicit)
|
|
5
|
+
return 'explicit_file';
|
|
6
|
+
const basename = path.basename(resolved);
|
|
7
|
+
return basename === 'hypequery.ts' || basename === 'api.ts' || basename === 'queries.ts' ? basename : 'unknown';
|
|
8
|
+
}
|
|
9
|
+
export function devSessionConfig(resolved, explicit, options) {
|
|
10
|
+
const cache = options.cache;
|
|
11
|
+
return {
|
|
12
|
+
entry_type: entryType(resolved, explicit), watch: options.watch !== false,
|
|
13
|
+
cache_provider: cache === false || cache === 'none' ? 'none' : cache === 'memory' || cache === 'redis' ? cache : 'unknown',
|
|
14
|
+
cors: options.cors === true, open: options.open === true,
|
|
15
|
+
custom_port: options.port !== undefined && options.port !== 4000, quiet: options.quiet === true,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-process lock for the telemetry settings file.
|
|
3
|
+
*
|
|
4
|
+
* The lock is a directory holding exactly one owner file named
|
|
5
|
+
* `owner.<pid>.<host>.<token>`. It is published with an atomic rename, so it
|
|
6
|
+
* never exists without an owner. A writer never removes a lock it does not own:
|
|
7
|
+
*
|
|
8
|
+
* - Recovery takes over a lock only when its owner process no longer exists on
|
|
9
|
+
* this host, by renaming that exact owner file to the new owner's name. The
|
|
10
|
+
* rename fails if the lock was already replaced or taken over, so concurrent
|
|
11
|
+
* recoverers cannot both win and a replacement lock is never touched.
|
|
12
|
+
* - A live owner keeps its lock however long it runs: age is never evidence of
|
|
13
|
+
* abandonment, because a slow or suspended writer is still alive.
|
|
14
|
+
* - Release deletes only this writer's own owner file.
|
|
15
|
+
*
|
|
16
|
+
* A lock owned on another host (shared home directory) cannot be checked and
|
|
17
|
+
* fails closed. PID reuse makes a dead owner look alive, which also fails closed.
|
|
18
|
+
*/
|
|
19
|
+
export interface SettingsLockIdentity {
|
|
20
|
+
/** Owner process ID; injectable so tests can model separate processes. */
|
|
21
|
+
readonly pid: number;
|
|
22
|
+
/** Liveness of a process on this host. */
|
|
23
|
+
readonly isAlive: (pid: number) => boolean;
|
|
24
|
+
readonly host: string;
|
|
25
|
+
}
|
|
26
|
+
export interface SettingsLock {
|
|
27
|
+
/** Removes this writer's ownership only; never another writer's lock. */
|
|
28
|
+
release(): Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
/** Only an ownerless lock (mid-publish crash on platforms without dir replace) ages out. */
|
|
31
|
+
export declare const ORPHANED_LOCK_MS = 10000;
|
|
32
|
+
export declare function processIsAlive(pid: number): boolean;
|
|
33
|
+
export declare function localIdentity(): SettingsLockIdentity;
|
|
34
|
+
/** Acquires the lock, retrying a held one up to `attempts` times; null when unavailable. */
|
|
35
|
+
export declare function acquireSettingsLock(directory: string, attempts: number, identity?: SettingsLockIdentity): Promise<SettingsLock | null>;
|
|
36
|
+
//# sourceMappingURL=settings-lock.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings-lock.d.ts","sourceRoot":"","sources":["../../../src/utils/telemetry/settings-lock.ts"],"names":[],"mappings":"AAMA;;;;;;;;;;;;;;;;;GAiBG;AAEH,MAAM,WAAW,oBAAoB;IACnC,0EAA0E;IAC1E,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,0CAA0C;IAC1C,QAAQ,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IAC3C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,YAAY;IAC3B,yEAAyE;IACzE,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED,4FAA4F;AAC5F,eAAO,MAAM,gBAAgB,QAAS,CAAC;AAIvC,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAQnD;AAED,wBAAgB,aAAa,IAAI,oBAAoB,CAEpD;AAMD,4FAA4F;AAC5F,wBAAsB,mBAAmB,CACvC,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,QAAQ,GAAE,oBAAsC,GAC/C,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAiB9B"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { hostname } from 'node:os';
|
|
3
|
+
import { mkdir, readdir, rename, rm, rmdir, stat, unlink, writeFile } from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
6
|
+
/** Only an ownerless lock (mid-publish crash on platforms without dir replace) ages out. */
|
|
7
|
+
export const ORPHANED_LOCK_MS = 10_000;
|
|
8
|
+
const OWNER = /^owner\.(\d+)\.([0-9a-f]{12})\.([0-9a-f-]{36})$/;
|
|
9
|
+
export function processIsAlive(pid) {
|
|
10
|
+
try {
|
|
11
|
+
process.kill(pid, 0);
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
// EPERM: the process exists but belongs to another user.
|
|
16
|
+
return error.code !== 'ESRCH';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function localIdentity() {
|
|
20
|
+
return { pid: process.pid, isAlive: processIsAlive, host: hostname() };
|
|
21
|
+
}
|
|
22
|
+
function hostHash(host) {
|
|
23
|
+
return createHash('sha256').update(host).digest('hex').slice(0, 12);
|
|
24
|
+
}
|
|
25
|
+
/** Acquires the lock, retrying a held one up to `attempts` times; null when unavailable. */
|
|
26
|
+
export async function acquireSettingsLock(directory, attempts, identity = localIdentity()) {
|
|
27
|
+
const lockPath = path.join(directory, 'telemetry.lock');
|
|
28
|
+
const owner = `owner.${identity.pid}.${hostHash(identity.host)}.${randomUUID()}`;
|
|
29
|
+
const staged = path.join(directory, `telemetry.lock.${randomUUID()}.tmp`);
|
|
30
|
+
await mkdir(staged, { mode: 0o700 });
|
|
31
|
+
try {
|
|
32
|
+
await writeFile(path.join(staged, owner), '', { flag: 'wx', mode: 0o600 });
|
|
33
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
34
|
+
if (await publish(staged, lockPath) || await takeOver(lockPath, owner, identity)) {
|
|
35
|
+
return { release: () => release(lockPath, owner) };
|
|
36
|
+
}
|
|
37
|
+
if (attempt + 1 < attempts)
|
|
38
|
+
await delay(10);
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
await rm(staged, { recursive: true, force: true }).catch(() => undefined);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** Atomically publishes the staged lock; false when a lock already exists. */
|
|
47
|
+
async function publish(staged, lockPath) {
|
|
48
|
+
try {
|
|
49
|
+
await rename(staged, lockPath);
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
const code = error.code;
|
|
54
|
+
if (code === 'EEXIST' || code === 'ENOTEMPTY' || code === 'EPERM' || code === 'EACCES' || code === 'ENOENT')
|
|
55
|
+
return false;
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** Takes over a lock whose owner is dead on this host; never touches a live or replaced lock. */
|
|
60
|
+
async function takeOver(lockPath, owner, identity) {
|
|
61
|
+
let entries;
|
|
62
|
+
try {
|
|
63
|
+
entries = await readdir(lockPath);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
const owners = entries.filter(entry => entry.startsWith('owner.'));
|
|
69
|
+
if (owners.length === 0) {
|
|
70
|
+
// Ownerless: a publish or release in progress, or one interrupted on a
|
|
71
|
+
// platform where rename cannot replace an empty directory. rmdir only
|
|
72
|
+
// removes it while still empty, so a newly published lock survives.
|
|
73
|
+
try {
|
|
74
|
+
if (Date.now() - (await stat(lockPath)).mtimeMs >= ORPHANED_LOCK_MS)
|
|
75
|
+
await rmdir(lockPath);
|
|
76
|
+
}
|
|
77
|
+
catch { /* Not empty, already gone, or unavailable. */ }
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
const match = owners.length === 1 ? OWNER.exec(owners[0]) : null;
|
|
81
|
+
if (!match || match[2] !== hostHash(identity.host) || identity.isAlive(Number(match[1])))
|
|
82
|
+
return false;
|
|
83
|
+
try {
|
|
84
|
+
// Conditional on that exact owner file: only one recoverer can succeed.
|
|
85
|
+
await rename(path.join(lockPath, owners[0]), path.join(lockPath, owner));
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async function release(lockPath, owner) {
|
|
93
|
+
try {
|
|
94
|
+
await unlink(path.join(lockPath, owner));
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// Not ours any more (or unavailable): never remove someone else's lock.
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
// Removes the directory only while empty; a lock published meanwhile survives.
|
|
101
|
+
await rmdir(lockPath).catch(() => undefined);
|
|
102
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type DispatchBatch } from './detached-dispatch.js';
|
|
2
|
+
export declare const MAX_BATCH_BYTES: number;
|
|
3
|
+
export declare const MAX_BATCH_EVENTS = 32;
|
|
4
|
+
export interface TransportOptions {
|
|
5
|
+
readonly enabled: boolean;
|
|
6
|
+
readonly endpoint?: string;
|
|
7
|
+
readonly debug?: boolean;
|
|
8
|
+
/** Injectable for tests; defaults to a detached sender process. */
|
|
9
|
+
readonly dispatch?: DispatchBatch;
|
|
10
|
+
readonly write?: (text: string) => void;
|
|
11
|
+
readonly now?: () => Date;
|
|
12
|
+
}
|
|
13
|
+
/** In-memory, one-batch delivery with no retry queue and no vendor dependency. */
|
|
14
|
+
export declare class TelemetryTransport {
|
|
15
|
+
private readonly options;
|
|
16
|
+
private queue;
|
|
17
|
+
private sent;
|
|
18
|
+
private flushing?;
|
|
19
|
+
constructor(options: TransportOptions);
|
|
20
|
+
private debug;
|
|
21
|
+
enqueue(input: unknown): void;
|
|
22
|
+
flush(): Promise<void>;
|
|
23
|
+
private send;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=transport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../../src/utils/telemetry/transport.ts"],"names":[],"mappings":"AACA,OAAO,EAAoB,KAAK,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAG9E,eAAO,MAAM,eAAe,QAAY,CAAC;AACzC,eAAO,MAAM,gBAAgB,KAAK,CAAC;AAEnC,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,mEAAmE;IACnE,QAAQ,CAAC,QAAQ,CAAC,EAAE,aAAa,CAAC;IAClC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CAC3B;AASD,kFAAkF;AAClF,qBAAa,kBAAkB;IAKjB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAJpC,OAAO,CAAC,KAAK,CAAmB;IAChC,OAAO,CAAC,IAAI,CAAS;IACrB,OAAO,CAAC,QAAQ,CAAC,CAAgB;gBAEJ,OAAO,EAAE,gBAAgB;IAEtD,OAAO,CAAC,KAAK;IAIb,OAAO,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI;IA2B7B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YAKR,IAAI;CAenB"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { validateTelemetryEvent } from './validation.js';
|
|
2
|
+
import { dispatchDetached } from './detached-dispatch.js';
|
|
3
|
+
import { telemetryEndpoint } from './endpoint.js';
|
|
4
|
+
export const MAX_BATCH_BYTES = 64 * 1024;
|
|
5
|
+
export const MAX_BATCH_EVENTS = 32;
|
|
6
|
+
/** In-memory, one-batch delivery with no retry queue and no vendor dependency. */
|
|
7
|
+
export class TelemetryTransport {
|
|
8
|
+
options;
|
|
9
|
+
queue = [];
|
|
10
|
+
sent = false;
|
|
11
|
+
flushing;
|
|
12
|
+
constructor(options) {
|
|
13
|
+
this.options = options;
|
|
14
|
+
}
|
|
15
|
+
debug(text) {
|
|
16
|
+
try {
|
|
17
|
+
(this.options.write ?? (message => { process.stderr.write(message); }))(text);
|
|
18
|
+
}
|
|
19
|
+
catch { /* Ignore broken stderr. */ }
|
|
20
|
+
}
|
|
21
|
+
enqueue(input) {
|
|
22
|
+
if (!this.options.enabled || this.sent || this.queue.length >= MAX_BATCH_EVENTS)
|
|
23
|
+
return;
|
|
24
|
+
try {
|
|
25
|
+
if (!validateTelemetryEvent(input)) {
|
|
26
|
+
if (this.options.debug)
|
|
27
|
+
this.debug('[telemetry] Dropped invalid event.\n');
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const snapshot = JSON.parse(JSON.stringify(input));
|
|
31
|
+
if (!validateTelemetryEvent(snapshot)) {
|
|
32
|
+
if (this.options.debug)
|
|
33
|
+
this.debug('[telemetry] Dropped invalid event.\n');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const event = {
|
|
37
|
+
event: snapshot.event, distinct_id: snapshot.properties.install_id,
|
|
38
|
+
properties: { ...snapshot.properties, $process_person_profile: false, $geoip_disable: true },
|
|
39
|
+
timestamp: (this.options.now?.() ?? new Date()).toISOString(),
|
|
40
|
+
};
|
|
41
|
+
if (Buffer.byteLength(JSON.stringify({ api_key: 'hypequery-cli', batch: [...this.queue, event] })) > MAX_BATCH_BYTES) {
|
|
42
|
+
if (this.options.debug)
|
|
43
|
+
this.debug('[telemetry] Dropped event: batch size limit.\n');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
this.queue.push(event);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
if (this.options.debug)
|
|
50
|
+
this.debug('[telemetry] Dropped invalid event.\n');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
flush() {
|
|
54
|
+
this.flushing ??= this.send();
|
|
55
|
+
return this.flushing;
|
|
56
|
+
}
|
|
57
|
+
async send() {
|
|
58
|
+
this.sent = true;
|
|
59
|
+
const events = this.queue;
|
|
60
|
+
this.queue = [];
|
|
61
|
+
if (!this.options.enabled || !events.length)
|
|
62
|
+
return;
|
|
63
|
+
if (this.options.debug) {
|
|
64
|
+
for (const event of events)
|
|
65
|
+
this.debug(`${JSON.stringify(event)}\n`);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const url = telemetryEndpoint(this.options.endpoint);
|
|
69
|
+
if (!url)
|
|
70
|
+
return;
|
|
71
|
+
try {
|
|
72
|
+
(this.options.dispatch ?? dispatchDetached)(url, JSON.stringify({ api_key: 'hypequery-cli', batch: events }));
|
|
73
|
+
}
|
|
74
|
+
catch { /* Temporary-file and spawn failures never affect the command. */ }
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type TelemetryEvent } from './catalog.js';
|
|
2
|
+
/** Reject the entire payload on unknown content; never guess how to redact it. */
|
|
3
|
+
export declare function validateTelemetryEvent(input: unknown): input is TelemetryEvent;
|
|
4
|
+
type DeepExact<A, E> = E extends unknown ? A extends E ? A extends readonly unknown[] ? A : A extends object ? {
|
|
5
|
+
[K in keyof A]: K extends keyof E ? DeepExact<A[K], E[K]> : never;
|
|
6
|
+
} : A : never : never;
|
|
7
|
+
/** The typed construction boundary also checks runtime data and returns a detached JSON snapshot. */
|
|
8
|
+
export declare function createTelemetryEvent<const T extends TelemetryEvent>(input: T & DeepExact<T, TelemetryEvent>): TelemetryEvent | null;
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=validation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../../src/utils/telemetry/validation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoC,KAAK,cAAc,EAA2B,MAAM,cAAc,CAAC;AAsC9G,kFAAkF;AAClF,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,cAAc,CAwB9E;AAED,KAAK,SAAS,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,SAAS,OAAO,GAAG,CAAC,SAAS,CAAC,GAClD,CAAC,SAAS,SAAS,OAAO,EAAE,GAAG,CAAC,GAAG,CAAC,SAAS,MAAM,GACjD;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,SAAS,MAAM,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK;CAAE,GACrE,CAAC,GACH,KAAK,GAAG,KAAK,CAAC;AAElB,qGAAqG;AACrG,wBAAgB,oBAAoB,CAAC,KAAK,CAAC,CAAC,SAAS,cAAc,EACjE,KAAK,EAAE,CAAC,GAAG,SAAS,CAAC,CAAC,EAAE,cAAc,CAAC,GACtC,cAAc,GAAG,IAAI,CAQvB"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { COMMON_PROPERTIES, EVENT_CATALOG } from './catalog.js';
|
|
2
|
+
import { COMMAND_FLAGS, GLOBAL_FLAGS } from './domains.js';
|
|
3
|
+
import { matchesTelemetryFormat } from './value-formats.js';
|
|
4
|
+
/** Only plain JSON data; reject accessors, custom prototypes and symbol keys. */
|
|
5
|
+
function plainRecord(value) {
|
|
6
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
7
|
+
return false;
|
|
8
|
+
const prototype = Object.getPrototypeOf(value);
|
|
9
|
+
return (prototype === Object.prototype || prototype === null)
|
|
10
|
+
&& Reflect.ownKeys(value).every(key => typeof key === 'string'
|
|
11
|
+
&& Object.getOwnPropertyDescriptor(value, key)?.enumerable === true
|
|
12
|
+
&& Object.hasOwn(Object.getOwnPropertyDescriptor(value, key) ?? {}, 'value'));
|
|
13
|
+
}
|
|
14
|
+
function validField(field, value) {
|
|
15
|
+
switch (field.kind) {
|
|
16
|
+
case 'boolean': return typeof value === 'boolean';
|
|
17
|
+
case 'enum': return field.values.includes(value);
|
|
18
|
+
case 'enum_list': return Array.isArray(value)
|
|
19
|
+
&& value.length <= field.values.length
|
|
20
|
+
&& Object.getPrototypeOf(value) === Array.prototype
|
|
21
|
+
&& Reflect.ownKeys(value).length === value.length + 1
|
|
22
|
+
&& Array.from({ length: value.length }, (_, index) => Object.getOwnPropertyDescriptor(value, String(index)))
|
|
23
|
+
.every(descriptor => descriptor?.enumerable === true && Object.hasOwn(descriptor, 'value'))
|
|
24
|
+
&& new Set(value).size === value.length
|
|
25
|
+
&& value.every(item => typeof item === 'string' && field.values.includes(item));
|
|
26
|
+
case 'format': return matchesTelemetryFormat(field.format, value);
|
|
27
|
+
case 'record': return validProperties(field.fields, value);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function validProperties(fields, value) {
|
|
31
|
+
return plainRecord(value)
|
|
32
|
+
&& Object.keys(value).every(key => Object.hasOwn(fields, key) && validField(fields[key], value[key]))
|
|
33
|
+
&& Object.entries(fields).every(([key, field]) => field.optional || Object.hasOwn(value, key));
|
|
34
|
+
}
|
|
35
|
+
/** Reject the entire payload on unknown content; never guess how to redact it. */
|
|
36
|
+
export function validateTelemetryEvent(input) {
|
|
37
|
+
try {
|
|
38
|
+
if (!plainRecord(input) || Object.keys(input).length !== 2
|
|
39
|
+
|| !Object.hasOwn(input, 'event') || !Object.hasOwn(input, 'properties')
|
|
40
|
+
|| typeof input.event !== 'string' || !Object.hasOwn(EVENT_CATALOG, input.event)
|
|
41
|
+
|| !plainRecord(input.properties))
|
|
42
|
+
return false;
|
|
43
|
+
const definition = EVENT_CATALOG[input.event];
|
|
44
|
+
let variant = {};
|
|
45
|
+
if ('variants' in definition) {
|
|
46
|
+
const command = input.properties.command;
|
|
47
|
+
if (typeof command !== 'string' || !Object.hasOwn(definition.variants, command))
|
|
48
|
+
return false;
|
|
49
|
+
variant = definition.variants[command];
|
|
50
|
+
}
|
|
51
|
+
if (!validProperties({ ...COMMON_PROPERTIES, ...definition.properties, ...variant }, input.properties))
|
|
52
|
+
return false;
|
|
53
|
+
if (input.event === 'cli_command_completed') {
|
|
54
|
+
const command = input.properties.command;
|
|
55
|
+
const allowed = [...COMMAND_FLAGS[command], ...GLOBAL_FLAGS];
|
|
56
|
+
if (!input.properties.flags_used.every(flag => allowed.includes(flag)))
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Proxies and malformed untrusted objects must not affect the command.
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** The typed construction boundary also checks runtime data and returns a detached JSON snapshot. */
|
|
67
|
+
export function createTelemetryEvent(input) {
|
|
68
|
+
if (!validateTelemetryEvent(input))
|
|
69
|
+
return null;
|
|
70
|
+
try {
|
|
71
|
+
const snapshot = JSON.parse(JSON.stringify(input));
|
|
72
|
+
return validateTelemetryEvent(snapshot) ? snapshot : null;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Format rules are shared by validation, snapshots and generated documentation. */
|
|
2
|
+
export declare const VALUE_FORMATS: {
|
|
3
|
+
readonly uuid: {
|
|
4
|
+
readonly pattern: RegExp;
|
|
5
|
+
readonly maxLength: 36;
|
|
6
|
+
readonly description: "Random UUID v4 (36 characters)";
|
|
7
|
+
};
|
|
8
|
+
readonly hash: {
|
|
9
|
+
readonly pattern: RegExp;
|
|
10
|
+
readonly maxLength: 64;
|
|
11
|
+
readonly description: "64 lowercase hexadecimal characters (salted SHA-256)";
|
|
12
|
+
};
|
|
13
|
+
readonly version: {
|
|
14
|
+
readonly pattern: RegExp;
|
|
15
|
+
readonly maxLength: 80;
|
|
16
|
+
readonly description: "Exact major.minor.patch (1–3 digits per component); optional canary/alpha/beta/rc/dev/snapshot suffix with 1–14 digit numeric components, or `unknown`; at most 80 characters";
|
|
17
|
+
};
|
|
18
|
+
readonly node_version: {
|
|
19
|
+
readonly pattern: RegExp;
|
|
20
|
+
readonly maxLength: 80;
|
|
21
|
+
readonly description: "Numeric major.minor (1–3 digits per component), or `unknown`";
|
|
22
|
+
};
|
|
23
|
+
readonly major_version: {
|
|
24
|
+
readonly pattern: RegExp;
|
|
25
|
+
readonly maxLength: 80;
|
|
26
|
+
readonly description: "Numeric major (1–3 digits), or `unknown`";
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
export declare function matchesTelemetryFormat(format: keyof typeof VALUE_FORMATS, value: unknown): boolean;
|
|
30
|
+
//# sourceMappingURL=value-formats.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"value-formats.d.ts","sourceRoot":"","sources":["../../../src/utils/telemetry/value-formats.ts"],"names":[],"mappings":"AAAA,oFAAoF;AACpF,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;CAMhB,CAAC;AAEX,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,OAAO,aAAa,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAIlG"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Format rules are shared by validation, snapshots and generated documentation. */
|
|
2
|
+
export const VALUE_FORMATS = {
|
|
3
|
+
uuid: { pattern: /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i, maxLength: 36, description: 'Random UUID v4 (36 characters)' },
|
|
4
|
+
hash: { pattern: /^[0-9a-f]{64}$/, maxLength: 64, description: '64 lowercase hexadecimal characters (salted SHA-256)' },
|
|
5
|
+
version: { pattern: /^(?:unknown|\d{1,3}\.\d{1,3}\.\d{1,3}(?:-(?:canary|alpha|beta|rc|dev|snapshot)(?:[.-]\d{1,14})*)?)$/, maxLength: 80, description: 'Exact major.minor.patch (1–3 digits per component); optional canary/alpha/beta/rc/dev/snapshot suffix with 1–14 digit numeric components, or `unknown`; at most 80 characters' },
|
|
6
|
+
node_version: { pattern: /^(?:unknown|\d{1,3}\.\d{1,3})$/, maxLength: 80, description: 'Numeric major.minor (1–3 digits per component), or `unknown`' },
|
|
7
|
+
major_version: { pattern: /^(?:unknown|\d{1,3})$/, maxLength: 80, description: 'Numeric major (1–3 digits), or `unknown`' },
|
|
8
|
+
};
|
|
9
|
+
export function matchesTelemetryFormat(format, value) {
|
|
10
|
+
const rule = VALUE_FORMATS[format];
|
|
11
|
+
return typeof value === 'string' && value.length <= rule.maxLength
|
|
12
|
+
&& rule.pattern.exec(value)?.[0] === value;
|
|
13
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hypequery/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.24.0",
|
|
4
4
|
"description": "CLI for scaffolding type-safe ClickHouse analytics, semantic datasets, React manifests, and deployments",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"clickhouse",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"dist"
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@clickhouse/client": "^1.
|
|
25
|
+
"@clickhouse/client": "^1.24.0",
|
|
26
26
|
"chalk": "^5.3.0",
|
|
27
27
|
"commander": "^12.0.0",
|
|
28
28
|
"dotenv": "^16.4.7",
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
"open": "^10.0.0",
|
|
31
31
|
"ora": "^8.0.1",
|
|
32
32
|
"prompts": "^2.4.2",
|
|
33
|
-
"@hypequery/deployment": "0.9.
|
|
34
|
-
"@hypequery/protocol": "0.
|
|
33
|
+
"@hypequery/deployment": "0.9.5",
|
|
34
|
+
"@hypequery/protocol": "0.19.0"
|
|
35
35
|
},
|
|
36
36
|
"optionalDependencies": {
|
|
37
37
|
"@napi-rs/keyring": "1.3.0"
|
|
@@ -49,10 +49,10 @@
|
|
|
49
49
|
"@types/node": "^20.11.30",
|
|
50
50
|
"@types/prompts": "^2.4.9",
|
|
51
51
|
"@vitest/coverage-v8": "^5.0.2",
|
|
52
|
-
"typescript": "^
|
|
52
|
+
"typescript": "^6.0.3",
|
|
53
53
|
"vitest": "^5.0.2",
|
|
54
|
-
"@hypequery/mcp": "0.
|
|
55
|
-
"@hypequery/serve": "0.
|
|
54
|
+
"@hypequery/mcp": "0.9.0",
|
|
55
|
+
"@hypequery/serve": "0.21.0"
|
|
56
56
|
},
|
|
57
57
|
"repository": {
|
|
58
58
|
"type": "git",
|
|
@@ -70,6 +70,10 @@
|
|
|
70
70
|
"build": "tsc --project tsconfig.json",
|
|
71
71
|
"dev": "tsc --project tsconfig.json --watch",
|
|
72
72
|
"lint": "eslint \"src/**/*.ts\"",
|
|
73
|
-
"test": "vitest run"
|
|
73
|
+
"test": "pnpm build && vitest run && pnpm test:types",
|
|
74
|
+
"test:types": "tsc --project tsconfig.type-tests.json --noEmit",
|
|
75
|
+
"telemetry:docs": "pnpm build && node scripts/generate-telemetry-docs.mjs",
|
|
76
|
+
"telemetry:docs:check": "pnpm build && node scripts/generate-telemetry-docs.mjs --check",
|
|
77
|
+
"telemetry:benchmark": "pnpm build && node scripts/benchmark-telemetry.mjs"
|
|
74
78
|
}
|
|
75
79
|
}
|